feat(jobs): clinic-side 'Düzeltme İste' (revision request) flow
Up to now the only thing a clinic could do on a prova was approve it.
If the casting didn't fit there was no way to bounce the case back to
the lab short of cancelling the whole thing. Real-world flow needs a
'try again, this is what's wrong' lever, so:
- requestRevisionAction (clinic only): pre-conditions
in_progress + at_clinic + currentStep set; flips location → at_lab
while leaving currentStep untouched so the same prova stage repeats
after the lab redoes the work. Requires a note (the lab can't fix
what it doesn't know is broken) — appended to job_status_history
with a '[Düzeltme talebi]' prefix and surfaced to the lab via
notification.
- JobActionsPanel: when the clinic side sees a prova (in_progress +
at_clinic) it now shows two buttons — Onayla as before, plus
Düzeltme İste (variant=destructive). The dialog requires a note
before submit.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
Play,
|
Play,
|
||||||
|
RotateCcw,
|
||||||
Send,
|
Send,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
cancelJobAction,
|
cancelJobAction,
|
||||||
handToClinicAction,
|
handToClinicAction,
|
||||||
markDeliveredAction,
|
markDeliveredAction,
|
||||||
|
requestRevisionAction,
|
||||||
} from "@/lib/appwrite/job-actions";
|
} from "@/lib/appwrite/job-actions";
|
||||||
import { initialJobActionState } from "@/lib/appwrite/job-types";
|
import { initialJobActionState } from "@/lib/appwrite/job-types";
|
||||||
import type { Job, TenantKind } from "@/lib/appwrite/schema";
|
import type { Job, TenantKind } from "@/lib/appwrite/schema";
|
||||||
@@ -67,7 +69,10 @@ export function JobActionsPanel({
|
|||||||
|
|
||||||
{/* Clinic finished the prova — approve and send back to lab */}
|
{/* Clinic finished the prova — approve and send back to lab */}
|
||||||
{isClinic && job.status === "in_progress" && isAtClinic && (
|
{isClinic && job.status === "in_progress" && isAtClinic && (
|
||||||
<ApproveAtClinicButton job={job} />
|
<>
|
||||||
|
<ApproveAtClinicButton job={job} />
|
||||||
|
<RequestRevisionButton job={job} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Final delivery — clinic took it from the lab */}
|
{/* Final delivery — clinic took it from the lab */}
|
||||||
@@ -236,6 +241,68 @@ function ApproveAtClinicButton({ job }: { job: Job }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RequestRevisionButton({ job }: { job: Job }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [state, action, pending] = useActionState(
|
||||||
|
requestRevisionAction,
|
||||||
|
initialJobActionState,
|
||||||
|
);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.ok) {
|
||||||
|
toast.success("Düzeltme talebi gönderildi.");
|
||||||
|
setOpen(false);
|
||||||
|
router.refresh();
|
||||||
|
} else if (state.error) {
|
||||||
|
toast.error(state.error);
|
||||||
|
}
|
||||||
|
}, [state, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(true)}>
|
||||||
|
<RotateCcw className="size-4" />
|
||||||
|
Düzeltme İste
|
||||||
|
</Button>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Provayı reddet, lab'a geri gönder</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Bu aşamayı reddettiğinizde iş aynı adımda kalır ve laboratuvar
|
||||||
|
yeniden çalışır. Neyin düzeltilmesi gerektiğini lütfen yazın.
|
||||||
|
</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">Düzeltme notu *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="note"
|
||||||
|
name="note"
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
maxLength={1000}
|
||||||
|
placeholder="Örn. Distalde temas yok, oklüzyon yüksek geldi."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
Vazgeç
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" variant="destructive" disabled={pending}>
|
||||||
|
{pending ? <Loader2 className="size-4 animate-spin" /> : <RotateCcw className="size-4" />}
|
||||||
|
Düzeltme İste
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DeliverButton({ jobId }: { jobId: string }) {
|
function DeliverButton({ jobId }: { jobId: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [state, action, pending] = useActionState(markDeliveredAction, initialJobActionState);
|
const [state, action, pending] = useActionState(markDeliveredAction, initialJobActionState);
|
||||||
|
|||||||
@@ -522,6 +522,95 @@ export async function approveAtClinicAction(
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clinic rejects the prova and asks the lab to redo this stage. The job
|
||||||
|
* goes back to the lab without advancing the step, so the same prova
|
||||||
|
* stage will repeat after the lab finishes the rework. A note explaining
|
||||||
|
* what's wrong is required — there's no point bouncing a case back
|
||||||
|
* without telling the lab what to fix.
|
||||||
|
*/
|
||||||
|
export async function requestRevisionAction(
|
||||||
|
_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();
|
||||||
|
if (!note) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Düzeltme talebi için neyin yanlış olduğunu yazmanız gerek.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx;
|
||||||
|
try {
|
||||||
|
ctx = await requireTenant();
|
||||||
|
requireRole(ctx, ["owner", "admin", "member"]);
|
||||||
|
requireTenantKind(ctx, ["clinic"]);
|
||||||
|
} catch {
|
||||||
|
return { ok: false, error: "Düzeltme talebini yalnızca klinik açabilir." };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 için düzeltme istenebilir." };
|
||||||
|
}
|
||||||
|
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." };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { tablesDB } = createAdminClient();
|
||||||
|
await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
|
||||||
|
location: "at_lab",
|
||||||
|
// currentStep stays the same — the lab will rework this stage.
|
||||||
|
});
|
||||||
|
await appendJobHistory({
|
||||||
|
job,
|
||||||
|
step: job.currentStep,
|
||||||
|
completedBy: ctx.user.id,
|
||||||
|
note: `[Düzeltme talebi] ${note}`,
|
||||||
|
});
|
||||||
|
void logAudit({
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
action: "update",
|
||||||
|
entityType: "job",
|
||||||
|
entityId: jobId,
|
||||||
|
changes: {
|
||||||
|
location: "at_lab",
|
||||||
|
revisionRequestedAtStep: job.currentStep,
|
||||||
|
note,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const stepLabel =
|
||||||
|
job.currentStep === "alt_yapi_prova"
|
||||||
|
? "alt yapı"
|
||||||
|
: job.currentStep === "ust_yapi_prova"
|
||||||
|
? "üst yapı"
|
||||||
|
: "cila/bitim";
|
||||||
|
void createNotification({
|
||||||
|
tenantId: job.labTenantId,
|
||||||
|
jobId,
|
||||||
|
message: `Hasta ${job.patientCode} ${stepLabel} provası için düzeltme istendi: ${note.slice(0, 120)}`,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: appwriteError(e, "Düzeltme talebi gönderilemedi.") };
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user