feat(services): customer-linked services CRUD module

Same pattern as customers. Each service belongs to one customer (FK), has
unit price (TRY), billing period (monthly/yearly/onetime), and recurring
flag.

- lib/validation/services.ts: Zod schema with TRY price coercion (accepts
  comma decimal), enum billing period, recurring boolean coercion.
- lib/appwrite/service-actions.ts: createServiceAction, updateServiceAction,
  deleteServiceAction. Same tenant guard, audit log, team-scoped row perms.
- lib/appwrite/service-queries.ts: listServices, listServicesByCustomer.
- lib/format.ts: formatTRY (Intl.NumberFormat tr-TR), formatDate/DateTime,
  BILLING_PERIOD_LABEL mapping (Aylık/Yıllık/Tek seferlik).
- /services page (server): joins services with customer names via
  in-memory map.
- ServicesClient: TanStack table with global filter (name/customer/desc),
  Repeat icon next to recurring badges, formatted TRY column.
- ServiceFormSheet: customer dropdown (disabled if no customers exist),
  unit price + billing period + recurring switch.
- DeleteServiceDialog: same destructive confirmation pattern.

Empty state on /services CTA's user back to add a customer first if none
exist.
This commit is contained in:
kovakmedya
2026-04-30 05:46:55 +03:00
parent 94f2c92da1
commit a15a1c1c1a
10 changed files with 950 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import { z } from "zod";
export const serviceSchema = z.object({
customerId: z.string().min(1, "Müşteri seçin."),
name: z.string().trim().min(1, "Hizmet adı zorunlu.").max(255),
description: z
.string()
.trim()
.max(2000)
.optional()
.transform((v) => (v ? v : undefined)),
unitPrice: z
.union([z.number(), z.string()])
.transform((v) => (typeof v === "string" ? Number(v.replace(",", ".")) : v))
.pipe(z.number().nonnegative("Negatif olamaz.")),
recurring: z
.union([z.boolean(), z.literal("on"), z.literal(""), z.undefined()])
.transform((v) => v === true || v === "on")
.optional(),
billingPeriod: z.enum(["monthly", "yearly", "onetime"]).optional().default("onetime"),
});
export type ServiceInput = z.infer<typeof serviceSchema>;