3import { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { Icon } from '@/contexts/IconContext';
12interface DepartmentNode extends Department {
17export default function DepartmentHierarchySettings() {
18 const [departments, setDepartments] = useState<Department[]>([]);
19 const [hierarchy, setHierarchy] = useState<Record<number, number[]>>({});
20 const [nodes, setNodes] = useState<Record<number, DepartmentNode>>({});
21 const [loading, setLoading] = useState(true);
22 const [saving, setSaving] = useState(false);
23 const [draggedDept, setDraggedDept] = useState<number | null>(null);
24 const [dragOverDept, setDragOverDept] = useState<number | null>(null);
25 const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
31 const loadData = async () => {
36 const deptResult = await apiClient.getDepartments();
37 if (deptResult.success && deptResult.data) {
38 const deptArray = (deptResult.data as any)?.DATS || [];
39 const deptList: Department[] = deptArray.map((dept: any) => ({
40 id: dept.f100 || dept.Depid,
41 name: dept.f101 || dept.Name
42 })).filter((d: Department) => d.id && d.name);
44 setDepartments(deptList.sort((a, b) => a.id - b.id));
46 // Load hierarchy configuration
47 const hierarchyResult = await fetch('/api/v1/departments/hierarchy');
48 const hierarchyData = await hierarchyResult.json();
50 if (hierarchyData.success) {
51 const loadedHierarchy = hierarchyData.data || {};
52 setHierarchy(loadedHierarchy);
54 // Build node structure
55 const nodeMap: Record<number, DepartmentNode> = {};
56 deptList.forEach(dept => {
59 children: loadedHierarchy[dept.id] || [],
67 console.error('Error loading data:', error);
68 showMessage('error', 'Failed to load department data');
74 const showMessage = (type: 'success' | 'error', text: string) => {
75 setMessage({ type, text });
76 setTimeout(() => setMessage(null), 3000);
79 const handleSave = async () => {
83 // Convert nodes back to hierarchy format
84 const newHierarchy: Record<number, number[]> = {};
85 Object.values(nodes).forEach(node => {
86 if (node.children.length > 0) {
87 newHierarchy[node.id] = node.children;
91 const response = await fetch('/api/v1/departments/hierarchy', {
93 headers: { 'Content-Type': 'application/json' },
94 body: JSON.stringify({ hierarchy: newHierarchy })
97 const result = await response.json();
100 setHierarchy(newHierarchy);
101 showMessage('success', 'Department hierarchy saved successfully!');
103 showMessage('error', result.error || 'Failed to save hierarchy');
106 console.error('Error saving hierarchy:', error);
107 showMessage('error', 'Failed to save department hierarchy');
113 const handleDragStart = (e: React.DragEvent, deptId: number) => {
114 setDraggedDept(deptId);
115 e.dataTransfer.effectAllowed = 'move';
116 // Add visual feedback
117 if (e.currentTarget instanceof HTMLElement) {
118 e.currentTarget.style.opacity = '0.5';
122 const handleDragEnd = (e: React.DragEvent) => {
123 setDraggedDept(null);
124 setDragOverDept(null);
125 // Reset visual feedback
126 if (e.currentTarget instanceof HTMLElement) {
127 e.currentTarget.style.opacity = '1';
131 const handleDragOver = (e: React.DragEvent, targetId: number) => {
133 e.stopPropagation(); // Stop bubbling to parent elements
134 e.dataTransfer.dropEffect = 'move';
135 setDragOverDept(targetId);
138 const handleDragLeave = (e: React.DragEvent) => {
139 e.stopPropagation(); // Stop bubbling to parent elements
140 // Only clear if actually leaving the element
141 if (e.currentTarget === e.target) {
142 setDragOverDept(null);
146 const handleDrop = (e: React.DragEvent, parentId: number) => {
148 e.stopPropagation(); // Stop bubbling to parent elements
150 if (draggedDept === null || draggedDept === parentId) {
151 setDraggedDept(null);
155 const updatedNodes = { ...nodes };
156 const parent = updatedNodes[parentId];
158 // Check if already a child
159 if (parent.children.includes(draggedDept)) {
160 setDraggedDept(null);
164 // Remove from any existing parent (if moving within hierarchy)
165 Object.values(updatedNodes).forEach(node => {
166 if (node.children.includes(draggedDept)) {
167 node.children = node.children.filter(id => id !== draggedDept);
172 parent.children = [...parent.children, draggedDept];
173 setNodes(updatedNodes);
175 setDraggedDept(null);
178 const removeChild = (parentId: number, childId: number) => {
179 const updatedNodes = { ...nodes };
180 updatedNodes[parentId].children = updatedNodes[parentId].children.filter(id => id !== childId);
181 setNodes(updatedNodes);
184 const moveChild = (parentId: number, childId: number, direction: 'up' | 'down') => {
185 const updatedNodes = { ...nodes };
186 const children = [...updatedNodes[parentId].children];
187 const index = children.indexOf(childId);
189 if (direction === 'up' && index > 0) {
190 [children[index], children[index - 1]] = [children[index - 1], children[index]];
191 } else if (direction === 'down' && index < children.length - 1) {
192 [children[index], children[index + 1]] = [children[index + 1], children[index]];
195 updatedNodes[parentId].children = children;
196 setNodes(updatedNodes);
199 const toggleExpand = (deptId: number) => {
200 const updatedNodes = { ...nodes };
201 updatedNodes[deptId].isExpanded = !updatedNodes[deptId].isExpanded;
202 setNodes(updatedNodes);
205 const clearHierarchy = () => {
206 if (confirm('Are you sure you want to clear all department hierarchy settings?')) {
207 const clearedNodes = { ...nodes };
208 Object.keys(clearedNodes).forEach(key => {
209 clearedNodes[Number(key)].children = [];
210 clearedNodes[Number(key)].isExpanded = false;
212 setNodes(clearedNodes);
218 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
219 <div className="text-center py-12">
220 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#00946b] mx-auto"></div>
221 <p className="mt-4 text-muted">Loading departments...</p>
228 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
230 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
231 <div className="mb-8">
232 <h1 className="text-3xl font-bold text-text flex items-center gap-2">
233 <Icon name="account_tree" size={32} className="text-brand" />
234 Department Hierarchy Configuration
236 <p className="mt-2 text-muted">
237 Configure how departments (also called categories) cascade in product forms. Drag departments onto others to create parent-child relationships.
239 <p className="mt-1 text-sm text-muted italic">
240 Note: "Department" and "Category" mean the same thing - they're different ways to classify your products.
244 {/* Message Banner */}
246 <div className={`mb-6 p-4 rounded-lg ${
247 message.type === 'success' ? 'bg-success/20 text-success' : 'bg-danger/20 text-danger'
253 {/* Action Buttons */}
254 <div className="mb-6 flex justify-end items-center">
258 className="px-6 py-3 bg-success text-surface rounded-lg hover:bg-success/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-semibold flex items-center gap-2"
260 <Icon name={saving ? "hourglass_empty" : "save"} size={20} />
261 {saving ? 'Saving...' : 'Save Configuration'}
265 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
266 {/* Available Departments (Source) */}
267 <div className="bg-surface rounded-lg shadow-md p-6">
268 <h2 className="text-xl font-bold text-text mb-4">Available Departments (Categories)</h2>
269 <p className="text-sm text-muted mb-4">
270 Drag departments from here onto departments in the hierarchy section to create parent-child relationships.
272 <div className="space-y-2">
273 {departments.map(dept => (
277 onDragStart={(e) => handleDragStart(e, dept.id)}
278 onDragEnd={handleDragEnd}
279 className={`p-3 bg-surface-2 border-2 rounded-lg cursor-move transition-all transform ${
280 draggedDept === dept.id
281 ? 'border-brand bg-info/10 scale-105 shadow-lg'
282 : 'border-border hover:bg-surface-2 hover:border-brand hover:shadow-md'
285 <div className="flex items-center">
286 <Icon name="drag_indicator" size={20} className="text-muted mr-2" />
287 <span className="font-medium text-text">{dept.name}</span>
288 <span className="ml-2 text-sm text-muted">(ID: {dept.id})</span>
295 {/* Hierarchy Configuration (Target) */}
296 <div className="bg-surface rounded-lg shadow-md p-6">
297 <h2 className="text-xl font-bold text-text mb-4">Department Hierarchy</h2>
298 <p className="text-sm text-muted mb-4">
299 Drop departments here to define which categories can be sub-categories of others. This creates a cascading selection in product forms.
301 <div className="mb-3 p-3 bg-info/10 border border-info/30 rounded text-sm text-info">
302 <strong>How levels work:</strong> Children you add here become <strong>Department 2</strong> options when the parent is selected as <strong>Department 1</strong>. Continue the chain for levels 3 and 4.
304 <div className="space-y-3">
305 {departments.map(dept => {
306 const node = nodes[dept.id];
307 if (!node) return null;
312 onDragOver={(e) => handleDragOver(e, dept.id)}
313 onDragLeave={handleDragLeave}
314 onDrop={(e) => handleDrop(e, dept.id)}
315 className={`border-2 rounded-lg transition-all relative ${
316 draggedDept === dept.id
317 ? 'border-border bg-surface-2 opacity-50'
318 : dragOverDept === dept.id
319 ? 'border-brand bg-info/10 shadow-lg scale-102 ring-4 ring-blue-300 animate-pulse'
320 : 'border-border hover:border-[#00946b] bg-surface'
323 {/* Drop Indicator */}
324 {dragOverDept === dept.id && draggedDept !== dept.id && (
325 <div className="absolute top-1 right-1 bg-brand text-surface px-3 py-1 rounded text-xs font-bold shadow-xl z-10 pointer-events-none animate-bounce flex items-center gap-1">
326 <Icon name="add" size={14} /> Add as Level 2
329 <div className="p-3 flex items-center justify-between">
330 <div className="flex items-center flex-1">
331 {node.children.length > 0 && (
333 onClick={() => toggleExpand(dept.id)}
334 className="mr-2 text-muted hover:text-text"
336 <Icon name={node.isExpanded ? "expand_more" : "chevron_right"} size={20} />
339 <div className="flex items-center gap-2">
340 <span className="font-semibold text-text">{dept.name}</span>
341 <span className="text-xs text-muted">(ID: {dept.id})</span>
342 {node.children.length > 0 && (
343 <span className="text-xs bg-info/20 text-info px-2 py-0.5 rounded font-medium">
349 {node.children.length > 0 && (
350 <span className="text-xs bg-success text-surface px-2 py-1 rounded">
351 {node.children.length} child{node.children.length !== 1 ? 'ren' : ''}
357 {node.isExpanded && node.children.length > 0 && (
358 <div className="px-3 pb-3 space-y-2">
359 <div className="ml-8 pl-4 border-l-2 border-border space-y-2">
360 {node.children.map((childId, childIndex) => {
361 const childDept = departments.find(d => d.id === childId);
362 if (!childDept) return null;
363 const childNode = nodes[childId];
364 const isFirst = childIndex === 0;
365 const isLast = childIndex === node.children.length - 1;
370 onDragStart={(e) => handleDragStart(e, childId)}
371 onDragEnd={handleDragEnd}
372 onDragOver={(e) => handleDragOver(e, childId)}
373 onDragLeave={handleDragLeave}
374 onDrop={(e) => handleDrop(e, childId)}
375 className={`relative flex items-center justify-between p-2 bg-surface-2 rounded border-2 transition-all cursor-move ${
376 draggedDept === childId
377 ? 'border-border opacity-50'
378 : dragOverDept === childId
379 ? 'border-accent bg-accent/20 shadow-xl ring-4 ring-accent/30 animate-pulse'
380 : 'border-border hover:border-accent'
383 {/* Drop Indicator for Level 3 */}
384 {dragOverDept === childId && draggedDept !== childId && (
385 <div className="absolute top-1 right-1 bg-info text-surface px-3 py-1 rounded text-xs font-bold shadow-xl z-10 pointer-events-none animate-bounce flex items-center gap-1">
386 <Icon name="add" size={14} /> Add as Level 3
389 <div className="flex items-center gap-2 flex-1">
390 {childNode?.children.length > 0 && (
392 onClick={() => toggleExpand(childId)}
393 className="text-muted hover:text-text text-xs"
395 <Icon name={childNode.isExpanded ? "expand_more" : "chevron_right"} size={16} />
398 <span className="text-sm text-text">↳ {childDept.name}</span>
399 {childNode?.children.length > 0 && (
400 <span className="text-xs bg-accent/20 text-accent px-2 py-0.5 rounded font-medium">
405 <div className="flex items-center gap-1">
407 onClick={() => moveChild(dept.id, childId, 'up')}
409 className="text-muted/70 hover:text-text disabled:opacity-30 disabled:cursor-not-allowed text-xs px-1"
415 onClick={() => moveChild(dept.id, childId, 'down')}
417 className="text-muted/70 hover:text-text disabled:opacity-30 disabled:cursor-not-allowed text-xs px-1"
423 onClick={() => removeChild(dept.id, childId)}
424 className="text-danger hover:text-danger/80 text-xs px-2 py-1"
431 {/* Grandchildren (Level 3) */}
432 {childNode?.children.length > 0 && childNode.isExpanded && (
433 <div className="ml-8 pl-4 mt-2 border-l-2 border-border space-y-2">
434 {childNode.children.map((grandchildId, grandchildIndex) => {
435 const grandchildDept = departments.find(d => d.id === grandchildId);
436 if (!grandchildDept) return null;
437 const grandchildNode = nodes[grandchildId];
438 const isFirst = grandchildIndex === 0;
439 const isLast = grandchildIndex === childNode.children.length - 1;
441 <div key={grandchildId}>
444 onDragStart={(e) => handleDragStart(e, grandchildId)}
445 onDragEnd={handleDragEnd}
446 onDragOver={(e) => handleDragOver(e, grandchildId)}
447 onDragLeave={handleDragLeave}
448 onDrop={(e) => handleDrop(e, grandchildId)}
449 className={`relative flex items-center justify-between p-2 bg-surface-2 rounded border-2 transition-all cursor-move ${
450 draggedDept === grandchildId
451 ? 'border-border opacity-50'
452 : dragOverDept === grandchildId
453 ? 'border-success bg-success/20 shadow-xl ring-4 ring-success/30 animate-pulse'
454 : 'border-border hover:border-success'
457 {/* Drop Indicator for Level 4 */}
458 {dragOverDept === grandchildId && draggedDept !== grandchildId && (
459 <div className="absolute top-1 right-1 bg-success text-surface px-3 py-1 rounded text-xs font-bold shadow-xl z-10 pointer-events-none animate-bounce flex items-center gap-1">
460 <Icon name="add" size={14} /> Add as Level 4
463 <div className="flex items-center gap-2 flex-1">
464 {grandchildNode?.children.length > 0 && (
466 onClick={() => toggleExpand(grandchildId)}
467 className="text-muted hover:text-text text-xs"
469 <Icon name={grandchildNode.isExpanded ? "expand_more" : "chevron_right"} size={16} />
472 <span className="text-sm text-text">↳↳ {grandchildDept.name}</span>
473 {grandchildNode?.children.length > 0 && (
474 <span className="text-xs bg-success/20 text-success px-2 py-0.5 rounded font-medium">
479 <div className="flex items-center gap-1">
481 onClick={() => moveChild(childId, grandchildId, 'up')}
483 className="text-muted/70 hover:text-text disabled:opacity-30 disabled:cursor-not-allowed text-xs px-1"
489 onClick={() => moveChild(childId, grandchildId, 'down')}
491 className="text-muted/70 hover:text-text disabled:opacity-30 disabled:cursor-not-allowed text-xs px-1"
497 onClick={() => removeChild(childId, grandchildId)}
498 className="text-danger hover:text-danger/80 text-xs px-2 py-1"
505 {/* Great-grandchildren (Level 4) */}
506 {grandchildNode?.children.length > 0 && grandchildNode.isExpanded && (
507 <div className="ml-8 pl-4 mt-2 border-l-2 border-border space-y-2">
508 {grandchildNode.children.map(greatGrandchildId => {
509 const greatGrandchildDept = departments.find(d => d.id === greatGrandchildId);
510 if (!greatGrandchildDept) return null;
512 <div key={greatGrandchildId} className="flex items-center justify-between p-2 bg-surface-2 rounded border border-border">
513 <div className="flex items-center gap-2">
514 <span className="text-sm text-text">↳↳↳ {greatGrandchildDept.name}</span>
515 <span className="text-xs bg-success/20 text-success px-2 py-0.5 rounded font-medium">
520 onClick={() => removeChild(grandchildId, greatGrandchildId)}
521 className="text-danger hover:text-danger/80 text-xs px-2 py-1"
549 <div className="mt-8 bg-info/10 border border-info/30 rounded-lg p-6">
550 <h3 className="font-semibold text-info mb-2">How it works:</h3>
551 <ul className="space-y-2 text-info text-sm">
552 <li>• <strong>Level 1 → 2:</strong> Drag a department from the left panel and drop it onto a department on the right</li>
553 <li>• <strong>Level 2 → 3:</strong> Click the arrow to expand Level 2 items, then drag and drop onto the purple children</li>
554 <li>• <strong>Level 3 → 4:</strong> Expand Level 3 items and drag onto the orange children</li>
555 <li>• Each level creates a cascading dropdown in product forms</li>
556 <li>• Hover over items to see which level they accept (border color changes)</li>
557 <li>• Click "Remove" to delete a relationship</li>
558 <li>• Don't forget to click "Save Configuration" when you're done!</li>