From 479972e9a91115c866fc32c3aad7a18e72094185 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Fri, 22 May 2026 01:31:49 +0300 Subject: [PATCH] feat(workflow): split job step from location, model back-and-forth between lab and clinic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real prosthetic production isn't a one-way pipeline — the work moves between lab and clinic multiple times. After substructure is produced the lab hands it to the clinic for a fitting, the clinic approves it back to the lab, the lab builds the superstructure, hands it back for a second fitting, the clinic approves again, the lab does cila/bitim, and finally delivers it to the clinic for handover to the patient. Previously we only had a single 'advance step' action callable by the lab, which collapsed all of that into a linear forward push and didn't capture who physically had the work at any given moment. DB - New jobs.location enum (at_clinic | at_lab, default at_clinic). - Existing jobs keep working via a 'location ?? at_lab' fallback in code; no manual backfill required for the four test rows. State machine - acceptJobAction (lab): pending → in_progress, currentStep=alt_yapi_prova, location=at_lab. Skips the implicit 'olcu' production step now that accepting the job means the lab has the impression in hand. - handToClinicAction (lab, NEW): at_lab → at_clinic, step stays the same. If step is cila_bitim, status becomes 'sent' (final delivery) and finance sync fires. - approveAtClinicAction (clinic, NEW): at_clinic → at_lab, step advances to the next stage so the lab knows what to produce next. - markDeliveredAction unchanged — clinic confirms the final handoff. - advanceStepAction removed; its single forward push doesn't fit the new bidirectional flow. UI - JobActionsPanel now picks the right button from the role + status + location matrix: * Lab + pending → 'İşleme Al' * Lab + in_progress + at_lab + cila_bitim → 'Cila Bitim — Nihai Teslime Gönder' * Lab + in_progress + at_lab + other → '{stage} Provaya Gönder' * Clinic + in_progress + at_clinic → '{stage} Provası Tamam' * Clinic + sent → 'Teslim Aldım' * Both + pending → 'İptal Et' - Job detail surfaces a new 'Şu An' info row that resolves to a human-readable location ('Klinikte', 'Laboratuvarda', 'Hasta'ya teslim edildi', ...) so anyone glancing at the page can tell where the work physically is. --- .../[jobId]/components/job-actions-panel.tsx | 130 +++++++++--- src/app/(dashboard)/jobs/[jobId]/page.tsx | 10 + src/lib/appwrite/job-actions.ts | 191 ++++++++++++++---- src/lib/appwrite/job-types.ts | 7 +- src/lib/appwrite/schema.ts | 2 + 5 files changed, 270 insertions(+), 70 deletions(-) diff --git a/src/app/(dashboard)/jobs/[jobId]/components/job-actions-panel.tsx b/src/app/(dashboard)/jobs/[jobId]/components/job-actions-panel.tsx index 33170cc..b4c54ca 100644 --- a/src/app/(dashboard)/jobs/[jobId]/components/job-actions-panel.tsx +++ b/src/app/(dashboard)/jobs/[jobId]/components/job-actions-panel.tsx @@ -7,8 +7,9 @@ import { Check, CircleAlert, Loader2, - Play, PackageCheck, + Play, + Send, X, } from "lucide-react"; import { toast } from "sonner"; @@ -27,15 +28,12 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { acceptJobAction, - advanceStepAction, + approveAtClinicAction, cancelJobAction, + handToClinicAction, markDeliveredAction, } from "@/lib/appwrite/job-actions"; -import { - JOB_STEP_LABELS, - JOB_STEP_ORDER, - initialJobActionState, -} from "@/lib/appwrite/job-types"; +import { initialJobActionState } from "@/lib/appwrite/job-types"; import type { Job, TenantKind } from "@/lib/appwrite/schema"; type Side = "clinic" | "lab"; @@ -53,12 +51,29 @@ export function JobActionsPanel({ const isLab = side === "lab"; const isClinic = side === "clinic"; + const location = job.location ?? "at_lab"; + const isAtLab = location === "at_lab"; + const isAtClinic = location === "at_clinic"; return (
+ {/* Pending pickup — lab accepts */} {isLab && job.status === "pending" && } - {isLab && job.status === "in_progress" && } + + {/* Lab is producing — push to clinic for prova / final delivery */} + {isLab && job.status === "in_progress" && isAtLab && ( + + )} + + {/* Clinic finished the prova — approve and send back to lab */} + {isClinic && job.status === "in_progress" && isAtClinic && ( + + )} + + {/* Final delivery — clinic took it from the lab */} {isClinic && job.status === "sent" && } + + {/* Cancel — only while the job hasn't started yet */} {(isClinic || isLab) && job.status === "pending" && ( )} @@ -72,7 +87,7 @@ function AcceptButton({ jobId }: { jobId: string }) { useEffect(() => { if (state.ok) { - toast.success("İş işleme alındı."); + toast.success("İş işleme alındı, alt yapı üretimi başladı."); router.refresh(); } else if (state.error) { toast.error(state.error); @@ -90,14 +105,14 @@ function AcceptButton({ jobId }: { jobId: string }) { ); } -function AdvanceButton({ job }: { job: Job }) { +function HandToClinicButton({ job }: { job: Job }) { const router = useRouter(); - const [state, action, pending] = useActionState(advanceStepAction, initialJobActionState); + const [state, action, pending] = useActionState(handToClinicAction, initialJobActionState); const [open, setOpen] = useState(false); useEffect(() => { if (state.ok) { - toast.success("Aşama ilerletildi."); + toast.success("Klinik tarafına gönderildi."); setOpen(false); router.refresh(); } else if (state.error) { @@ -105,28 +120,29 @@ function AdvanceButton({ job }: { job: Job }) { } }, [state, router]); - const currentIdx = job.currentStep ? JOB_STEP_ORDER.indexOf(job.currentStep) : -1; - const isFinal = currentIdx === JOB_STEP_ORDER.length - 1; - const currentLabel = job.currentStep ? JOB_STEP_LABELS[job.currentStep] : "—"; - const nextLabel = isFinal - ? "Gönderildi olarak işaretle" - : JOB_STEP_LABELS[JOB_STEP_ORDER[currentIdx + 1]]; + const isFinal = job.currentStep === "cila_bitim"; + const stageLabel = + job.currentStep === "alt_yapi_prova" + ? "alt yapı" + : job.currentStep === "ust_yapi_prova" + ? "üst yapı" + : "cila/bitim"; return ( - {isFinal ? "İşi gönderilmiş olarak işaretle" : `${currentLabel} tamamlandı`} + {isFinal ? "Nihai teslime gönderilsin mi?" : "Kliniğe gönderilsin mi?"} {isFinal - ? "İş artık 'Gönderildi' durumuna geçecek; klinik 'Teslim Aldım' onayını verecek." - : `Sonraki aşama: ${nextLabel}. İsterseniz bir not ekleyebilirsiniz.`} + ? "Cila ve bitim tamamlandı; iş 'Gönderildi' durumuna geçer. Klinik teslim aldığında nihai onay verecek." + : `${stageLabel === "alt yapı" ? "Alt yapı" : "Üst yapı"} provası için iş klinik tarafına geçer. Klinik provayı onayladığında size geri dönecek.`}
@@ -138,7 +154,7 @@ function AdvanceButton({ job }: { job: Job }) { name="note" rows={3} maxLength={1000} - placeholder="Örn. Renk kontrolü yapıldı, hasta provası onaylandı." + placeholder="Örn. Renk A2, oklüzal kontak tamam" />
@@ -148,8 +164,70 @@ function AdvanceButton({ job }: { job: Job }) { + + + + + ); +} + +function ApproveAtClinicButton({ job }: { job: Job }) { + const router = useRouter(); + const [state, action, pending] = useActionState(approveAtClinicAction, initialJobActionState); + const [open, setOpen] = useState(false); + + useEffect(() => { + if (state.ok) { + toast.success("Prova onaylandı, lab tarafına gönderildi."); + setOpen(false); + router.refresh(); + } else if (state.error) { + toast.error(state.error); + } + }, [state, router]); + + const stageLabel = job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı"; + + return ( + + + + + + {`${stageLabel === "alt yapı" ? "Alt yapı" : "Üst yapı"} provası onaylansın mı?`} + + + Prova başarılı işaretlendiğinde iş bir sonraki aşamaya geçer ve + laboratuvara geri döner. + + +
+ +
+ +