From 3de06add716bad7000b57f41e70dda699505db71 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Fri, 22 May 2026 16:13:59 +0300 Subject: [PATCH] feat(jobs): bulk-accept all pending inbox items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lab opening its inbox first thing in the morning shouldn't have to click 'İşleme Al' on every overnight submission. Added a single bulk action that flips every currently-pending job into in_progress in one shot. - bulkAcceptPendingJobsAction (lab only, owner/admin/member): lists every pending job for this lab (limit 200), then for each row in parallel writes status=in_progress + currentStep=alt_yapi_prova + location=at_lab. History rows and clinic notifications fire as fire-and-forget so a single failure doesn't block the rest. Returns { accepted } — count actually moved. - BulkAcceptButton (client island, /jobs/inbound only) shows when the current filtered list has at least one pending row, with a confirm dialog. Disabled / spinner while in flight. - 'Tümünü okundu işaretle' bulk action on /notifications was already in place, so nothing else needed there. Notifications mark-all was already wired earlier; this commit covers the inbox half. --- .../inbound/components/bulk-accept-button.tsx | 68 +++++++++++++++++++ src/app/(dashboard)/jobs/inbound/page.tsx | 15 ++-- src/lib/appwrite/job-actions.ts | 62 +++++++++++++++++ 3 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 src/app/(dashboard)/jobs/inbound/components/bulk-accept-button.tsx diff --git a/src/app/(dashboard)/jobs/inbound/components/bulk-accept-button.tsx b/src/app/(dashboard)/jobs/inbound/components/bulk-accept-button.tsx new file mode 100644 index 0000000..d166296 --- /dev/null +++ b/src/app/(dashboard)/jobs/inbound/components/bulk-accept-button.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { CheckCheck, Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { bulkAcceptPendingJobsAction } from "@/lib/appwrite/job-actions"; + +export function BulkAcceptButton({ count }: { count: number }) { + const router = useRouter(); + const [open, setOpen] = useState(false); + const [pending, startTransition] = useTransition(); + + if (count === 0) return null; + + function onConfirm() { + startTransition(async () => { + const res = await bulkAcceptPendingJobsAction(); + if (res.ok) { + toast.success(`${res.accepted ?? 0} iş işleme alındı.`); + setOpen(false); + router.refresh(); + } else { + toast.error(res.error ?? "İşlem başarısız."); + } + }); + } + + return ( + + + + + {count} iş işleme alınsın mı? + + Tüm bekleyen işler aynı anda işleme alınır; her birinde alt yapı + üretimine başlanmış sayılır. Klinikler ayrı ayrı bilgilendirilir. + + + + + + + + + + + ); +} diff --git a/src/app/(dashboard)/jobs/inbound/page.tsx b/src/app/(dashboard)/jobs/inbound/page.tsx index 6482507..a29e662 100644 --- a/src/app/(dashboard)/jobs/inbound/page.tsx +++ b/src/app/(dashboard)/jobs/inbound/page.tsx @@ -5,6 +5,7 @@ import { listInboundJobs } from "@/lib/appwrite/job-queries"; import { requireTenant } from "@/lib/appwrite/tenant-guard"; import { JobsFilterBar } from "../_components/jobs-filter-bar"; import { JobsTable } from "../_components/jobs-table"; +import { BulkAcceptButton } from "./components/bulk-accept-button"; export const metadata = { title: "DLS — Gelen İşler", @@ -29,14 +30,18 @@ export default async function InboundJobsPage({ q: sp.q, }; const rows = ctx.kind === "lab" ? await listInboundJobs(ctx.tenantId, filters) : []; + const pendingCount = rows.filter((j) => j.status === "pending").length; return (
-
-

Gelen İşler

-

- Bağlı kliniklerden size yönlendirilmiş protez işleri. -

+
+
+

Gelen İşler

+

+ Bağlı kliniklerden size yönlendirilmiş protez işleri. +

+
+ {ctx.kind === "lab" && }
diff --git a/src/lib/appwrite/job-actions.ts b/src/lib/appwrite/job-actions.ts index 2ebffaf..e19e9d6 100644 --- a/src/lib/appwrite/job-actions.ts +++ b/src/lib/appwrite/job-actions.ts @@ -333,6 +333,68 @@ export async function acceptJobAction( return { ok: true }; } +/** + * Lab takes all currently-pending jobs in one go. Same effect as calling + * acceptJobAction for each row individually — status flips to in_progress, + * step jumps to alt_yapi_prova, location lands at_lab. Partial failures + * are tolerated; we return how many actually moved. + */ +export async function bulkAcceptPendingJobsAction(): Promise< + JobActionState & { accepted?: number } +> { + let ctx; + try { + ctx = await requireTenant(); + requireRole(ctx, ["owner", "admin", "member"]); + requireTenantKind(ctx, ["lab"]); + } catch { + return { ok: false, error: "Bu işlemi yalnızca laboratuvar yapabilir." }; + } + + const { tablesDB } = createAdminClient(); + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.jobs, + queries: [ + Query.equal("labTenantId", ctx.tenantId), + Query.equal("status", "pending"), + Query.limit(200), + ], + }); + const rows = result.rows as unknown as Job[]; + if (rows.length === 0) return { ok: true, accepted: 0 }; + + const outcomes = await Promise.allSettled( + rows.map(async (job) => { + await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, job.$id, { + status: "in_progress", + currentStep: "alt_yapi_prova", + location: "at_lab", + }); + void appendJobHistory({ job, step: "olcu", completedBy: ctx.user.id }); + void createNotification({ + tenantId: job.clinicTenantId, + jobId: job.$id, + message: `${ctx.settings?.companyName ?? "Lab"} hasta ${job.patientCode} işini işleme aldı, alt yapı üretiminde.`, + }); + }), + ); + const accepted = outcomes.filter((o) => o.status === "fulfilled").length; + + void logAudit({ + tenantId: ctx.tenantId, + userId: ctx.user.id, + action: "update", + entityType: "job", + entityId: "bulk", + changes: { bulk: "accept_pending", count: accepted }, + }); + + revalidatePath("/jobs/inbound"); + revalidatePath("/dashboard"); + return { ok: true, accepted }; +} + /** * 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