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:
Satyam-Rastogi
2026-04-06 18:33:48 +05:30
parent fe9ddda941
commit c927f4c44d
3 changed files with 335 additions and 6 deletions
+32 -6
View File
@@ -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>
);
}