feat(estimates): template access control — owner can grant edit rights by role or individual
- Add TemplateAccessModal: role-level toggles (All Admins / All Field Agents) + per-person overrides - Three-state access logic: role grant, individual direct grant, individual exclusion override - Excluded people (role ON but toggled off) pinned to top with amber 'Excluded' chip - Direct grants (role OFF but toggled on) show green 'Direct' chip - canEditTemplates replaces isOwner for all template CRUD guards in EstimatesPage - Owner retains exclusive access to Manage Access button - State persisted in mockStore: templateAccessRoles, templateAccessUsers, templateAccessExcluded
This commit is contained in:
@@ -9,11 +9,12 @@ import {
|
||||
Plus, Search, FileText, Layers, Calendar, DollarSign,
|
||||
User, CheckCircle, Clock,
|
||||
Pencil, Trash2, Copy, ChevronRight, Tag, Package,
|
||||
ArrowDownUp, ArrowUp, ArrowDown, X as XIcon
|
||||
ArrowDownUp, ArrowUp, ArrowDown, X as XIcon, Shield
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import TemplateEditorModal from '../components/estimates/TemplateEditorModal';
|
||||
import EstimateDetailModal from '../components/estimates/EstimateDetailModal';
|
||||
import TemplateAccessModal from '../components/estimates/TemplateAccessModal';
|
||||
import { ALL_STATUS_CONFIG } from '../components/estimates/EstimateDetailModal';
|
||||
import { computeGrandTotal } from '../utils/estimateExport';
|
||||
|
||||
@@ -225,11 +226,19 @@ export default function EstimatesPage() {
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useTheme();
|
||||
const { user } = useAuth();
|
||||
const { estimates, templates, addTemplate, updateTemplate, deleteTemplate } = useMockStore();
|
||||
const {
|
||||
estimates, templates, addTemplate, updateTemplate, deleteTemplate,
|
||||
templateAccessRoles, templateAccessUsers, templateAccessExcluded,
|
||||
} = useMockStore();
|
||||
|
||||
const isOwner = user?.role === ROLES.OWNER;
|
||||
const basePath = user?.role === 'OWNER' ? '/owner' : user?.role === 'ADMIN' ? '/admin' : '/emp/fa';
|
||||
|
||||
// Effective template edit permission (Owner always; others via role/individual grant minus exclusions)
|
||||
const canEditTemplates = isOwner ||
|
||||
(templateAccessRoles.includes(user?.role) && !templateAccessExcluded.includes(user?.id)) ||
|
||||
templateAccessUsers.includes(user?.id);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('estimates'); // 'estimates' | 'templates'
|
||||
const [estimateSearch, setEstimateSearch] = useState('');
|
||||
const [estimateStatusFilter, setEstimateStatusFilter] = useState('All');
|
||||
@@ -250,6 +259,9 @@ export default function EstimatesPage() {
|
||||
// Delete confirm
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
|
||||
// Access management modal (Owner only)
|
||||
const [accessModalOpen, setAccessModalOpen] = useState(false);
|
||||
|
||||
// ---- filtered + sorted estimates ----
|
||||
const STATUS_SORT_ORDER = ['Draft', 'Sent', 'Waiting Approval', 'Approved', 'Follow Up Required', 'Revision Required', 'Rejected', 'Expired'];
|
||||
|
||||
@@ -553,7 +565,7 @@ export default function EstimatesPage() {
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
{isOwner && (
|
||||
{canEditTemplates && (
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold transition-colors shadow-sm ml-1"
|
||||
@@ -561,10 +573,18 @@ export default function EstimatesPage() {
|
||||
<Plus size={13} /> New Template
|
||||
</button>
|
||||
)}
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={() => setAccessModalOpen(true)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-600 dark:text-zinc-300 text-xs font-semibold hover:border-zinc-300 dark:hover:border-zinc-600 transition-colors"
|
||||
>
|
||||
<Shield size={13} /> Manage Access
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isOwner && (
|
||||
{!canEditTemplates && (
|
||||
<div className="mb-4 px-4 py-3 rounded-xl bg-zinc-100 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Templates are managed by the Owner. You can apply these when creating a new estimate.
|
||||
</div>
|
||||
@@ -574,7 +594,7 @@ export default function EstimatesPage() {
|
||||
<div className="text-center py-16 text-zinc-400 dark:text-zinc-600">
|
||||
<Layers size={32} className="mx-auto mb-3 opacity-40" />
|
||||
<p className="text-sm font-medium">No templates found</p>
|
||||
{isOwner && (
|
||||
{canEditTemplates && (
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="mt-4 flex items-center gap-1.5 mx-auto px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold transition-colors"
|
||||
@@ -589,7 +609,7 @@ export default function EstimatesPage() {
|
||||
<TemplateCard
|
||||
key={tpl.id}
|
||||
template={tpl}
|
||||
isOwner={isOwner}
|
||||
isOwner={canEditTemplates}
|
||||
onEdit={openEdit}
|
||||
onDuplicate={openDuplicate}
|
||||
onDelete={setDeleteTarget}
|
||||
@@ -629,6 +649,12 @@ export default function EstimatesPage() {
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Template access modal — Owner only */}
|
||||
<TemplateAccessModal
|
||||
isOpen={accessModalOpen}
|
||||
onClose={() => setAccessModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user