mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
Seatmap rendering and other updates
This commit is contained in:
@@ -2,57 +2,388 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Search, Edit, Trash2 } from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, RefreshCw } from 'lucide-react';
|
||||
import DataTable from '@/components/ui/DataTable';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import ActionButton from '@/components/ui/ActionButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { usersApi, BackofficeUser } from '@/lib/api/users';
|
||||
|
||||
export default function UserManagementPage() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filters, setFilters] = useState({ search: '', role: '', status: '', page: 1, pageSize: 10 });
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<any>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; user: any | null }>({ isOpen: false, user: null });
|
||||
const [resetPasswordModal, setResetPasswordModal] = useState<{ isOpen: boolean; user: any | null }>({ isOpen: false, user: null });
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['users', filters],
|
||||
queryFn: () => usersApi.getAll(filters),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: usersApi.create,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setShowModal(false);
|
||||
setEditingUser(null);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: any }) => usersApi.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setShowModal(false);
|
||||
setEditingUser(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: usersApi.delete,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
},
|
||||
});
|
||||
|
||||
const resetPasswordMutation = useMutation({
|
||||
mutationFn: ({ id, tempPassword }: { id: string; tempPassword: string }) =>
|
||||
usersApi.resetPassword(id, tempPassword),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setResetPasswordModal({ isOpen: false, user: null });
|
||||
setNewPassword('');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
const userData = {
|
||||
email: formData.get('email') as string,
|
||||
fullName: formData.get('fullName') as string,
|
||||
role: formData.get('role') as string,
|
||||
status: formData.get('status') as 'ACTIVE' | 'INACTIVE',
|
||||
} as any;
|
||||
|
||||
if (!editingUser) {
|
||||
userData.password = formData.get('password') as string;
|
||||
}
|
||||
|
||||
if (editingUser) {
|
||||
await updateMutation.mutateAsync({ id: editingUser.id, data: userData });
|
||||
} else {
|
||||
await createMutation.mutateAsync(userData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (user: any) => {
|
||||
setDeleteConfirm({ isOpen: true, user });
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (deleteConfirm.user) {
|
||||
await deleteMutation.mutateAsync(deleteConfirm.user.id);
|
||||
setDeleteConfirm({ isOpen: false, user: null });
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
if (resetPasswordModal.user && newPassword) {
|
||||
await resetPasswordMutation.mutateAsync({
|
||||
id: resetPasswordModal.user.id,
|
||||
tempPassword: newPassword,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'fullName',
|
||||
label: 'Full Name',
|
||||
sortable: true,
|
||||
render: (user: BackofficeUser) => (
|
||||
<div>
|
||||
<div className="font-medium">{user.fullName}</div>
|
||||
<div className="text-sm text-muted-foreground">{user.email}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: 'Role',
|
||||
render: (user: BackofficeUser) => (
|
||||
<Badge variant="status" status={user.role}>
|
||||
{user.role}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
render: (user: BackofficeUser) => (
|
||||
<Badge variant="status" status={user.status === 'ACTIVE' ? 'CONFIRMED' : 'CANCELLED'}>
|
||||
{user.status}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'lastLogin',
|
||||
label: 'Last Login',
|
||||
render: (user: BackofficeUser) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.lastLogin ? new Date(user.lastLogin).toLocaleString() : 'Never'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick: (user: BackofficeUser) => {
|
||||
setEditingUser(user);
|
||||
setShowModal(true);
|
||||
},
|
||||
variant: 'secondary' as const,
|
||||
icon: Edit,
|
||||
},
|
||||
{
|
||||
label: 'Reset Password',
|
||||
onClick: (user: BackofficeUser) => {
|
||||
setResetPasswordModal({ isOpen: true, user });
|
||||
setNewPassword('');
|
||||
},
|
||||
variant: 'secondary' as const,
|
||||
icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: handleDelete,
|
||||
variant: 'danger' as const,
|
||||
icon: Trash2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">User Management</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage system users and permissions</p>
|
||||
<h1 className="text-2xl font-bold text-foreground">User Management</h1>
|
||||
<p className="text-muted-foreground">Manage backoffice users and their permissions</p>
|
||||
</div>
|
||||
<ActionButton
|
||||
icon={Plus}
|
||||
onClick={() => {
|
||||
setEditingUser(null);
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
Add User
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="card">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users by name or email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="input pl-10"
|
||||
placeholder="Search users..."
|
||||
className="input"
|
||||
value={filters.search}
|
||||
onChange={(e) => setFilters({ ...filters, search: e.target.value, page: 1 })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
className="input"
|
||||
value={filters.role}
|
||||
onChange={(e) => setFilters({ ...filters, role: e.target.value, page: 1 })}
|
||||
>
|
||||
<option value="">All Roles</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="SUPERVISOR">Supervisor</option>
|
||||
<option value="STAFF">Staff</option>
|
||||
<option value="AGENT">Agent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
className="input"
|
||||
value={filters.status}
|
||||
onChange={(e) => setFilters({ ...filters, status: e.target.value, page: 1 })}
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">Active</option>
|
||||
<option value="INACTIVE">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-foreground">Name</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-foreground">Email</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-foreground">Role</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-foreground">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">
|
||||
User management coming soon
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Users Table */}
|
||||
<DataTable
|
||||
data={data?.items || []}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={isLoading}
|
||||
emptyMessage="No users found"
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<ConfirmDialog
|
||||
isOpen={deleteConfirm.isOpen}
|
||||
onClose={() => setDeleteConfirm({ isOpen: false, user: null })}
|
||||
onConfirm={confirmDelete}
|
||||
title="Delete User"
|
||||
message={`Are you sure you want to delete ${deleteConfirm.user?.fullName}? This action cannot be undone.`}
|
||||
confirmText="Delete"
|
||||
isDanger={true}
|
||||
/>
|
||||
|
||||
{/* Reset Password Modal */}
|
||||
<Modal
|
||||
isOpen={resetPasswordModal.isOpen}
|
||||
onClose={() => setResetPasswordModal({ isOpen: false, user: null })}
|
||||
title="Reset User Password"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded p-3 text-sm">
|
||||
<p className="font-semibold text-blue-900 dark:text-blue-200">Temporary Password</p>
|
||||
<p className="text-blue-800 dark:text-blue-300 mt-1">
|
||||
Set a temporary password for {resetPasswordModal.user?.fullName}. They will need to change it on first login.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Temporary Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
className="input"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Enter temporary password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<ActionButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setResetPasswordModal({ isOpen: false, user: null })}
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={handleResetPassword}
|
||||
loading={resetPasswordMutation.isPending}
|
||||
disabled={!newPassword}
|
||||
>
|
||||
Reset Password
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Modal
|
||||
isOpen={showModal}
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
setEditingUser(null);
|
||||
}}
|
||||
title={`${editingUser ? 'Edit' : 'Add'} User`}
|
||||
size="lg"
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="label">Full Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
className="input"
|
||||
defaultValue={editingUser?.fullName}
|
||||
required
|
||||
placeholder="e.g., John Doe"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="input"
|
||||
defaultValue={editingUser?.email}
|
||||
required
|
||||
placeholder="e.g., john@example.com"
|
||||
disabled={!!editingUser}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Role *</label>
|
||||
<select
|
||||
name="role"
|
||||
className="input"
|
||||
defaultValue={editingUser?.role || 'STAFF'}
|
||||
required
|
||||
>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="SUPERVISOR">Supervisor</option>
|
||||
<option value="STAFF">Staff</option>
|
||||
<option value="AGENT">Agent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Status</label>
|
||||
<select
|
||||
name="status"
|
||||
className="input"
|
||||
defaultValue={editingUser?.status || 'ACTIVE'}
|
||||
>
|
||||
<option value="ACTIVE">Active</option>
|
||||
<option value="INACTIVE">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
{!editingUser && (
|
||||
<div>
|
||||
<label className="label">Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="input"
|
||||
required
|
||||
placeholder="Minimum 8 characters"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<ActionButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
setEditingUser(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
type="submit"
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
{editingUser ? 'Update' : 'Create'} User
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user