diff --git a/frontend/opsce/features/departments/DepartmentsPage.tsx b/frontend/opsce/features/departments/DepartmentsPage.tsx new file mode 100644 index 00000000..9e0f9bbf --- /dev/null +++ b/frontend/opsce/features/departments/DepartmentsPage.tsx @@ -0,0 +1,334 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, Trash2, Building2, Tag, Package } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { + useDepartmentsList, + useCreateDepartment, + useDeleteDepartment, + useCategories, + useCreateCategory, + useDeleteCategory, +} from '@/lib/query/hooks/useAssets'; +import { DepartmentWithCount, CategoryWithCount } from '@/lib/api/assets'; + +type Tab = 'departments' | 'categories'; + +export default function DepartmentsPage() { + const [tab, setTab] = useState('departments'); + + return ( +
+ {/* Header */} +
+

Organisation

+

Manage departments and asset categories

+
+ + {/* Tabs */} +
+ {([ + { key: 'departments' as Tab, label: 'Departments', icon: }, + { key: 'categories' as Tab, label: 'Categories', icon: }, + ] as const).map(({ key, label, icon }) => ( + + ))} +
+ + {tab === 'departments' && } + {tab === 'categories' && } +
+ ); +} + +// ── Departments Tab ────────────────────────────────────────── + +function DepartmentsTab() { + const { data: departments = [], isLoading } = useDepartmentsList(); + const createDept = useCreateDepartment(); + const deleteDept = useDeleteDepartment(); + + const [showForm, setShowForm] = useState(false); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [formError, setFormError] = useState(''); + const [deleteTarget, setDeleteTarget] = useState(null); + + const handleCreate = async () => { + if (!name.trim()) { setFormError('Name is required'); return; } + setFormError(''); + try { + await createDept.mutateAsync({ name: name.trim(), description: description.trim() || undefined }); + setName(''); + setDescription(''); + setShowForm(false); + } catch (err: unknown) { + const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; + setFormError(msg || 'Failed to create department.'); + } + }; + + const handleDelete = async () => { + if (!deleteTarget) return; + await deleteDept.mutateAsync(deleteTarget.id); + setDeleteTarget(null); + }; + + return ( +
+
+

+ {departments.length} department{departments.length !== 1 ? 's' : ''} +

+ +
+ + {/* Inline create form */} + {showForm && ( +
+

New Department

+
+ setName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreate()} + /> + setDescription(e.target.value)} + /> + {formError &&

{formError}

} +
+ + +
+
+
+ )} + + {/* List */} + {isLoading ? ( +
Loading departments...
+ ) : departments.length === 0 ? ( + } + title="No departments yet" + message='Click "Add Department" to create your first one.' + /> + ) : ( +
+ {departments.map((dept) => ( + setDeleteTarget(dept)} + /> + ))} +
+ )} + + {deleteTarget && ( + setDeleteTarget(null)} + loading={deleteDept.isPending} + /> + )} +
+ ); +} + +// ── Categories Tab ─────────────────────────────────────────── + +function CategoriesTab() { + const { data: categories = [], isLoading } = useCategories(); + const createCat = useCreateCategory(); + const deleteCat = useDeleteCategory(); + + const [showForm, setShowForm] = useState(false); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [formError, setFormError] = useState(''); + const [deleteTarget, setDeleteTarget] = useState(null); + + const handleCreate = async () => { + if (!name.trim()) { setFormError('Name is required'); return; } + setFormError(''); + try { + await createCat.mutateAsync({ name: name.trim(), description: description.trim() || undefined }); + setName(''); + setDescription(''); + setShowForm(false); + } catch (err: unknown) { + const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; + setFormError(msg || 'Failed to create category.'); + } + }; + + const handleDelete = async () => { + if (!deleteTarget) return; + await deleteCat.mutateAsync(deleteTarget.id); + setDeleteTarget(null); + }; + + return ( +
+
+

+ {categories.length} categor{categories.length !== 1 ? 'ies' : 'y'} +

+ +
+ + {/* Inline create form */} + {showForm && ( +
+

New Category

+
+ setName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreate()} + /> + setDescription(e.target.value)} + /> + {formError &&

{formError}

} +
+ + +
+
+
+ )} + + {/* List */} + {isLoading ? ( +
Loading categories...
+ ) : categories.length === 0 ? ( + } + title="No categories yet" + message='Click "Add Category" to create your first one.' + /> + ) : ( +
+ {categories.map((cat) => ( + setDeleteTarget(cat)} + /> + ))} +
+ )} + + {deleteTarget && ( + setDeleteTarget(null)} + loading={deleteCat.isPending} + /> + )} +
+ ); +} + +// ── Shared components ──────────────────────────────────────── + +function EntityCard({ + name, + description, + count, + countLabel, + onDelete, +}: { + name: string; + description?: string | null; + count: number; + countLabel: string; + onDelete: () => void; +}) { + return ( +
+
+

{name}

+ {description && ( +

{description}

+ )} +
+ + {count} {countLabel}{count !== 1 ? 's' : ''} +
+
+ +
+ ); +} + +function EmptyState({ icon, title, message }: { icon: React.ReactNode; title: string; message: string }) { + return ( +
+
{icon}
+

{title}

+

{message}

+
+ ); +}