diff --git a/src/app/(dashboard)/jobs/_components/jobs-filter-bar.tsx b/src/app/(dashboard)/jobs/_components/jobs-filter-bar.tsx new file mode 100644 index 0000000..a0b0b2c --- /dev/null +++ b/src/app/(dashboard)/jobs/_components/jobs-filter-bar.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useEffect, useState, useTransition } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { Search, X } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const STATUS_OPTIONS = [ + { value: "all", label: "Tüm durumlar" }, + { value: "pending", label: "Bekliyor" }, + { value: "in_progress", label: "İşlemde" }, + { value: "sent", label: "Gönderildi" }, + { value: "delivered", label: "Teslim alındı" }, + { value: "cancelled", label: "İptal" }, +]; + +const LOCATION_OPTIONS = [ + { value: "all", label: "Her yerde" }, + { value: "at_lab", label: "Laboratuvarda" }, + { value: "at_clinic", label: "Klinikte" }, +]; + +export function JobsFilterBar() { + const router = useRouter(); + const pathname = usePathname(); + const params = useSearchParams(); + const [, startTransition] = useTransition(); + + const [q, setQ] = useState(params.get("q") ?? ""); + const status = params.get("status") ?? "all"; + const location = params.get("location") ?? "all"; + + // Debounce text input so we don't refetch on every keystroke. + useEffect(() => { + const current = params.get("q") ?? ""; + if (current === q) return; + const t = setTimeout(() => commit({ q }), 250); + return () => clearTimeout(t); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [q]); + + function commit(patch: Record) { + const next = new URLSearchParams(params.toString()); + for (const [key, value] of Object.entries(patch)) { + if (!value || value === "all") next.delete(key); + else next.set(key, value); + } + startTransition(() => { + router.replace(`${pathname}?${next.toString()}`); + }); + } + + const isFiltering = + q.trim() !== "" || status !== "all" || location !== "all"; + + return ( +
+
+ + setQ(e.target.value)} + placeholder="Hasta kodu veya karşı taraf ara..." + className="pl-9" + /> +
+ + + {isFiltering && ( + + )} +
+ ); +} diff --git a/src/app/(dashboard)/jobs/inbound/page.tsx b/src/app/(dashboard)/jobs/inbound/page.tsx index 6f78094..6482507 100644 --- a/src/app/(dashboard)/jobs/inbound/page.tsx +++ b/src/app/(dashboard)/jobs/inbound/page.tsx @@ -3,13 +3,18 @@ import { redirect } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 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"; export const metadata = { title: "DLS — Gelen İşler", }; -export default async function InboundJobsPage() { +export default async function InboundJobsPage({ + searchParams, +}: { + searchParams: Promise<{ status?: string; location?: string; q?: string }>; +}) { let ctx; try { ctx = await requireTenant(); @@ -17,10 +22,13 @@ export default async function InboundJobsPage() { redirect("/onboarding"); } - // Inbound = jobs where this tenant is the lab side. - // A clinic tenant can also receive jobs only via labTenantId match, which - // would be unusual; we still surface whatever matches. - const rows = ctx.kind === "lab" ? await listInboundJobs(ctx.tenantId) : []; + const sp = await searchParams; + const filters = { + status: sp.status && sp.status !== "all" ? sp.status : undefined, + location: sp.location && sp.location !== "all" ? sp.location : undefined, + q: sp.q, + }; + const rows = ctx.kind === "lab" ? await listInboundJobs(ctx.tenantId, filters) : []; return (
@@ -37,18 +45,21 @@ export default async function InboundJobsPage() { {ctx.kind === "lab" ? rows.length === 0 - ? "Henüz gelen iş yok." + ? "Filtreye uyan iş yok." : `${rows.length} kalem` : "Bu sayfa laboratuvar hesapları içindir."} - + {ctx.kind === "lab" ? ( - + <> + + + ) : (

Klinik hesabıyla giriş yaptınız — gelen iş listesi sadece laboratuvar tarafında görünür. diff --git a/src/app/(dashboard)/jobs/outbound/page.tsx b/src/app/(dashboard)/jobs/outbound/page.tsx index a63dd3b..ad90d48 100644 --- a/src/app/(dashboard)/jobs/outbound/page.tsx +++ b/src/app/(dashboard)/jobs/outbound/page.tsx @@ -3,13 +3,18 @@ import { redirect } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { listOutboundJobs } 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"; export const metadata = { title: "DLS — Giden İşler", }; -export default async function OutboundJobsPage() { +export default async function OutboundJobsPage({ + searchParams, +}: { + searchParams: Promise<{ status?: string; location?: string; q?: string }>; +}) { let ctx; try { ctx = await requireTenant(); @@ -17,7 +22,13 @@ export default async function OutboundJobsPage() { redirect("/onboarding"); } - const rows = ctx.kind === "clinic" ? await listOutboundJobs(ctx.tenantId) : []; + const sp = await searchParams; + const filters = { + status: sp.status && sp.status !== "all" ? sp.status : undefined, + location: sp.location && sp.location !== "all" ? sp.location : undefined, + q: sp.q, + }; + const rows = ctx.kind === "clinic" ? await listOutboundJobs(ctx.tenantId, filters) : []; return (

@@ -34,18 +45,21 @@ export default async function OutboundJobsPage() { {ctx.kind === "clinic" ? rows.length === 0 - ? "Henüz iş göndermediniz." + ? "Filtreye uyan iş yok." : `${rows.length} kalem` : "Bu sayfa klinik hesapları içindir."} - + {ctx.kind === "clinic" ? ( - + <> + + + ) : (

Laboratuvar hesabıyla giriş yaptınız — giden iş listesi sadece klinik tarafında görünür. diff --git a/src/lib/appwrite/job-queries.ts b/src/lib/appwrite/job-queries.ts index 2dc8e13..875903e 100644 --- a/src/lib/appwrite/job-queries.ts +++ b/src/lib/appwrite/job-queries.ts @@ -45,38 +45,68 @@ function enrichJob(j: Job, counterpartId: string, map: Map { +export type JobListFilters = { + status?: string; + location?: string; + /** Free-text matched client-side against patientCode + counterpart name. */ + q?: string; +}; + +async function listJobsFor( + side: "lab" | "clinic", + tenantId: string, + filters: JobListFilters = {}, +): Promise { const { tablesDB } = createAdminClient(); + const sideField = side === "lab" ? "labTenantId" : "clinicTenantId"; + const queries = [ + Query.equal(sideField, tenantId), + Query.orderDesc("$createdAt"), + Query.limit(200), + ]; + if (filters.status) queries.unshift(Query.equal("status", filters.status)); + if (filters.location) queries.unshift(Query.equal("location", filters.location)); + const result = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.jobs, - queries: [ - Query.equal("labTenantId", labTenantId), - Query.orderDesc("$createdAt"), - Query.limit(200), - ], + queries, }); const jobs = result.rows as unknown as Job[]; - const map = await fetchTenants(Array.from(new Set(jobs.map((j) => j.clinicTenantId)))); - return toPlain(jobs.map((j) => enrichJob(j, j.clinicTenantId, map))); + const counterpartField = side === "lab" ? "clinicTenantId" : "labTenantId"; + const map = await fetchTenants( + Array.from(new Set(jobs.map((j) => j[counterpartField]))), + ); + const enriched = jobs.map((j) => enrichJob(j, j[counterpartField], map)); + + // Free-text filter applied after fetch — only against the fields a user + // would actually type (patient code, counterpart company name). + const q = filters.q?.trim().toLocaleLowerCase("tr-TR"); + const filtered = q + ? enriched.filter((j) => { + const hay = `${j.patientCode} ${j.counterpart?.companyName ?? ""}` + .toLocaleLowerCase("tr-TR"); + return hay.includes(q); + }) + : enriched; + + return toPlain(filtered); +} + +/** Inbound for a lab tenant — jobs the lab has received. */ +export async function listInboundJobs( + labTenantId: string, + filters: JobListFilters = {}, +): Promise { + return listJobsFor("lab", labTenantId, filters); } /** Outbound for a clinic tenant — jobs the clinic has sent. */ -export async function listOutboundJobs(clinicTenantId: string): Promise { - const { tablesDB } = createAdminClient(); - const result = await tablesDB.listRows({ - databaseId: DATABASE_ID, - tableId: TABLES.jobs, - queries: [ - Query.equal("clinicTenantId", clinicTenantId), - Query.orderDesc("$createdAt"), - Query.limit(200), - ], - }); - const jobs = result.rows as unknown as Job[]; - const map = await fetchTenants(Array.from(new Set(jobs.map((j) => j.labTenantId)))); - return toPlain(jobs.map((j) => enrichJob(j, j.labTenantId, map))); +export async function listOutboundJobs( + clinicTenantId: string, + filters: JobListFilters = {}, +): Promise { + return listJobsFor("clinic", clinicTenantId, filters); } /** For clinic's "Yeni İş Yayınla" form: list approved labs they can send to. */