feat(workflow): split job step from location, model back-and-forth between lab and clinic
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.
This commit is contained in:
@@ -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 (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Pending pickup — lab accepts */}
|
||||
{isLab && job.status === "pending" && <AcceptButton jobId={job.$id} />}
|
||||
{isLab && job.status === "in_progress" && <AdvanceButton job={job} />}
|
||||
|
||||
{/* Lab is producing — push to clinic for prova / final delivery */}
|
||||
{isLab && job.status === "in_progress" && isAtLab && (
|
||||
<HandToClinicButton job={job} />
|
||||
)}
|
||||
|
||||
{/* Clinic finished the prova — approve and send back to lab */}
|
||||
{isClinic && job.status === "in_progress" && isAtClinic && (
|
||||
<ApproveAtClinicButton job={job} />
|
||||
)}
|
||||
|
||||
{/* Final delivery — clinic took it from the lab */}
|
||||
{isClinic && job.status === "sent" && <DeliverButton jobId={job.$id} />}
|
||||
|
||||
{/* Cancel — only while the job hasn't started yet */}
|
||||
{(isClinic || isLab) && job.status === "pending" && (
|
||||
<CancelButton jobId={job.$id} />
|
||||
)}
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
{isFinal ? <PackageCheck className="size-4" /> : <ArrowRight className="size-4" />}
|
||||
{isFinal ? "Gönderildi" : "Sonraki Aşama"}
|
||||
{isFinal ? <PackageCheck className="size-4" /> : <Send className="size-4" />}
|
||||
{isFinal ? "Cila Bitim — Nihai Teslime Gönder" : `${stageLabel === "alt yapı" ? "Alt Yapı" : "Üst Yapı"} Provaya Gönder`}
|
||||
</Button>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isFinal ? "İşi gönderilmiş olarak işaretle" : `${currentLabel} tamamlandı`}
|
||||
{isFinal ? "Nihai teslime gönderilsin mi?" : "Kliniğe gönderilsin mi?"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{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.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form action={action} className="grid gap-3">
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -148,8 +164,70 @@ function AdvanceButton({ job }: { job: Job }) {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending ? <Loader2 className="size-4 animate-spin" /> : <Check className="size-4" />}
|
||||
Onayla
|
||||
{pending ? <Loader2 className="size-4 animate-spin" /> : <Send className="size-4" />}
|
||||
Gönder
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Check className="size-4" />
|
||||
{stageLabel === "alt yapı" ? "Alt Yapı Provası Tamam" : "Üst Yapı Provası Tamam"}
|
||||
</Button>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{`${stageLabel === "alt yapı" ? "Alt yapı" : "Üst yapı"} provası onaylansın mı?`}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Prova başarılı işaretlendiğinde iş bir sonraki aşamaya geçer ve
|
||||
laboratuvara geri döner.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form action={action} className="grid gap-3">
|
||||
<input type="hidden" name="jobId" value={job.$id} />
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="note">Not (opsiyonel)</Label>
|
||||
<Textarea
|
||||
id="note"
|
||||
name="note"
|
||||
rows={3}
|
||||
maxLength={1000}
|
||||
placeholder="Örn. Renk uyumlu, oklüzyon tamam"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Vazgeç
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending ? <Loader2 className="size-4 animate-spin" /> : <ArrowRight className="size-4" />}
|
||||
Onayla ve gönder
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user