Skip to content

Commit d24dc15

Browse files
Merge pull request #1362 from brightMedina5050/feature/roles-customfields-developers-myassets-qr
Merging per repo maintainer review. Pre-existing CI failures predate this PR.
2 parents e0d572a + 51b5483 commit d24dc15

7 files changed

Lines changed: 854 additions & 3 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Package, AlertTriangle, ArrowRightLeft, Bell } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
import { StatusBadge } from "@/components/assets/status-badge";
7+
import { ConditionBadge } from "@/components/assets/condition-badge";
8+
9+
interface MyAsset {
10+
id: string;
11+
name: string;
12+
assetId: string;
13+
condition: string;
14+
status: string;
15+
imageUrl?: string;
16+
checkedOutAt?: string;
17+
dueDate?: string;
18+
}
19+
20+
interface MyRequest {
21+
id: string;
22+
type: "transfer" | "maintenance";
23+
title: string;
24+
status: string;
25+
createdAt: string;
26+
}
27+
28+
const MOCK_ASSETS: MyAsset[] = [];
29+
const MOCK_REQUESTS: MyRequest[] = [];
30+
31+
export default function MyAssetsPage() {
32+
const [tab, setTab] = useState<"assets" | "requests">("assets");
33+
34+
return (
35+
<div>
36+
<div className="mb-6">
37+
<h1 className="text-2xl font-bold text-gray-900">My Assets</h1>
38+
<p className="text-sm text-gray-500 mt-1">Assets assigned to you and your requests</p>
39+
</div>
40+
41+
{/* Tabs */}
42+
<div className="flex gap-1 mb-6 bg-gray-100 p-1 rounded-lg w-fit">
43+
<button onClick={() => setTab("assets")} className={`px-4 py-2 rounded-md text-sm font-medium ${
44+
tab === "assets" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"
45+
}`}>
46+
My Assets ({MOCK_ASSETS.length})
47+
</button>
48+
<button onClick={() => setTab("requests")} className={`px-4 py-2 rounded-md text-sm font-medium ${
49+
tab === "requests" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"
50+
}`}>
51+
My Requests ({MOCK_REQUESTS.length})
52+
</button>
53+
</div>
54+
55+
{tab === "assets" && (
56+
<div className="space-y-3">
57+
{MOCK_ASSETS.length === 0 ? (
58+
<div className="bg-white border rounded-xl p-12 text-center">
59+
<Package className="w-12 h-12 text-gray-300 mx-auto mb-3" />
60+
<p className="text-gray-500">No assets assigned to you yet</p>
61+
</div>
62+
) : (
63+
MOCK_ASSETS.map((asset) => (
64+
<div key={asset.id} className="bg-white border rounded-xl p-4 flex items-center gap-4">
65+
{asset.imageUrl ? (
66+
<img src={asset.imageUrl} alt="" className="w-12 h-12 rounded-lg object-cover" />
67+
) : (
68+
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center">
69+
<Package className="w-6 h-6 text-gray-400" />
70+
</div>
71+
)}
72+
<div className="flex-1">
73+
<p className="font-medium text-gray-900">{asset.name}</p>
74+
<p className="text-xs text-gray-500">{asset.assetId}</p>
75+
</div>
76+
<StatusBadge status={asset.status} />
77+
<ConditionBadge condition={asset.condition} />
78+
<div className="flex gap-2">
79+
<Button variant="outline" size="sm">
80+
<AlertTriangle className="w-3 h-3 mr-1" /> Report Issue
81+
</Button>
82+
<Button variant="outline" size="sm">
83+
<ArrowRightLeft className="w-3 h-3 mr-1" /> Request Transfer
84+
</Button>
85+
</div>
86+
</div>
87+
))
88+
)}
89+
</div>
90+
)}
91+
92+
{tab === "requests" && (
93+
<div className="space-y-3">
94+
{MOCK_REQUESTS.length === 0 ? (
95+
<div className="bg-white border rounded-xl p-12 text-center">
96+
<Bell className="w-12 h-12 text-gray-300 mx-auto mb-3" />
97+
<p className="text-gray-500">No requests yet</p>
98+
</div>
99+
) : (
100+
MOCK_REQUESTS.map((req) => (
101+
<div key={req.id} className="bg-white border rounded-xl p-4 flex items-center gap-4">
102+
<div className="flex-1">
103+
<p className="font-medium text-gray-900">{req.title}</p>
104+
<p className="text-xs text-gray-500 capitalize">{req.type} · {new Date(req.createdAt).toLocaleDateString()}</p>
105+
</div>
106+
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
107+
req.status === "PENDING" ? "bg-yellow-100 text-yellow-700" :
108+
req.status === "APPROVED" ? "bg-green-100 text-green-700" :
109+
"bg-gray-100 text-gray-600"
110+
}`}>{req.status}</span>
111+
</div>
112+
))
113+
)}
114+
</div>
115+
)}
116+
</div>
117+
);
118+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Plus, Trash2, GripVertical, Settings } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
import { Input } from "@/components/ui/input";
7+
import { Badge } from "@/components/ui/badge";
8+
9+
const FIELD_TYPES = ["text", "number", "date", "select", "boolean"] as const;
10+
type FieldType = typeof FIELD_TYPES[number];
11+
12+
interface FieldDef {
13+
id: string;
14+
key: string;
15+
label: string;
16+
type: FieldType;
17+
options?: string[];
18+
required: boolean;
19+
isActive: boolean;
20+
}
21+
22+
const CATEGORIES = ["Vehicles", "Electronics", "Furniture", "Software", "General"];
23+
24+
export default function CustomFieldsPage() {
25+
const [selectedCategory, setSelectedCategory] = useState(CATEGORIES[0]);
26+
const [fields, setFields] = useState<Record<string, FieldDef[]>>(() =>
27+
Object.fromEntries(CATEGORIES.map((c) => [c, []]))
28+
);
29+
const [showAdd, setShowAdd] = useState(false);
30+
const [editField, setEditField] = useState<FieldDef | null>(null);
31+
32+
const currentFields = fields[selectedCategory] ?? [];
33+
34+
const addField = (field: Omit<FieldDef, "id" | "isActive">) => {
35+
setFields((prev) => ({
36+
...prev,
37+
[selectedCategory]: [...(prev[selectedCategory] ?? []), { ...field, id: Date.now().toString(), isActive: true }],
38+
}));
39+
setShowAdd(false);
40+
};
41+
42+
const deleteField = (id: string) => {
43+
setFields((prev) => ({
44+
...prev,
45+
[selectedCategory]: prev[selectedCategory].filter((f) => f.id !== id),
46+
}));
47+
};
48+
49+
const toggleActive = (id: string) => {
50+
setFields((prev) => ({
51+
...prev,
52+
[selectedCategory]: prev[selectedCategory].map((f) =>
53+
f.id === id ? { ...f, isActive: !f.isActive } : f
54+
),
55+
}));
56+
};
57+
58+
return (
59+
<div>
60+
<div className="flex items-center justify-between mb-6">
61+
<div>
62+
<h1 className="text-2xl font-bold text-gray-900">Custom Fields</h1>
63+
<p className="text-sm text-gray-500 mt-1">Define per-category fields for your assets</p>
64+
</div>
65+
</div>
66+
67+
<div className="grid grid-cols-12 gap-6">
68+
{/* Category sidebar */}
69+
<div className="col-span-3 bg-white border rounded-xl p-4">
70+
<h3 className="text-sm font-medium text-gray-700 mb-3">Categories</h3>
71+
<div className="space-y-1">
72+
{CATEGORIES.map((cat) => (
73+
<button key={cat} onClick={() => setSelectedCategory(cat)}
74+
className={`w-full text-left px-3 py-2 rounded-lg text-sm ${
75+
selectedCategory === cat ? "bg-gray-900 text-white" : "text-gray-600 hover:bg-gray-50"
76+
}`}>
77+
{cat}
78+
<span className="ml-2 text-xs opacity-60">{(fields[cat] ?? []).length}</span>
79+
</button>
80+
))}
81+
</div>
82+
</div>
83+
84+
{/* Fields list */}
85+
<div className="col-span-9 space-y-4">
86+
<div className="flex items-center justify-between">
87+
<h2 className="text-lg font-semibold">{selectedCategory} Fields</h2>
88+
<Button size="sm" onClick={() => setShowAdd(true)}>
89+
<Plus className="w-4 h-4 mr-1" /> Add Field
90+
</Button>
91+
</div>
92+
93+
{currentFields.length === 0 ? (
94+
<div className="bg-white border rounded-xl p-12 text-center text-gray-400 text-sm">
95+
No custom fields defined for this category yet.
96+
</div>
97+
) : (
98+
<div className="space-y-2">
99+
{currentFields.map((field) => (
100+
<div key={field.id} className={`bg-white border rounded-xl p-4 flex items-center gap-4 ${
101+
!field.isActive ? "opacity-50" : ""
102+
}`}>
103+
<GripVertical className="w-4 h-4 text-gray-300 cursor-grab" />
104+
<div className="flex-1">
105+
<p className="text-sm font-medium text-gray-900">{field.label}</p>
106+
<p className="text-xs text-gray-500">Key: {field.key} · Type: {field.type}</p>
107+
</div>
108+
<Badge className={field.required ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-600"}>
109+
{field.required ? "Required" : "Optional"}
110+
</Badge>
111+
{field.type === "select" && field.options && (
112+
<span className="text-xs text-gray-400">{field.options.length} options</span>
113+
)}
114+
<button onClick={() => toggleActive(field.id)} className="text-xs text-gray-500 hover:text-gray-700">
115+
{field.isActive ? "Deactivate" : "Activate"}
116+
</button>
117+
<button onClick={() => deleteField(field.id)} className="text-red-400 hover:text-red-600">
118+
<Trash2 className="w-4 h-4" />
119+
</button>
120+
</div>
121+
))}
122+
</div>
123+
)}
124+
</div>
125+
</div>
126+
127+
{/* Add/Edit Field Modal */}
128+
{(showAdd || editField) && (
129+
<FieldModal
130+
field={editField}
131+
onSave={(data) => {
132+
if (editField) {
133+
setFields((prev) => ({
134+
...prev,
135+
[selectedCategory]: prev[selectedCategory].map((f) =>
136+
f.id === editField.id ? { ...f, ...data } : f
137+
),
138+
}));
139+
setEditField(null);
140+
} else {
141+
addField(data);
142+
}
143+
}}
144+
onClose={() => { setShowAdd(false); setEditField(null); }}
145+
/>
146+
)}
147+
</div>
148+
);
149+
}
150+
151+
function FieldModal({ field, onSave, onClose }: { field: FieldDef | null; onSave: (data: Omit<FieldDef, "id" | "isActive">) => void; onClose: () => void }) {
152+
const [form, setForm] = useState({
153+
key: field?.key ?? "",
154+
label: field?.label ?? "",
155+
type: field?.type ?? "text" as FieldType,
156+
options: field?.options?.join(", ") ?? "",
157+
required: field?.required ?? false,
158+
});
159+
160+
const handleSubmit = (e: React.FormEvent) => {
161+
e.preventDefault();
162+
onSave({
163+
key: form.key,
164+
label: form.label,
165+
type: form.type,
166+
options: form.type === "select" ? form.options.split(",").map((o) => o.trim()).filter(Boolean) : undefined,
167+
required: form.required,
168+
});
169+
};
170+
171+
return (
172+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
173+
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
174+
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-md p-6">
175+
<h2 className="text-base font-semibold mb-4">{field ? "Edit" : "Add"} Field</h2>
176+
<form onSubmit={handleSubmit} className="space-y-3">
177+
<Input placeholder="Key (e.g. plate_number)" required value={form.key} onChange={(e) => setForm((p) => ({ ...p, key: e.target.value }))} />
178+
<Input placeholder="Label (e.g. Plate Number)" required value={form.label} onChange={(e) => setForm((p) => ({ ...p, label: e.target.value }))} />
179+
<select value={form.type} onChange={(e) => setForm((p) => ({ ...p, type: e.target.value as FieldType }))}
180+
className="w-full border rounded-lg px-3 py-2 text-sm">
181+
{FIELD_TYPES.map((t) => <option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}</option>)}
182+
</select>
183+
{form.type === "select" && (
184+
<Input placeholder="Options (comma-separated)" value={form.options} onChange={(e) => setForm((p) => ({ ...p, options: e.target.value }))} />
185+
)}
186+
<label className="flex items-center gap-2 text-sm">
187+
<input type="checkbox" checked={form.required} onChange={(e) => setForm((p) => ({ ...p, required: e.target.checked }))} className="rounded" />
188+
Required
189+
</label>
190+
<div className="flex justify-end gap-2 pt-2">
191+
<Button type="button" variant="outline" onClick={onClose}>Cancel</Button>
192+
<Button type="submit">{field ? "Save" : "Add Field"}</Button>
193+
</div>
194+
</form>
195+
</div>
196+
</div>
197+
);
198+
}

0 commit comments

Comments
 (0)