feat(software): catalog + customer assignments (M2M)
Software catalog with per-customer assignments via the customer_software
join table. Two tabs in one /software page:
Catalog tab:
- Software CRUD: name, version, description, defaultFee (TRY).
- Deleting a software cascades and removes all its assignments first
(best-effort loop, then the catalog row), all wrapped in audit logs.
Assignments tab:
- M2M between customer and software with own fee (overrides defaultFee),
billingPeriod (monthly default), startDate/endDate, notes.
- Form auto-fills fee from selected software's defaultFee.
- Both Sheet forms localized; date inputs round-tripped via toIsoDate
(Appwrite expects ISO 8601 with TZ; HTML date input gives YYYY-MM-DD).
- Delete dialogs differentiated for catalog ('siliniyor') vs assignment
('kaldırılıyor').
New files:
- lib/validation/software.ts (softwareSchema + customerSoftwareSchema)
- lib/appwrite/software-actions.ts (6 server actions)
- lib/appwrite/software-queries.ts (listSoftware, listAssignments)
- lib/appwrite/software-types.ts (form state)
- /software route with SoftwareClient (Tabs), SoftwareFormSheet,
AssignmentFormSheet, inline delete dialogs.
Empty states surface the right next-step CTA: 'önce müşteri ekleyin', or
'önce yazılım ekleyin', as appropriate.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const softwareSchema = z.object({
|
||||
name: z.string().trim().min(1, "Yazılım adı zorunlu.").max(255),
|
||||
version: z.string().trim().max(50).optional().transform((v) => (v ? v : undefined)),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2000)
|
||||
.optional()
|
||||
.transform((v) => (v ? v : undefined)),
|
||||
defaultFee: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return undefined;
|
||||
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}),
|
||||
});
|
||||
|
||||
export type SoftwareInput = z.infer<typeof softwareSchema>;
|
||||
|
||||
export const customerSoftwareSchema = z.object({
|
||||
customerId: z.string().min(1, "Müşteri seçin."),
|
||||
softwareId: z.string().min(1, "Yazılım seçin."),
|
||||
startDate: z.string().optional().transform((v) => (v ? v : undefined)),
|
||||
endDate: z.string().optional().transform((v) => (v ? v : undefined)),
|
||||
fee: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return undefined;
|
||||
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}),
|
||||
billingPeriod: z.enum(["monthly", "yearly", "onetime"]).optional().default("monthly"),
|
||||
notes: z.string().trim().max(1000).optional().transform((v) => (v ? v : undefined)),
|
||||
});
|
||||
|
||||
export type CustomerSoftwareInput = z.infer<typeof customerSoftwareSchema>;
|
||||
Reference in New Issue
Block a user