183 lines
7.8 KiB
TypeScript
183 lines
7.8 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { VehicleGroup, Vehicle, createGroup, updateGroup, assignVehicleToGroup } from '@/lib/api';
|
||
|
|
import { Users, Plus, Edit2, X, Check } from 'lucide-react';
|
||
|
|
|
||
|
|
interface GroupManagerProps {
|
||
|
|
groups: VehicleGroup[];
|
||
|
|
vehicles: Vehicle[];
|
||
|
|
onGroupChanged: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function GroupManager({ groups, vehicles, onGroupChanged }: GroupManagerProps) {
|
||
|
|
const [isCreating, setIsCreating] = useState(false);
|
||
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||
|
|
|
||
|
|
const [name, setName] = useState('');
|
||
|
|
const [description, setDescription] = useState('');
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
|
||
|
|
// Vehicle Assignment State
|
||
|
|
const [assignVin, setAssignVin] = useState('');
|
||
|
|
|
||
|
|
const startCreate = () => {
|
||
|
|
setIsCreating(true);
|
||
|
|
setEditingId(null);
|
||
|
|
setName('');
|
||
|
|
setDescription('');
|
||
|
|
setAssignVin('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const startEdit = (group: VehicleGroup) => {
|
||
|
|
setIsCreating(false);
|
||
|
|
setEditingId(group.id);
|
||
|
|
setName(group.name);
|
||
|
|
setDescription(group.description || '');
|
||
|
|
setAssignVin('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const cancel = () => {
|
||
|
|
setIsCreating(false);
|
||
|
|
setEditingId(null);
|
||
|
|
setName('');
|
||
|
|
setDescription('');
|
||
|
|
setAssignVin('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!name) return;
|
||
|
|
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
if (isCreating) {
|
||
|
|
await createGroup(name, description);
|
||
|
|
} else if (editingId) {
|
||
|
|
await updateGroup(editingId, name, description);
|
||
|
|
}
|
||
|
|
onGroupChanged();
|
||
|
|
cancel();
|
||
|
|
} catch (err) {
|
||
|
|
console.error(err);
|
||
|
|
alert('Operation failed');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAssignVehicle = async () => {
|
||
|
|
if (!editingId || !assignVin) return;
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
await assignVehicleToGroup(editingId, assignVin);
|
||
|
|
onGroupChanged();
|
||
|
|
alert('Vehicle assigned');
|
||
|
|
setAssignVin('');
|
||
|
|
} catch (err) {
|
||
|
|
console.error(err);
|
||
|
|
alert('Failed to assign vehicle');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="bg-zinc-900/50 backdrop-blur-md border border-zinc-800 rounded-xl p-6 shadow-xl">
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||
|
|
<Users className="w-5 h-5 text-orange-400" />
|
||
|
|
Vehicle Groups
|
||
|
|
</h2>
|
||
|
|
<button
|
||
|
|
onClick={startCreate}
|
||
|
|
className="text-xs bg-zinc-800 hover:bg-zinc-700 text-white px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1"
|
||
|
|
>
|
||
|
|
<Plus className="w-3 h-3" /> New Group
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{(isCreating || editingId) && (
|
||
|
|
<div className="mb-4 bg-zinc-950/50 p-4 rounded-lg border border-zinc-800/50">
|
||
|
|
<form onSubmit={handleSubmit} className="space-y-3 mb-4">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={name}
|
||
|
|
onChange={e => setName(e.target.value)}
|
||
|
|
placeholder="Group Name"
|
||
|
|
className="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-white text-sm outline-none focus:ring-1 focus:ring-orange-500"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={description}
|
||
|
|
onChange={e => setDescription(e.target.value)}
|
||
|
|
placeholder="Description (optional)"
|
||
|
|
className="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-white text-sm outline-none focus:ring-1 focus:ring-orange-500"
|
||
|
|
/>
|
||
|
|
<div className="flex items-center gap-2 pt-1">
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
disabled={loading}
|
||
|
|
className="flex-1 bg-orange-500 hover:bg-orange-600 text-white text-xs font-medium py-1.5 rounded transition-colors flex items-center justify-center gap-1"
|
||
|
|
>
|
||
|
|
<Check className="w-3 h-3" /> Save
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={cancel}
|
||
|
|
className="flex-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-medium py-1.5 rounded transition-colors flex items-center justify-center gap-1"
|
||
|
|
>
|
||
|
|
<X className="w-3 h-3" /> Cancel
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
{editingId && (
|
||
|
|
<div className="border-t border-zinc-800 pt-3">
|
||
|
|
<h4 className="text-xs font-medium text-zinc-400 mb-2">Assign Vehicle</h4>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<select
|
||
|
|
value={assignVin}
|
||
|
|
onChange={e => setAssignVin(e.target.value)}
|
||
|
|
className="flex-1 bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs text-white outline-none"
|
||
|
|
>
|
||
|
|
<option value="">Select...</option>
|
||
|
|
{vehicles.map(v => (
|
||
|
|
<option key={v.vin} value={v.vin}>{v.vin} {v.groupId === editingId ? '(Already in group)' : ''}</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
<button
|
||
|
|
onClick={handleAssignVehicle}
|
||
|
|
disabled={loading || !assignVin}
|
||
|
|
className="bg-zinc-800 hover:bg-zinc-700 text-white px-2 py-1 rounded text-xs"
|
||
|
|
>
|
||
|
|
<Plus className="w-3 h-3" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-1">
|
||
|
|
{groups.map(group => (
|
||
|
|
<div key={group.id} className="group flex items-center justify-between p-3 bg-zinc-900/30 border border-zinc-800/30 rounded-lg hover:border-zinc-700 transition-colors">
|
||
|
|
<div>
|
||
|
|
<h3 className="text-zinc-200 font-medium text-sm">{group.name}</h3>
|
||
|
|
<p className="text-zinc-500 text-xs">{group.vehicles?.length || 0} vehicles</p>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => startEdit(group)}
|
||
|
|
className="opacity-0 group-hover:opacity-100 p-1.5 text-zinc-500 hover:text-white hover:bg-zinc-800 rounded transition-all"
|
||
|
|
>
|
||
|
|
<Edit2 className="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{groups.length === 0 && <p className="text-center text-zinc-500 text-sm py-4">No groups created.</p>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|