nxtgauge-admin-solid/src/routes/admin/department.tsx
2026-04-15 06:23:29 +02:00

1109 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { For, Show, createMemo, createSignal, onMount } from "solid-js";
import { useSearchParams } from "@solidjs/router";
import type { CrudRecord } from "~/lib/admin/types";
const API = "";
type DepartmentRecord = CrudRecord & {
code?: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
departmentHead?: string;
departmentEmail?: string;
};
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"] },
];
type DepartmentListResponse = {
departments?: any[];
data?: any[];
items?: any[];
};
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;
return {
id: String(item.id ?? `dep-${idx + 1}`),
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,
status: isActive ? "ACTIVE" : "INACTIVE",
updatedAt: String(item.updated_at ?? ""),
createdDate: String(item.created_at ?? ""),
};
}
function StatusBadge(props: { status: string }) {
const active = () => props.status === "ACTIVE";
return (
<span
style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? "#FFD8C2" : "#D1D5DB"};background:${active() ? "#FFF1EB" : "#F3F4F6"};color:${active() ? "#FF5E13" : "#4B5563"};padding:2px 10px;font-size:12px;font-weight:500`}
>
<span
style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? "#FF5E13" : "#9CA3AF"};margin-right:5px;flex-shrink:0`}
/>
{active() ? "Active" : "Inactive"}
</span>
);
}
function FormInput(props: {
label: string;
required?: boolean;
value: string;
onInput: (v: string) => void;
placeholder?: string;
type?: string;
}) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}
{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<input
type={props.type ?? "text"}
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
required={props.required}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
);
}
export default function DepartmentManagementPage() {
const [searchParams] = useSearchParams();
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 [sortMenuOpen, setSortMenuOpen] = createSignal(false);
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
const [rows, setRows] = createSignal<DepartmentRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [openMenuPos, setOpenMenuPos] = createSignal({ x: 0, y: 0 });
const [editingId, setEditingId] = createSignal<string | null>(null);
const [viewingDept, setViewingDept] = createSignal<DepartmentRecord | null>(null);
const [deleteTarget, setDeleteTarget] = createSignal<DepartmentRecord | null>(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 [isLoading, setIsLoading] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false);
const [error, setError] = createSignal("");
const load = async () => {
setIsLoading(true);
setError("");
try {
const accessToken =
typeof sessionStorage !== "undefined"
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
: "";
const params = new URLSearchParams({
page: "1",
per_page: "100",
q: search().trim(),
});
if (statusFilter() !== "all") {
params.set("status", statusFilter());
}
const res = await fetch(`${API}/api/admin/departments?${params.toString()}`, {
headers: {
Accept: "application/json",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: "include",
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
const payload = (await res.json().catch(() => null)) as DepartmentListResponse | null;
const list = Array.isArray(payload)
? payload
: Array.isArray(payload?.departments)
? payload.departments
: Array.isArray(payload?.data)
? payload.data
: Array.isArray(payload?.items)
? payload.items
: [];
setRows(list.map(normalizeDepartment));
} catch (err: any) {
setError(err?.message || "Could not reach departments API.");
setRows([]);
} finally {
setIsLoading(false);
}
};
onMount(() => void load());
const filteredRows = createMemo(() => {
let r = rows();
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)
);
}
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);
return a.name.localeCompare(b.name);
});
r = sorted;
return r;
});
const resetForm = () => {
setEditingId(null);
setName("");
setCode("");
setDescription("");
setDepartmentHead("");
setDepartmentEmail("");
setStatus("ACTIVE");
setFormTab("general");
setError("");
};
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");
setFormTab("general");
setView("form");
setOpenMenuId(null);
};
const save = async () => {
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("");
const payload = {
name: name().trim(),
code: code().trim() || null,
description: description().trim() || null,
department_head: departmentHead().trim() || null,
department_email: departmentEmail().trim() || null,
status: status(),
};
try {
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 res = await fetch(endpoint, {
method,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
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");
resetForm();
await load();
} catch (err: any) {
setError(err?.message || "Failed to save department.");
} finally {
setIsSaving(false);
}
};
const formatDate = (v?: string) => {
const s = v || "";
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
return s.slice(0, 10) || "—";
};
return (
<div class="w-full space-y-6 pb-8">
{/* Page header */}
<div style="margin-bottom: 1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Department Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">
Manage all departments and organizational structure
</p>
</div>
{/* ── LIST VIEW ── */}
<Show when={view() === "list"}>
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{(
[
{
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) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? "color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px" : "color:#6B7280"}`}
>
{tab.label}
</button>
))}
</div>
{/* View Department panel */}
<Show when={listTab() === "view"}>
<Show when={!viewingDept()}>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
<p style="font-size:15px;font-weight:600;color:#111827">No department selected</p>
<p style="margin-top:6px;font-size:13px;color:#6B7280">
Click the <strong></strong> menu on any department row and choose{" "}
<strong>View Department</strong>.
</p>
</div>
</Show>
<Show when={viewingDept()}>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Header */}
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
<div>
<h2 style="font-size:18px;font-weight:700;color:#111827">
{viewingDept()!.name}
</h2>
<p style="margin-top:2px;font-size:13px;color:#6B7280">
{viewingDept()!.description || "No description"}
</p>
</div>
<StatusBadge status={viewingDept()!.status} />
</div>
{/* Details grid — 3 cols using flex rows */}
<div>
<div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Code
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.code || "—"}
</p>
</div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Head
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.departmentHead || "—"}
</p>
</div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Email
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.departmentEmail || "—"}
</p>
</div>
</div>
<div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Total Employees
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{String(viewingDept()!.totalEmployees ?? 0)}
</p>
</div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Created Date
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.createdDate?.slice(0, 10) || "—"}
</p>
</div>
</div>
</div>
{/* Actions */}
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
<button
type="button"
onClick={() => openEdit(viewingDept()!)}
style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"
>
Edit Department
</button>
<button
type="button"
onClick={() => {
setViewingDept(null);
setListTab("all");
}}
style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Back to List
</button>
</div>
</div>
</Show>
</Show>
{/* Table card */}
<div style={{ display: listTab() === "view" ? "none" : "block" }}>
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
{/* Filter bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => {
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"
/>
<div style="position:relative">
<button
type="button"
onClick={() => {
setSortMenuOpen((v) => !v);
setFilterMenuOpen(false);
}}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M7 4v13" />
<path d="m3 13 4 4 4-4" />
<path d="M17 20V7" />
<path d="m21 11-4-4-4 4" />
</svg>
Sort
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(["name_asc", "name_desc", "employees_desc", "employees_asc"] as const).map(
(s, i) => (
<button
type="button"
onClick={() => {
setSortBy(s);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? "#FF5E13" : "#374151"};background:${sortBy() === s ? "#FFF1EB" : "transparent"}`}
>
{
[
"Name (A-Z)",
"Name (Z-A)",
"Employees (High-Low)",
"Employees (Low-High)",
][i]
}
</button>
)
)}
</div>
</Show>
</div>
<div style="position:relative">
<button
type="button"
onClick={() => {
setFilterMenuOpen((v) => !v);
setSortMenuOpen(false);
}}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M3 5h18M6 12h12M10 19h4" />
</svg>
Filters
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(["all", "active", "inactive"] as const).map((s) => (
<button
type="button"
onClick={() => {
setStatusFilter(s);
setFilterMenuOpen(false);
void load();
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? "#FF5E13" : "#374151"};background:${statusFilter() === s ? "#FFF1EB" : "transparent"}`}
>
{s === "all" ? "All Status" : s === "active" ? "Active" : "Inactive"}
</button>
))}
</div>
</Show>
</div>
<button
type="button"
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer"
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Export
</button>
</div>
{/* Table */}
<div class="overflow-x-auto overflow-y-visible">
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Department Name
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Department Code
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Description
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Total Employees
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Status
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Created Date
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Actions
</th>
</tr>
</thead>
<tbody>
<Show
when={filteredRows().length > 0}
fallback={
<tr>
<td colspan="7" class="px-6 py-16 text-center">
<p class="text-[15px] font-semibold text-[#111827]">
No departments found
</p>
<p class="mt-1 text-[13px] text-[#6B7280]">
Create your first department to get started.
</p>
<button
type="button"
onClick={openCreate}
class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white"
>
Create Department
</button>
</td>
</tr>
}
>
<For each={filteredRows()}>
{(row) => (
<tr
style="border-bottom:1px solid #F3F4F6"
class="hover:bg-[#FAFAFA] transition-colors"
>
<td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">
{String(row.code || "—")}
</span>
</td>
<td style="padding:12px 20px;max-width:340px">
<p style="font-size:13px;color:#6B7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
{String(row.description || "—")}
</p>
</td>
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">
{Number(row.totalEmployees || 0)}
</td>
<td style="padding:12px 20px">
<StatusBadge status={row.status} />
</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">
{formatDate(String(row.createdDate || row.updatedAt || ""))}
</td>
<td style="padding:12px 20px;position:relative">
<button
type="button"
onClick={(e) => {
if (openMenuId() === row.id) {
setOpenMenuId(null);
return;
}
const rect = e.currentTarget.getBoundingClientRect();
setOpenMenuPos({ x: rect.right, y: rect.top - 8 });
setOpenMenuId(row.id);
}}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors"
aria-label="More actions"
>
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<circle cx="12" cy="5" r="1.5" />
<circle cx="12" cy="12" r="1.5" />
<circle cx="12" cy="19" r="1.5" />
</svg>
</button>
<Show when={openMenuId() === row.id}>
<div
style={`position:fixed;left:${openMenuPos().x}px;top:${openMenuPos().y}px;transform:translate(-100%, -100%);z-index:9999;width:210px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)`}
>
<button
type="button"
onClick={() => {
setViewingDept(row);
setOpenMenuId(null);
setListTab("view");
}}
style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left"
>
<svg
style="width:16px;height:16px;color:#FF5E13;flex-shrink:0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
View Department
</button>
<button
type="button"
onClick={() => openEdit(row)}
style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left"
>
<svg
style="width:16px;height:16px;color:#FF5E13;flex-shrink:0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M12 20h9" />
<path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z" />
</svg>
Edit Department
</button>
<button
type="button"
onClick={async () => {
try {
const res = await fetch(
`${API}/api/admin/departments/${row.id}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status:
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE",
}),
}
);
if (!res.ok)
throw new Error(`Request failed (${res.status})`);
} catch (err: any) {
setError(err?.message || "Failed to update status.");
} finally {
setOpenMenuId(null);
await load();
}
}}
style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left"
>
<svg
style="width:16px;height:16px;color:#FF5E13;flex-shrink:0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="9" />
<path d="M9 12l2 2 4-4" />
</svg>
{row.status === "ACTIVE"
? "Deactivate Department"
: "Activate Department"}
</button>
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
<button
type="button"
onClick={async () => {
setOpenMenuId(null);
setDeleteTarget(row);
}}
style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left"
>
<svg
style="width:16px;height:16px;flex-shrink:0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" />
</svg>
Delete Department
</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={filteredRows().length > 0}>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
<p style="font-size:13px;color:#6B7280">
Showing{" "}
<strong style="font-weight:600;color:#111827">1{filteredRows().length}</strong>{" "}
of{" "}
<strong style="font-weight:600;color:#111827">{filteredRows().length}</strong>{" "}
departments
</p>
<div style="display:flex;align-items:center;gap:4px">
<button
type="button"
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"
>
</button>
<button
type="button"
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer"
>
1
</button>
<button
type="button"
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer"
>
2
</button>
<button
type="button"
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer"
>
3
</button>
<button
type="button"
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"
>
</button>
</div>
</div>
</Show>
</div>
</div>
</div>
</Show>
<Show when={deleteTarget()}>
<div style="position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:rgba(17,24,39,0.45);padding:16px">
<div style="width:min(92vw,480px);border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 24px 48px rgba(0,0,0,0.2)">
<div style="padding:20px 22px;border-bottom:1px solid #F3F4F6">
<h3 style="font-size:18px;font-weight:700;color:#111827">Delete Department?</h3>
<p style="margin-top:8px;font-size:13px;line-height:1.5;color:#4B5563">
You are about to permanently delete
<strong style="color:#111827"> {deleteTarget()?.name}</strong>. This action cannot
be undone.
</p>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;padding:14px 22px">
<button
type="button"
onClick={() => setDeleteTarget(null)}
disabled={isDeleting()}
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Cancel
</button>
<button
type="button"
disabled={isDeleting()}
onClick={async () => {
const target = deleteTarget();
if (!target) return;
setIsDeleting(true);
try {
const res = await fetch(`${API}/api/admin/departments/${target.id}`, {
method: "DELETE",
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
setDeleteTarget(null);
await load();
} catch (err: any) {
setError(err?.message || "Failed to delete department.");
} finally {
setIsDeleting(false);
}
}}
style="height:38px;border-radius:10px;border:none;background:#0D0D2A;padding:0 14px;font-size:13px;font-weight:700;color:white;cursor:pointer"
>
{isDeleting() ? "Deleting..." : "Delete Department"}
</button>
</div>
</div>
</div>
</Show>
{/* ── FORM VIEW (Create / Edit) ── */}
<Show when={view() === "form"}>
{/* Top tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
<button
type="button"
onClick={() => setView("list")}
style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer"
>
All Departments
</button>
<button
type="button"
style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px"
>
{editingId() ? "Edit Department" : "Create Department"}
</button>
</div>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "settings", "permissions"] as const).map((tab, i) => {
const labels = ["General Information", "Department Settings", "Permissions"];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
</div>
<div style="padding:24px">
<Show when={error()}>
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
{error()}
</div>
</Show>
{/* General Information */}
<Show when={formTab() === "general"}>
<div style="display:flex;flex-direction:column;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput
label="Department Name"
required
value={name()}
onInput={setName}
placeholder="e.g. Engineering"
/>
<FormInput
label="Department Code"
required
value={code()}
onInput={setCode}
placeholder="e.g. ENG-001"
/>
</div>
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Brief description of this department's purpose..."
rows="3"
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
/>
</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput
label="Department Head"
value={departmentHead()}
onInput={setDepartmentHead}
placeholder="e.g. Arun Kumar"
/>
<FormInput
label="Department Email"
type="email"
value={departmentEmail()}
onInput={setDepartmentEmail}
placeholder="dept@nxtgauge.com"
/>
</div>
</div>
</Show>
{/* Department Settings */}
<Show when={formTab() === "settings"}>
<div style="display:flex;flex-direction:column;gap:32px">
<div>
<p style="font-size:14px;font-weight:600;color:#111827">Department Status</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">
Set whether this department is currently active
</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(["ACTIVE", "INACTIVE"] as const).map((s) => (
<button
type="button"
onClick={() => setStatus(s)}
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? "#FF5E13" : "#E5E7EB"};background:${status() === s ? "#FFF3EE" : "white"};color:${status() === s ? "#FF5E13" : "#6B7280"}`}
>
{s === "ACTIVE" ? "Active" : "Inactive"}
</button>
))}
</div>
</div>
</div>
</div>
<div>
<p style="font-size:14px;font-weight:600;color:#111827">Department Visibility</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">
Choose who can see this department
</p>
<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px">
{[
{
key: "INTERNAL",
label: "Internal",
desc: "Visible to internal employees only",
},
{
key: "EXTERNAL",
label: "External",
desc: "Visible to external users and partners",
},
].map((opt) => (
<button
type="button"
onClick={() => setVisibility(opt.key as "INTERNAL" | "EXTERNAL")}
style={`display:flex;align-items:flex-start;gap:10px;border-radius:12px;border:1px solid ${visibility() === opt.key ? "#FF5E13" : "#E5E7EB"};background:${visibility() === opt.key ? "#FFF7ED" : "#F9FAFB"};padding:14px 16px;text-align:left;cursor:pointer`}
>
<div
style={`margin-top:2px;width:16px;height:16px;border-radius:50%;border:2px solid ${visibility() === opt.key ? "#FF5E13" : "#D1D5DB"};display:flex;align-items:center;justify-content:center;flex-shrink:0`}
>
<Show when={visibility() === opt.key}>
<div style="width:6px;height:6px;border-radius:50%;background:#FF5E13" />
</Show>
</div>
<div>
<p style="font-size:13px;font-weight:600;color:#111827">{opt.label}</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">{opt.desc}</p>
</div>
</button>
))}
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">
Allow Employee Transfers
</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">
Employees can request to transfer into this department
</p>
</div>
<button
type="button"
onClick={() => setTransfersEnabled((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${transfersEnabled() ? "#FF5E13" : "#E5E7EB"};transition:background 0.2s;flex-shrink:0`}
>
<span
style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${transfersEnabled() ? "22px" : "2px"}`}
/>
</button>
</div>
</div>
</Show>
{/* Permissions */}
<Show when={formTab() === "permissions"}>
<div style="display:flex;flex-direction:column;gap:24px">
<p style="font-size:13px;color:#6B7280">
Select the permissions available to employees in this department.
</p>
<For each={permissionGroups}>
{(group) => (
<div>
<p style="margin-bottom:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#9CA3AF">
{group.title}
</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<For each={group.items}>
{(item) => (
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
<input
type="checkbox"
style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer"
/>
<span style="font-size:13px;font-weight:500;color:#374151">
{item}
</span>
</label>
)}
</For>
</div>
</div>
)}
</For>
</div>
</Show>
</div>
{/* Form actions */}
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
<button
type="button"
onClick={() => {
setView("list");
resetForm();
}}
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Cancel
</button>
<button
type="button"
onClick={() => void save()}
disabled={isSaving()}
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer;opacity:1"
>
{isSaving() ? "Saving..." : editingId() ? "Update Department" : "Create Department"}
</button>
</div>
</div>
</Show>
</div>
);
}