113988273f
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.
42 lines
1.5 KiB
TypeScript
42 lines
1.5 KiB
TypeScript
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>;
|