ca-grow-ops-manager/frontend/src/pages/RolesPage.tsx
fullsizemalt 71e58dd4c7
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
feat: Linear-inspired UI redesign with Space Grotesk headlines
- Complete UI refactor with charcoal/bone color palette
- Add Space Grotesk font for headlines, Inter for body
- Update all 24+ pages with new design system
- Add LinearPrimitives reusable components
- Improve dark mode support throughout
- Add subtle micro-animations and transitions
2025-12-12 14:29:47 -08:00

161 lines
6.9 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Shield, Plus, Edit2, Trash2, Users } from 'lucide-react';
import { rolesApi, Role } from '../lib/rolesApi';
import RoleModal from '../components/roles/RoleModal';
import { PageHeader, EmptyState, ActionButton, CardSkeleton } from '../components/ui/LinearPrimitives';
export default function RolesPage() {
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<Role | undefined>(undefined);
useEffect(() => {
loadRoles();
}, []);
const loadRoles = async () => {
setLoading(true);
try {
const data = await rolesApi.getAll();
setRoles(data);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const handleEdit = (role: Role) => {
setSelectedRole(role);
setIsModalOpen(true);
};
const handleCreate = () => {
setSelectedRole(undefined);
setIsModalOpen(true);
};
const handleDelete = async (role: Role) => {
if (role.isSystem) {
alert('Cannot delete system roles.');
return;
}
if (!confirm(`Delete role "${role.name}"?`)) return;
try {
await rolesApi.delete(role.id);
loadRoles();
} catch (e) {
console.error(e);
alert('Failed to delete role');
}
};
return (
<div className="max-w-4xl mx-auto space-y-6 pb-20 animate-in">
<PageHeader
title="Roles & Permissions"
subtitle="Manage staff access levels"
actions={
<button onClick={handleCreate} className="btn btn-primary">
<Plus size={16} />
<span className="hidden sm:inline">New Role</span>
</button>
}
/>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)}
</div>
) : roles.length === 0 ? (
<EmptyState
icon={Shield}
title="No roles configured"
description="Create your first role to manage permissions."
action={
<button onClick={handleCreate} className="btn btn-primary">
<Plus size={16} />
Create Role
</button>
}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{roles.map(role => (
<div key={role.id} className="card card-interactive p-4 flex flex-col h-full group">
<div className="flex justify-between items-start mb-3">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-primary text-sm flex items-center gap-2">
{role.name}
{role.isSystem && (
<span className="badge text-[10px]">System</span>
)}
</h3>
<p className="text-xs text-tertiary mt-1 truncate">
{role.description || 'No description'}
</p>
</div>
<div className="w-8 h-8 rounded-md bg-accent-muted flex items-center justify-center flex-shrink-0">
<Shield size={14} className="text-accent" />
</div>
</div>
<div className="flex-1">
<h4 className="text-[10px] font-medium text-tertiary uppercase tracking-wider mb-2">
Access
</h4>
<div className="flex flex-wrap gap-1">
{Object.keys(role.permissions).slice(0, 4).map(res => (
<span key={res} className="badge-success text-[10px]">
{res}
</span>
))}
{Object.keys(role.permissions).length > 4 && (
<span className="badge text-[10px]">
+{Object.keys(role.permissions).length - 4}
</span>
)}
{Object.keys(role.permissions).length === 0 && (
<span className="text-xs text-tertiary">No permissions</span>
)}
</div>
</div>
<div className="border-t border-subtle mt-4 pt-3 flex justify-between items-center">
<div className="flex items-center gap-1 text-xs text-tertiary">
<Users size={12} />
<span>{role._count?.users || 0}</span>
</div>
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity duration-fast">
<ActionButton
icon={Edit2}
label="Edit"
onClick={() => handleEdit(role)}
variant="accent"
/>
{!role.isSystem && (
<ActionButton
icon={Trash2}
label="Delete"
onClick={() => handleDelete(role)}
variant="destructive"
/>
)}
</div>
</div>
</div>
))}
</div>
)}
<RoleModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSuccess={loadRoles}
role={selectedRole}
/>
</div>
);
}