updates
This commit is contained in:
@@ -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<typeof login>[1])
|
||||
onSuccess: async (data:any) => {
|
||||
if (data.token && data.user) {
|
||||
await login(data.token, data.user as Parameters<typeof login>[1])
|
||||
router.push("/")
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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<typeof checkInSchema>
|
||||
|
||||
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<CheckInFormValues>({
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="min-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">Check In</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Rhform form={form} onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfDateField name="check_in_date" label="Check-in Date" />
|
||||
<RhfTimeField name="check_in_time" label="Check-in Time" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfTextField name="km_in" label="KM In" type="number" placeholder="e.g. 50321" />
|
||||
<RhfSelectField name="fuel_level" label="Fuel Level" options={FUEL_LEVEL_OPTIONS} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfDateField name="start_date" label="Start Date" />
|
||||
<RhfTimeField name="start_time" label="Start Time" />
|
||||
</div>
|
||||
<RhfAsyncSelectField
|
||||
name="department"
|
||||
label="Department"
|
||||
placeholder="Select department"
|
||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||
listFn={() => api.departments.list()}
|
||||
mapOption={(op:any)=> ({value: op.id, label: op.name})}
|
||||
{...STORE_OBJECT}
|
||||
getOptionLabel={op=>op.label}
|
||||
getOptionValue={op=>op}
|
||||
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfDateField name="delivery_date" label="Delivery Date" />
|
||||
<RhfTimeField name="delivery_time" label="Delivery Time" />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? "Checking in..." : "Check In"}
|
||||
</Button>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</Rhform>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof deliverySchema>
|
||||
|
||||
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<DeliveryFormValues>({
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="min-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">Delivery</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Rhform form={form} onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfDateField name="check_out_date" label="Check-out Date" />
|
||||
<RhfTimeField name="check_out_time" label="Check-out Time" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfTextField name="km_out" label="KM Out" type="number" placeholder="e.g. 50480" />
|
||||
<RhfSelectField name="fuel_level" label="Fuel Level" options={FUEL_LEVEL_OPTIONS} />
|
||||
</div>
|
||||
<RhfAsyncSelectField
|
||||
name="department"
|
||||
label="Department"
|
||||
placeholder="Select department"
|
||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||
listFn={() => api.departments.list()}
|
||||
mapOption={(op: any) => ({ value: op.id, label: op.name })}
|
||||
{...STORE_OBJECT}
|
||||
getOptionLabel={op => op.label}
|
||||
getOptionValue={op => op}
|
||||
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? "Delivering..." : "Mark as Delivered"}
|
||||
</Button>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</Rhform>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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 }) {
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Check-in Date"
|
||||
value={jobCard.check_in_date}
|
||||
value={formatDate(jobCard.check_in_date)}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Gauge}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
PackageCheck,
|
||||
CheckCircle2,
|
||||
} from "lucide-react"
|
||||
import { JobCardCheckInDialog } from "./job-card-check-in-dialog"
|
||||
import { JobCardDeliveryDialog } from "./job-card-delivery-dialog"
|
||||
|
||||
// ── Status icon mapping ──
|
||||
|
||||
@@ -45,6 +48,9 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) {
|
||||
|
||||
const currentIndex = JOB_CARD_STATUSES.findIndex((s) => 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) {
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClick(step.value, index)}
|
||||
onClick={() => handleClick(step.value)}
|
||||
disabled={!isClickable}
|
||||
className={cn(
|
||||
"relative flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all",
|
||||
@@ -120,6 +134,26 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<JobCardCheckInDialog
|
||||
jobCardId={jobCardId}
|
||||
open={checkInOpen}
|
||||
onOpenChange={setCheckInOpen}
|
||||
onSuccess={() => {
|
||||
setCheckInOpen(false)
|
||||
;(jobCard as any)?.setStatus("check_in")
|
||||
}}
|
||||
/>
|
||||
|
||||
<JobCardDeliveryDialog
|
||||
jobCardId={jobCardId}
|
||||
open={deliveryOpen}
|
||||
onOpenChange={setDeliveryOpen}
|
||||
onSuccess={() => {
|
||||
setDeliveryOpen(false)
|
||||
;(jobCard as any)?.setStatus("delivered")
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user