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:
kovakmedya
2026-04-30 05:50:33 +03:00
parent a15a1c1c1a
commit 113988273f
9 changed files with 1483 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
import "server-only";
import { Query } from "node-appwrite";
import { createAdminClient } from "./server";
import {
DATABASE_ID,
TABLES,
type CustomerSoftware,
type Software,
} from "./schema";
export async function listSoftware(tenantId: string): Promise<Software[]> {
try {
const { tablesDB } = createAdminClient();
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.software,
queries: [
Query.equal("tenantId", tenantId),
Query.orderAsc("name"),
Query.limit(500),
],
});
return result.rows as unknown as Software[];
} catch {
return [];
}
}
export async function listAssignments(tenantId: string): Promise<CustomerSoftware[]> {
try {
const { tablesDB } = createAdminClient();
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.customerSoftware,
queries: [
Query.equal("tenantId", tenantId),
Query.orderDesc("$createdAt"),
Query.limit(1000),
],
});
return result.rows as unknown as CustomerSoftware[];
} catch {
return [];
}
}