feat(modules): connections, products, jobs (list/form/detail-placeholder)
Connections (clinic ↔ lab)
- request via member number, approve/reject (counterparty), cancel pending,
delete approved
- permission rows opened to both teams; audit log for every mutation
- /connections page: own code card, request form, pending inbound/outbound
tables, approved connections table with delete confirm
Products (lab catalog)
- createProstheticAction + update + archive/restore + delete (lab-only)
- zod validation, dev-mode error surfacing
- /products page: catalog table + add form + edit dialog. Hidden from
clinic accounts via requireTenantKind.
Jobs (work orders)
- createJobAction (clinic-only) — checks approved connection before write,
permissions opened to both clinic and lab teams
- listInboundJobs (lab perspective), listOutboundJobs (clinic perspective),
listApprovedLabsForClinic for the new-job form
- /jobs/inbound + /jobs/outbound tables with role-aware copy
- /jobs/new full form (lab select, patient code, prosthetic type, member
count, color, due date, price/currency, description)
- /jobs/[jobId] placeholder detail page with stepper visualisation;
status/step updates and file upload come next session
All new mutations follow the memory rules: schema-checked row payloads,
admin client behind requireTenant + requireRole/requireTenantKind, audit
log calls best-effort, no empty-string Radix Select values.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { createAdminClient } from "@/lib/appwrite/server";
|
||||
import { DATABASE_ID, TABLES, type Job, type TenantSettings } from "@/lib/appwrite/schema";
|
||||
import { Query } from "node-appwrite";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
import {
|
||||
JOB_STATUS_LABELS,
|
||||
JOB_STEP_LABELS,
|
||||
JOB_STEP_ORDER,
|
||||
PROSTHETIC_TYPE_LABELS,
|
||||
} from "@/lib/appwrite/job-types";
|
||||
|
||||
export const metadata = {
|
||||
title: "DLS — İş Detay",
|
||||
};
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
function formatMoney(amount: number, currency: string) {
|
||||
try {
|
||||
return new Intl.NumberFormat("tr-TR", { style: "currency", currency }).format(amount);
|
||||
} catch {
|
||||
return `${amount.toFixed(2)} ${currency}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function JobDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ jobId: string }>;
|
||||
}) {
|
||||
const { jobId } = await params;
|
||||
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
let job: Job;
|
||||
try {
|
||||
const row = await tablesDB.getRow(DATABASE_ID, TABLES.jobs, jobId);
|
||||
job = row as unknown as Job;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (job.clinicTenantId !== ctx.tenantId && job.labTenantId !== ctx.tenantId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const counterpartId =
|
||||
job.clinicTenantId === ctx.tenantId ? job.labTenantId : job.clinicTenantId;
|
||||
const counterpartLabel = job.clinicTenantId === ctx.tenantId ? "Laboratuvar" : "Klinik";
|
||||
|
||||
const counterpartRes = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.tenantSettings,
|
||||
queries: [Query.equal("tenantId", counterpartId), Query.limit(1)],
|
||||
});
|
||||
const counterpart = counterpartRes.rows[0] as unknown as TenantSettings | undefined;
|
||||
|
||||
const currentStepIdx = job.currentStep ? JOB_STEP_ORDER.indexOf(job.currentStep) : -1;
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{counterpartLabel}: {counterpart?.companyName ?? "—"}
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Hasta {job.patientCode}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{PROSTHETIC_TYPE_LABELS[job.prostheticType]} · {job.memberCount} üye
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{JOB_STATUS_LABELS[job.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>İş Bilgileri</CardTitle>
|
||||
<CardDescription>{dateFormatter.format(new Date(job.$createdAt))}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 text-sm md:grid-cols-2">
|
||||
<Info label="Renk">{job.color || "—"}</Info>
|
||||
<Info label="Termin">
|
||||
{job.dueDate ? dateFormatter.format(new Date(job.dueDate)) : "—"}
|
||||
</Info>
|
||||
<Info label="Fiyat">
|
||||
{typeof job.price === "number"
|
||||
? formatMoney(job.price, job.currency || "TRY")
|
||||
: "—"}
|
||||
</Info>
|
||||
<Info label="Mevcut Aşama">
|
||||
{job.currentStep ? JOB_STEP_LABELS[job.currentStep] : "—"}
|
||||
</Info>
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-muted-foreground mb-1 text-xs font-medium uppercase tracking-wide">
|
||||
Açıklama
|
||||
</p>
|
||||
<p className="whitespace-pre-wrap text-sm">{job.description || "—"}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Aşamalar</CardTitle>
|
||||
<CardDescription>Ölçü → Alt Yapı → Üst Yapı → Cila/Bitim</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ol className="space-y-3">
|
||||
{JOB_STEP_ORDER.map((step, idx) => {
|
||||
const done = currentStepIdx > idx || job.status === "delivered";
|
||||
const active = currentStepIdx === idx && job.status !== "delivered";
|
||||
return (
|
||||
<li key={step} className="flex items-center gap-3">
|
||||
<span
|
||||
className={
|
||||
done
|
||||
? "bg-primary text-primary-foreground"
|
||||
: active
|
||||
? "bg-primary/15 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: "50%",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className={active ? "font-medium" : ""}>{JOB_STEP_LABELS[step]}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<p className="text-muted-foreground mt-4 text-xs">
|
||||
Aşama güncelleme ve dosya yükleme sonraki sürümde.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button asChild variant="outline">
|
||||
<Link href={ctx.kind === "clinic" ? "/jobs/outbound" : "/jobs/inbound"}>
|
||||
← Listeye dön
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Info({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
{label}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm">{children}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
JOB_STATUS_LABELS,
|
||||
PROSTHETIC_TYPE_LABELS,
|
||||
} from "@/lib/appwrite/job-types";
|
||||
import type { JobWithCounterpart } from "@/lib/appwrite/job-queries";
|
||||
import type { JobStatus } from "@/lib/appwrite/schema";
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
function statusVariant(status: JobStatus): "default" | "secondary" | "outline" | "destructive" {
|
||||
switch (status) {
|
||||
case "delivered":
|
||||
return "default";
|
||||
case "sent":
|
||||
return "secondary";
|
||||
case "in_progress":
|
||||
return "secondary";
|
||||
case "cancelled":
|
||||
return "destructive";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
}
|
||||
|
||||
export function JobsTable({
|
||||
rows,
|
||||
counterpartLabel,
|
||||
emptyMessage,
|
||||
}: {
|
||||
rows: JobWithCounterpart[];
|
||||
counterpartLabel: string;
|
||||
emptyMessage: string;
|
||||
}) {
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground py-6 text-center text-sm">{emptyMessage}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{counterpartLabel}</TableHead>
|
||||
<TableHead>Hasta Kodu</TableHead>
|
||||
<TableHead>Üye</TableHead>
|
||||
<TableHead>Renk</TableHead>
|
||||
<TableHead>Tür</TableHead>
|
||||
<TableHead>Durum</TableHead>
|
||||
<TableHead>Tarih</TableHead>
|
||||
<TableHead className="text-right">İşlem</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((j) => (
|
||||
<TableRow key={j.$id}>
|
||||
<TableCell className="font-medium">{j.counterpart?.companyName ?? "—"}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{j.patientCode}</TableCell>
|
||||
<TableCell className="tabular-nums">{j.memberCount}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{j.color || "—"}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{PROSTHETIC_TYPE_LABELS[j.prostheticType] ?? j.prostheticType}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusVariant(j.status)}>{JOB_STATUS_LABELS[j.status]}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs">
|
||||
{dateFormatter.format(new Date(j.$createdAt))}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={`/jobs/${j.$id}`}>Detay</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,60 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 { JobsTable } from "../_components/jobs-table";
|
||||
|
||||
export const metadata = {
|
||||
title: "DLS — Gelen İşler",
|
||||
};
|
||||
|
||||
export default async function InboundJobsPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
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) : [];
|
||||
|
||||
export default function InboundJobsPage() {
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Gelen İşler</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Bağlı kliniklerden gelen protez işleri burada listelenecek.
|
||||
Bağlı kliniklerden size yönlendirilmiş protez işleri.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Gelen iş listesi, filtreleme ve detay görünümü sonraki sürümde eklenecek.</CardDescription>
|
||||
<CardTitle>Tüm Gelen İşler</CardTitle>
|
||||
<CardDescription>
|
||||
{ctx.kind === "lab"
|
||||
? rows.length === 0
|
||||
? "Henüz gelen iş yok."
|
||||
: `${rows.length} kalem`
|
||||
: "Bu sayfa laboratuvar hesapları içindir."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
<CardContent>
|
||||
{ctx.kind === "lab" ? (
|
||||
<JobsTable
|
||||
rows={rows}
|
||||
counterpartLabel="Klinik"
|
||||
emptyMessage="Henüz size gönderilmiş iş yok. Klinik tarafa Bağlantı Kodunuzu paylaşın."
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted-foreground py-6 text-center text-sm">
|
||||
Klinik hesabıyla giriş yaptınız — gelen iş listesi sadece laboratuvar tarafında görünür.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2, Send } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { createJobAction } from "@/lib/appwrite/job-actions";
|
||||
import {
|
||||
PROSTHETIC_TYPE_LABELS,
|
||||
initialJobFormState,
|
||||
} from "@/lib/appwrite/job-types";
|
||||
import type { JobCounterpart } from "@/lib/appwrite/job-queries";
|
||||
import type { ProstheticType } from "@/lib/appwrite/schema";
|
||||
|
||||
const PROSTHETIC_TYPES: ProstheticType[] = [
|
||||
"metal_porselen",
|
||||
"zirkonyum",
|
||||
"implant_ustu_zirkonyum",
|
||||
"gecici",
|
||||
"e_max",
|
||||
"diger",
|
||||
];
|
||||
|
||||
export function NewJobForm({
|
||||
labs,
|
||||
defaultCurrency,
|
||||
}: {
|
||||
labs: JobCounterpart[];
|
||||
defaultCurrency: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [state, action, pending] = useActionState(createJobAction, initialJobFormState);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
toast.success("İş yayınlandı.");
|
||||
router.push("/jobs/outbound");
|
||||
} else if (state.error) {
|
||||
toast.error(state.error);
|
||||
}
|
||||
}, [state, router]);
|
||||
|
||||
return (
|
||||
<form action={action} className="grid gap-5">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label htmlFor="labTenantId">Laboratuvar *</Label>
|
||||
<Select name="labTenantId" required defaultValue={labs[0]?.tenantId}>
|
||||
<SelectTrigger id="labTenantId">
|
||||
<SelectValue placeholder="Bir laboratuvar seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{labs.map((l) => (
|
||||
<SelectItem key={l.tenantId} value={l.tenantId}>
|
||||
{l.companyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{state.fieldErrors?.labTenantId && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.labTenantId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="patientCode">Hasta Kodu *</Label>
|
||||
<Input
|
||||
id="patientCode"
|
||||
name="patientCode"
|
||||
required
|
||||
maxLength={50}
|
||||
placeholder="Örn. 000892"
|
||||
/>
|
||||
{state.fieldErrors?.patientCode && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.patientCode}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="memberCount">Üye Sayısı *</Label>
|
||||
<Input
|
||||
id="memberCount"
|
||||
name="memberCount"
|
||||
type="number"
|
||||
min="1"
|
||||
max="32"
|
||||
required
|
||||
defaultValue={1}
|
||||
/>
|
||||
{state.fieldErrors?.memberCount && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.memberCount}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="prostheticType">Protez Türü *</Label>
|
||||
<Select name="prostheticType" required>
|
||||
<SelectTrigger id="prostheticType">
|
||||
<SelectValue placeholder="Tür seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROSTHETIC_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{PROSTHETIC_TYPE_LABELS[t]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{state.fieldErrors?.prostheticType && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.prostheticType}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="color">Renk (Vita)</Label>
|
||||
<Input id="color" name="color" maxLength={20} placeholder="Örn. A2" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dueDate">Termin Tarihi</Label>
|
||||
<Input id="dueDate" name="dueDate" type="date" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_100px] gap-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="price">Fiyat</Label>
|
||||
<Input
|
||||
id="price"
|
||||
name="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="currency">Para</Label>
|
||||
<Input
|
||||
id="currency"
|
||||
name="currency"
|
||||
defaultValue={defaultCurrency}
|
||||
maxLength={8}
|
||||
style={{ textTransform: "uppercase" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 md:col-span-2">
|
||||
<Label htmlFor="description">Açıklama</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows={4}
|
||||
maxLength={2000}
|
||||
placeholder="Lab'a iletmek istediğiniz notlar — hijyenik gövde, materyal tercihi, vs."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={pending || labs.length === 0}>
|
||||
{pending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Gönderiliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="size-4" />
|
||||
İşi Yayınla
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { listApprovedLabsForClinic } from "@/lib/appwrite/job-queries";
|
||||
import { requireTenant, requireTenantKind } from "@/lib/appwrite/tenant-guard";
|
||||
import { NewJobForm } from "./components/new-job-form";
|
||||
|
||||
export const metadata = {
|
||||
title: "DLS — Yeni İş Yayınla",
|
||||
};
|
||||
|
||||
export default async function NewJobPage() {
|
||||
let ctx;
|
||||
@@ -12,6 +20,9 @@ export default async function NewJobPage() {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
const labs = await listApprovedLabsForClinic(ctx.tenantId);
|
||||
const defaultCurrency = ctx.settings?.defaultCurrency ?? "TRY";
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -20,15 +31,34 @@ export default async function NewJobPage() {
|
||||
Bağlı laboratuvarınıza yeni bir protez işi gönderin.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>
|
||||
Form (lab seçimi, hasta kodu, protez türü, renk, dosya yükleme) sonraki sürümde eklenecek.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
|
||||
{labs.length === 0 ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Önce bir laboratuvarla bağlantı kurun</CardTitle>
|
||||
<CardDescription>
|
||||
İş gönderebilmeniz için onaylanmış bir laboratuvar bağlantınız olmalı.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button asChild>
|
||||
<Link href="/connections">Bağlantı Kur</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>İş Bilgileri</CardTitle>
|
||||
<CardDescription>
|
||||
Hasta kodu, protez türü ve diğer detayları girin. Dosya yüklemesi sonraki sürümde.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<NewJobForm labs={labs} defaultCurrency={defaultCurrency} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,57 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 { JobsTable } from "../_components/jobs-table";
|
||||
|
||||
export const metadata = {
|
||||
title: "DLS — Giden İşler",
|
||||
};
|
||||
|
||||
export default async function OutboundJobsPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
const rows = ctx.kind === "clinic" ? await listOutboundJobs(ctx.tenantId) : [];
|
||||
|
||||
export default function OutboundJobsPage() {
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Giden İşler</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Karşı tarafa gönderilen protez işleri burada listelenecek.
|
||||
Bağlı laboratuvarlara gönderdiğiniz işler.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Giden iş listesi sonraki sürümde eklenecek.</CardDescription>
|
||||
<CardTitle>Tüm Giden İşler</CardTitle>
|
||||
<CardDescription>
|
||||
{ctx.kind === "clinic"
|
||||
? rows.length === 0
|
||||
? "Henüz iş göndermediniz."
|
||||
: `${rows.length} kalem`
|
||||
: "Bu sayfa klinik hesapları içindir."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
<CardContent>
|
||||
{ctx.kind === "clinic" ? (
|
||||
<JobsTable
|
||||
rows={rows}
|
||||
counterpartLabel="Laboratuvar"
|
||||
emptyMessage="Henüz iş göndermediniz. 'Yeni İş Yayınla' butonundan başlayabilirsiniz."
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted-foreground py-6 text-center text-sm">
|
||||
Laboratuvar hesabıyla giriş yaptınız — giden iş listesi sadece klinik tarafında görünür.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user