This commit is contained in:
Mohammad Khyata 2026-04-07 14:45:29 +03:00
parent c47f7c1b6a
commit 38ef10da4d
11 changed files with 1006 additions and 13 deletions

View File

@ -58,11 +58,11 @@ export default function JobCardsPage() {
title: "Job Cards", title: "Job Cards",
actions: ( actions: (
<FormDialog classNames={{ dialogContent: 'min-w-6xl' }} title="Job Card" > <FormDialog classNames={{ dialogContent: 'min-w-6xl' }} title="Job Card" >
{(resourceId) => ( {(resourceId, {close}) => (
<JobCardForm <JobCardForm
resourceId={resourceId} resourceId={resourceId}
initialData={selectedItem} initialData={selectedItem}
onSuccess={invalidateQuery} onSuccess={()=>{ invalidateQuery(); close();}}
/> />
)} )}
</FormDialog> </FormDialog>

View File

@ -55,9 +55,9 @@ export function LoginForm({
const { mutate, error, isPending: isSubmitting } = useMutation({ const { mutate, error, isPending: isSubmitting } = useMutation({
mutationFn: (values: LoginFormValues) => api.auth.login(values), mutationFn: (values: LoginFormValues) => api.auth.login(values),
onSuccess: async (data) => { onSuccess: async (data:any) => {
if (data.data?.token && data.data?.user) { if (data.token && data.user) {
await login(data.data.token, data.data.user as Parameters<typeof login>[1]) await login(data.token, data.user as Parameters<typeof login>[1])
router.push("/") router.push("/")
} }
}, },

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -28,6 +28,7 @@ import { CrudShowResponse, JobCardsClient, PAYMENT_RECEIVED_ROUTES, PaymentRecei
import { ResourcePage } from "@/shared/data-view/resource-page" import { ResourcePage } from "@/shared/data-view/resource-page"
import PaymentReceivedPage from "@/app/(authenticated)/sales/payment-received/page" import PaymentReceivedPage from "@/app/(authenticated)/sales/payment-received/page"
import JobCardPaymentsReceived from "./job-card-payments-received" import JobCardPaymentsReceived from "./job-card-payments-received"
import { formatDate } from "@/shared/utils/formatters"
@ -116,7 +117,7 @@ export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCard }) {
<InfoItem <InfoItem
icon={Calendar} icon={Calendar}
label="Check-in Date" label="Check-in Date"
value={jobCard.check_in_date} value={formatDate(jobCard.check_in_date)}
/> />
<InfoItem <InfoItem
icon={Gauge} icon={Gauge}

View File

@ -1,5 +1,6 @@
"use client" "use client"
import { useState } from "react"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { toast } from "sonner" import { toast } from "sonner"
import { cn } from "@/shared/lib/utils" import { cn } from "@/shared/lib/utils"
@ -20,6 +21,8 @@ import {
PackageCheck, PackageCheck,
CheckCircle2, CheckCircle2,
} from "lucide-react" } from "lucide-react"
import { JobCardCheckInDialog } from "./job-card-check-in-dialog"
import { JobCardDeliveryDialog } from "./job-card-delivery-dialog"
// ── Status icon mapping ── // ── Status icon mapping ──
@ -45,6 +48,9 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) {
const currentIndex = JOB_CARD_STATUSES.findIndex((s) => s.value === currentStatus) 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({ const { mutate, isPending, variables } = useMutation({
mutationFn: async (status: JobCardStatus) => { mutationFn: async (status: JobCardStatus) => {
const promise = api.jobCards.changeStatus(jobCardId, { status }) 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 (isPending) return
if (status === currentStatus) return if (status === currentStatus) return
if (status === "check_in") {
setCheckInOpen(true)
return
}
if (status === "delivered") {
setDeliveryOpen(true)
return
}
mutate(status) mutate(status)
} }
@ -82,7 +96,7 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
onClick={() => handleClick(step.value, index)} onClick={() => handleClick(step.value)}
disabled={!isClickable} disabled={!isClickable}
className={cn( className={cn(
"relative flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all", "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> </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> </TooltipProvider>
) )
} }

View File

@ -59,7 +59,9 @@ export function useFormDialog(paramKey?: string) {
} }
export default function FormDialog(props: { export default function FormDialog(props: {
children: (resourceId: string | null) => React.ReactNode children: (resourceId: string | null,
ctx: { open: (resourceId?: string) => void, close: () => void,isOpen:boolean }
) => React.ReactNode
title: string title: string
paramKey?: string paramKey?: string
classNames?: { classNames?: {
@ -85,7 +87,7 @@ export default function FormDialog(props: {
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<ScrollArea className={`max-h-[80vh] px-4 ${cn(props.classNames?.scrollArea)}`}> <ScrollArea className={`max-h-[80vh] px-4 ${cn(props.classNames?.scrollArea)}`}>
{props.children(resourceId)} {props.children(resourceId, { open, close , isOpen})}
</ScrollArea> </ScrollArea>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -24549,6 +24549,255 @@
} }
} }
}, },
"/api/job-cards/{id}/check-in": {
"post": {
"tags": [
"Job Cards"
],
"summary": "POST /api/job-cards/{id}/check-in",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"check_in_date": {
"type": "string"
},
"check_in_time": {
"type": "string"
},
"km_in": {
"type": "integer"
},
"fuel_level": {
"type": "string"
},
"start_date": {
"type": "string"
},
"start_time": {
"type": "string"
},
"department_id": {
"type": "integer"
},
"delivery_date": {
"type": "string"
},
"delivery_time": {
"type": "string"
}
}
},
"example": {
"check_in_date": "2026-04-07",
"check_in_time": "09:30",
"km_in": 50321,
"fuel_level": "half",
"start_date": "2026-04-07",
"start_time": "10:00",
"department_id": 1,
"delivery_date": "2026-04-08",
"delivery_time": "17:15"
}
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
},
"check_in_date": {
"type": "string",
"format": "date-time"
},
"check_in_time": {
"type": "string"
},
"km_in": {
"type": "integer"
},
"fuel_level": {
"type": "string"
},
"start_date": {
"type": "string",
"format": "date-time"
},
"start_time": {
"type": "string"
},
"department_id": {
"type": "integer"
},
"delivery_date": {
"type": "string",
"format": "date-time"
},
"delivery_time": {
"type": "string"
}
}
}
}
},
"example": {
"message": "Job card checked in successfully.",
"data": {
"id": 1,
"status": "check_in",
"check_in_date": "2026-04-07T00:00:00.000000Z",
"check_in_time": "09:30:00",
"km_in": 50321,
"fuel_level": "half",
"start_date": "2026-04-07T00:00:00.000000Z",
"start_time": "10:00:00",
"department_id": 1,
"delivery_date": "2026-04-08T00:00:00.000000Z",
"delivery_time": "17:15:00"
}
}
}
}
}
}
}
},
"/api/job-cards/{id}/delivery": {
"post": {
"tags": [
"Job Cards"
],
"summary": "POST /api/job-cards/{id}/delivery",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"check_out_date": {
"type": "string"
},
"check_out_time": {
"type": "string"
},
"km_out": {
"type": "integer"
},
"fuel_level": {
"type": "string"
},
"department_id": {
"type": "integer"
}
}
},
"example": {
"check_out_date": "2026-04-08",
"check_out_time": "17:15",
"km_out": 50480,
"fuel_level": "quarter",
"department_id": 1
}
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
},
"check_out_date": {
"type": "string"
},
"check_out_time": {
"type": "string"
},
"km_out": {
"type": "integer"
},
"fuel_level": {
"type": "string"
},
"department_id": {
"type": "integer"
}
}
}
}
},
"example": {
"message": "Job card marked as delivered successfully.",
"data": {
"id": 1,
"status": "delivered",
"check_out_date": "2026-04-08",
"check_out_time": "17:15:00",
"km_out": 50480,
"fuel_level": "quarter",
"department_id": 1
}
}
}
}
}
}
}
},
"/api/job-cards/{id}/add-customer-remark": { "/api/job-cards/{id}/add-customer-remark": {
"post": { "post": {
"tags": [ "tags": [

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "4fac95bb-3550-48ab-9943-212b6e3a11d8", "_postman_id": "bf46649e-48e0-49d5-aa87-1790dcbc3c24",
"name": "Reparee Collection", "name": "Reparee Collection",
"description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.", "description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
@ -8025,7 +8025,7 @@
"{{table}}" "{{table}}"
] ]
}, },
"description": "Set {{table}} to job_cards or estimates. Returns next sequence like ORD-0001 or EST-0001." "description": "Set {{table}} to any supported table with a number column. Explicit prefixes: job_cards=ORD-, estimates=EST-, invoices=INV-, purchase_orders=PO-, bills=BILL-, expenses=EXP-, tasks=TASK-, appointments=APPT-, services=SRV-, parts=PART-, expense_items=EXP-ITEM-, inspections=INS-. Returns next sequence like ORD-0001."
}, },
"response": [ "response": [
{ {
@ -16771,6 +16771,222 @@
} }
] ]
}, },
{
"name": "POST /api/job-cards/{id}/check-in",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{auth_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"check_in_date\": \"2026-04-07\",\n \"check_in_time\": \"09:30\",\n \"km_in\": 50321,\n \"fuel_level\": \"half\",\n \"start_date\": \"2026-04-07\",\n \"start_time\": \"10:00\",\n \"department_id\": 1,\n \"delivery_date\": \"2026-04-08\",\n \"delivery_time\": \"17:15\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/job-cards/{{id}}/check-in",
"host": [
"{{base_url}}"
],
"path": [
"api",
"job-cards",
"{{id}}",
"check-in"
]
},
"description": "Marks job card as `check_in` and updates check-in context. Optional fields: `check_in_date`, `check_in_time` (HH:MM or HH:MM:SS), `km_in`, `fuel_level`, `start_date`, `start_time`, `department_id`, `delivery_date`, `delivery_time` (HH:MM or HH:MM:SS). If check-in date/time is omitted, server current date/time is used."
},
"response": [
{
"name": "200 OK",
"originalRequest": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{auth_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"check_in_date\": \"2026-04-07\",\n \"check_in_time\": \"09:30\",\n \"km_in\": 50321,\n \"fuel_level\": \"half\",\n \"start_date\": \"2026-04-07\",\n \"start_time\": \"10:00\",\n \"department_id\": 1,\n \"delivery_date\": \"2026-04-08\",\n \"delivery_time\": \"17:15\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/job-cards/{{id}}/check-in",
"host": [
"{{base_url}}"
],
"path": [
"api",
"job-cards",
"{{id}}",
"check-in"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"cookie": [],
"body": "{\n \"message\": \"Job card checked in successfully.\",\n \"data\": {\n \"id\": 1,\n \"status\": \"check_in\",\n \"check_in_date\": \"2026-04-07T00:00:00.000000Z\",\n \"check_in_time\": \"09:30:00\",\n \"km_in\": 50321,\n \"fuel_level\": \"half\",\n \"start_date\": \"2026-04-07T00:00:00.000000Z\",\n \"start_time\": \"10:00:00\",\n \"department_id\": 1,\n \"delivery_date\": \"2026-04-08T00:00:00.000000Z\",\n \"delivery_time\": \"17:15:00\"\n }\n}"
}
]
},
{
"name": "POST /api/job-cards/{id}/delivery",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{auth_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"check_out_date\": \"2026-04-08\",\n \"check_out_time\": \"17:15\",\n \"km_out\": 50480,\n \"fuel_level\": \"quarter\",\n \"department_id\": 1\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/job-cards/{{id}}/delivery",
"host": [
"{{base_url}}"
],
"path": [
"api",
"job-cards",
"{{id}}",
"delivery"
]
},
"description": "Marks job card as `delivered` and updates delivery context. Optional fields: `check_out_date`, `check_out_time` (HH:MM or HH:MM:SS), `km_out`, `fuel_level`, `department_id`. If check-out date/time is omitted, server current date/time is used."
},
"response": [
{
"name": "200 OK",
"originalRequest": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{auth_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"check_out_date\": \"2026-04-08\",\n \"check_out_time\": \"17:15\",\n \"km_out\": 50480,\n \"fuel_level\": \"quarter\",\n \"department_id\": 1\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/job-cards/{{id}}/delivery",
"host": [
"{{base_url}}"
],
"path": [
"api",
"job-cards",
"{{id}}",
"delivery"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"cookie": [],
"body": "{\n \"message\": \"Job card marked as delivered successfully.\",\n \"data\": {\n \"id\": 1,\n \"status\": \"delivered\",\n \"check_out_date\": \"2026-04-08\",\n \"check_out_time\": \"17:15:00\",\n \"km_out\": 50480,\n \"fuel_level\": \"quarter\",\n \"department_id\": 1\n }\n}"
}
]
},
{ {
"name": "POST /api/job-cards/{id}/add-customer-remark", "name": "POST /api/job-cards/{id}/add-customer-remark",
"request": { "request": {

View File

@ -1,13 +1,15 @@
import { CrudClient } from "../infra/crud-client" import { CrudClient } from "../infra/crud-client"
import type { ApiClientOptions } from "../infra/client" import type { ApiClientOptions } from "../infra/client"
import type { ApiOperationResponse, ApiPath, ApiRequestBody, ApiResponse } from "../infra/types" import type { ApiOperationResponse, ApiPath, ApiRequestBody, ApiResponse } from "../infra/types"
import { ApiBaseResponse } from "../contracts/types" import { ApiBaseResponse } from "src/contracts/types"
export const JOB_CARD_ROUTES = { export const JOB_CARD_ROUTES = {
INDEX: "/api/job-cards", INDEX: "/api/job-cards",
BY_ID: "/api/job-cards/{id}", BY_ID: "/api/job-cards/{id}",
CHANGE_DATE: "/api/job-cards/{id}/change-date", CHANGE_DATE: "/api/job-cards/{id}/change-date",
CHANGE_STATUS: "/api/job-cards/{id}/change-status", CHANGE_STATUS: "/api/job-cards/{id}/change-status",
CHECK_IN: "/api/job-cards/{id}/check-in",
DELIVERY: "/api/job-cards/{id}/delivery",
ADD_CUSTOMER_REMARK: "/api/job-cards/{id}/add-customer-remark", ADD_CUSTOMER_REMARK: "/api/job-cards/{id}/add-customer-remark",
EDIT_CUSTOMER_REMARK: "/api/job-cards/{id}/edit-customer-remark", EDIT_CUSTOMER_REMARK: "/api/job-cards/{id}/edit-customer-remark",
DELETE_CUSTOMER_REMARK: "/api/job-cards/{id}/delete-customer-remark", DELETE_CUSTOMER_REMARK: "/api/job-cards/{id}/delete-customer-remark",
@ -53,6 +55,14 @@ export class JobCardsClient extends CrudClient<
return this.post(JOB_CARD_ROUTES.CHANGE_STATUS, payload, { params: { id } }) return this.post(JOB_CARD_ROUTES.CHANGE_STATUS, payload, { params: { id } })
} }
async checkIn(id: string, payload: ApiRequestBody<typeof JOB_CARD_ROUTES.CHECK_IN, "post">) {
return this.post(JOB_CARD_ROUTES.CHECK_IN, payload, { params: { id } })
}
async delivery(id: string, payload: ApiRequestBody<typeof JOB_CARD_ROUTES.DELIVERY, "post">) {
return this.post(JOB_CARD_ROUTES.DELIVERY, payload, { params: { id } })
}
async addCustomerRemark(id: string, payload: ApiRequestBody<typeof JOB_CARD_ROUTES.ADD_CUSTOMER_REMARK, "post">) { async addCustomerRemark(id: string, payload: ApiRequestBody<typeof JOB_CARD_ROUTES.ADD_CUSTOMER_REMARK, "post">) {
return this.post(JOB_CARD_ROUTES.ADD_CUSTOMER_REMARK, payload, { params: { id } }) return this.post(JOB_CARD_ROUTES.ADD_CUSTOMER_REMARK, payload, { params: { id } })
} }

View File

@ -16105,6 +16105,189 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/job-cards/{id}/check-in": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** POST /api/job-cards/{id}/check-in */
post: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
/**
* @example {
* "check_in_date": "2026-04-07",
* "check_in_time": "09:30",
* "km_in": 50321,
* "fuel_level": "half",
* "start_date": "2026-04-07",
* "start_time": "10:00",
* "department_id": 1,
* "delivery_date": "2026-04-08",
* "delivery_time": "17:15"
* }
*/
"application/json": {
check_in_date?: string;
check_in_time?: string;
km_in?: number;
fuel_level?: string;
start_date?: string;
start_time?: string;
department_id?: number;
delivery_date?: string;
delivery_time?: string;
};
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
/**
* @example {
* "message": "Job card checked in successfully.",
* "data": {
* "id": 1,
* "status": "check_in",
* "check_in_date": "2026-04-07T00:00:00.000000Z",
* "check_in_time": "09:30:00",
* "km_in": 50321,
* "fuel_level": "half",
* "start_date": "2026-04-07T00:00:00.000000Z",
* "start_time": "10:00:00",
* "department_id": 1,
* "delivery_date": "2026-04-08T00:00:00.000000Z",
* "delivery_time": "17:15:00"
* }
* }
*/
"application/json": {
message?: string;
data?: {
id?: number;
status?: string;
/** Format: date-time */
check_in_date?: string;
check_in_time?: string;
km_in?: number;
fuel_level?: string;
/** Format: date-time */
start_date?: string;
start_time?: string;
department_id?: number;
/** Format: date-time */
delivery_date?: string;
delivery_time?: string;
};
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/job-cards/{id}/delivery": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** POST /api/job-cards/{id}/delivery */
post: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
/**
* @example {
* "check_out_date": "2026-04-08",
* "check_out_time": "17:15",
* "km_out": 50480,
* "fuel_level": "quarter",
* "department_id": 1
* }
*/
"application/json": {
check_out_date?: string;
check_out_time?: string;
km_out?: number;
fuel_level?: string;
department_id?: number;
};
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
/**
* @example {
* "message": "Job card marked as delivered successfully.",
* "data": {
* "id": 1,
* "status": "delivered",
* "check_out_date": "2026-04-08",
* "check_out_time": "17:15:00",
* "km_out": 50480,
* "fuel_level": "quarter",
* "department_id": 1
* }
* }
*/
"application/json": {
message?: string;
data?: {
id?: number;
status?: string;
check_out_date?: string;
check_out_time?: string;
km_out?: number;
fuel_level?: string;
department_id?: number;
};
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/job-cards/{id}/add-customer-remark": { "/api/job-cards/{id}/add-customer-remark": {
parameters: { parameters: {
query?: never; query?: never;