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:
kovakmedya
2026-05-21 23:01:52 +03:00
parent 0dea028845
commit ca4ea87d37
6 changed files with 32 additions and 84 deletions
+10 -10
View File
@@ -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
-6
View File
@@ -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({
+2 -4
View File
@@ -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;
}
+5 -6
View File
@@ -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()