From 38ef10da4d63585d8f9b45c8452778afca54ee34 Mon Sep 17 00:00:00 2001 From: Mohammad Khyata Date: Tue, 7 Apr 2026 14:45:29 +0300 Subject: [PATCH] updates --- .../(authenticated)/sales/job-cards/page.tsx | 4 +- apps/dashboard/modules/auth/login-form.tsx | 6 +- .../job-cards/job-card-check-in-dialog.tsx | 159 +++++++++++ .../job-cards/job-card-delivery-dialog.tsx | 139 ++++++++++ .../job-cards/job-card-general-info.tsx | 3 +- .../job-cards/job-card-status-stepper.tsx | 38 ++- .../shared/components/form-dialog.tsx | 6 +- packages/api/open-api/schema.json | 249 ++++++++++++++++++ packages/api/postman/collection.json | 220 +++++++++++++++- packages/api/src/clients/job-cards.ts | 12 +- packages/api/types/index.ts | 183 +++++++++++++ 11 files changed, 1006 insertions(+), 13 deletions(-) create mode 100644 apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx create mode 100644 apps/dashboard/modules/job-cards/job-card-delivery-dialog.tsx diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/page.tsx index f2cd890..ce3b4d8 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/page.tsx @@ -58,11 +58,11 @@ export default function JobCardsPage() { title: "Job Cards", actions: ( - {(resourceId) => ( + {(resourceId, {close}) => ( { invalidateQuery(); close();}} /> )} diff --git a/apps/dashboard/modules/auth/login-form.tsx b/apps/dashboard/modules/auth/login-form.tsx index f13fb73..cc71dbe 100644 --- a/apps/dashboard/modules/auth/login-form.tsx +++ b/apps/dashboard/modules/auth/login-form.tsx @@ -55,9 +55,9 @@ export function LoginForm({ const { mutate, error, isPending: isSubmitting } = useMutation({ mutationFn: (values: LoginFormValues) => api.auth.login(values), - onSuccess: async (data) => { - if (data.data?.token && data.data?.user) { - await login(data.data.token, data.data.user as Parameters[1]) + onSuccess: async (data:any) => { + if (data.token && data.user) { + await login(data.token, data.user as Parameters[1]) router.push("/") } }, diff --git a/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx b/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx new file mode 100644 index 0000000..76386d6 --- /dev/null +++ b/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx @@ -0,0 +1,159 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfDateField, + RhfTimeField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { FieldGroup } from "@/shared/components/ui/field" +import { Button } from "@/shared/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/shared/components/ui/dialog" +import { DEPARTMENT_ROUTES } from "@garage/api" +import { FUEL_LEVEL_OPTIONS } from "./job-card.schema" +import { toId } from "@/shared/lib/utils" + +// ── Schema ── + +const checkInSchema = z.object({ + check_in_date: z.string().optional(), + check_in_time: z.string().optional(), + km_in: z.string().optional(), + fuel_level: z.string().optional(), + start_date: z.string().optional(), + start_time: z.string().optional(), + department: z.object({ value: z.any(), label: z.string() }).nullable().optional(), + delivery_date: z.string().optional(), + delivery_time: z.string().optional(), +}) + +type CheckInFormValues = z.infer + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name, +}) + +const STORE_OBJECT = { storeObject: true } as const + +// ── Props ── + +type JobCardCheckInDialogProps = { + jobCardId: string + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: () => void +} + +// ── Component ── + +export function JobCardCheckInDialog({ + jobCardId, + open, + onOpenChange, + onSuccess, +}: JobCardCheckInDialogProps) { + const api = useAuthApi() + + const now = new Date() + const todayStr = now.toISOString().split("T")[0] + const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}` + + const form = useForm({ + resolver: zodResolver(checkInSchema), + defaultValues: { + check_in_date: todayStr, + check_in_time: currentTime, + km_in: "", + fuel_level: "", + start_date: "", + start_time: "", + department: null, + delivery_date: "", + delivery_time: "", + }, + }) + + const handleSubmit = async (values: CheckInFormValues) => { + try { + await api.jobCards.checkIn(jobCardId, { + check_in_date: values.check_in_date || undefined, + check_in_time: values.check_in_time || undefined, + km_in: values.km_in ? Number(values.km_in) : undefined, + fuel_level: values.fuel_level || undefined, + start_date: values.start_date || undefined, + start_time: values.start_time || undefined, + department_id: values.department ? Number(toId(values.department)) : undefined, + delivery_date: values.delivery_date || undefined, + delivery_time: values.delivery_time || undefined, + }) + toast.success("Job card checked in successfully") + form.reset() + onSuccess?.() + } catch { + toast.error("Failed to check in job card") + } + } + + return ( + + + + Check In + + + +
+ + +
+
+ + +
+
+ + +
+ api.departments.list()} + mapOption={(op:any)=> ({value: op.id, label: op.name})} + {...STORE_OBJECT} + getOptionLabel={op=>op.label} + getOptionValue={op=>op} + + /> +
+ + +
+
+ + +
+
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/job-cards/job-card-delivery-dialog.tsx b/apps/dashboard/modules/job-cards/job-card-delivery-dialog.tsx new file mode 100644 index 0000000..ec71b62 --- /dev/null +++ b/apps/dashboard/modules/job-cards/job-card-delivery-dialog.tsx @@ -0,0 +1,139 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfDateField, + RhfTimeField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { FieldGroup } from "@/shared/components/ui/field" +import { Button } from "@/shared/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/shared/components/ui/dialog" +import { DEPARTMENT_ROUTES } from "@garage/api" +import { FUEL_LEVEL_OPTIONS } from "./job-card.schema" +import { toId } from "@/shared/lib/utils" + +// ── Schema ── + +const deliverySchema = z.object({ + check_out_date: z.string().optional(), + check_out_time: z.string().optional(), + km_out: z.string().optional(), + fuel_level: z.string().optional(), + department: z.object({ value: z.number(), label: z.string() }).nullable().optional(), +}) + +type DeliveryFormValues = z.infer + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name, +}) + +const STORE_OBJECT = { storeObject: true } as const + +// ── Props ── + +type JobCardDeliveryDialogProps = { + jobCardId: string + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: () => void +} + +// ── Component ── + +export function JobCardDeliveryDialog({ + jobCardId, + open, + onOpenChange, + onSuccess, +}: JobCardDeliveryDialogProps) { + const api = useAuthApi() + + const now = new Date() + const todayStr = now.toISOString().split("T")[0] + const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}` + + const form = useForm({ + resolver: zodResolver(deliverySchema), + defaultValues: { + check_out_date: todayStr, + check_out_time: currentTime, + km_out: "", + fuel_level: "", + department: null, + }, + }) + + const handleSubmit = async (values: DeliveryFormValues) => { + try { + await api.jobCards.delivery(jobCardId, { + check_out_date: values.check_out_date || undefined, + check_out_time: values.check_out_time || undefined, + km_out: values.km_out ? Number(values.km_out) : undefined, + fuel_level: values.fuel_level || undefined, + department_id: values.department ? Number(toId(values.department as any)) : undefined, + }) + toast.success("Job card marked as delivered successfully") + form.reset() + onSuccess?.() + } catch { + toast.error("Failed to mark job card as delivered") + } + } + + return ( + + + + Delivery + + + +
+ + +
+
+ + +
+ api.departments.list()} + mapOption={(op: any) => ({ value: op.id, label: op.name })} + {...STORE_OBJECT} + getOptionLabel={op => op.label} + getOptionValue={op => op} + + /> +
+ + +
+
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/job-cards/job-card-general-info.tsx b/apps/dashboard/modules/job-cards/job-card-general-info.tsx index dbaa260..3c95f3d 100644 --- a/apps/dashboard/modules/job-cards/job-card-general-info.tsx +++ b/apps/dashboard/modules/job-cards/job-card-general-info.tsx @@ -28,6 +28,7 @@ import { CrudShowResponse, JobCardsClient, PAYMENT_RECEIVED_ROUTES, PaymentRecei import { ResourcePage } from "@/shared/data-view/resource-page" import PaymentReceivedPage from "@/app/(authenticated)/sales/payment-received/page" import JobCardPaymentsReceived from "./job-card-payments-received" +import { formatDate } from "@/shared/utils/formatters" @@ -116,7 +117,7 @@ export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCard }) { s.value === currentStatus) + const [checkInOpen, setCheckInOpen] = useState(false) + const [deliveryOpen, setDeliveryOpen] = useState(false) + const { mutate, isPending, variables } = useMutation({ mutationFn: async (status: JobCardStatus) => { const promise = api.jobCards.changeStatus(jobCardId, { status }) @@ -60,9 +66,17 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) { }, }) - const handleClick = (status: JobCardStatus, index: number) => { + const handleClick = (status: JobCardStatus) => { if (isPending) return if (status === currentStatus) return + if (status === "check_in") { + setCheckInOpen(true) + return + } + if (status === "delivered") { + setDeliveryOpen(true) + return + } mutate(status) } @@ -82,7 +96,7 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) {