init: lab project bootstrapped from isletmem-kovakcrm

- CRM domain modules removed (customers, services, software, calendar, tasks, invoices, leads, finance, etc.)
- DLS branding: package name=lab, logo wordmark, sidebar nav, header CTA
- Tenant layer extended with kind dimension (lab|clinic) + requireTenantKind helper
- Schema rewritten for DLS domain: jobs, job_files, job_status_history, prosthetics, connections, finance_entries, notifications
- Onboarding form: clinic/lab account-type selection + auto-generated memberNumber
- Placeholder routes for jobs/{inbound,outbound,new}, products, finance, connections
- PDF spec + spec.md under belgeler/
- db: lab database + 13 collections + indexes + storage bucket (job-files) provisioned via Appwrite MCP

Ref: belgeler/dls-ui-tasarim.pdf
This commit is contained in:
kovakmedya
2026-05-21 18:28:38 +03:00
commit cb150f7a24
215 changed files with 54262 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
"use server";
import { ID, Permission, Query, Role } from "node-appwrite";
import { createAdminClient, createSessionClient } from "./server";
import { DATABASE_ID, TABLES } from "./schema";
export interface UserPrefs {
theme?: "dark" | "light" | "system";
colorTheme?: string;
tweakcnTheme?: string;
radius?: string;
sidebarVariant?: "sidebar" | "floating" | "inset";
sidebarCollapsible?: "offcanvas" | "icon" | "none";
sidebarSide?: "left" | "right";
}
export async function getUserPrefs(): Promise<UserPrefs> {
try {
const { account } = await createSessionClient();
const user = await account.get();
const { tablesDB } = createAdminClient();
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.userPreferences,
queries: [Query.equal("userId", user.$id), Query.limit(1)],
});
if (result.rows.length === 0) {
// Pre-create an empty row so saveUserPrefsAction always calls updateRow
// (createRow inside a Server Action causes router cache invalidation → remount loop)
try {
await tablesDB.createRow(
DATABASE_ID,
TABLES.userPreferences,
ID.unique(),
{ userId: user.$id },
[
Permission.read(Role.user(user.$id)),
Permission.update(Role.user(user.$id)),
Permission.delete(Role.user(user.$id)),
],
);
} catch {
// race condition or already exists — fine
}
return {};
}
const row = result.rows[0] as Record<string, unknown>;
const str = (v: unknown) => (v && typeof v === "string" ? v : undefined);
return {
theme: (row.theme as UserPrefs["theme"]) ?? undefined,
colorTheme: str(row.colorTheme),
tweakcnTheme: str(row.tweakcnTheme),
radius: str(row.radius),
sidebarVariant: (row.sidebarVariant as UserPrefs["sidebarVariant"]) ?? undefined,
sidebarCollapsible: (row.sidebarCollapsible as UserPrefs["sidebarCollapsible"]) ?? undefined,
sidebarSide: (row.sidebarSide as UserPrefs["sidebarSide"]) ?? undefined,
};
} catch {
return {};
}
}
export async function saveUserPrefsAction(
update: Partial<UserPrefs>,
): Promise<{ ok: boolean; error?: string }> {
try {
const { account } = await createSessionClient();
const user = await account.get();
const { tablesDB } = createAdminClient();
// undefined → skip, "" → null (Appwrite rejects empty strings on nullable attrs)
const clean: Record<string, unknown> = {};
for (const [k, v] of Object.entries(update)) {
if (v === undefined) continue;
clean[k] = v === "" ? null : v;
}
if (Object.keys(clean).length === 0) return { ok: true };
const existing = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.userPreferences,
queries: [Query.equal("userId", user.$id), Query.limit(1)],
});
const perms = [
Permission.read(Role.user(user.$id)),
Permission.update(Role.user(user.$id)),
Permission.delete(Role.user(user.$id)),
];
if (existing.rows.length === 0) {
await tablesDB.createRow(
DATABASE_ID,
TABLES.userPreferences,
ID.unique(),
{ userId: user.$id, ...clean },
perms,
);
} else {
await tablesDB.updateRow(
DATABASE_ID,
TABLES.userPreferences,
existing.rows[0].$id,
clean,
);
}
return { ok: true };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error("[saveUserPrefsAction]", msg);
return { ok: false, error: msg };
}
}