api ulandi
This commit is contained in:
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
9
src/features/plan-tour/lib/api.ts
Normal file
9
src/features/plan-tour/lib/api.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
0
src/features/plan-tour/lib/data.ts
Normal file
0
src/features/plan-tour/lib/data.ts
Normal 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
|
||||||
|
|||||||
@@ -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 qo‘shilmoqda
|
||||||
|
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 bo‘yicha monthlyData bo‘lmasa — qo‘shamiz
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
36
src/features/specification/lib/api.ts
Normal file
36
src/features/specification/lib/api.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
72
src/features/specification/lib/data.ts
Normal file
72
src/features/specification/lib/data.ts
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
1
src/global.d.ts
vendored
@@ -8,6 +8,7 @@ interface Window {
|
|||||||
last_name?: string;
|
last_name?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
sendData?: (data: string) => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user