api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-11-27 19:58:21 +05:00
parent bd23d3f2ad
commit 0c647ff5ff
33 changed files with 928 additions and 418 deletions

View File

@@ -69,7 +69,29 @@ export default function LoginForm() {
}, },
onError: (error: AxiosError) => { onError: (error: AxiosError) => {
const data = error.response?.data as { message?: string }; const data = error.response?.data as { message?: string };
toast.error(data?.message || "Xatolik yuz berdi"); const errorData = error.response?.data as {
messages?: {
token_class: string;
token_type: string;
message: string;
}[];
};
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
const message =
Array.isArray(errorName.data?.name) && errorName.data.name.length
? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -95,12 +95,16 @@ export default function District() {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });
@@ -128,12 +132,16 @@ export default function District() {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });
@@ -160,12 +168,16 @@ export default function District() {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -119,12 +119,16 @@ const CreateDoctor = () => {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });
@@ -150,12 +154,16 @@ const CreateDoctor = () => {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -55,12 +55,16 @@ const Doctor = () => {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -78,12 +78,16 @@ export default function Home() {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -68,13 +68,16 @@ const MyLocation: React.FC = () => {
}; };
}; };
toast.error( const message =
errorName.data?.name?.[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0]?.message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
setLoadingButtonId(null); : undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -87,12 +87,16 @@ const CreateObject = () => {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -85,12 +85,16 @@ const CreatePharmacy = () => {
}; };
}; };
toast.error( const message =
errorName.data?.name[0] || Array.isArray(errorName.data?.name) && errorName.data.name.length
data.message || ? errorName.data.name[0]
errorData?.messages?.[0].message || : data?.message ||
"Xatolik yuz berdi", (Array.isArray(errorData?.messages) && errorData.messages.length
); ? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -0,0 +1,9 @@
import httpClient from "@/shared/config/api/httpClient";
import { TOUR_PLAN } from "@/shared/config/api/URLs";
export const tour_plan_api = {
async list() {
const res = await httpClient.get(TOUR_PLAN);
return res;
},
};

View File

View File

@@ -1,60 +1,41 @@
import { order_api } from "@/features/specification/lib/api";
import type { OrderListData } from "@/features/specification/lib/data";
import { LanguageRoutes } from "@/shared/config/i18n/types"; import { LanguageRoutes } from "@/shared/config/i18n/types";
import { formatPrice } from "@/shared/lib/formatPrice"; import { formatPrice } from "@/shared/lib/formatPrice";
import { Alert, AlertDescription } from "@/shared/ui/alert"; import { Alert, AlertDescription } from "@/shared/ui/alert";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { Building2, Check, DollarSign, Edit2, MapPin, X } from "lucide-react"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react"; import type { AxiosError } from "axios";
import { FalkeDataPlanTour } from "../lib/mock"; import {
Banknote,
// TYPES Building2,
interface MonthData { Check,
amount: number; DollarSign,
locked: boolean; Edit2,
} Loader2,
X,
interface Pharmacy { } from "lucide-react";
id: number; import { useState } from "react";
name: string; import { toast } from "sonner";
district: string;
address: string;
monthlyData: Record<string, MonthData>;
}
interface PlanPriceProps { interface PlanPriceProps {
selectedMonth: string; selectedMonth: string;
pharmacies: OrderListData[];
} }
const PlanPrice = ({ selectedMonth }: PlanPriceProps) => { const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
const [pharmacies, setPharmacies] = useState(FalkeDataPlanTour);
const currentDate = new Date(); const currentDate = new Date();
const queryClient = useQueryClient();
const currentMonthKey = `${currentDate.getFullYear()}-${String( const currentMonthKey = `${currentDate.getFullYear()}-${String(
currentDate.getMonth() + 1, currentDate.getMonth() + 1,
).padStart(2, "0")}`; ).padStart(2, "0")}`;
useEffect(() => {
setPharmacies((prev) =>
prev.map((pharmacy) => {
if (pharmacy.monthlyData[selectedMonth]) return pharmacy;
return {
...pharmacy,
monthlyData: {
...pharmacy.monthlyData,
[selectedMonth]: { amount: 0, locked: false },
},
};
}),
);
}, [selectedMonth]);
// Editing
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
const [tempAmount, setTempAmount] = useState<string>(""); const [tempAmount, setTempAmount] = useState<string>("");
const [displayPrice, setDisplayPrice] = useState(""); const [displayPrice, setDisplayPrice] = useState("");
// === HELPERS ===
const formatCurrency = (amount: number): string => const formatCurrency = (amount: number): string =>
new Intl.NumberFormat("uz-UZ").format(amount) + " so'm"; new Intl.NumberFormat("uz-UZ").format(amount) + " so'm";
@@ -77,6 +58,43 @@ const PlanPrice = ({ selectedMonth }: PlanPriceProps) => {
return `${months[parseInt(month) - 1]} ${year}`; return `${months[parseInt(month) - 1]} ${year}`;
}; };
const { mutate, isPending } = useMutation({
mutationFn: ({ body, id }: { id: number; body: { paid_price: number } }) =>
order_api.update({ body, id }),
onSuccess: () => {
toast.success("Lokatsiya jo'natildi");
queryClient.refetchQueries({ queryKey: ["order_list"] });
setEditingId(null);
setTempAmount("");
},
onError: (error: AxiosError) => {
const data = error.response?.data as { message?: string };
const errorData = error.response?.data as {
messages?: {
token_class: string;
token_type: string;
message: string;
}[];
};
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
const message =
Array.isArray(errorName.data?.name) && errorName.data.name.length
? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
},
});
const handleEdit = (pharmacyId: number) => { const handleEdit = (pharmacyId: number) => {
const pharmacy = pharmacies.find((p) => p.id === pharmacyId); const pharmacy = pharmacies.find((p) => p.id === pharmacyId);
const currentAmount = pharmacy?.monthlyData[selectedMonth]?.amount ?? 0; const currentAmount = pharmacy?.monthlyData[selectedMonth]?.amount ?? 0;
@@ -88,25 +106,12 @@ const PlanPrice = ({ selectedMonth }: PlanPriceProps) => {
const handleSave = (pharmacyId: number) => { const handleSave = (pharmacyId: number) => {
const amount = parseInt(tempAmount) || 0; const amount = parseInt(tempAmount) || 0;
setPharmacies((prev) => mutate({
prev.map((pharmacy) => body: {
pharmacy.id === pharmacyId paid_price: amount,
? {
...pharmacy,
monthlyData: {
...pharmacy.monthlyData,
[selectedMonth]: {
amount,
locked: selectedMonth !== currentMonthKey,
}, },
}, id: pharmacyId,
} });
: pharmacy,
),
);
setEditingId(null);
setTempAmount("");
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -114,7 +119,7 @@ const PlanPrice = ({ selectedMonth }: PlanPriceProps) => {
setTempAmount(""); setTempAmount("");
}; };
const isLocked = (pharmacy: Pharmacy): boolean => const isLocked = (pharmacy: OrderListData): boolean =>
pharmacy.monthlyData[selectedMonth]?.locked === true || pharmacy.monthlyData[selectedMonth]?.locked === true ||
selectedMonth !== currentMonthKey; selectedMonth !== currentMonthKey;
@@ -158,18 +163,20 @@ const PlanPrice = ({ selectedMonth }: PlanPriceProps) => {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<CardTitle className="text-xl text-gray-900 mb-2"> <CardTitle className="text-xl text-gray-900 mb-2">
{pharmacy.name} {pharmacy.employee_name}
</CardTitle> </CardTitle>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<MapPin className="h-4 w-4 mr-2 text-blue-600" /> <Building2 className="h-4 w-4 mr-2 text-blue-600" />
{pharmacy.district} Farmasevtika: {pharmacy.factory.name}
</div>
</div> </div>
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<Building2 className="h-4 w-4 mr-2 text-blue-600" /> <Banknote className="h-4 w-4 mr-2 text-blue-600" />
{pharmacy.address} Qolgan summa: {formatPrice(pharmacy.overdue_price)}
</div> </div>
</div> </div>
</div> </div>
@@ -182,13 +189,9 @@ const PlanPrice = ({ selectedMonth }: PlanPriceProps) => {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-6"> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Oylik summa
</label>
{isEditing ? ( {isEditing ? (
<div className="space-y-3 flex flex-col"> <div className="space-y-3 flex flex-col">
<Input <Input
@@ -213,7 +216,11 @@ const PlanPrice = ({ selectedMonth }: PlanPriceProps) => {
className="flex-1 bg-green-600 hover:bg-green-700" className="flex-1 bg-green-600 hover:bg-green-700"
> >
<Check className="h-4 w-4 mr-2" /> <Check className="h-4 w-4 mr-2" />
Saqlash {isPending ? (
<Loader2 className="animate-spin" />
) : (
"Saqlash"
)}
</Button> </Button>
<Button <Button

View File

@@ -1,32 +1,62 @@
"use client";
import { order_api } from "@/features/specification/lib/api";
import type { OrderListData } from "@/features/specification/lib/data";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import { DashboardLayout } from "@/widgets/dashboard-layout/ui"; import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
import { Calendar } from "lucide-react"; import { useQuery } from "@tanstack/react-query";
import { Calendar, Loader2 } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FalkeDataPlanTour } from "../lib/mock";
import PlanPrice from "./PlanPrice"; import PlanPrice from "./PlanPrice";
const PlanTour = () => { const PlanTour = () => {
const [pharmacies, setPharmacies] = useState(FalkeDataPlanTour);
const currentDate = new Date(); const currentDate = new Date();
const currentMonthKey = `${currentDate.getFullYear()}-${String( const currentMonthKey = `${currentDate.getFullYear()}-${String(
currentDate.getMonth() + 1, currentDate.getMonth() + 1,
).padStart(2, "0")}`; ).padStart(2, "0")}`;
const { data, isLoading, isError } = useQuery({
queryKey: ["order_list"],
queryFn: () => order_api.order_list(),
select(data) {
return data.data.data as OrderListData[];
},
});
// 🌟 monthlyData frontendda qoshilmoqda
const [pharmacies, setPharmacies] = useState<OrderListData[]>([]);
useEffect(() => { useEffect(() => {
if (!data) return;
const enhanced = data.map((item) => ({
...item,
monthlyData: item.monthlyData || {}, // mavjud bo'lmasa yaratamiz
}));
setPharmacies(enhanced);
}, [data]);
// 🔥 Joriy oy boyicha monthlyData bolmasa — qoshamiz
useEffect(() => {
if (!pharmacies.length) return;
setPharmacies((prev) => setPharmacies((prev) =>
prev.map((pharmacy) => { prev.map((p) => {
if (pharmacy.monthlyData[currentMonthKey]) return pharmacy; if (p.monthlyData[currentMonthKey]) return p;
return { return {
...pharmacy, ...p,
monthlyData: { monthlyData: {
...pharmacy.monthlyData, ...p.monthlyData,
[currentMonthKey]: { amount: 0, locked: false }, [currentMonthKey]: { amount: 0, locked: false },
}, },
}; };
}), }),
); );
}, [currentMonthKey]); }, [currentMonthKey, pharmacies.length]);
const [selectedMonth, setSelectedMonth] = useState<string>(currentMonthKey); const [selectedMonth, setSelectedMonth] = useState<string>(currentMonthKey);
const getMonthName = (monthKey: string): string => { const getMonthName = (monthKey: string): string => {
@@ -49,16 +79,17 @@ const PlanTour = () => {
}; };
const availableMonths = (): string[] => { const availableMonths = (): string[] => {
const months = new Set<string>(); const set = new Set<string>();
pharmacies.forEach((p) => { pharmacies.forEach((p) => {
Object.keys(p.monthlyData).forEach((m) => months.add(m)); Object.keys(p.monthlyData).forEach((m) => set.add(m));
}); });
return Array.from(months).sort();
return Array.from(set).sort();
}; };
return ( return (
<DashboardLayout> <DashboardLayout>
<div>
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold text-gray-900 mb-2"> <h1 className="text-4xl font-bold text-gray-900 mb-2">
Oylik hisobotlar Oylik hisobotlar
@@ -66,13 +97,27 @@ const PlanTour = () => {
<p className="text-gray-600"> <p className="text-gray-600">
Dorixonalar uchun oylik summalarni boshqaring Dorixonalar uchun oylik summalarni boshqaring
</p> </p>
{isLoading && (
<div className="flex justify-center py-10">
<Loader2 className="w-10 h-10 animate-spin text-blue-600" />
</div>
)}
{isError && (
<div className="flex justify-center py-10 text-red-600 font-semibold">
Ma'lumotlarni yuklashda xatolik yuz berdi!
</div>
)}
<Card className="mb-4 border-0 p-0 mt-5"> <Card className="mb-4 border-0 p-0 mt-5">
<CardHeader className="bg-blue-500 p-3 from-blue-600 to-indigo-600 text-white"> <CardHeader className="bg-blue-500 p-3 text-white">
<CardTitle className="flex items-center gap-2 justify-center"> <CardTitle className="flex items-center gap-2 justify-center">
<Calendar className="h-5 w-5" /> <Calendar className="h-5 w-5" />
Oy tanlash Oy tanlash
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pb-6"> <CardContent className="pb-6">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{availableMonths().map((month) => ( {availableMonths().map((month) => (
@@ -80,11 +125,11 @@ const PlanTour = () => {
key={month} key={month}
onClick={() => setSelectedMonth(month)} onClick={() => setSelectedMonth(month)}
variant={selectedMonth === month ? "default" : "outline"} variant={selectedMonth === month ? "default" : "outline"}
className={`${ className={
selectedMonth === month selectedMonth === month
? "bg-blue-600 hover:bg-blue-700 text-white" ? "bg-blue-600 hover:bg-blue-700 text-white"
: "hover:bg-blue-50" : "hover:bg-blue-50"
}`} }
> >
{getMonthName(month)} {getMonthName(month)}
{month === currentMonthKey && ( {month === currentMonthKey && (
@@ -97,8 +142,9 @@ const PlanTour = () => {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<PlanPrice selectedMonth={selectedMonth} />
</div> {/* 🔥 Oylik narxlar */}
<PlanPrice selectedMonth={selectedMonth} pharmacies={pharmacies} />
</div> </div>
</DashboardLayout> </DashboardLayout>
); );

View File

@@ -49,10 +49,22 @@ export function PlanDetailsDialog({
message: string; message: string;
}[]; }[];
}; };
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
toast.error( const message =
data.message || errorData?.messages?.[0].message || "Xatolik yuz berdi", Array.isArray(errorName.data?.name) && errorName.data.name.length
); ? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -94,10 +94,22 @@ export const AddPlans = ({
message: string; message: string;
}[]; }[];
}; };
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
toast.error( const message =
data.message || errorData?.messages?.[0].message || "Xatolik yuz berdi", Array.isArray(errorName.data?.name) && errorName.data.name.length
); ? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });
@@ -119,10 +131,22 @@ export const AddPlans = ({
message: string; message: string;
}[]; }[];
}; };
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
toast.error( const message =
data.message || errorData?.messages?.[0].message || "Xatolik yuz berdi", Array.isArray(errorName.data?.name) && errorName.data.name.length
); ? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -61,10 +61,22 @@ export default function Plans() {
message: string; message: string;
}[]; }[];
}; };
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
toast.error( const message =
data.message || errorData?.messages?.[0].message || "Xatolik yuz berdi", Array.isArray(errorName.data?.name) && errorName.data.name.length
); ? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
}, },
}); });

View File

@@ -0,0 +1,36 @@
import type {
FactoryListRes,
OrderCreateReq,
OrderList,
ProductListRes,
} from "@/features/specification/lib/data";
import httpClient from "@/shared/config/api/httpClient";
import { FACTORY, ORDER, PRODUCT } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const order_api = {
async factory_list(): Promise<AxiosResponse<FactoryListRes>> {
const res = await httpClient.get(FACTORY);
return res;
},
async product_list(): Promise<AxiosResponse<ProductListRes>> {
const res = await httpClient.get(PRODUCT);
return res;
},
async order_list(): Promise<AxiosResponse<OrderList>> {
const res = await httpClient.get(`${ORDER}list/`);
return res;
},
async order_create(body: OrderCreateReq) {
const res = await httpClient.post(`${ORDER}create/`, body);
return res;
},
async update({ body, id }: { id: number; body: { paid_price: number } }) {
const res = await httpClient.patch(`${ORDER}${id}/update/`, body);
return res;
},
};

View File

@@ -0,0 +1,72 @@
export interface FactoryListRes {
status_code: number;
status: string;
message: string;
data: FactoryListData[];
}
export interface FactoryListData {
id: number;
name: string;
created_at: string;
}
export interface ProductListRes {
status_code: number;
status: string;
message: string;
data: ProductListData[];
}
export interface ProductListData {
id: number;
name: string;
price: string;
created_at: string;
}
export interface OrderCreateReq {
factory_id: number;
paid_price: string;
total_price: string;
advance: number;
employee_name: string;
items: {
product: number;
quantity: number;
total_price: string;
}[];
}
export interface OrderList {
status_code: number;
status: string;
message: string;
data: OrderListData[];
}
export interface OrderListData {
id: number;
factory: {
id: number;
name: string;
};
total_price: string;
paid_price: string;
advance: number;
employee_name: string;
overdue_price: string;
order_items: {
id: number;
product: number;
quantity: number;
total_price: string;
}[];
file: string;
monthlyData: {
[key: string]: {
amount: number;
locked: boolean;
};
};
}

View File

@@ -1,81 +1,35 @@
"use client"; "use client";
import formatDate from "@/shared/lib/formatDate"; import { order_api } from "@/features/specification/lib/api";
import { formatPrice } from "@/shared/lib/formatPrice"; import { formatPrice } from "@/shared/lib/formatPrice";
import { Button } from "@/shared/ui/button";
import { DashboardLayout } from "@/widgets/dashboard-layout/ui"; import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
import { Building2, Calendar, User } from "lucide-react"; import { useQuery } from "@tanstack/react-query";
import { Building2, User } from "lucide-react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
interface MedicineItem {
id: number;
name: string;
price: number;
quantity: number;
total: number;
}
interface SpecificationHistory {
id: string;
date: string;
buyerName: string;
pharmacy: string;
priceType: "regular" | "special";
paymentPercentage: number;
medicines: MedicineItem[];
totalAmount: number;
paymentAmount: number;
}
// Sample data
const SAMPLE_HISTORY: SpecificationHistory[] = [
{
id: "1",
date: "2024-11-15 14:30",
buyerName: "Abdullayev Aziz",
pharmacy: "Farmaciya #1",
priceType: "special",
paymentPercentage: 100,
medicines: [
{ id: 1, name: "Aspirin", price: 3500, quantity: 2, total: 7000 },
{ id: 2, name: "Paracetamol", price: 5500, quantity: 3, total: 16500 },
],
totalAmount: 23500,
paymentAmount: 23500,
},
{
id: "2",
date: "2024-11-14 10:15",
buyerName: "Karimova Malika",
pharmacy: "Farmaciya #2",
priceType: "regular",
paymentPercentage: 50,
medicines: [
{ id: 3, name: "Ibuprofen", price: 10000, quantity: 1, total: 10000 },
{ id: 4, name: "Amoxicillin", price: 15000, quantity: 2, total: 30000 },
{ id: 5, name: "Metformin", price: 20000, quantity: 1, total: 20000 },
],
totalAmount: 60000,
paymentAmount: 30000,
},
{
id: "3",
date: "2024-11-13 16:45",
buyerName: "Tursunov Bobur",
pharmacy: "Farmaciya #3",
priceType: "special",
paymentPercentage: 75,
medicines: [
{ id: 6, name: "Lisinopril", price: 8500, quantity: 4, total: 34000 },
],
totalAmount: 34000,
paymentAmount: 25500,
},
];
export function DetailViewPage() { export function DetailViewPage() {
const { id } = useParams(); const { id } = useParams();
const item = SAMPLE_HISTORY.find((h) => h.id === id); const { data } = useQuery({
queryKey: ["order_list"],
queryFn: () => order_api.order_list(),
select(data) {
return data.data.data;
},
});
const { data: product } = useQuery({
queryKey: ["product_list"],
queryFn: () => order_api.product_list(),
select(data) {
return data.data.data;
},
});
const item = data && data.find((h) => h.id === Number(id));
const handleDownloadPDF = async () => {};
if (!item) return <div>{"Ma'lumot topilmadi"}</div>; if (!item) return <div>{"Ma'lumot topilmadi"}</div>;
@@ -92,22 +46,24 @@ export function DetailViewPage() {
{/* Info Cards */} {/* Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-6 bg-gray-50"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-6 bg-gray-50">
<div className="bg-white p-4 rounded-lg shadow-sm"> {/* <div className="bg-white p-4 rounded-lg shadow-sm">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<Calendar className="text-blue-600" size={20} /> <Calendar className="text-blue-600" size={20} />
<span className="text-sm text-gray-600">Sana</span> <span className="text-sm text-gray-600">Sana</span>
</div> </div>
<p className="font-semibold text-gray-900"> <p className="font-semibold text-gray-900">
{formatDate.format(item.date, "DD-MM-YYYY")} {formatDate.format(item., "DD-MM-YYYY")}
</p> </p>
</div> </div> */}
<div className="bg-white p-4 rounded-lg shadow-sm"> <div className="bg-white p-4 rounded-lg shadow-sm">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<User className="text-green-600" size={20} /> <User className="text-green-600" size={20} />
<span className="text-sm text-gray-600">Xaridor ismi</span> <span className="text-sm text-gray-600">Xaridor ismi</span>
</div> </div>
<p className="font-semibold text-gray-900">{item.buyerName}</p> <p className="font-semibold text-gray-900">
{item.employee_name}
</p>
</div> </div>
<div className="bg-white p-4 rounded-lg shadow-sm"> <div className="bg-white p-4 rounded-lg shadow-sm">
@@ -117,7 +73,9 @@ export function DetailViewPage() {
Farmasevtikaga tegishli Farmasevtikaga tegishli
</span> </span>
</div> </div>
<p className="font-semibold text-gray-900">{item.pharmacy}</p> <p className="font-semibold text-gray-900">
{item.factory.name}
</p>
</div> </div>
</div> </div>
@@ -143,22 +101,28 @@ export function DetailViewPage() {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
{item.medicines.map((medicine) => ( {item.order_items.map((medicine) => {
const pro =
product &&
product.find((e) => medicine.product === e.id);
return (
<tr key={medicine.id} className="hover:bg-gray-50"> <tr key={medicine.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm text-gray-900 font-medium"> <td className="px-4 py-3 text-sm text-gray-900 font-medium">
{medicine.name} {pro && pro.name}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-700 text-right"> <td className="px-4 py-3 text-sm text-gray-700 text-right">
{formatPrice(medicine.price)} {pro && formatPrice(pro.price)}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-700 text-right"> <td className="px-4 py-3 text-sm text-gray-700 text-right">
{medicine.quantity} {medicine.quantity}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900 font-semibold text-right"> <td className="px-4 py-3 text-sm text-gray-900 font-semibold text-right">
{formatPrice(medicine.total)} {formatPrice(medicine.total_price)}
</td> </td>
</tr> </tr>
))} );
})}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -170,22 +134,24 @@ export function DetailViewPage() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-gray-700">Jami summa:</span> <span className="text-gray-700">Jami summa:</span>
<span className="text-xl font-bold text-gray-900"> <span className="text-xl font-bold text-gray-900">
{formatPrice(item.totalAmount)} {formatPrice(item.total_price)}
</span> </span>
</div> </div>
<div className="flex justify-between items-center pt-3 border-t-2 border-gray-200"> <div className="flex justify-between items-center pt-3 border-t-2 border-gray-200">
<span className="text-gray-700"> <span className="text-gray-700">
{"To'langan"} ({item.paymentPercentage}%): {"To'langan"} ({item.advance}%):
</span> </span>
<span className="text-2xl font-bold text-green-600"> <span className="text-2xl font-bold text-green-600">
{formatPrice(item.paymentAmount)} {formatPrice(item.paid_price)}
</span> </span>
</div> </div>
{item.paymentPercentage < 100 && ( {item.advance < 100 && (
<div className="flex justify-between items-center text-sm pt-2"> <div className="flex justify-between items-center text-sm pt-2">
<span className="text-gray-600">Qoldiq:</span> <span className="text-gray-600">Qoldiq:</span>
<span className="font-semibold text-red-600"> <span className="font-semibold text-red-600">
{formatPrice(item.totalAmount - item.paymentAmount)} {formatPrice(
Number(item.total_price) - Number(item.paid_price),
)}
</span> </span>
</div> </div>
)} )}
@@ -193,6 +159,13 @@ export function DetailViewPage() {
</div> </div>
</div> </div>
</div> </div>
<Button
onClick={handleDownloadPDF}
className="w-full h-14 mt-5 text-md bg-green-600 hover:bg-green-700 text-white"
size="lg"
>
Yuklab olish
</Button>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );

View File

@@ -1,16 +1,48 @@
"use client"; "use client";
import { order_api } from "@/features/specification/lib/api";
import { formatPrice } from "@/shared/lib/formatPrice"; import { formatPrice } from "@/shared/lib/formatPrice";
import AddedButton from "@/shared/ui/added-button"; import AddedButton from "@/shared/ui/added-button";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { DashboardLayout } from "@/widgets/dashboard-layout/ui"; import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
import { useQuery } from "@tanstack/react-query";
import { Eye } from "lucide-react"; import { Eye } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { SAMPLE_HISTORY } from "../lib/mock";
export function HistoryListPage() { export function HistoryListPage() {
const router = useNavigate(); const router = useNavigate();
const { data, isLoading, isError } = useQuery({
queryKey: ["order_list"],
queryFn: () => order_api.order_list(),
select(data) {
return data.data.data;
},
});
if (isLoading) {
return (
<DashboardLayout>
<div className="min-h-screen flex justify-center items-center">
<p className="text-gray-500 text-lg">Yuklanmoqda...</p>
</div>
</DashboardLayout>
);
}
if (isError) {
return (
<DashboardLayout>
<div className="min-h-screen flex flex-col justify-center items-center">
<p className="text-red-600 text-lg mb-2">Xatolik yuz berdi</p>
<Button onClick={() => window.location.reload()}>
Qayta yuklash
</Button>
</div>
</DashboardLayout>
);
}
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
@@ -29,8 +61,8 @@ export function HistoryListPage() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{SAMPLE_HISTORY.length > 0 ? ( {data && data.length > 0 ? (
SAMPLE_HISTORY.map((item) => ( data.map((item) => (
<div <div
key={item.id} key={item.id}
className="bg-white border rounded-xl p-5 shadow-sm hover:shadow-md transition" className="bg-white border rounded-xl p-5 shadow-sm hover:shadow-md transition"
@@ -40,27 +72,24 @@ export function HistoryListPage() {
<p className="text-lg font-semibold text-gray-900"> <p className="text-lg font-semibold text-gray-900">
Buyurtma {item.id} Buyurtma {item.id}
</p> </p>
<p className="text-sm text-gray-500 mt-1">
Sana: {item.date}
</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<div> <div>
<p className="text-gray-500 text-sm">Mijoz</p> <p className="text-gray-500 text-sm">Mijoz</p>
<p className="font-medium">{item.buyerName}</p> <p className="font-medium">{item.employee_name}</p>
</div> </div>
<div> <div>
<p className="text-gray-500 text-sm">Farmasevtika</p> <p className="text-gray-500 text-sm">Farmasevtika</p>
<p className="font-medium">{item.pharmacy}</p> <p className="font-medium">{item.factory.name}</p>
</div> </div>
<div> <div>
<p className="text-gray-500 text-sm">Hisoblangan narxi</p> <p className="text-gray-500 text-sm">Hisoblangan narxi</p>
<p className="font-medium"> <p className="font-medium">
{formatPrice(item.totalAmount)} {formatPrice(item.total_price)}
</p> </p>
</div> </div>
@@ -68,7 +97,7 @@ export function HistoryListPage() {
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
{"To'langan foizi"} {"To'langan foizi"}
</p> </p>
<p className="font-medium">{item.paymentPercentage}%</p> <p className="font-medium">{item.advance}%</p>
</div> </div>
<div> <div>
@@ -76,7 +105,7 @@ export function HistoryListPage() {
{"To'langan narxi"} {"To'langan narxi"}
</p> </p>
<p className="font-medium"> <p className="font-medium">
{formatPrice(item.paymentAmount)} {formatPrice(item.paid_price)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,26 +1,21 @@
"use client"; "use client";
import type { ProductListData } from "@/features/specification/lib/data";
import { LanguageRoutes } from "@/shared/config/i18n/types"; import { LanguageRoutes } from "@/shared/config/i18n/types";
import { formatPrice } from "@/shared/lib/formatPrice"; import { formatPrice } from "@/shared/lib/formatPrice";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
interface Medicine { type MedicineItem = ProductListData & { quantity: number };
id: number;
name: string;
regularPrice: number;
specialPrice: number;
quantity: number;
}
interface MedicineRowProps { interface MedicineRowProps {
medicine: Medicine; medicine: MedicineItem;
onQuantityChange: (id: number, quantity: number) => void; onQuantityChange: (id: number, quantity: number) => void;
} }
export function MedicineRow({ medicine, onQuantityChange }: MedicineRowProps) { export function MedicineRow({ medicine, onQuantityChange }: MedicineRowProps) {
const { locale } = useParams(); const { locale } = useParams();
const total = medicine.regularPrice * medicine.quantity; const total = Number(medicine.price) * medicine.quantity;
const handleQuantityChange = (value: string) => { const handleQuantityChange = (value: string) => {
const numValue = parseInt(value, 10) || 0; const numValue = parseInt(value, 10) || 0;
@@ -33,8 +28,7 @@ export function MedicineRow({ medicine, onQuantityChange }: MedicineRowProps) {
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-foreground">{medicine.name}</p> <p className="font-medium text-foreground">{medicine.name}</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Narxi:{" "} Narxi: {formatPrice(medicine.price, locale as LanguageRoutes, true)}
{formatPrice(medicine.regularPrice, locale as LanguageRoutes, true)}
</p> </p>
</div> </div>

View File

@@ -1,5 +1,10 @@
"use client"; "use client";
import { order_api } from "@/features/specification/lib/api";
import type {
OrderCreateReq,
ProductListData,
} from "@/features/specification/lib/data";
import type { LanguageRoutes } from "@/shared/config/i18n/types"; import type { LanguageRoutes } from "@/shared/config/i18n/types";
import { formatPrice } from "@/shared/lib/formatPrice"; import { formatPrice } from "@/shared/lib/formatPrice";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
@@ -13,40 +18,100 @@ import {
SelectValue, SelectValue,
} from "@/shared/ui/select"; } from "@/shared/ui/select";
import { DashboardLayout } from "@/widgets/dashboard-layout/ui"; import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { MedicineRow } from "./MedicineRow"; import { MedicineRow } from "./MedicineRow";
import { PaymentPercentage } from "./PaymentPercentageProps"; import { PaymentPercentage } from "./PaymentPercentageProps";
// Sample medicines data type MedicineItem = ProductListData & { quantity: number };
const MEDICINES_DATA = [
{ id: 1, name: "Aspirin", regularPrice: 5000, specialPrice: 3500 },
{ id: 2, name: "Paracetamol", regularPrice: 8000, specialPrice: 5500 },
{ id: 3, name: "Ibuprofen", regularPrice: 10000, specialPrice: 7000 },
{ id: 4, name: "Amoxicillin", regularPrice: 15000, specialPrice: 10500 },
{ id: 5, name: "Metformin", regularPrice: 20000, specialPrice: 14000 },
{ id: 6, name: "Lisinopril", regularPrice: 12000, specialPrice: 8500 },
];
const PHARMACIES = [
"Farmasevtika #1",
"Farmasevtika #2",
"Farmasevtika #3",
"Farmasevtika #4",
];
interface MedicineItem {
id: number;
name: string;
regularPrice: number;
specialPrice: number;
quantity: number;
}
export function SpecificationPage() { export function SpecificationPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: product } = useQuery({
queryKey: ["product_list"],
queryFn: () => order_api.product_list(),
select(data) {
return data.data.data;
},
});
const { data } = useQuery({
queryKey: ["factory_list"],
queryFn: () => order_api.factory_list(),
select(data) {
return data.data.data;
},
});
// const { data: pharmacy } = useQuery({
// queryKey: ["pharmacy_list"],
// queryFn: () => pharmacy_api.list(),
// select(data) {
// return data.data.data;
// },
// });
const { mutate, isPending } = useMutation({
mutationFn: (body: OrderCreateReq) => order_api.order_create(body),
onSuccess: () => {
navigate("/specification");
queryClient.refetchQueries({ queryKey: ["order_list"] });
},
onError: (error: AxiosError) => {
const data = error.response?.data as { message?: string };
const errorData = error.response?.data as {
messages?: {
token_class: string;
token_type: string;
message: string;
}[];
};
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
const message =
Array.isArray(errorName.data?.name) && errorName.data.name.length
? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
},
});
const [medicines, setMedicines] = useState<MedicineItem[]>( const [medicines, setMedicines] = useState<MedicineItem[]>(
MEDICINES_DATA.map((m) => ({ ...m, quantity: 0 })), product?.map((m) => ({ ...m, quantity: 0 })) || [],
); );
const [selectedPharmacy, setSelectedPharmacy] = useState(PHARMACIES[0]);
const [selectedPharmacy, setSelectedPharmacy] = useState(
data ? data[0].id : "",
);
// const [selectedPharm, setSelectedPharm] = useState(
// pharmacy ? pharmacy[0].id : "",
// );
useEffect(() => {
if (product) {
setMedicines(product?.map((m) => ({ ...m, quantity: 0 })));
}
if (data) {
setSelectedPharmacy(data[0].id);
}
// if (pharmacy) {
// setSelectedPharm(pharmacy[0].id);
// }
}, [product, data]);
const [buyerName, setBuyerName] = useState(""); const [buyerName, setBuyerName] = useState("");
const [paymentPercentage, setPaymentPercentage] = useState(100); const [paymentPercentage, setPaymentPercentage] = useState(100);
@@ -58,8 +123,8 @@ export function SpecificationPage() {
); );
}; };
const getPrice = (medicine: MedicineItem) => { const getPrice = (medicine: ProductListData) => {
return medicine.regularPrice; return Number(medicine.price);
}; };
const calculateTotal = () => { const calculateTotal = () => {
@@ -80,16 +145,21 @@ export function SpecificationPage() {
pharmacy: selectedPharmacy, pharmacy: selectedPharmacy,
paymentPercentage, paymentPercentage,
medicines: selectedMedicines.map((m) => ({ medicines: selectedMedicines.map((m) => ({
id: m.id, product: m.id,
name: m.name,
price: getPrice(m),
quantity: m.quantity, quantity: m.quantity,
total: getPrice(m) * m.quantity, total_price: String(Number(m.price) * m.quantity),
})), })),
totalAmount: calculateTotal(), totalAmount: calculateTotal(),
paymentAmount: calculatePaymentAmount(), paymentAmount: calculatePaymentAmount(),
}; };
console.log("PDF Data:", data); mutate({
employee_name: data.buyerName,
factory_id: Number(data.pharmacy),
items: data.medicines,
paid_price: String(data.paymentAmount),
total_price: String(data.totalAmount),
advance: data.paymentPercentage,
});
}; };
const hasSelectedMedicines = medicines.some((m) => m.quantity > 0); const hasSelectedMedicines = medicines.some((m) => m.quantity > 0);
@@ -128,22 +198,49 @@ export function SpecificationPage() {
<Card className="p-3 shadow-sm gap-2"> <Card className="p-3 shadow-sm gap-2">
<h3 className="font-semibold text-foreground">Farmasevtika</h3> <h3 className="font-semibold text-foreground">Farmasevtika</h3>
<Select <Select
value={selectedPharmacy} value={String(selectedPharmacy)}
onValueChange={setSelectedPharmacy} onValueChange={setSelectedPharmacy}
> >
<SelectTrigger className="w-full h-12"> <SelectTrigger className="w-full h-12">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{PHARMACIES.map((pharmacy) => ( {data &&
<SelectItem key={pharmacy} value={pharmacy}> data.map((pharmacy) => (
{pharmacy} <SelectItem
key={pharmacy.id}
value={String(pharmacy.id)}
>
{pharmacy.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</Card> </Card>
{/* <Card className="p-3 shadow-sm gap-2">
<h3 className="font-semibold text-foreground">Dorixa</h3>
<Select
value={String(selectedPharm)}
onValueChange={setSelectedPharm}
>
<SelectTrigger className="w-full h-12">
<SelectValue />
</SelectTrigger>
<SelectContent>
{pharmacy &&
pharmacy.map((pharmacy) => (
<SelectItem
key={pharmacy.id}
value={String(pharmacy.id)}
>
{pharmacy.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Card> */}
{/* Buyer name */} {/* Buyer name */}
<Card className="p-3 shadow-sm gap-2"> <Card className="p-3 shadow-sm gap-2">
<h3 className="font-semibold text-foreground">Xaridor Nomi</h3> <h3 className="font-semibold text-foreground">Xaridor Nomi</h3>
@@ -204,7 +301,11 @@ export function SpecificationPage() {
className="w-full h-14 text-md bg-green-600 hover:bg-green-700 text-white" className="w-full h-14 text-md bg-green-600 hover:bg-green-700 text-white"
size="lg" size="lg"
> >
PDF ga yuklab olish {isPending ? (
<Loader2 className="animate-spin" />
) : (
"Saqlash va yuklab olish"
)}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -1,9 +1,25 @@
import type { TourItem } from "@/features/tour-plan/lib/types";
import httpClient from "@/shared/config/api/httpClient"; import httpClient from "@/shared/config/api/httpClient";
import { TOUR_PLAN } from "@/shared/config/api/URLs"; import { TOUR_PLAN } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const tour_plan_api = { export const tour_plan_api = {
async list() { async list(): Promise<AxiosResponse<TourItem>> {
const res = await httpClient.get(TOUR_PLAN); const res = await httpClient.get(`${TOUR_PLAN}list/`);
return res;
},
async send_location({
id,
body,
}: {
body: {
longitude: number;
latitude: number;
};
id: number;
}): Promise<AxiosResponse<TourItem>> {
const res = await httpClient.patch(`${TOUR_PLAN}${id}/update/`, body);
return res; return res;
}, },
}; };

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import type { TourItem } from "@/features/tour-plan/lib/types"; import type { TourItemData } from "@/features/tour-plan/lib/types";
import formatDate from "@/shared/lib/formatDate"; import formatDate from "@/shared/lib/formatDate";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { Send } from "lucide-react"; import { Send } from "lucide-react";
@@ -9,9 +9,9 @@ export const getColumns = ({
sendLocation, sendLocation,
canSend, canSend,
}: { }: {
sendLocation: (tour: TourItem) => void; sendLocation: (tour: TourItemData) => void;
canSend: (date: string) => boolean; canSend: (date: string) => boolean;
}): ColumnDef<TourItem>[] => [ }): ColumnDef<TourItemData>[] => [
{ {
accessorKey: "date", accessorKey: "date",
header: "Sana", header: "Sana",
@@ -24,6 +24,9 @@ export const getColumns = ({
{ {
accessorKey: "district", accessorKey: "district",
header: "Tuman", header: "Tuman",
cell: ({ row }) => (
<div className="text-center">{row.original.place_name}</div>
),
}, },
{ {
id: "actions", id: "actions",
@@ -35,9 +38,13 @@ export const getColumns = ({
<div className="flex gap-3 items-center justify-center"> <div className="flex gap-3 items-center justify-center">
<Send <Send
className={`cursor-pointer ${ className={`cursor-pointer ${
canSend(tour.date) ? "text-green-600" : "text-gray-400" canSend(tour.date) && !tour.location_send
? "text-green-600"
: "text-gray-400"
}`} }`}
onClick={() => canSend(tour.date) && sendLocation(tour)} onClick={() =>
canSend(tour.date) && !tour.location_send && sendLocation(tour)
}
/> />
</div> </div>
); );

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import type { TourItem } from "@/features/tour-plan/lib/types"; import type { TourItemData } from "@/features/tour-plan/lib/types";
import { import {
Table, Table,
TableBody, TableBody,
@@ -16,12 +16,11 @@ import {
type ColumnDef, type ColumnDef,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
interface DataTableProps<TourItem> { interface DataTableProps<TourItemData> {
columns: ColumnDef<TourItem>[]; columns: ColumnDef<TourItemData>[];
data: TourItem[]; data: TourItemData[];
} }
export function DataTable({ columns, data }: DataTableProps<TourItemData>) {
export function DataTable({ columns, data }: DataTableProps<TourItem>) {
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,

View File

@@ -1,11 +1,16 @@
export interface TourItem { export interface TourItemData {
id: number; id: number;
place_name: string;
longitude: number;
latitude: number;
location_send: boolean;
created_at: string;
date: string; date: string;
district: string;
} }
export const mockTourData: TourItem[] = [ export interface TourItem {
{ id: 1, date: "2025-11-01", district: "Yunusobod tumani" }, status_code: number;
{ id: 2, date: "2025-11-15", district: "Mirzo Ulug'bek tumani" }, status: string;
{ id: 3, date: "2025-11-20", district: "Chilonzor tumani" }, message: string;
]; data: TourItemData[];
}

View File

@@ -1,7 +1,8 @@
"use client"; "use client";
import { DataTable } from "@/features/doctor/lib/data-table"; import { tour_plan_api } from "@/features/tour-plan/lib/api";
import type { TourItem } from "@/features/tour-plan/lib/types"; import { DataTable } from "@/features/tour-plan/lib/data-table";
import type { TourItemData } from "@/features/tour-plan/lib/types";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -11,42 +12,89 @@ import {
SelectValue, SelectValue,
} from "@/shared/ui/select"; } from "@/shared/ui/select";
import { DashboardLayout } from "@/widgets/dashboard-layout/ui"; import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2 } from "lucide-react"; // 🔥 spinner
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner";
import { getColumns } from "../lib/column"; import { getColumns } from "../lib/column";
const mockTourData: TourItem[] = [
{
id: 1,
date: "2025-11-01",
district: "Yunusobod tumani",
},
{
id: 2,
date: "2025-11-15",
district: "Mirzo Ulug'bek tumani",
},
{
id: 3,
date: "2025-11-20",
district: "Chilonzor tumani",
},
];
export default function TourPlan() { export default function TourPlan() {
const currentYear = new Date().getFullYear().toString(); const currentYear = new Date().getFullYear().toString();
const currentMonth = (new Date().getMonth() + 1).toString().padStart(2, "0"); const currentMonth = (new Date().getMonth() + 1).toString().padStart(2, "0");
const queryClient = useQueryClient();
const { data, isLoading, isError } = useQuery({
queryKey: ["tour_plan_list"],
queryFn: () => tour_plan_api.list(),
select(data) {
return data.data.data;
},
});
const { mutate } = useMutation({
mutationFn: ({
id,
body,
}: {
body: {
longitude: number;
latitude: number;
};
id: number;
}) => tour_plan_api.send_location({ body, id }),
onSuccess: () => {
toast.success("Lokatsiya jo'natildi");
queryClient.refetchQueries({ queryKey: ["tour_plan_list"] });
},
onError: (error: AxiosError) => {
const data = error.response?.data as { message?: string };
const errorData = error.response?.data as {
messages?: {
token_class: string;
token_type: string;
message: string;
}[];
};
const errorName = error.response?.data as {
data?: {
name: string[];
};
};
const message =
Array.isArray(errorName.data?.name) && errorName.data.name.length
? errorName.data.name[0]
: data?.message ||
(Array.isArray(errorData?.messages) && errorData.messages.length
? errorData.messages[0].message
: undefined) ||
"Xatolik yuz berdi";
toast.error(message);
},
});
const [year, setYear] = useState(currentYear); const [year, setYear] = useState(currentYear);
const [month, setMonth] = useState(currentMonth); const [month, setMonth] = useState(currentMonth);
const sendLocation = () => {
const sendLocation = (tour: TourItemData) => {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { (pos) => {
console.log("📌 Lokatsiya olindi:"); const latitude = pos.coords.latitude;
console.log("Latitude:", pos.coords.latitude); const longitude = pos.coords.longitude;
console.log("Longitude:", pos.coords.longitude);
mutate({
id: tour.id,
body: {
latitude,
longitude,
},
});
}, },
(err) => { (err) => {
console.error("Lokatsiya olishda xatolik:", err); console.error("Lokatsiya olishda xatolik:", err);
toast.error("Lokatsiya olishga ruxsat berilmadi!");
}, },
{ {
enableHighAccuracy: true, enableHighAccuracy: true,
@@ -57,17 +105,25 @@ export default function TourPlan() {
}; };
const canSend = (date: string) => { const canSend = (date: string) => {
return new Date(date) >= new Date(); const d = new Date(date);
const today = new Date();
d.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
return d.getTime() >= today.getTime();
}; };
const columnsProps = getColumns({ const columnsProps = getColumns({
sendLocation: sendLocation, sendLocation,
canSend: canSend, canSend,
}); });
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
return mockTourData.filter((item) => { if (!data) return [];
const d = new Date(item.date);
return data.filter((item) => {
const d = new Date(item.date + "T00:00:00");
const itemYear = d.getFullYear().toString(); const itemYear = d.getFullYear().toString();
const itemMonth = (d.getMonth() + 1).toString().padStart(2, "0"); const itemMonth = (d.getMonth() + 1).toString().padStart(2, "0");
@@ -77,18 +133,19 @@ export default function TourPlan() {
return yearMatch && monthMatch; return yearMatch && monthMatch;
}); });
}, [year, month]); }, [data, year, month]);
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-3xl font-bold">Tur Plan</h1> <h1 className="text-3xl font-bold">Tur Plan</h1>
{/* 🔹 FILTER UI */} {/* FILTERS */}
<div className="flex gap-2 justify-end items-end"> <div className="flex gap-2 justify-end items-end">
{/* Year */}
<Select onValueChange={(e) => setYear(e)} value={year}> <Select onValueChange={(e) => setYear(e)} value={year}>
<SelectTrigger className="w-fit !h-10"> <SelectTrigger className="w-fit h-10">
<SelectValue /> <SelectValue placeholder="Yil" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -101,9 +158,10 @@ export default function TourPlan() {
</SelectContent> </SelectContent>
</Select> </Select>
{/* Month */}
<Select onValueChange={(e) => setMonth(e)} value={month}> <Select onValueChange={(e) => setMonth(e)} value={month}>
<SelectTrigger className="w-fit !h-10"> <SelectTrigger className="w-fit h-10">
<SelectValue /> <SelectValue placeholder="Oy" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -124,9 +182,26 @@ export default function TourPlan() {
</Select> </Select>
</div> </div>
{/* LOADING UI */}
{isLoading && (
<div className="w-full flex justify-center py-10">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
)}
{/* ERROR UI */}
{isError && (
<div className="w-full flex justify-center py-10 text-red-500 font-semibold">
Ma'lumotlarni yuklashda xatolik yuz berdi!
</div>
)}
{/* TABLE */}
{!isLoading && !isError && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<DataTable columns={columnsProps} data={filteredData} /> <DataTable columns={columnsProps} data={filteredData} />
</div> </div>
)}
</div> </div>
</DashboardLayout> </DashboardLayout>
); );

1
src/global.d.ts vendored
View File

@@ -8,6 +8,7 @@ interface Window {
last_name?: string; last_name?: string;
}; };
}; };
sendData?: (data: string) => void;
}; };
}; };
} }

View File

@@ -1,7 +1,12 @@
import { SpecificationPage } from "@/features/specification/ui/Specification"; import { SpecificationPage } from "@/features/specification/ui/Specification";
import TokenLayout from "@/token-layaout";
const SpecificationAdded = () => { const SpecificationAdded = () => {
return <SpecificationPage />; return (
<TokenLayout>
<SpecificationPage />
</TokenLayout>
);
}; };
export default SpecificationAdded; export default SpecificationAdded;

View File

@@ -1,7 +1,12 @@
import { DetailViewPage } from "@/features/specification/ui/DetailViewPage"; import { DetailViewPage } from "@/features/specification/ui/DetailViewPage";
import TokenLayout from "@/token-layaout";
const SpecificationDetail = () => { const SpecificationDetail = () => {
return <DetailViewPage />; return (
<TokenLayout>
<DetailViewPage />
</TokenLayout>
);
}; };
export default SpecificationDetail; export default SpecificationDetail;

View File

@@ -1,5 +1,10 @@
import { HistoryListPage } from "@/features/specification/ui/HistoryListPage"; import { HistoryListPage } from "@/features/specification/ui/HistoryListPage";
import TokenLayout from "@/token-layaout";
export default function Specification() { export default function Specification() {
return <HistoryListPage />; return (
<TokenLayout>
<HistoryListPage />
</TokenLayout>
);
} }

View File

@@ -1,7 +1,12 @@
import TourPlan from "@/features/tour-plan/ui/TourPlan"; import TourPlan from "@/features/tour-plan/ui/TourPlan";
import TokenLayout from "@/token-layaout";
export const TourPlanPage = () => { export const TourPlanPage = () => {
return <TourPlan />; return (
<TokenLayout>
<TourPlan />
</TokenLayout>
);
}; };
export default TourPlanPage; export default TourPlanPage;

View File

@@ -1,5 +1,10 @@
import PlanTour from "@/features/plan-tour/ui/PlanTour"; import PlanTour from "@/features/plan-tour/ui/PlanTour";
import TokenLayout from "@/token-layaout";
export const TypePlan = () => { export const TypePlan = () => {
return <PlanTour />; return (
<TokenLayout>
<PlanTour />
</TokenLayout>
);
}; };

View File

@@ -10,18 +10,24 @@ const OBJECT = "/api/v1/shared/place/";
const DOCTOR = "/api/v1/shared/doctor/"; const DOCTOR = "/api/v1/shared/doctor/";
const PHARMACY = "/api/v1/shared/pharmacy/"; const PHARMACY = "/api/v1/shared/pharmacy/";
const LOCATION = "/api/v1/shared/location/"; const LOCATION = "/api/v1/shared/location/";
const TOUR_PLAN = "/api/v1/shared/tour_plan/list"; const TOUR_PLAN = "/api/v1/shared/tour_plan/";
const FACTORY = "/api/v1/shared/factory/list/";
const PRODUCT = "/api/v1/orders/product/list/";
const ORDER = "/api/v1/orders/order/";
export { export {
BASE_URL, BASE_URL,
CREATE_USER, CREATE_USER,
DISCTRICT, DISCTRICT,
DOCTOR, DOCTOR,
FACTORY,
LOCATION, LOCATION,
LOGIN_USER, LOGIN_USER,
OBJECT, OBJECT,
ORDER,
PHARMACY, PHARMACY,
PLANS, PLANS,
PRODUCT,
REGIONS, REGIONS,
TOUR_PLAN, TOUR_PLAN,
}; };