feat(settings): /settings/workspace — edit company info + invoice defaults

Owner/admin edit, member read-only.

Schema/validation:
- lib/validation/workspace.ts (workspaceSettingsSchema)
- lib/appwrite/workspace-actions.ts:
  * updateWorkspaceSettingsAction — requireRole owner|admin, upserts the
    tenant_settings row (creates one with team-scoped perms if absent,
    e.g. for tenants created before tenant_settings was a table; just
    defense). Audit-logged.
  * Forces invoicePrefix to uppercase. defaultVatRate clamped to [0, 100].
  * revalidatePath('/', 'layout') so the new company name updates in
    sidebar header and dashboard greeting on next render.

UI:
- /settings/workspace page (server) — pulls active tenant settings
  via requireTenant, shows form pre-filled.
- WorkspaceSettingsForm: 2 cards
  * Şirket — name (required), tax id, phone, email, address
  * Faturalama — invoicePrefix, defaultVatRate, read-only invoiceCounter
- All inputs disabled if user is a member (canEdit=false). Submit button
  hidden in that case. Description on the page changes accordingly.
- Toast feedback for success/error.

Skipped: logo upload (storage bucket pending). Will revisit.
This commit is contained in:
kovakmedya
2026-04-30 06:44:11 +03:00
parent 02a02ba9e6
commit fc091b9e0d
5 changed files with 399 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
import { z } from "zod";
export const workspaceSettingsSchema = z.object({
companyName: z.string().trim().min(1, "Şirket adı zorunlu.").max(255),
companyTaxId: z
.string()
.trim()
.max(50)
.optional()
.transform((v) => (v ? v : undefined)),
companyAddress: z
.string()
.trim()
.max(500)
.optional()
.transform((v) => (v ? v : undefined)),
companyEmail: z
.union([z.string().email("Geçerli bir email girin."), z.literal("")])
.optional()
.transform((v) => (v ? v : undefined)),
companyPhone: z
.string()
.trim()
.max(30)
.optional()
.transform((v) => (v ? v : undefined)),
defaultVatRate: z
.union([z.number(), z.string()])
.optional()
.transform((v) => {
if (v === undefined || v === "") return 20;
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
return Number.isFinite(n) ? n : 20;
})
.pipe(z.number().min(0, "Negatif olamaz.").max(100, "100'den büyük olamaz.")),
invoicePrefix: z
.string()
.trim()
.max(10)
.optional()
.transform((v) => (v ? v.toUpperCase() : "INV")),
});
export type WorkspaceSettingsInput = z.infer<typeof workspaceSettingsSchema>;