Seatmap rendering and other updates

This commit is contained in:
Stephanos A
2026-06-09 15:22:27 +03:00
parent bf8cc7e6cf
commit c2bab6cae8
30 changed files with 2020 additions and 329 deletions

View File

@@ -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>
);
}