From 94f2c92da1b922bea15ab9ea080d1177a2979c99 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 30 Apr 2026 05:44:00 +0300 Subject: [PATCH] =?UTF-8?q?feat(customers):=20full=20CRUD=20module=20?= =?UTF-8?q?=E2=80=94=20pattern=20for=20all=20other=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes the multi-tenant module pattern. Subsequent modules (services, software, calendar, tasks, finance, invoices) will copy this structure. Validation: - lib/validation/customers.ts: Zod schema with Turkish messages, optional fields normalized to undefined. Server actions (lib/appwrite/customer-actions.ts): - createCustomerAction, updateCustomerAction, deleteCustomerAction - All call requireTenant() guard, write team-scoped row permissions (read+update by team, delete by owner|admin), and emit audit log. - Update/delete cross-check tenantId on the existing row before mutating (defense in depth even though row-level perms already enforce it). - Field-level errors flattened from Zod for inline form display. Server-side queries (lib/appwrite/customer-queries.ts): - listCustomers(tenantId), getCustomer(tenantId, id) — admin SDK with Query.equal('tenantId',...) tenant scope. UI: - /customers page (server component): pulls active tenant context, lists customers, hands off to CustomersClient. - CustomersClient: TanStack Table with global filter (name/email/phone/ taxId), column sorting on name + createdAt, pagination (20/page), status badges, row actions (Edit/Delete dropdown), empty-state CTA. - CustomerFormSheet: shadcn Sheet-based add/edit form with all fields, toast feedback (sonner), inline field errors. Reused for create + update by switching the action. - DeleteCustomerDialog: confirmation modal with destructive button. Infrastructure: - Added sonner Toaster to root layout (richColors, closeButton). - Updated metadata to 'İşletmem KovakCRM' and html lang='tr'. - Renamed theme storage key to isletmem-ui-theme. --- .../components/customer-form-sheet.tsx | 187 +++++++++++ .../customers/components/customers-client.tsx | 314 ++++++++++++++++++ .../components/delete-customer-dialog.tsx | 76 +++++ .../(dashboard)/customers/components/types.ts | 11 + src/app/(dashboard)/customers/page.tsx | 47 +++ src/app/layout.tsx | 14 +- src/lib/appwrite/customer-actions.ts | 185 +++++++++++ src/lib/appwrite/customer-queries.ts | 42 +++ src/lib/appwrite/customer-types.ts | 7 + src/lib/validation/customers.ts | 16 + 10 files changed, 892 insertions(+), 7 deletions(-) create mode 100644 src/app/(dashboard)/customers/components/customer-form-sheet.tsx create mode 100644 src/app/(dashboard)/customers/components/customers-client.tsx create mode 100644 src/app/(dashboard)/customers/components/delete-customer-dialog.tsx create mode 100644 src/app/(dashboard)/customers/components/types.ts create mode 100644 src/app/(dashboard)/customers/page.tsx create mode 100644 src/lib/appwrite/customer-actions.ts create mode 100644 src/lib/appwrite/customer-queries.ts create mode 100644 src/lib/appwrite/customer-types.ts create mode 100644 src/lib/validation/customers.ts diff --git a/src/app/(dashboard)/customers/components/customer-form-sheet.tsx b/src/app/(dashboard)/customers/components/customer-form-sheet.tsx new file mode 100644 index 0000000..530e5d9 --- /dev/null +++ b/src/app/(dashboard)/customers/components/customer-form-sheet.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useActionState, useEffect } from "react"; +import { Loader2, Save } 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 { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Textarea } from "@/components/ui/textarea"; +import { + createCustomerAction, + updateCustomerAction, +} from "@/lib/appwrite/customer-actions"; +import { initialCustomerState } from "@/lib/appwrite/customer-types"; +import type { CustomerRow } from "./types"; + +type Props = { + open: boolean; + onOpenChange: (v: boolean) => void; + customer?: CustomerRow | null; +}; + +export function CustomerFormSheet({ open, onOpenChange, customer }: Props) { + const isEdit = Boolean(customer); + const action = isEdit ? updateCustomerAction : createCustomerAction; + const [state, formAction, isPending] = useActionState(action, initialCustomerState); + + useEffect(() => { + if (state.ok) { + toast.success(isEdit ? "Müşteri güncellendi." : "Müşteri eklendi."); + onOpenChange(false); + } else if (state.error) { + toast.error(state.error); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state]); + + return ( + + + + {isEdit ? "Müşteriyi düzenle" : "Yeni müşteri"} + + {isEdit + ? "Müşteri bilgilerini güncelleyin." + : "Yeni bir müşteri ekleyin. * işaretli alanlar zorunludur."} + + + +
+ {isEdit && customer && } + +
+
+ + + {state.fieldErrors?.name && ( +

{state.fieldErrors.name}

+ )} +
+ +
+
+ + + {state.fieldErrors?.email && ( +

{state.fieldErrors.email}

+ )} +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +