Skip to content

Commit b307c1a

Browse files
Merge pull request #507 from PeterOche/feat/transfer
Feat/transfer
2 parents 202e2df + dc82479 commit b307c1a

File tree

5 files changed

+412
-7
lines changed

5 files changed

+412
-7
lines changed

frontend/app/(dashboard)/assets/[id]/page.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ import { Button } from "@/components/ui/button";
88
import { StatusBadge } from "@/components/assets/status-badge";
99
import { ConditionBadge } from "@/components/assets/condition-badge";
1010
import { useAsset, useAssetHistory } from "@/lib/query/hooks/useAsset";
11+
import { useAuthStore } from "@/store/auth.store";
12+
import { TransferAssetDialog } from "@/components/assets/transfer-dialog";
13+
import { MoveHorizontal } from "lucide-react";
1114

1215
type Tab = "overview" | "history" | "documents";
1316

1417
export default function AssetDetailPage() {
1518
const { id } = useParams<{ id: string }>();
1619
const router = useRouter();
1720
const [tab, setTab] = useState<Tab>("overview");
21+
const [isTransferOpen, setIsTransferOpen] = useState(false);
22+
const { user } = useAuthStore();
1823

1924
const { data: asset, isLoading } = useAsset(id);
2025
const { data: history = [] } = useAssetHistory(id);
@@ -73,6 +78,16 @@ export default function AssetDetailPage() {
7378
<ConditionBadge condition={asset.condition} />
7479
</div>
7580
</div>
81+
82+
{(user?.role === 'ADMIN' || user?.role === 'MANAGER') && (
83+
<Button
84+
onClick={() => setIsTransferOpen(true)}
85+
className="flex items-center gap-2"
86+
>
87+
<MoveHorizontal size={16} />
88+
Transfer Asset
89+
</Button>
90+
)}
7691
</div>
7792
</div>
7893

@@ -82,11 +97,10 @@ export default function AssetDetailPage() {
8297
<button
8398
key={key}
8499
onClick={() => setTab(key)}
85-
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${
86-
tab === key
100+
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${tab === key
87101
? "border-gray-900 text-gray-900"
88102
: "border-transparent text-gray-500 hover:text-gray-700"
89-
}`}
103+
}`}
90104
>
91105
{icon}
92106
{label}
@@ -235,6 +249,14 @@ export default function AssetDetailPage() {
235249
)}
236250

237251
{tab === "documents" && <AssetDocumentsSection assetId={id} />}
252+
253+
{isTransferOpen && (
254+
<TransferAssetDialog
255+
assetId={id}
256+
assetName={asset.name}
257+
onClose={() => setIsTransferOpen(false)}
258+
/>
259+
)}
238260
</div>
239261
);
240262
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import {
5+
useDepartmentsList,
6+
useCreateDepartment,
7+
useDeleteDepartment,
8+
useCategories,
9+
useCreateCategory,
10+
useDeleteCategory
11+
} from '@/lib/query/hooks/query.hook';
12+
import { DepartmentWithCount, CategoryWithCount } from '@/lib/api/assets';
13+
import { Plus, Trash2, LayoutGrid, Tags, Loader2, AlertCircle } from 'lucide-react';
14+
import { toast } from 'react-toastify';
15+
16+
type TabType = 'departments' | 'categories';
17+
18+
export default function DepartmentsPage() {
19+
const [activeTab, setActiveTab] = useState<TabType>('departments');
20+
const [isAdding, setIsAdding] = useState(false);
21+
const [formData, setFormData] = useState({ name: '', description: '' });
22+
23+
const { data: departments, isLoading: isLoadingDepts } = useDepartmentsList();
24+
const { data: categories, isLoading: isLoadingCats } = useCategories();
25+
26+
const createDept = useCreateDepartment();
27+
const deleteDept = useDeleteDepartment();
28+
const createCat = useCreateCategory();
29+
const deleteCat = useDeleteCategory();
30+
31+
const handleCreate = async (e: React.FormEvent) => {
32+
e.preventDefault();
33+
if (!formData.name.trim()) return;
34+
35+
try {
36+
if (activeTab === 'departments') {
37+
await createDept.mutateAsync(formData);
38+
} else {
39+
await createCat.mutateAsync(formData);
40+
}
41+
toast.success(`${activeTab === 'departments' ? 'Department' : 'Category'} created successfully`);
42+
setFormData({ name: '', description: '' });
43+
setIsAdding(false);
44+
} catch (err: any) {
45+
toast.error(err.message || `Failed to create ${activeTab}`);
46+
}
47+
};
48+
49+
const handleDelete = async (id: string, name: string) => {
50+
const confirmMessage = `Are you sure you want to delete ${name}? Assets in this ${activeTab === 'departments' ? 'department' : 'category'} will need to be reassigned/recategorised.`;
51+
if (!window.confirm(confirmMessage)) return;
52+
53+
try {
54+
if (activeTab === 'departments') {
55+
await deleteDept.mutateAsync(id);
56+
} else {
57+
await deleteCat.mutateAsync(id);
58+
}
59+
toast.success(`${activeTab === 'departments' ? 'Department' : 'Category'} deleted successfully`);
60+
} catch (err: any) {
61+
toast.error(err.message || `Failed to delete ${activeTab}`);
62+
}
63+
};
64+
65+
const items = activeTab === 'departments' ? departments : categories;
66+
const isLoading = activeTab === 'departments' ? isLoadingDepts : isLoadingCats;
67+
68+
return (
69+
<div className="p-6 max-w-7xl mx-auto space-y-6">
70+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
71+
<h1 className="text-3xl font-bold tracking-tight">Management</h1>
72+
73+
<div className="flex bg-gray-100 p-1 rounded-lg">
74+
<button
75+
onClick={() => { setActiveTab('departments'); setIsAdding(false); }}
76+
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-all ${activeTab === 'departments'
77+
? 'bg-white shadow-sm text-blue-600 font-medium'
78+
: 'text-gray-600 hover:text-gray-900'
79+
}`}
80+
>
81+
<LayoutGrid size={18} />
82+
Departments
83+
{departments && <span className="ml-1 text-xs bg-gray-200 px-2 py-0.5 rounded-full text-gray-600">{departments.length}</span>}
84+
</button>
85+
<button
86+
onClick={() => { setActiveTab('categories'); setIsAdding(false); }}
87+
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-all ${activeTab === 'categories'
88+
? 'bg-white shadow-sm text-blue-600 font-medium'
89+
: 'text-gray-600 hover:text-gray-900'
90+
}`}
91+
>
92+
<Tags size={18} />
93+
Categories
94+
{categories && <span className="ml-1 text-xs bg-gray-200 px-2 py-0.5 rounded-full text-gray-600">{categories.length}</span>}
95+
</button>
96+
</div>
97+
</div>
98+
99+
<div className="flex justify-between items-center py-4 border-b">
100+
<h2 className="text-xl font-semibold capitalize">
101+
{activeTab} ({items?.length || 0})
102+
</h2>
103+
<button
104+
onClick={() => setIsAdding(!isAdding)}
105+
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
106+
>
107+
<Plus size={20} />
108+
Add {activeTab === 'departments' ? 'Department' : 'Category'}
109+
</button>
110+
</div>
111+
112+
{isAdding && (
113+
<form onSubmit={handleCreate} className="bg-white p-6 rounded-xl shadow-sm border animate-in fade-in slide-in-from-top-4 duration-300">
114+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
115+
<div className="space-y-1">
116+
<label className="text-sm font-medium text-gray-700">Name *</label>
117+
<input
118+
required
119+
type="text"
120+
placeholder="Enter name"
121+
value={formData.name}
122+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
123+
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
124+
/>
125+
{activeTab === 'departments' ? createDept.isError && (
126+
<p className="text-xs text-red-500 flex items-center gap-1 mt-1">
127+
<AlertCircle size={12} /> {createDept.error?.message}
128+
</p>
129+
) : createCat.isError && (
130+
<p className="text-xs text-red-500 flex items-center gap-1 mt-1">
131+
<AlertCircle size={12} /> {createCat.error?.message}
132+
</p>
133+
)}
134+
</div>
135+
<div className="space-y-1">
136+
<label className="text-sm font-medium text-gray-700">Description (Optional)</label>
137+
<input
138+
type="text"
139+
placeholder="Enter description"
140+
value={formData.description}
141+
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
142+
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
143+
/>
144+
</div>
145+
</div>
146+
<div className="flex justify-end gap-3 mt-6">
147+
<button
148+
type="button"
149+
onClick={() => setIsAdding(false)}
150+
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
151+
>
152+
Cancel
153+
</button>
154+
<button
155+
type="submit"
156+
disabled={createDept.isPending || createCat.isPending}
157+
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white px-6 py-2 rounded-lg font-medium transition-colors"
158+
>
159+
{(createDept.isPending || createCat.isPending) && <Loader2 size={18} className="animate-spin" />}
160+
Save {activeTab === 'departments' ? 'Department' : 'Category'}
161+
</button>
162+
</div>
163+
</form>
164+
)}
165+
166+
{isLoading ? (
167+
<div className="flex flex-col items-center justify-center py-20 text-gray-400 space-y-4">
168+
<Loader2 size={40} className="animate-spin text-blue-500" />
169+
<p>Loading {activeTab}...</p>
170+
</div>
171+
) : (
172+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
173+
{items?.map((item: any) => (
174+
<div key={item.id} className="group relative bg-white p-6 rounded-xl border hover:shadow-md transition-all duration-300">
175+
<div className="flex justify-between items-start mb-4">
176+
<h3 className="text-lg font-semibold text-gray-900 truncate pr-8">{item.name}</h3>
177+
<button
178+
onClick={() => handleDelete(item.id, item.name)}
179+
className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all"
180+
title="Delete"
181+
>
182+
<Trash2 size={18} />
183+
</button>
184+
</div>
185+
<p className="text-sm text-gray-500 leading-relaxed min-h-[40px] mb-4">
186+
{item.description || 'No description provided.'}
187+
</p>
188+
<div className="flex items-center justify-between pt-4 border-t border-gray-50">
189+
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">Asset Count</span>
190+
<span className="text-lg font-bold text-blue-600">{item.assetCount || 0}</span>
191+
</div>
192+
</div>
193+
))}
194+
{!isLoading && items?.length === 0 && (
195+
<div className="col-span-full bg-gray-50 border-2 border-dashed rounded-xl py-20 flex flex-col items-center justify-center text-gray-400">
196+
<AlertCircle size={40} className="mb-2" />
197+
<p>No {activeTab} found. Create one to get started.</p>
198+
</div>
199+
)}
200+
</div>
201+
)}
202+
</div>
203+
);
204+
}

0 commit comments

Comments
 (0)