feat: Add time block approval workflow and staff permission system

- Add TimeBlock approval status with manager approval workflow
- Create core mixins for staff permission restrictions (DenyStaffWritePermission, etc.)
- Add StaffDashboard page for staff-specific views
- Refactor MyAvailability page for time block management
- Update field mobile status machine and views
- Add per-user permission overrides via JSONField
- Document core mixins and permission system in CLAUDE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-07 17:49:37 -05:00
parent 01020861c7
commit 410b46a896
27 changed files with 3192 additions and 1237 deletions

View File

@@ -158,8 +158,9 @@ export const useMyBlocks = () => {
id: String(b.id),
resource: b.resource ? String(b.resource) : null,
})),
resource_id: String(data.resource_id),
resource_id: data.resource_id ? String(data.resource_id) : null,
resource_name: data.resource_name,
can_self_approve: data.can_self_approve,
};
},
});
@@ -248,6 +249,75 @@ export const useToggleTimeBlock = () => {
});
};
// =============================================================================
// Time Block Approval Hooks
// =============================================================================
export interface PendingReviewsResponse {
count: number;
pending_blocks: TimeBlockListItem[];
}
/**
* Hook to fetch pending time block reviews (for managers/owners)
*/
export const usePendingReviews = () => {
return useQuery<PendingReviewsResponse>({
queryKey: ['time-block-pending-reviews'],
queryFn: async () => {
const { data } = await apiClient.get('/time-blocks/pending_reviews/');
return {
count: data.count,
pending_blocks: data.pending_blocks.map((b: any) => ({
...b,
id: String(b.id),
resource: b.resource ? String(b.resource) : null,
})),
};
},
});
};
/**
* Hook to approve a time block
*/
export const useApproveTimeBlock = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, notes }: { id: string; notes?: string }) => {
const { data } = await apiClient.post(`/time-blocks/${id}/approve/`, { notes });
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['time-blocks'] });
queryClient.invalidateQueries({ queryKey: ['blocked-dates'] });
queryClient.invalidateQueries({ queryKey: ['my-blocks'] });
queryClient.invalidateQueries({ queryKey: ['time-block-pending-reviews'] });
},
});
};
/**
* Hook to deny a time block
*/
export const useDenyTimeBlock = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, notes }: { id: string; notes?: string }) => {
const { data } = await apiClient.post(`/time-blocks/${id}/deny/`, { notes });
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['time-blocks'] });
queryClient.invalidateQueries({ queryKey: ['blocked-dates'] });
queryClient.invalidateQueries({ queryKey: ['my-blocks'] });
queryClient.invalidateQueries({ queryKey: ['time-block-pending-reviews'] });
},
});
};
/**
* Hook to check for conflicts before creating a time block
*/