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:
kovakmedya
2026-05-22 01:31:49 +03:00
parent cdb2a15643
commit 479972e9a9
5 changed files with 270 additions and 70 deletions
@@ -7,8 +7,9 @@ import {
Check, Check,
CircleAlert, CircleAlert,
Loader2, Loader2,
Play,
PackageCheck, PackageCheck,
Play,
Send,
X, X,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -27,15 +28,12 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
acceptJobAction, acceptJobAction,
advanceStepAction, approveAtClinicAction,
cancelJobAction, cancelJobAction,
handToClinicAction,
markDeliveredAction, markDeliveredAction,
} from "@/lib/appwrite/job-actions"; } from "@/lib/appwrite/job-actions";
import { import { initialJobActionState } from "@/lib/appwrite/job-types";
JOB_STEP_LABELS,
JOB_STEP_ORDER,
initialJobActionState,
} from "@/lib/appwrite/job-types";
import type { Job, TenantKind } from "@/lib/appwrite/schema"; import type { Job, TenantKind } from "@/lib/appwrite/schema";
type Side = "clinic" | "lab"; type Side = "clinic" | "lab";
@@ -53,12 +51,29 @@ export function JobActionsPanel({
const isLab = side === "lab"; const isLab = side === "lab";
const isClinic = side === "clinic"; const isClinic = side === "clinic";
const location = job.location ?? "at_lab";
const isAtLab = location === "at_lab";
const isAtClinic = location === "at_clinic";
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{/* Pending pickup — lab accepts */}
{isLab && job.status === "pending" && <AcceptButton jobId={job.$id} />} {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} />} {isClinic && job.status === "sent" && <DeliverButton jobId={job.$id} />}
{/* Cancel — only while the job hasn't started yet */}
{(isClinic || isLab) && job.status === "pending" && ( {(isClinic || isLab) && job.status === "pending" && (
<CancelButton jobId={job.$id} /> <CancelButton jobId={job.$id} />
)} )}
@@ -72,7 +87,7 @@ function AcceptButton({ jobId }: { jobId: string }) {
useEffect(() => { useEffect(() => {
if (state.ok) { if (state.ok) {
toast.success("İş işleme alındı."); toast.success("İş işleme alındı, alt yapı üretimi başladı.");
router.refresh(); router.refresh();
} else if (state.error) { } else if (state.error) {
toast.error(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 router = useRouter();
const [state, action, pending] = useActionState(advanceStepAction, initialJobActionState); const [state, action, pending] = useActionState(handToClinicAction, initialJobActionState);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (state.ok) { if (state.ok) {
toast.success("Aşama ilerletildi."); toast.success("Klinik tarafına gönderildi.");
setOpen(false); setOpen(false);
router.refresh(); router.refresh();
} else if (state.error) { } else if (state.error) {
@@ -105,28 +120,29 @@ function AdvanceButton({ job }: { job: Job }) {
} }
}, [state, router]); }, [state, router]);
const currentIdx = job.currentStep ? JOB_STEP_ORDER.indexOf(job.currentStep) : -1; const isFinal = job.currentStep === "cila_bitim";
const isFinal = currentIdx === JOB_STEP_ORDER.length - 1; const stageLabel =
const currentLabel = job.currentStep ? JOB_STEP_LABELS[job.currentStep] : "—"; job.currentStep === "alt_yapi_prova"
const nextLabel = isFinal ? "alt yapı"
? "Gönderildi olarak işaretle" : job.currentStep === "ust_yapi_prova"
: JOB_STEP_LABELS[JOB_STEP_ORDER[currentIdx + 1]]; ? "üst yapı"
: "cila/bitim";
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<Button onClick={() => setOpen(true)}> <Button onClick={() => setOpen(true)}>
{isFinal ? <PackageCheck className="size-4" /> : <ArrowRight className="size-4" />} {isFinal ? <PackageCheck className="size-4" /> : <Send className="size-4" />}
{isFinal ? "Gönderildi" : "Sonraki Aşama"} {isFinal ? "Cila Bitim — Nihai Teslime Gönder" : `${stageLabel === "alt yapı" ? "Alt Yapı" : "Üst Yapı"} Provaya Gönder`}
</Button> </Button>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{isFinal ? "İşi gönderilmiş olarak işaretle" : `${currentLabel} tamamlandı`} {isFinal ? "Nihai teslime gönderilsin mi?" : "Kliniğe gönderilsin mi?"}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{isFinal {isFinal
? "İş artık 'Gönderildi' durumuna geçecek; klinik 'Teslim Aldım' onayını verecek." ? "Cila ve bitim tamamlandı; iş 'Gönderildi' durumuna geçer. Klinik teslim aldığında nihai onay verecek."
: `Sonraki aşama: ${nextLabel}. İsterseniz bir not ekleyebilirsiniz.`} : `${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> </DialogDescription>
</DialogHeader> </DialogHeader>
<form action={action} className="grid gap-3"> <form action={action} className="grid gap-3">
@@ -138,7 +154,7 @@ function AdvanceButton({ job }: { job: Job }) {
name="note" name="note"
rows={3} rows={3}
maxLength={1000} maxLength={1000}
placeholder="Örn. Renk kontrolü yapıldı, hasta provası onaylandı." placeholder="Örn. Renk A2, oklüzal kontak tamam"
/> />
</div> </div>
<DialogFooter> <DialogFooter>
@@ -148,8 +164,70 @@ function AdvanceButton({ job }: { job: Job }) {
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" disabled={pending}> <Button type="submit" disabled={pending}>
{pending ? <Loader2 className="size-4 animate-spin" /> : <Check className="size-4" />} {pending ? <Loader2 className="size-4 animate-spin" /> : <Send className="size-4" />}
Onayla 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 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> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
+10
View File
@@ -10,6 +10,7 @@ import { listJobHistory } from "@/lib/appwrite/job-history-queries";
import { getPatient } from "@/lib/appwrite/patient-queries"; import { getPatient } from "@/lib/appwrite/patient-queries";
import { toPlain } from "@/lib/appwrite/serialize"; import { toPlain } from "@/lib/appwrite/serialize";
import { import {
JOB_LOCATION_LABELS,
JOB_STATUS_LABELS, JOB_STATUS_LABELS,
JOB_STEP_LABELS, JOB_STEP_LABELS,
JOB_STEP_ORDER, JOB_STEP_ORDER,
@@ -141,6 +142,15 @@ export default async function JobDetailPage({
<Info label="Mevcut Aşama"> <Info label="Mevcut Aşama">
{job.currentStep ? JOB_STEP_LABELS[job.currentStep] : "—"} {job.currentStep ? JOB_STEP_LABELS[job.currentStep] : "—"}
</Info> </Info>
<Info label="Şu An">
{job.status === "pending"
? "Klinikte (lab teslim alacak)"
: job.status === "delivered"
? "Hasta'ya teslim edildi"
: job.status === "cancelled"
? "İptal"
: JOB_LOCATION_LABELS[job.location ?? "at_lab"]}
</Info>
<div className="md:col-span-2"> <div className="md:col-span-2">
<p className="text-muted-foreground mb-1 text-xs font-medium uppercase tracking-wide"> <p className="text-muted-foreground mb-1 text-xs font-medium uppercase tracking-wide">
Dişler ({job.teeth?.length ?? job.memberCount}) Dişler ({job.teeth?.length ?? job.memberCount})
+148 -43
View File
@@ -297,9 +297,12 @@ export async function acceptJobAction(
try { try {
const { tablesDB } = createAdminClient(); const { tablesDB } = createAdminClient();
// Accepting the job = lab took the impression, started substructure work.
// Step jumps straight to alt_yapi_prova; location flips to at_lab.
await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, { await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
status: "in_progress", status: "in_progress",
currentStep: "olcu", currentStep: "alt_yapi_prova",
location: "at_lab",
}); });
await appendJobHistory({ job, step: "olcu", completedBy: ctx.user.id }); await appendJobHistory({ job, step: "olcu", completedBy: ctx.user.id });
void logAudit({ void logAudit({
@@ -308,12 +311,16 @@ export async function acceptJobAction(
action: "update", action: "update",
entityType: "job", entityType: "job",
entityId: jobId, entityId: jobId,
changes: { status: "in_progress", currentStep: "olcu" }, changes: {
status: "in_progress",
currentStep: "alt_yapi_prova",
location: "at_lab",
},
}); });
void createNotification({ void createNotification({
tenantId: job.clinicTenantId, tenantId: job.clinicTenantId,
jobId, jobId,
message: `${ctx.settings?.companyName ?? "Lab"} hasta ${job.patientCode} işini işleme aldı.`, message: `${ctx.settings?.companyName ?? "Lab"} hasta ${job.patientCode} işini işleme aldı, alt yapı üretiminde.`,
}); });
} catch (e) { } catch (e) {
return { ok: false, error: appwriteError(e, "Kabul edilemedi.") }; return { ok: false, error: appwriteError(e, "Kabul edilemedi.") };
@@ -325,7 +332,14 @@ export async function acceptJobAction(
return { ok: true }; return { ok: true };
} }
export async function advanceStepAction( /**
* Lab hands the work back to the clinic for the next physical step
* (prova or final delivery). The current step stays the same — only the
* location flips at_lab → at_clinic. If the lab is finishing the last
* production step (cila_bitim), that's the final delivery and the job
* status becomes "sent".
*/
export async function handToClinicAction(
_prev: JobActionState, _prev: JobActionState,
formData: FormData, formData: FormData,
): Promise<JobActionState> { ): Promise<JobActionState> {
@@ -339,7 +353,7 @@ export async function advanceStepAction(
requireRole(ctx, ["owner", "admin", "member"]); requireRole(ctx, ["owner", "admin", "member"]);
requireTenantKind(ctx, ["lab"]); requireTenantKind(ctx, ["lab"]);
} catch { } catch {
return { ok: false, error: "Sadece laboratuvar aşama ilerletebilir." }; return { ok: false, error: "Sadece laboratuvar kliniğe gönderebilir." };
} }
const job = await loadJobForTenant(jobId, ctx.tenantId); const job = await loadJobForTenant(jobId, ctx.tenantId);
@@ -347,36 +361,28 @@ export async function advanceStepAction(
return { ok: false, error: "İş bulunamadı." }; return { ok: false, error: "İş bulunamadı." };
} }
if (job.status !== "in_progress") { if (job.status !== "in_progress") {
return { ok: false, error: "Yalnızca işleme alınmış işler ilerletilebilir." }; return { ok: false, error: "Sadece işlemdeki işler kliniğe gönderilebilir." };
}
if (job.location !== "at_lab") {
return { ok: false, error: "İş zaten kliniğe gönderilmiş." };
}
if (!job.currentStep) {
return { ok: false, error: "Mevcut aşama bilinmiyor." };
} }
const currentIdx = job.currentStep ? JOB_STEP_ORDER.indexOf(job.currentStep) : -1;
if (currentIdx < 0) return { ok: false, error: "Mevcut aşama bilinmiyor." };
const nextIdx = currentIdx + 1; const isFinalStep = job.currentStep === "cila_bitim";
const isFinalStepComplete = currentIdx === JOB_STEP_ORDER.length - 1;
try { try {
const { tablesDB } = createAdminClient(); const { tablesDB } = createAdminClient();
if (isFinalStepComplete) { if (isFinalStep) {
// Final delivery — production is done, status moves to sent.
await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, { await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
status: "sent", status: "sent",
}); location: "at_clinic",
void logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "update",
entityType: "job",
entityId: jobId,
changes: { status: "sent" },
});
} else {
const nextStep = JOB_STEP_ORDER[nextIdx];
await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
currentStep: nextStep,
}); });
await appendJobHistory({ await appendJobHistory({
job, job,
step: job.currentStep!, step: "cila_bitim",
completedBy: ctx.user.id, completedBy: ctx.user.id,
note, note,
}); });
@@ -386,27 +392,43 @@ export async function advanceStepAction(
action: "update", action: "update",
entityType: "job", entityType: "job",
entityId: jobId, entityId: jobId,
changes: { currentStep: nextStep, completedStep: job.currentStep }, changes: { status: "sent", location: "at_clinic" },
});
void syncFinanceForJob({ ...job, status: "sent" });
void createNotification({
tenantId: job.clinicTenantId,
jobId,
message: `Hasta ${job.patientCode} cila/bitim tamamlandı, nihai teslime gönderildi.`,
});
} else {
// Prova için klinike geçici teslim — step aynı, location değişti.
await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
location: "at_clinic",
});
await appendJobHistory({
job,
step: job.currentStep,
completedBy: ctx.user.id,
note,
});
void logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "update",
entityType: "job",
entityId: jobId,
changes: { location: "at_clinic", handedOffStep: job.currentStep },
});
const stepLabel =
job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı";
void createNotification({
tenantId: job.clinicTenantId,
jobId,
message: `Hasta ${job.patientCode} ${stepLabel} provasına hazır, kliniğe gönderildi.`,
}); });
} }
} catch (e) { } catch (e) {
return { ok: false, error: appwriteError(e, "İlerletilemedi.") }; return { ok: false, error: appwriteError(e, "Gönderilemedi.") };
}
if (isFinalStepComplete) {
// Record completion of the last step too, then mark sent.
await appendJobHistory({
job,
step: job.currentStep!,
completedBy: ctx.user.id,
note,
});
void syncFinanceForJob({ ...job, status: "sent" });
void createNotification({
tenantId: job.clinicTenantId,
jobId,
message: `Hasta ${job.patientCode} işi gönderildi. Teslim alındığında onaylayın.`,
});
} }
revalidatePath(`/jobs/${jobId}`); revalidatePath(`/jobs/${jobId}`);
@@ -416,6 +438,89 @@ export async function advanceStepAction(
return { ok: true }; return { ok: true };
} }
/**
* Clinic confirms the prova was successful. Step advances to the next
* production stage and location flips back at_clinic → at_lab so the
* lab can pick the work back up.
*/
export async function approveAtClinicAction(
_prev: JobActionState,
formData: FormData,
): Promise<JobActionState> {
const jobId = String(formData.get("jobId") ?? "").trim();
if (!jobId) return { ok: false, error: "İş bulunamadı." };
const note = String(formData.get("note") ?? "").trim() || undefined;
let ctx;
try {
ctx = await requireTenant();
requireRole(ctx, ["owner", "admin", "member"]);
requireTenantKind(ctx, ["clinic"]);
} catch {
return { ok: false, error: "Sadece klinik provayı onaylayabilir." };
}
const job = await loadJobForTenant(jobId, ctx.tenantId);
if (!job || job.clinicTenantId !== ctx.tenantId) {
return { ok: false, error: "İş bulunamadı." };
}
if (job.status !== "in_progress") {
return { ok: false, error: "Yalnızca işlemdeki provalar onaylanabilir." };
}
if (job.location !== "at_clinic") {
return { ok: false, error: "İş şu an klinikte değil." };
}
if (!job.currentStep) {
return { ok: false, error: "Mevcut aşama bilinmiyor." };
}
const currentIdx = JOB_STEP_ORDER.indexOf(job.currentStep);
const nextStep = JOB_STEP_ORDER[currentIdx + 1];
if (!nextStep) {
return { ok: false, error: "Bu aşamadan ileri gidilemez." };
}
try {
const { tablesDB } = createAdminClient();
await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
currentStep: nextStep,
location: "at_lab",
});
await appendJobHistory({
job,
step: job.currentStep,
completedBy: ctx.user.id,
note,
});
void logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "update",
entityType: "job",
entityId: jobId,
changes: {
currentStep: nextStep,
location: "at_lab",
completedStep: job.currentStep,
},
});
const stepLabel =
job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı";
void createNotification({
tenantId: job.labTenantId,
jobId,
message: `Hasta ${job.patientCode} ${stepLabel} provası onaylandı, lab tarafına geri döndü.`,
});
} catch (e) {
return { ok: false, error: appwriteError(e, "Onaylanamadı.") };
}
revalidatePath(`/jobs/${jobId}`);
revalidatePath("/jobs/inbound");
revalidatePath("/jobs/outbound");
return { ok: true };
}
export async function markDeliveredAction( export async function markDeliveredAction(
_prev: JobActionState, _prev: JobActionState,
formData: FormData, formData: FormData,
+6 -1
View File
@@ -1,4 +1,4 @@
import type { JobStatus, JobStep, ProstheticType } from "./schema"; import type { JobLocation, JobStatus, JobStep, ProstheticType } from "./schema";
export type JobFormState = { export type JobFormState = {
ok: boolean; ok: boolean;
@@ -38,6 +38,11 @@ export const JOB_STEP_ORDER: JobStep[] = [
"cila_bitim", "cila_bitim",
]; ];
export const JOB_LOCATION_LABELS: Record<JobLocation, string> = {
at_clinic: "Klinikte",
at_lab: "Laboratuvarda",
};
export const PROSTHETIC_TYPE_LABELS: Record<ProstheticType, string> = { export const PROSTHETIC_TYPE_LABELS: Record<ProstheticType, string> = {
metal_porselen: "Metal Porselen", metal_porselen: "Metal Porselen",
zirkonyum: "Zirkonyum", zirkonyum: "Zirkonyum",
+2
View File
@@ -75,6 +75,7 @@ export interface Connection extends Row {
export type JobStatus = "pending" | "in_progress" | "sent" | "delivered" | "cancelled"; export type JobStatus = "pending" | "in_progress" | "sent" | "delivered" | "cancelled";
export type JobStep = "olcu" | "alt_yapi_prova" | "ust_yapi_prova" | "cila_bitim"; export type JobStep = "olcu" | "alt_yapi_prova" | "ust_yapi_prova" | "cila_bitim";
export type JobLocation = "at_clinic" | "at_lab";
export type ProstheticType = export type ProstheticType =
| "metal_porselen" | "metal_porselen"
| "zirkonyum" | "zirkonyum"
@@ -109,6 +110,7 @@ export interface Job extends Row {
currency?: string; currency?: string;
status: JobStatus; status: JobStatus;
currentStep?: JobStep; currentStep?: JobStep;
location?: JobLocation;
dueDate?: string; dueDate?: string;
} }