From 232309e353a5ec47dffe6861dc2d35d13d5afd3c Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Wed, 15 Apr 2026 06:23:29 +0200 Subject: [PATCH] fix: update admin forms to match DB schema --- admin-solid.dev.log | 0 admin-solid.dev.pid | 1 + src/routes/admin/coupon.tsx | 589 ++++--- src/routes/admin/department.tsx | 1383 ++++++++++------ src/routes/admin/designation.tsx | 1457 +++++++++++------ src/routes/admin/employees/create.tsx | 559 ++++--- .../external-dashboard-management/index.tsx | 1216 +++++++++----- src/routes/admin/jobs/[id].tsx | 147 +- src/routes/admin/kb/articles/[id]/edit.tsx | 195 ++- src/routes/admin/roles/create.tsx | 663 ++++---- src/routes/admin/support.tsx | 924 +++++++---- src/routes/admin/users/[id]/edit.tsx | 206 ++- 12 files changed, 4520 insertions(+), 2820 deletions(-) create mode 100644 admin-solid.dev.log create mode 100644 admin-solid.dev.pid diff --git a/admin-solid.dev.log b/admin-solid.dev.log new file mode 100644 index 0000000..e69de29 diff --git a/admin-solid.dev.pid b/admin-solid.dev.pid new file mode 100644 index 0000000..d7be241 --- /dev/null +++ b/admin-solid.dev.pid @@ -0,0 +1 @@ +7741 diff --git a/src/routes/admin/coupon.tsx b/src/routes/admin/coupon.tsx index 097387c..77d02cf 100644 --- a/src/routes/admin/coupon.tsx +++ b/src/routes/admin/coupon.tsx @@ -1,42 +1,42 @@ -import { createSignal, createMemo, onMount, Show, For } from 'solid-js'; +import { createSignal, createMemo, onMount, Show, For } from "solid-js"; -const API = ''; +const API = ""; function getToken(): string { - return typeof sessionStorage !== 'undefined' - ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' - : ''; + return typeof sessionStorage !== "undefined" + ? sessionStorage.getItem("nxtgauge_admin_access_token") || "" + : ""; } function authHeaders(): Record { const token = getToken(); return { - Accept: 'application/json', - 'Content-Type': 'application/json', + Accept: "application/json", + "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } const ROLE_OPTIONS = [ - 'company', - 'customer', - 'job_seeker', - 'photographer', - 'video_editor', - 'graphic_designer', - 'social_media_manager', - 'fitness_trainer', - 'catering_services', - 'makeup_artist', - 'tutor', - 'developer', + "company", + "customer", + "job_seeker", + "photographer", + "video_editor", + "graphic_designer", + "social_media_manager", + "fitness_trainer", + "catering_services", + "makeup_artist", + "tutor", + "developer", ]; type Coupon = { id: string; code: string; title: string; - type: 'PERCENT' | 'FIXED'; + type: "PERCENT" | "FIXED"; value: number; min_order_amount: number; used_count: number; @@ -45,44 +45,50 @@ type Coupon = { role_keys: string[]; }; - const defaultForm = () => ({ - id: '', - code: '', - title: '', - type: 'PERCENT' as 'PERCENT' | 'FIXED', + id: "", + code: "", + title: "", + type: "PERCENT" as "PERCENT" | "FIXED", value: 10, min_order_amount: 0, - max_uses: '', - role_keys: ['company', 'customer'] as string[], + max_uses: "", + applies_to: "ALL" as "ALL" | "ROLE", + role_keys: ["company", "customer"] as string[], }); export default function CouponPage() { const [coupons, setCoupons] = createSignal([]); const [loading, setLoading] = createSignal(true); - const [loadError, setLoadError] = createSignal(''); - const [activeTab, setActiveTab] = createSignal<'list' | 'create'>('list'); + const [loadError, setLoadError] = createSignal(""); + const [activeTab, setActiveTab] = createSignal<"list" | "create">("list"); const [form, setForm] = createSignal(defaultForm()); const [saving, setSaving] = createSignal(false); - const [toggling, setToggling] = createSignal(''); - const [formError, setFormError] = createSignal(''); + const [toggling, setToggling] = createSignal(""); + const [formError, setFormError] = createSignal(""); // Filters - const [search, setSearch] = createSignal(''); - const [statusFilter, setStatusFilter] = createSignal('all'); - const [sortBy, setSortBy] = createSignal<'newest' | 'oldest' | 'code_asc' | 'code_desc'>('newest'); + const [search, setSearch] = createSignal(""); + const [statusFilter, setStatusFilter] = createSignal("all"); + const [sortBy, setSortBy] = createSignal<"newest" | "oldest" | "code_asc" | "code_desc">( + "newest" + ); const [sortMenuOpen, setSortMenuOpen] = createSignal(false); const [filterMenuOpen, setFilterMenuOpen] = createSignal(false); const load = async () => { - setLoading(true); setLoadError(''); + setLoading(true); + setLoadError(""); try { - const res = await fetch(`${API}/api/admin/coupons`, { headers: authHeaders(), credentials: 'include' }); + const res = await fetch(`${API}/api/admin/coupons`, { + headers: authHeaders(), + credentials: "include", + }); if (!res.ok) throw new Error(`Request failed (${res.status})`); const data = await res.json(); setCoupons(Array.isArray(data) ? data : (data.coupons ?? [])); } catch (err: any) { - setLoadError(err.message || 'Could not load coupons.'); + setLoadError(err.message || "Could not load coupons."); setCoupons([]); } finally { setLoading(false); @@ -94,59 +100,62 @@ export default function CouponPage() { const filteredCoupons = createMemo(() => { let r = coupons(); const q = search().toLowerCase(); - if (q) r = r.filter((c) => c.code.toLowerCase().includes(q) || (c.title || '').toLowerCase().includes(q)); - if (statusFilter() === 'active') r = r.filter((c) => c.is_active); - if (statusFilter() === 'inactive') r = r.filter((c) => !c.is_active); + if (q) + r = r.filter( + (c) => c.code.toLowerCase().includes(q) || (c.title || "").toLowerCase().includes(q) + ); + if (statusFilter() === "active") r = r.filter((c) => c.is_active); + if (statusFilter() === "inactive") r = r.filter((c) => !c.is_active); const sorted = [...r]; sorted.sort((a, b) => { - if (sortBy() === 'oldest') return String(a.id || '').localeCompare(String(b.id || '')); - if (sortBy() === 'code_asc') return String(a.code || '').localeCompare(String(b.code || '')); - if (sortBy() === 'code_desc') return String(b.code || '').localeCompare(String(a.code || '')); - return String(b.id || '').localeCompare(String(a.id || '')); + if (sortBy() === "oldest") return String(a.id || "").localeCompare(String(b.id || "")); + if (sortBy() === "code_asc") return String(a.code || "").localeCompare(String(b.code || "")); + if (sortBy() === "code_desc") return String(b.code || "").localeCompare(String(a.code || "")); + return String(b.id || "").localeCompare(String(a.id || "")); }); r = sorted; return r; }); const exportCsv = () => { - const headers = ['Code', 'Title', 'Type', 'Value', 'Max Uses', 'Status']; + const headers = ["Code", "Title", "Type", "Value", "Max Uses", "Status"]; const rows = filteredCoupons().map((item) => [ item.code, - item.title || '', + item.title || "", item.type, - item.type === 'PERCENT' ? `${item.value}%` : `₹${item.value}`, - item.usage_limit != null ? String(item.usage_limit) : '—', - item.is_active ? 'Active' : 'Inactive', + item.type === "PERCENT" ? `${item.value}%` : `₹${item.value}`, + item.usage_limit != null ? String(item.usage_limit) : "—", + item.is_active ? "Active" : "Inactive", ]); const csv = [headers, ...rows] - .map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) - .join('\n'); - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + .map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")) + .join("\n"); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = url; - link.download = 'coupon-management.csv'; + link.download = "coupon-management.csv"; link.click(); URL.revokeObjectURL(url); }; const resetForm = () => { setForm(defaultForm()); - setFormError(''); + setFormError(""); }; const startEdit = (coupon: Coupon) => { setForm({ id: coupon.id, code: coupon.code, - title: coupon.title || '', + title: coupon.title || "", type: coupon.type, value: coupon.value, min_order_amount: coupon.min_order_amount || 0, - max_uses: coupon.usage_limit != null ? String(coupon.usage_limit) : '', + max_uses: coupon.usage_limit != null ? String(coupon.usage_limit) : "", role_keys: Array.isArray(coupon.role_keys) ? coupon.role_keys : [], }); - setActiveTab('create'); + setActiveTab("create"); }; const toggleRole = (role: string) => { @@ -162,32 +171,33 @@ export default function CouponPage() { e.preventDefault(); try { setSaving(true); - setFormError(''); + setFormError(""); const f = form(); const body: Record = { code: f.code.toUpperCase(), title: f.title, - type: f.type, - value: Number(f.value), + discount_type: f.type, + discount_value: Number(f.value), + applies_to: f.applies_to, min_order_amount: Number(f.min_order_amount), role_keys: f.role_keys, }; if (f.max_uses) body.max_uses = Number(f.max_uses); const url = f.id ? `${API}/api/admin/coupons/${f.id}` : `${API}/api/admin/coupons`; - const method = f.id ? 'PATCH' : 'POST'; + const method = f.id ? "PATCH" : "POST"; const res = await fetch(url, { method, headers: authHeaders(), - credentials: 'include', + credentials: "include", body: JSON.stringify(body), }); - if (!res.ok) throw new Error('Failed to save coupon'); + if (!res.ok) throw new Error("Failed to save coupon"); resetForm(); await load(); - setActiveTab('list'); + setActiveTab("list"); } catch (err: unknown) { - setFormError(err instanceof Error ? err.message : 'Failed to save'); + setFormError(err instanceof Error ? err.message : "Failed to save"); } finally { setSaving(false); } @@ -197,48 +207,59 @@ export default function CouponPage() { try { setToggling(coupon.id); const res = await fetch(`${API}/api/admin/coupons/${coupon.id}`, { - method: 'PATCH', + method: "PATCH", headers: authHeaders(), - credentials: 'include', + credentials: "include", body: JSON.stringify({ is_active: !coupon.is_active }), }); - if (!res.ok) throw new Error('Failed to toggle'); + if (!res.ok) throw new Error("Failed to toggle"); await load(); } catch { // ignore } finally { - setToggling(''); + setToggling(""); } }; return ( -
-
-

Coupon Management

-

Reusable coupon codes for package checkout

-
+
+
+

Coupon Management

+

Reusable coupon codes for package checkout

+
- {/* Tabs */} -
- - -
+ {/* Tabs */} +
+ + +
-
- -
+
+ +
- + {(item) => ( - )} @@ -278,21 +328,44 @@ export default function CouponPage() {
- + {(item) => ( - )} @@ -301,13 +374,30 @@ export default function CouponPage() {
-
-
{loadError()}
+
+ {loadError()} +
@@ -325,35 +415,62 @@ export default function CouponPage() { - Loading... + + + Loading... + + - No coupons found. + + + No coupons found. + + 0}> {(item) => ( - {item.code} - {item.title || '—'} + + {item.code} + + {item.title || "—"} {item.type} - {item.type === 'PERCENT' ? `${item.value}%` : `₹${item.value}`} - {item.usage_limit != null ? item.usage_limit : '—'} + + {item.type === "PERCENT" ? `${item.value}%` : `₹${item.value}`} + + + {item.usage_limit != null ? item.usage_limit : "—"} + - - - {item.is_active ? 'Active' : 'Inactive'} + + + {item.is_active ? "Active" : "Inactive"}
- +
@@ -370,112 +487,146 @@ export default function CouponPage() {
-
-
+
+ - -
-

{form().id ? 'Edit Coupon' : 'Create Coupon'}

- -
{formError()}
-
-
+ +
+

+ {form().id ? "Edit Coupon" : "Create Coupon"} +

+ +
+ {formError()} +
+
+ +
+ + setForm({ ...form(), code: e.currentTarget.value.toUpperCase() })} + required + placeholder="e.g. SAVE10" + style="text-transform:uppercase" + /> +
+
+ + setForm({ ...form(), title: e.currentTarget.value })} + required + placeholder="e.g. 10% off for companies" + /> +
+
- + + +
+
+ setForm({ ...form(), code: e.currentTarget.value.toUpperCase() })} + type="number" + value={form().value} + onInput={(e) => setForm({ ...form(), value: Number(e.currentTarget.value) })} required - placeholder="e.g. SAVE10" - style="text-transform:uppercase" + min="1" + /> +
+
+
+
+ + + setForm({ ...form(), min_order_amount: Number(e.currentTarget.value) }) + } + min="0" + placeholder="0" />
- + setForm({ ...form(), title: e.currentTarget.value })} - required - placeholder="e.g. 10% off for companies" + type="number" + value={form().max_uses} + onInput={(e) => setForm({ ...form(), max_uses: e.currentTarget.value })} + min="1" + placeholder="Unlimited" />
-
-
- - -
-
- - setForm({ ...form(), value: Number(e.currentTarget.value) })} - required - min="1" - /> -
+
+ +
-
-
- - setForm({ ...form(), min_order_amount: Number(e.currentTarget.value) })} - min="0" - placeholder="0" - /> -
-
- - setForm({ ...form(), max_uses: e.currentTarget.value })} - min="1" - placeholder="Unlimited" - /> -
+
+
+

+ Applicable Roles +

+
+ + {(role) => { + const active = () => form().role_keys.includes(role); + return ( + + ); + }} +
-
-

Applicable Roles

-
- - {(role) => { - const active = () => form().role_keys.includes(role); - return ( - - ); - }} - -
-
-
-
+
+ + + - - - -
- -
-
-
+
+
+ + +
+
); } diff --git a/src/routes/admin/department.tsx b/src/routes/admin/department.tsx index 522cc4c..43f8967 100644 --- a/src/routes/admin/department.tsx +++ b/src/routes/admin/department.tsx @@ -1,8 +1,8 @@ -import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; -import { useSearchParams } from '@solidjs/router'; -import type { CrudRecord } from '~/lib/admin/types'; +import { For, Show, createMemo, createSignal, onMount } from "solid-js"; +import { useSearchParams } from "@solidjs/router"; +import type { CrudRecord } from "~/lib/admin/types"; -const API = ''; +const API = ""; type DepartmentRecord = CrudRecord & { code?: string; @@ -11,15 +11,15 @@ type DepartmentRecord = CrudRecord & { createdDate?: string; departmentHead?: string; departmentEmail?: string; - transfersEnabled?: boolean; - visibility?: 'INTERNAL' | 'EXTERNAL'; }; - const permissionGroups = [ - { title: 'Employee Management', items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'] }, - { title: 'Role Management', items: ['View Roles', 'Assign Roles'] }, - { title: 'Department Settings', items: ['Manage Department Settings'] }, + { + title: "Employee Management", + items: ["View Employees", "Create Employees", "Edit Employees", "Delete Employees"], + }, + { title: "Role Management", items: ["View Roles", "Assign Roles"] }, + { title: "Department Settings", items: ["Manage Department Settings"] }, ]; type DepartmentListResponse = { @@ -29,66 +29,76 @@ type DepartmentListResponse = { }; function normalizeDepartment(item: any, idx: number): DepartmentRecord { - const status = String(item.status ?? '').toUpperCase(); - const isActive = typeof item.is_active === 'boolean' - ? item.is_active - : status - ? status === 'ACTIVE' - : true; + const status = String(item.status ?? "").toUpperCase(); + const isActive = + typeof item.is_active === "boolean" ? item.is_active : status ? status === "ACTIVE" : true; return { id: String(item.id ?? `dep-${idx + 1}`), - name: String(item.name ?? ''), + name: String(item.name ?? ""), code: item.code ? String(item.code) : undefined, description: item.description ? String(item.description) : undefined, totalEmployees: Number(item.total_employees ?? 0), departmentHead: item.department_head ? String(item.department_head) : undefined, departmentEmail: item.department_email ? String(item.department_email) : undefined, - transfersEnabled: Boolean(item.transfers_enabled ?? false), - visibility: String(item.visibility ?? 'INTERNAL').toUpperCase() === 'EXTERNAL' ? 'EXTERNAL' : 'INTERNAL', - status: isActive ? 'ACTIVE' : 'INACTIVE', - updatedAt: String(item.updated_at ?? ''), - createdDate: String(item.created_at ?? ''), + status: isActive ? "ACTIVE" : "INACTIVE", + updatedAt: String(item.updated_at ?? ""), + createdDate: String(item.created_at ?? ""), }; } function StatusBadge(props: { status: string }) { - const active = () => props.status === 'ACTIVE'; + const active = () => props.status === "ACTIVE"; return ( - - - {active() ? 'Active' : 'Inactive'} + + + {active() ? "Active" : "Inactive"} ); } -function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) { - return ( - - ); +function FormInput(props: { + label: string; + required?: boolean; + value: string; + onInput: (v: string) => void; + placeholder?: string; + type?: string; + }) { + return ( + + ); } export default function DepartmentManagementPage() { const [searchParams] = useSearchParams(); - const isPreview = () => searchParams._preview === '1'; + const isPreview = () => searchParams._preview === "1"; - const [view, setView] = createSignal<'list' | 'form'>('list'); - const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general'); - const [listTab, setListTab] = createSignal<'all' | 'create' | 'view' | 'inactive'>('all'); - const [search, setSearch] = createSignal(''); - const [statusFilter, setStatusFilter] = createSignal('all'); - const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'employees_desc' | 'employees_asc'>('name_asc'); + const [view, setView] = createSignal<"list" | "form">("list"); + const [formTab, setFormTab] = createSignal<"general" | "settings" | "permissions">("general"); + const [listTab, setListTab] = createSignal<"all" | "create" | "view" | "inactive">("all"); + const [search, setSearch] = createSignal(""); + const [statusFilter, setStatusFilter] = createSignal("all"); + const [sortBy, setSortBy] = createSignal< + "name_asc" | "name_desc" | "employees_desc" | "employees_asc" + >("name_asc"); const [sortMenuOpen, setSortMenuOpen] = createSignal(false); const [filterMenuOpen, setFilterMenuOpen] = createSignal(false); const [rows, setRows] = createSignal([]); @@ -99,39 +109,38 @@ export default function DepartmentManagementPage() { const [deleteTarget, setDeleteTarget] = createSignal(null); const [isDeleting, setIsDeleting] = createSignal(false); - const [name, setName] = createSignal(''); - const [code, setCode] = createSignal(''); - const [description, setDescription] = createSignal(''); - const [departmentHead, setDepartmentHead] = createSignal(''); - const [departmentEmail, setDepartmentEmail] = createSignal(''); - const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE'); - const [transfersEnabled, setTransfersEnabled] = createSignal(false); - const [visibility, setVisibility] = createSignal<'INTERNAL' | 'EXTERNAL'>('INTERNAL'); + const [name, setName] = createSignal(""); + const [code, setCode] = createSignal(""); + const [description, setDescription] = createSignal(""); + const [departmentHead, setDepartmentHead] = createSignal(""); + const [departmentEmail, setDepartmentEmail] = createSignal(""); + const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE"); const [isLoading, setIsLoading] = createSignal(false); const [isSaving, setIsSaving] = createSignal(false); - const [error, setError] = createSignal(''); + const [error, setError] = createSignal(""); const load = async () => { setIsLoading(true); - setError(''); + setError(""); try { - const accessToken = typeof sessionStorage !== 'undefined' - ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' - : ''; + const accessToken = + typeof sessionStorage !== "undefined" + ? sessionStorage.getItem("nxtgauge_admin_access_token") || "" + : ""; const params = new URLSearchParams({ - page: '1', - per_page: '100', + page: "1", + per_page: "100", q: search().trim(), }); - if (statusFilter() !== 'all') { - params.set('status', statusFilter()); + if (statusFilter() !== "all") { + params.set("status", statusFilter()); } const res = await fetch(`${API}/api/admin/departments?${params.toString()}`, { headers: { - Accept: 'application/json', + Accept: "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, - credentials: 'include', + credentials: "include", }); if (!res.ok) throw new Error(`Request failed (${res.status})`); const payload = (await res.json().catch(() => null)) as DepartmentListResponse | null; @@ -146,7 +155,7 @@ export default function DepartmentManagementPage() { : []; setRows(list.map(normalizeDepartment)); } catch (err: any) { - setError(err?.message || 'Could not reach departments API.'); + setError(err?.message || "Could not reach departments API."); setRows([]); } finally { setIsLoading(false); @@ -157,21 +166,28 @@ export default function DepartmentManagementPage() { const filteredRows = createMemo(() => { let r = rows(); - if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase()); + if (statusFilter() !== "all") r = r.filter((d) => d.status === statusFilter().toUpperCase()); const q = search().toLowerCase(); if (q) { - r = r.filter((d) => - d.name.toLowerCase().includes(q) - || String(d.code ?? '').toLowerCase().includes(q) - || String(d.description ?? '').toLowerCase().includes(q) + r = r.filter( + (d) => + d.name.toLowerCase().includes(q) || + String(d.code ?? "") + .toLowerCase() + .includes(q) || + String(d.description ?? "") + .toLowerCase() + .includes(q) ); } const sorted = [...r]; const mode = sortBy(); sorted.sort((a, b) => { - if (mode === 'name_desc') return b.name.localeCompare(a.name); - if (mode === 'employees_desc') return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0); - if (mode === 'employees_asc') return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0); + if (mode === "name_desc") return b.name.localeCompare(a.name); + if (mode === "employees_desc") + return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0); + if (mode === "employees_asc") + return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0); return a.name.localeCompare(b.name); }); r = sorted; @@ -179,107 +195,138 @@ export default function DepartmentManagementPage() { }); const resetForm = () => { - setEditingId(null); setName(''); setCode(''); setDescription(''); - setDepartmentHead(''); setDepartmentEmail(''); setStatus('ACTIVE'); - setTransfersEnabled(false); setVisibility('INTERNAL'); setFormTab('general'); setError(''); + setEditingId(null); + setName(""); + setCode(""); + setDescription(""); + setDepartmentHead(""); + setDepartmentEmail(""); + setStatus("ACTIVE"); + setFormTab("general"); + setError(""); }; - const openCreate = () => { resetForm(); setView('form'); }; + const openCreate = () => { + resetForm(); + setView("form"); + }; const openEdit = (row: DepartmentRecord) => { setEditingId(row.id); - setName(row.name || ''); setCode(String(row.code || '')); - setDescription(String(row.description || '')); - setDepartmentHead(String(row.departmentHead || '')); - setDepartmentEmail(String(row.departmentEmail || '')); - setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE'); - setTransfersEnabled(Boolean(row.transfersEnabled)); - setVisibility(row.visibility === 'EXTERNAL' ? 'EXTERNAL' : 'INTERNAL'); - setFormTab('general'); setView('form'); setOpenMenuId(null); + setName(row.name || ""); + setCode(String(row.code || "")); + setDescription(String(row.description || "")); + setDepartmentHead(String(row.departmentHead || "")); + setDepartmentEmail(String(row.departmentEmail || "")); + setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE"); + setFormTab("general"); + setView("form"); + setOpenMenuId(null); }; const save = async () => { - if (!name().trim() || !code().trim()) { - setError('Department name and code are required.'); - setFormTab('general'); + if (!name().trim()) { + setError("Department name is required."); + setFormTab("general"); + return; + } + if (!code().trim()) { + setError("Department code is required."); + setFormTab("general"); return; } setIsSaving(true); - setError(''); + setError(""); const payload = { name: name().trim(), - code: code().trim(), + code: code().trim() || null, description: description().trim() || null, department_head: departmentHead().trim() || null, department_email: departmentEmail().trim() || null, status: status(), - visibility: visibility(), - transfers_enabled: transfersEnabled(), }; try { - const accessToken = typeof sessionStorage !== 'undefined' - ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' - : ''; + const accessToken = + typeof sessionStorage !== "undefined" + ? sessionStorage.getItem("nxtgauge_admin_access_token") || "" + : ""; const endpoint = editingId() ? `${API}/api/admin/departments/${editingId()}` : `${API}/api/admin/departments`; - const method = editingId() ? 'PATCH' : 'POST'; + const method = editingId() ? "PATCH" : "POST"; const res = await fetch(endpoint, { method, headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', + "Content-Type": "application/json", + Accept: "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, - credentials: 'include', + credentials: "include", body: JSON.stringify(payload), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error((body as any).message || `Request failed (${res.status})`); } - setView('list'); + setView("list"); resetForm(); await load(); } catch (err: any) { - setError(err?.message || 'Failed to save department.'); + setError(err?.message || "Failed to save department."); } finally { setIsSaving(false); } }; const formatDate = (v?: string) => { - const s = v || ''; + const s = v || ""; if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; - return s.slice(0, 10) || '—'; + return s.slice(0, 10) || "—"; }; return ( -
- - {/* Page header */} -
-

Department Management

-

Manage all departments and organizational structure

-
- - {/* ── LIST VIEW ── */} - -
+
+ {/* Page header */} +
+

Department Management

+

+ Manage all departments and organizational structure +

+
+ {/* ── LIST VIEW ── */} + +
{/* Tabs */}
- {([ - { key: 'all', label: 'All Departments', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } }, - { key: 'create', label: 'Create Department', action: () => { setListTab('create'); openCreate(); } }, - { key: 'view', label: 'View Department', action: () => setListTab('view') }, - ] as const).map((tab) => ( + {( + [ + { + key: "all", + label: "All Departments", + action: () => { + setListTab("all"); + setStatusFilter("all"); + void load(); + }, + }, + { + key: "create", + label: "Create Department", + action: () => { + setListTab("create"); + openCreate(); + }, + }, + { key: "view", label: "View Department", action: () => setListTab("view") }, + ] as const + ).map((tab) => ( @@ -287,13 +334,14 @@ export default function DepartmentManagementPage() {
{/* View Department panel */} - - + +

No department selected

-

Click the menu on any department row and choose View Department.

+

+ Click the menu on any department row and choose{" "} + View Department. +

@@ -301,8 +349,12 @@ export default function DepartmentManagementPage() { {/* Header */}
-

{viewingDept()!.name}

-

{viewingDept()!.description || 'No description'}

+

+ {viewingDept()!.name} +

+

+ {viewingDept()!.description || "No description"} +

@@ -310,445 +362,748 @@ export default function DepartmentManagementPage() {
-

Department Code

-

{viewingDept()!.code || '—'}

+

+ Department Code +

+

+ {viewingDept()!.code || "—"} +

-

Department Head

-

{viewingDept()!.departmentHead || '—'}

+

+ Department Head +

+

+ {viewingDept()!.departmentHead || "—"} +

-

Department Email

-

{viewingDept()!.departmentEmail || '—'}

+

+ Department Email +

+

+ {viewingDept()!.departmentEmail || "—"} +

-

Total Employees

-

{String(viewingDept()!.totalEmployees ?? 0)}

-
-
-

Visibility

-

{viewingDept()!.visibility || 'INTERNAL'}

+

+ Total Employees +

+

+ {String(viewingDept()!.totalEmployees ?? 0)} +

+
-

Created Date

-

{viewingDept()!.createdDate?.slice(0, 10) || '—'}

+

+ Created Date +

+

+ {viewingDept()!.createdDate?.slice(0, 10) || "—"} +

{/* Actions */}
- - + +
{/* Table card */} -
-
- - {/* Filter bar */} -
- { setSearch(e.currentTarget.value); void load(); }} - placeholder="Search departments..." - style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none" - /> -
- - -
- {(['name_asc', 'name_desc', 'employees_desc', 'employees_asc'] as const).map((s, i) => ( - - ))} -
-
-
-
- - -
- {(['all', 'active', 'inactive'] as const).map((s) => ( - - ))} -
-
-
- -
- - {/* Table */} -
- - - - - - - - - - - - - - 0} - fallback={ - - - - } - > - - {(row) => ( - - - - - - - - - - )} - - - -
Department NameDepartment CodeDescriptionTotal EmployeesStatusCreated DateActions
-

No departments found

-

Create your first department to get started.

- -
-

{row.name}

-
- {String(row.code || '—')} - -

{String(row.description || '—')}

-
{Number(row.totalEmployees || 0)}{formatDate(String(row.createdDate || row.updatedAt || ''))} - - -
- - - -
- -
- -
-
- - {/* Pagination */} - 0}> -
-

- Showing 1–{filteredRows().length} of {filteredRows().length} departments -

-
- - - - - -
-
-
-
-
-
- - - -
-
-
-

Delete Department?

-

- You are about to permanently delete - {deleteTarget()?.name}. - This action cannot be undone. -

-
-
- - -
-
-
-
- - {/* ── FORM VIEW (Create / Edit) ── */} - - {/* Top tabs */} -
- - -
- -
- {/* Sub-tabs */} -
- {(['general', 'settings', 'permissions'] as const).map((tab, i) => { - const labels = ['General Information', 'Department Settings', 'Permissions']; - const active = () => formTab() === tab; - return ( + placeholder="Search departments..." + style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none" + /> +
- ); - })} -
- -
- -
- {error()} + +
+ {(["name_asc", "name_desc", "employees_desc", "employees_asc"] as const).map( + (s, i) => ( + + ) + )} +
+
-
- - {/* General Information */} - -
-
- - -
-