Files
lab/src/lib/appwrite/schema.ts
T
kovakmedya 95f2d065b4 feat(pricing): tooth-based selection, lab-owned pricing, clinic-specific overrides
What changed
  - jobs.teeth (FDI string[]). memberCount becomes a derived field (teeth.length).
    A new TeethChart component renders the full permanent dentition as a
    16-column grid for each arch with click-toggle selection.
  - /jobs/new: removed the price + currency inputs and the manual memberCount
    field. Clinics now pick teeth via the chart; the form blocks submission
    until at least one tooth is selected.
  - createJobAction calls a new calculateJobPrice() helper that walks the
    pricing cascade and writes price + currency on the job server-side. A
    clinic-supplied price hidden field would now be ignored — the field
    isn't even in the schema.

Pricing cascade (calculateJobPrice, lib/appwrite/pricing.ts)
  1. clinic_pricing row matching (lab, clinic, type) with customUnitPrice
     → use that flat unit price.
  2. clinic_pricing row with discountPercent → catalog unitPrice × (1-d).
  3. lab's prosthetics catalog row matching type (not archived).
  4. nothing → price stays null; lab can still set it manually later.

Clinic-specific overrides (clinic_pricing table)
  - Unique on (labTenantId, clinicTenantId, prostheticType) so each
    combination has at most one rule.
  - Row permissions: read by both teams (transparency for clinic), write
    only by lab — clinic can see the discount they're getting but cannot
    edit it.
  - setClinicPricingAction validates an approved connection exists before
    creating/updating, and rejects requests where neither customUnitPrice
    nor discountPercent is set.
  - clearClinicPricingAction wipes a rule (catalog price re-applies).

UI
  - /connections 'Bağlantılarım' table gets a new column showing the active
    pricing rules per counterpart. Lab side has a 'Fiyatlandırma' button
    that opens a dialog (PROSTHETIC_TYPE × customPrice|discountPercent form
    + list of active rules with delete). Clinic side is read-only.
  - Job detail: 'Fiyat' field now shows 'Lab tarafından belirlenecek' when
    null, instead of a literal —. Adds a 'Dişler' info block listing the
    selected FDI numbers.
2026-05-21 22:04:26 +03:00

212 lines
4.6 KiB
TypeScript

export const DATABASE_ID = "lab";
export const BUCKETS = {
tenantLogos: "tenant-logos",
jobFiles: "job-files",
} as const;
export const TABLES = {
tenantSettings: "tenant_settings",
profiles: "profiles",
connections: "connections",
patients: "patients",
clinicPricing: "clinic_pricing",
jobs: "jobs",
jobFiles: "job_files",
jobStatusHistory: "job_status_history",
prosthetics: "prosthetics",
financeEntries: "finance_entries",
notifications: "notifications",
auditLogs: "audit_logs",
inviteLinks: "invite_links",
passwordResets: "password_resets",
userPreferences: "user_preferences",
} as const;
export type TableId = (typeof TABLES)[keyof typeof TABLES];
export type SystemRow = {
$id: string;
$createdAt: string;
$updatedAt: string;
$permissions: string[];
$databaseId?: string;
$tableId?: string;
$sequence?: number;
};
type Row = SystemRow;
export type TenantRole = "owner" | "admin" | "member";
export type TenantKind = "lab" | "clinic";
export interface TenantSettings extends Row {
tenantId: string;
kind: TenantKind;
memberNumber: string;
companyName: string;
companyTaxId?: string;
companyAddress?: string;
companyEmail?: string;
companyPhone?: string;
logo?: string;
defaultCurrency?: string;
}
export interface Profile extends Row {
tenantId: string;
userId: string;
displayName?: string;
phone?: string;
title?: string;
}
export type ConnectionStatus = "pending" | "approved" | "rejected";
export interface Connection extends Row {
clinicTenantId: string;
labTenantId: string;
status: ConnectionStatus;
requestedBy: string;
requestedAt: string;
approvedAt?: string;
rejectedAt?: string;
}
export type JobStatus = "pending" | "in_progress" | "sent" | "delivered" | "cancelled";
export type JobStep = "olcu" | "alt_yapi_prova" | "ust_yapi_prova" | "cila_bitim";
export type ProstheticType =
| "metal_porselen"
| "zirkonyum"
| "implant_ustu_zirkonyum"
| "gecici"
| "e_max"
| "diger";
export interface Patient extends Row {
clinicTenantId: string;
createdBy: string;
patientCode: string;
firstName: string;
lastName: string;
phone?: string;
dateOfBirth?: string;
notes?: string;
archived?: boolean;
}
export interface Job extends Row {
clinicTenantId: string;
labTenantId: string;
createdBy: string;
patientCode: string;
patientId?: string;
prostheticType: ProstheticType;
memberCount: number;
teeth?: string[];
color?: string;
description?: string;
price?: number;
currency?: string;
status: JobStatus;
currentStep?: JobStep;
dueDate?: string;
}
export interface ClinicPricing extends Row {
labTenantId: string;
clinicTenantId: string;
prostheticType: ProstheticType;
customUnitPrice?: number;
discountPercent?: number;
currency?: string;
createdBy: string;
}
export type JobFileKind = "scan" | "image" | "document";
export interface JobFile extends Row {
jobId: string;
clinicTenantId: string;
labTenantId: string;
uploadedBy: string;
kind: JobFileKind;
fileId: string;
name: string;
size: number;
mimeType?: string;
}
export interface JobStatusHistory extends Row {
jobId: string;
clinicTenantId: string;
labTenantId: string;
step: JobStep;
completedBy: string;
completedAt: string;
note?: string;
}
export interface Prosthetic extends Row {
tenantId: string;
createdBy: string;
name: string;
type: ProstheticType;
unitPrice: number;
currency?: string;
archived?: boolean;
}
export type FinanceType = "income" | "expense" | "receivable" | "payable";
export type FinanceStatus = "pending" | "paid" | "cancelled";
export interface FinanceEntry extends Row {
tenantId: string;
createdBy: string;
jobId?: string;
counterpartTenantId?: string;
type: FinanceType;
amount: number;
currency?: string;
status: FinanceStatus;
date: string;
description?: string;
}
export interface Notification extends Row {
tenantId: string;
userId?: string;
jobId?: string;
connectionId?: string;
message: string;
read: boolean;
}
export type AuditAction = "create" | "update" | "delete";
export interface AuditLog extends Row {
tenantId: string;
userId: string;
action: AuditAction;
entityType: string;
entityId: string;
changes?: string;
ipAddress?: string;
userAgent?: string;
}
export type InviteRole = "admin" | "member";
export type InviteStatus = "pending" | "accepted" | "cancelled" | "expired";
export interface InviteLink extends Row {
tenantId: string;
code: string;
email: string;
role?: InviteRole;
status?: InviteStatus;
invitedBy: string;
expiresAt?: string;
acceptedAt?: string;
acceptedBy?: string;
}