feat(patients): drop phone/dateOfBirth, name fields optional
Reduced the patient record to the minimum a dental clinic actually needs:
just a code, optional first/last name and free-text notes. Phone and
date-of-birth fields are gone from the UI everywhere — Add form, edit
dialog inside the table, the Bağlantı Bilgileri block on job detail, and
the table column list. The patient list now surfaces 'Notlar' instead.
Backend
- DB: firstName and lastName columns set to required=false via Appwrite
MCP (tables_db_update_string_column). Existing rows untouched.
- schema.ts Patient interface: firstName/lastName now optional, phone
and dateOfBirth removed from the type entirely. The underlying columns
are still in the DB so legacy rows aren't broken — we just stop
referencing them in code.
- validation/patient.ts: firstName/lastName drop min(1), phone and dob
fields removed.
- patient-actions.ts: pickFields no longer reads phone/dob, create and
update payloads no longer write them.
UI fallbacks
- PatientsTable: header has 'Notlar' instead of Telefon/Doğum. Ad Soyad
cell shows the joined name or em-dash. Edit dialog mirrors the same
simplified form.
- jobs/[jobId] detail page: when patient row has neither name, the page
title falls back to 'Hasta {patientCode}' (same as before for jobs
without a linked patient). The Hasta Bilgileri card now shows Ad Soyad
and Patient Code side by side, with notes spanning both columns.
This commit is contained in:
@@ -100,10 +100,13 @@ export default async function JobDetailPage({
|
||||
{counterpartLabel}: {counterpart?.companyName ?? "—"}
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{patient ? `${patient.firstName} ${patient.lastName}` : `Hasta ${job.patientCode}`}
|
||||
{(() => {
|
||||
const name = [patient?.firstName, patient?.lastName].filter(Boolean).join(" ");
|
||||
return name || `Hasta ${job.patientCode}`;
|
||||
})()}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{patient && (
|
||||
{patient && (patient.firstName || patient.lastName) && (
|
||||
<>
|
||||
<span className="font-mono">{job.patientCode}</span> ·{" "}
|
||||
</>
|
||||
@@ -211,18 +214,15 @@ export default async function JobDetailPage({
|
||||
dışında bir veri görmez.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 text-sm md:grid-cols-3">
|
||||
<CardContent className="grid gap-4 text-sm md:grid-cols-2">
|
||||
<Info label="Ad Soyad">
|
||||
{patient.firstName} {patient.lastName}
|
||||
{[patient.firstName, patient.lastName].filter(Boolean).join(" ") || "—"}
|
||||
</Info>
|
||||
<Info label="Telefon">{patient.phone || "—"}</Info>
|
||||
<Info label="Doğum Tarihi">
|
||||
{patient.dateOfBirth
|
||||
? dateFormatter.format(new Date(patient.dateOfBirth))
|
||||
: "—"}
|
||||
<Info label="Hasta Kodu">
|
||||
<span className="font-mono">{patient.patientCode}</span>
|
||||
</Info>
|
||||
{patient.notes && (
|
||||
<div className="md:col-span-3">
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-muted-foreground mb-1 text-xs font-medium uppercase tracking-wide">
|
||||
Notlar
|
||||
</p>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import { createPatientAction } from "@/lib/appwrite/patient-actions";
|
||||
import { initialPatientFormState } from "@/lib/appwrite/patient-types";
|
||||
|
||||
@@ -48,38 +49,21 @@ export function PatientForm() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="firstName">Ad *</Label>
|
||||
<Input id="firstName" name="firstName" required maxLength={100} />
|
||||
<Label htmlFor="firstName">Ad</Label>
|
||||
<Input id="firstName" name="firstName" maxLength={100} />
|
||||
{state.fieldErrors?.firstName && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.firstName}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="lastName">Soyad *</Label>
|
||||
<Input id="lastName" name="lastName" required maxLength={100} />
|
||||
<Label htmlFor="lastName">Soyad</Label>
|
||||
<Input id="lastName" name="lastName" maxLength={100} />
|
||||
{state.fieldErrors?.lastName && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.lastName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">Telefon</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
maxLength={30}
|
||||
placeholder="+90 555 123 45 67"
|
||||
autoComplete="tel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dateOfBirth">Doğum Tarihi</Label>
|
||||
<Input id="dateOfBirth" name="dateOfBirth" type="date" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="notes">Notlar</Label>
|
||||
<Textarea
|
||||
|
||||
@@ -36,12 +36,6 @@ import {
|
||||
} from "@/lib/appwrite/patient-types";
|
||||
import type { Patient } from "@/lib/appwrite/schema";
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
export function PatientsTable({ rows }: { rows: Patient[] }) {
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
@@ -57,8 +51,7 @@ export function PatientsTable({ rows }: { rows: Patient[] }) {
|
||||
<TableRow>
|
||||
<TableHead>Kod</TableHead>
|
||||
<TableHead>Ad Soyad</TableHead>
|
||||
<TableHead>Telefon</TableHead>
|
||||
<TableHead>Doğum</TableHead>
|
||||
<TableHead>Notlar</TableHead>
|
||||
<TableHead>Durum</TableHead>
|
||||
<TableHead className="text-right">İşlem</TableHead>
|
||||
</TableRow>
|
||||
@@ -92,11 +85,12 @@ function PatientRow({ row }: { row: Patient }) {
|
||||
<TableRow className={row.archived ? "opacity-60" : ""}>
|
||||
<TableCell className="font-mono text-xs">{row.patientCode}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{row.firstName} {row.lastName}
|
||||
{[row.firstName, row.lastName].filter(Boolean).join(" ") || (
|
||||
<span className="text-muted-foreground">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{row.phone || "—"}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{row.dateOfBirth ? dateFormatter.format(new Date(row.dateOfBirth)) : "—"}
|
||||
<TableCell className="text-muted-foreground max-w-[280px] truncate">
|
||||
{row.notes || "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{row.archived ? (
|
||||
@@ -170,45 +164,24 @@ function EditPatientDialog({
|
||||
<input type="hidden" name="patientCode" value={row.patientCode} />
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`first-${row.$id}`}>Ad *</Label>
|
||||
<Label htmlFor={`first-${row.$id}`}>Ad</Label>
|
||||
<Input
|
||||
id={`first-${row.$id}`}
|
||||
name="firstName"
|
||||
defaultValue={row.firstName}
|
||||
required
|
||||
defaultValue={row.firstName ?? ""}
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`last-${row.$id}`}>Soyad *</Label>
|
||||
<Label htmlFor={`last-${row.$id}`}>Soyad</Label>
|
||||
<Input
|
||||
id={`last-${row.$id}`}
|
||||
name="lastName"
|
||||
defaultValue={row.lastName}
|
||||
required
|
||||
defaultValue={row.lastName ?? ""}
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`phone-${row.$id}`}>Telefon</Label>
|
||||
<Input
|
||||
id={`phone-${row.$id}`}
|
||||
name="phone"
|
||||
type="tel"
|
||||
defaultValue={row.phone ?? ""}
|
||||
maxLength={30}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`dob-${row.$id}`}>Doğum Tarihi</Label>
|
||||
<Input
|
||||
id={`dob-${row.$id}`}
|
||||
name="dateOfBirth"
|
||||
type="date"
|
||||
defaultValue={row.dateOfBirth ? row.dateOfBirth.slice(0, 10) : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`notes-${row.$id}`}>Notlar</Label>
|
||||
<Textarea
|
||||
|
||||
@@ -37,8 +37,6 @@ function pickFields(formData: FormData) {
|
||||
patientCode: String(formData.get("patientCode") ?? "").trim(),
|
||||
firstName: String(formData.get("firstName") ?? "").trim(),
|
||||
lastName: String(formData.get("lastName") ?? "").trim(),
|
||||
phone: String(formData.get("phone") ?? "").trim(),
|
||||
dateOfBirth: String(formData.get("dateOfBirth") ?? "").trim(),
|
||||
notes: String(formData.get("notes") ?? "").trim(),
|
||||
};
|
||||
}
|
||||
@@ -134,8 +132,6 @@ export async function createPatientAction(
|
||||
patientCode: code,
|
||||
firstName: parsed.data.firstName,
|
||||
lastName: parsed.data.lastName,
|
||||
phone: parsed.data.phone,
|
||||
dateOfBirth: parsed.data.dateOfBirth,
|
||||
notes: parsed.data.notes,
|
||||
archived: false,
|
||||
},
|
||||
@@ -193,8 +189,6 @@ export async function updatePatientAction(
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.patients, id, {
|
||||
firstName: parsed.data.firstName,
|
||||
lastName: parsed.data.lastName,
|
||||
phone: parsed.data.phone,
|
||||
dateOfBirth: parsed.data.dateOfBirth,
|
||||
notes: parsed.data.notes,
|
||||
});
|
||||
await logAudit({
|
||||
|
||||
@@ -87,10 +87,8 @@ export interface Patient extends Row {
|
||||
clinicTenantId: string;
|
||||
createdBy: string;
|
||||
patientCode: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone?: string;
|
||||
dateOfBirth?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
notes?: string;
|
||||
archived?: boolean;
|
||||
}
|
||||
|
||||
@@ -7,19 +7,18 @@ export const patientSchema = z.object({
|
||||
.max(50)
|
||||
.optional()
|
||||
.transform((v) => (v ? v.toUpperCase() : undefined)),
|
||||
firstName: z.string().trim().min(1, "Ad zorunlu.").max(100),
|
||||
lastName: z.string().trim().min(1, "Soyad zorunlu.").max(100),
|
||||
phone: z
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(30)
|
||||
.max(100)
|
||||
.optional()
|
||||
.transform((v) => (v ? v : undefined)),
|
||||
dateOfBirth: z
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100)
|
||||
.optional()
|
||||
.transform((v) => (v ? new Date(v).toISOString() : undefined)),
|
||||
.transform((v) => (v ? v : undefined)),
|
||||
notes: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
Reference in New Issue
Block a user