From 410b46a896a4e8f28ba4a22b745df74f9c071ec8 Mon Sep 17 00:00:00 2001 From: poduck Date: Sun, 7 Dec 2025 17:49:37 -0500 Subject: [PATCH] feat: Add time block approval workflow and staff permission system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 137 +++ frontend/src/App.tsx | 3 +- frontend/src/api/auth.ts | 2 + frontend/src/components/StaffPermissions.tsx | 9 + .../time-blocks/TimeBlockCreatorModal.tsx | 234 ++--- frontend/src/hooks/useTimeBlocks.ts | 72 +- frontend/src/i18n/locales/en.json | 34 +- frontend/src/pages/MyAvailability.tsx | 844 ++++++------------ frontend/src/pages/StaffDashboard.tsx | 627 +++++++++++++ frontend/src/pages/TimeBlocks.tsx | 325 +++++++ frontend/src/types.ts | 13 +- mobile/field-app/src/types/index.ts | 38 +- smoothschedule/core/mixins.py | 546 +++++++++++ smoothschedule/payments/urls.py | 10 +- smoothschedule/payments/views.py | 139 ++- .../migrations/0030_time_block_approval.py | 40 + smoothschedule/schedule/models.py | 45 +- smoothschedule/schedule/serializers.py | 30 +- smoothschedule/schedule/signals.py | 281 +++++- smoothschedule/schedule/urls.py | 9 +- smoothschedule/schedule/views.py | 802 ++++++++++------- .../field_mobile/services/status_machine.py | 40 +- .../smoothschedule/field_mobile/urls.py | 10 +- .../smoothschedule/field_mobile/views.py | 86 +- .../smoothschedule/users/api_views.py | 20 +- smoothschedule/smoothschedule/users/models.py | 21 + smoothschedule/smoothschedule/users/urls.py | 12 +- 27 files changed, 3192 insertions(+), 1237 deletions(-) create mode 100644 frontend/src/pages/StaffDashboard.tsx create mode 100644 smoothschedule/core/mixins.py create mode 100644 smoothschedule/schedule/migrations/0030_time_block_approval.py diff --git a/CLAUDE.md b/CLAUDE.md index e807cec..27be45f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,143 @@ docker compose -f docker-compose.local.yml exec django python manage.py import('./pages/marketing/TermsOfSer // Import pages const Dashboard = React.lazy(() => import('./pages/Dashboard')); +const StaffDashboard = React.lazy(() => import('./pages/StaffDashboard')); const StaffSchedule = React.lazy(() => import('./pages/StaffSchedule')); const Scheduler = React.lazy(() => import('./pages/Scheduler')); const Customers = React.lazy(() => import('./pages/Customers')); @@ -667,7 +668,7 @@ const AppContent: React.FC = () => { {/* Regular Routes */} : } + element={user.role === 'resource' ? : user.role === 'staff' ? : } /> {/* Staff Schedule - vertical timeline view */} ; can_invite_staff?: boolean; can_access_tickets?: boolean; + can_edit_schedule?: boolean; + linked_resource_id?: number; quota_overages?: QuotaOverage[]; } diff --git a/frontend/src/components/StaffPermissions.tsx b/frontend/src/components/StaffPermissions.tsx index 94ae6ff..9dadfa8 100644 --- a/frontend/src/components/StaffPermissions.tsx +++ b/frontend/src/components/StaffPermissions.tsx @@ -87,6 +87,15 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [ defaultValue: true, roles: ['staff'], }, + { + key: 'can_self_approve_time_off', + labelKey: 'staff.canSelfApproveTimeOff', + labelDefault: 'Can self-approve time off', + hintKey: 'staff.canSelfApproveTimeOffHint', + hintDefault: 'Add time off without requiring manager/owner approval', + defaultValue: false, + roles: ['staff'], + }, // Shared permissions (both manager and staff) { key: 'can_access_tickets', diff --git a/frontend/src/components/time-blocks/TimeBlockCreatorModal.tsx b/frontend/src/components/time-blocks/TimeBlockCreatorModal.tsx index 6e085a2..6a64a2a 100644 --- a/frontend/src/components/time-blocks/TimeBlockCreatorModal.tsx +++ b/frontend/src/components/time-blocks/TimeBlockCreatorModal.tsx @@ -155,6 +155,10 @@ interface TimeBlockCreatorModalProps { holidays: Holiday[]; resources: Resource[]; isResourceLevel?: boolean; + /** Staff mode: hides level selector, locks to resource, pre-selects resource */ + staffMode?: boolean; + /** Pre-selected resource ID for staff mode */ + staffResourceId?: string | number | null; } type Step = 'preset' | 'details' | 'schedule' | 'review'; @@ -168,6 +172,8 @@ const TimeBlockCreatorModal: React.FC = ({ holidays, resources, isResourceLevel: initialIsResourceLevel = false, + staffMode = false, + staffResourceId = null, }) => { const { t } = useTranslation(); const [step, setStep] = useState(editingBlock ? 'details' : 'preset'); @@ -177,7 +183,8 @@ const TimeBlockCreatorModal: React.FC = ({ // Form state const [title, setTitle] = useState(editingBlock?.title || ''); const [description, setDescription] = useState(editingBlock?.description || ''); - const [blockType, setBlockType] = useState(editingBlock?.block_type || 'HARD'); + // In staff mode, default to SOFT blocks (time-off requests) + const [blockType, setBlockType] = useState(editingBlock?.block_type || (staffMode ? 'SOFT' : 'HARD')); const [recurrenceType, setRecurrenceType] = useState(editingBlock?.recurrence_type || 'NONE'); const [allDay, setAllDay] = useState(editingBlock?.all_day ?? true); const [startTime, setStartTime] = useState(editingBlock?.start_time || '09:00'); @@ -270,7 +277,8 @@ const TimeBlockCreatorModal: React.FC = ({ setAllDay(true); setStartTime('09:00'); setEndTime('17:00'); - setResourceId(null); + // In staff mode, pre-select the staff's resource + setResourceId(staffMode && staffResourceId ? String(staffResourceId) : null); setSelectedDates([]); setDaysOfWeek([]); setDaysOfMonth([]); @@ -279,10 +287,11 @@ const TimeBlockCreatorModal: React.FC = ({ setHolidayCodes([]); setRecurrenceStart(''); setRecurrenceEnd(''); - setIsResourceLevel(initialIsResourceLevel); + // In staff mode, always resource-level + setIsResourceLevel(staffMode ? true : initialIsResourceLevel); } } - }, [isOpen, editingBlock, initialIsResourceLevel]); + }, [isOpen, editingBlock, initialIsResourceLevel, staffMode, staffResourceId]); // Apply preset configuration const applyPreset = (presetId: string) => { @@ -293,7 +302,8 @@ const TimeBlockCreatorModal: React.FC = ({ setTitle(preset.config.title); setRecurrenceType(preset.config.recurrence_type); setAllDay(preset.config.all_day); - setBlockType(preset.config.block_type); + // In staff mode, always use SOFT blocks regardless of preset + setBlockType(staffMode ? 'SOFT' : preset.config.block_type); if (preset.config.start_time) setStartTime(preset.config.start_time); if (preset.config.end_time) setEndTime(preset.config.end_time); @@ -367,12 +377,15 @@ const TimeBlockCreatorModal: React.FC = ({ }; const handleSubmit = () => { + // In staff mode, always use the staff's resource ID + const effectiveResourceId = staffMode ? staffResourceId : resourceId; + const baseData: any = { description: description || undefined, block_type: blockType, recurrence_type: recurrenceType, all_day: allDay, - resource: isResourceLevel ? resourceId : null, + resource: isResourceLevel ? effectiveResourceId : null, }; if (!allDay) { @@ -425,7 +438,8 @@ const TimeBlockCreatorModal: React.FC = ({ return true; case 'details': if (!title.trim()) return false; - if (isResourceLevel && !resourceId) return false; + // In staff mode, resource is auto-selected; otherwise check if selected + if (isResourceLevel && !staffMode && !resourceId) return false; return true; case 'schedule': if (recurrenceType === 'NONE' && selectedDates.length === 0) return false; @@ -556,63 +570,65 @@ const TimeBlockCreatorModal: React.FC = ({ {/* Step 2: Details */} {step === 'details' && (
- {/* Block Level Selector */} -
- -
- +
- - + +
- + )} {/* Title */}
@@ -642,8 +658,8 @@ const TimeBlockCreatorModal: React.FC = ({ />
- {/* Resource (if resource-level) */} - {isResourceLevel && ( + {/* Resource (if resource-level) - Hidden in staff mode since it's auto-selected */} + {isResourceLevel && !staffMode && (
)} - {/* Block Type */} -
- -
-
-

- Completely prevents bookings. Cannot be overridden. -

- - +
-

- Shows a warning but allows bookings with override. -

- +

+ Shows a warning but allows bookings with override. +

+ + - + )} {/* All Day Toggle & Time */}
@@ -1188,11 +1206,11 @@ const TimeBlockCreatorModal: React.FC = ({ )}
- {isResourceLevel && resourceId && ( + {isResourceLevel && (resourceId || staffResourceId) && (
Resource
- {resources.find(r => r.id === resourceId)?.name || resourceId} + {resources.find(r => String(r.id) === String(staffMode ? staffResourceId : resourceId))?.name || (staffMode ? staffResourceId : resourceId)}
)} diff --git a/frontend/src/hooks/useTimeBlocks.ts b/frontend/src/hooks/useTimeBlocks.ts index eaef979..bcf7f9b 100644 --- a/frontend/src/hooks/useTimeBlocks.ts +++ b/frontend/src/hooks/useTimeBlocks.ts @@ -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({ + 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 */ diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index d6224cd..23d5edf 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -491,7 +491,39 @@ "reactivateAccount": "Reactivate Account", "deactivateHint": "Prevent this user from logging in while keeping their data", "reactivateHint": "Allow this user to log in again", - "deactivate": "Deactivate" + "deactivate": "Deactivate", + "canSelfApproveTimeOff": "Can self-approve time off", + "canSelfApproveTimeOffHint": "Add time off without requiring manager/owner approval" + }, + "staffDashboard": { + "welcomeTitle": "Welcome, {{name}}!", + "weekOverview": "Here's your week at a glance", + "noResourceLinked": "Your account is not linked to a resource yet. Please contact your manager to set up your schedule.", + "currentAppointment": "Current Appointment", + "nextAppointment": "Next Appointment", + "viewSchedule": "View Schedule", + "todayAppointments": "Today", + "thisWeek": "This Week", + "completed": "Completed", + "hoursWorked": "Hours Worked", + "appointmentsLabel": "appointments", + "totalAppointments": "total appointments", + "completionRate": "completion rate", + "thisWeekLabel": "this week", + "upcomingAppointments": "Upcoming", + "noUpcoming": "No upcoming appointments", + "weeklyOverview": "This Week", + "appointments": "Appointments", + "today": "Today", + "tomorrow": "Tomorrow", + "scheduled": "Scheduled", + "inProgress": "In Progress", + "cancelled": "Cancelled", + "noShows": "No-Shows", + "viewMySchedule": "View My Schedule", + "viewScheduleDesc": "See your daily appointments and manage your time", + "manageAvailability": "Manage Availability", + "availabilityDesc": "Set your working hours and time off" }, "tickets": { "title": "Support Tickets", diff --git a/frontend/src/pages/MyAvailability.tsx b/frontend/src/pages/MyAvailability.tsx index 75f8a08..7b63c1d 100644 --- a/frontend/src/pages/MyAvailability.tsx +++ b/frontend/src/pages/MyAvailability.tsx @@ -2,17 +2,17 @@ * My Availability Page * * Staff-facing page to view and manage their own time blocks. + * Uses the same UI as TimeBlocks but locked to the staff's own resource. * Shows business-level blocks (read-only) and personal blocks (editable). */ -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useOutletContext } from 'react-router-dom'; import { TimeBlockListItem, BlockType, RecurrenceType, - RecurrencePattern, User, } from '../types'; import { @@ -22,10 +22,10 @@ import { useDeleteTimeBlock, useToggleTimeBlock, useHolidays, - CreateTimeBlockData, } from '../hooks/useTimeBlocks'; import Portal from '../components/Portal'; import YearlyBlockCalendar from '../components/time-blocks/YearlyBlockCalendar'; +import TimeBlockCreatorModal from '../components/time-blocks/TimeBlockCreatorModal'; import { Calendar, Building2, @@ -33,7 +33,6 @@ import { Plus, Pencil, Trash2, - X, AlertTriangle, Clock, CalendarDays, @@ -42,8 +41,13 @@ import { Power, PowerOff, Info, + CheckCircle, + XCircle, + HourglassIcon, } from 'lucide-react'; +type AvailabilityTab = 'blocks' | 'calendar'; + const RECURRENCE_TYPE_LABELS: Record = { NONE: 'One-time', WEEKLY: 'Weekly', @@ -57,43 +61,6 @@ const BLOCK_TYPE_LABELS: Record = { SOFT: 'Soft Block', }; -const DAY_ABBREVS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - -const MONTH_NAMES = [ - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December' -]; - -interface TimeBlockFormData { - title: string; - description: string; - block_type: BlockType; - recurrence_type: RecurrenceType; - start_date: string; - end_date: string; - all_day: boolean; - start_time: string; - end_time: string; - recurrence_pattern: RecurrencePattern; - recurrence_start: string; - recurrence_end: string; -} - -const defaultFormData: TimeBlockFormData = { - title: '', - description: '', - block_type: 'SOFT', - recurrence_type: 'NONE', - start_date: '', - end_date: '', - all_day: true, - start_time: '09:00', - end_time: '17:00', - recurrence_pattern: {}, - recurrence_start: '', - recurrence_end: '', -}; - interface MyAvailabilityProps { user?: User; } @@ -103,9 +70,9 @@ const MyAvailability: React.FC = (props) => { const contextUser = useOutletContext<{ user?: User }>()?.user; const user = props.user || contextUser; + const [activeTab, setActiveTab] = useState('blocks'); const [isModalOpen, setIsModalOpen] = useState(false); const [editingBlock, setEditingBlock] = useState(null); - const [formData, setFormData] = useState(defaultFormData); const [deleteConfirmId, setDeleteConfirmId] = useState(null); // Fetch data @@ -118,105 +85,20 @@ const MyAvailability: React.FC = (props) => { const deleteBlock = useDeleteTimeBlock(); const toggleBlock = useToggleTimeBlock(); - // Check if user can create hard blocks - const canCreateHardBlocks = user?.permissions?.can_create_hard_blocks ?? false; - // Modal handlers const openCreateModal = () => { setEditingBlock(null); - setFormData(defaultFormData); setIsModalOpen(true); }; const openEditModal = (block: TimeBlockListItem) => { setEditingBlock(block); - setFormData({ - title: block.title, - description: '', - block_type: block.block_type, - recurrence_type: block.recurrence_type, - start_date: '', - end_date: '', - all_day: true, - start_time: '09:00', - end_time: '17:00', - recurrence_pattern: {}, - recurrence_start: '', - recurrence_end: '', - }); setIsModalOpen(true); }; const closeModal = () => { setIsModalOpen(false); setEditingBlock(null); - setFormData(defaultFormData); - }; - - // Form handlers - const handleFormChange = (field: keyof TimeBlockFormData, value: any) => { - setFormData((prev) => ({ ...prev, [field]: value })); - }; - - const handlePatternChange = (field: keyof RecurrencePattern, value: any) => { - setFormData((prev) => ({ - ...prev, - recurrence_pattern: { ...prev.recurrence_pattern, [field]: value }, - })); - }; - - const handleDayOfWeekToggle = (day: number) => { - const current = formData.recurrence_pattern.days_of_week || []; - const newDays = current.includes(day) - ? current.filter((d) => d !== day) - : [...current, day].sort(); - handlePatternChange('days_of_week', newDays); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!myBlocksData?.resource_id) { - console.error('No resource linked to user'); - return; - } - - const payload: CreateTimeBlockData = { - title: formData.title, - description: formData.description || undefined, - resource: myBlocksData.resource_id, - block_type: formData.block_type, - recurrence_type: formData.recurrence_type, - all_day: formData.all_day, - }; - - // Add type-specific fields - if (formData.recurrence_type === 'NONE') { - payload.start_date = formData.start_date; - payload.end_date = formData.end_date || formData.start_date; - } - - if (!formData.all_day) { - payload.start_time = formData.start_time; - payload.end_time = formData.end_time; - } - - if (formData.recurrence_type !== 'NONE') { - payload.recurrence_pattern = formData.recurrence_pattern; - if (formData.recurrence_start) payload.recurrence_start = formData.recurrence_start; - if (formData.recurrence_end) payload.recurrence_end = formData.recurrence_end; - } - - try { - if (editingBlock) { - await updateBlock.mutateAsync({ id: editingBlock.id, updates: payload }); - } else { - await createBlock.mutateAsync(payload); - } - closeModal(); - } catch (error) { - console.error('Failed to save time block:', error); - } }; const handleDelete = async (id: string) => { @@ -264,6 +146,35 @@ const MyAvailability: React.FC = (props) => { ); + // Render approval status badge + const renderApprovalBadge = (status: string | undefined) => { + if (!status || status === 'APPROVED') { + return ( + + + {t('myAvailability.approved', 'Approved')} + + ); + } + if (status === 'PENDING') { + return ( + + + {t('myAvailability.pending', 'Pending Review')} + + ); + } + if (status === 'DENIED') { + return ( + + + {t('myAvailability.denied', 'Denied')} + + ); + } + return null; + }; + // Handle no linked resource if (!isLoading && !myBlocksData?.resource_id) { return ( @@ -290,6 +201,12 @@ const MyAvailability: React.FC = (props) => { ); } + // Create a mock resource for the modal + const staffResource = myBlocksData?.resource_id ? { + id: myBlocksData.resource_id, + name: myBlocksData.resource_name || 'My Resource', + } : null; + return (
{/* Header */} @@ -299,447 +216,271 @@ const MyAvailability: React.FC = (props) => { {t('myAvailability.title', 'My Availability')}

- {myBlocksData?.resource_name && ( - - - {myBlocksData.resource_name} - - )} + {t('myAvailability.subtitle', 'Manage your time off and unavailability')}

- + {/* Approval Required Banner */} + {myBlocksData?.can_self_approve === false && ( +
+
+ +
+

+ {t('myAvailability.approvalRequired', 'Approval Required')} +

+

+ {t('myAvailability.approvalRequiredInfo', 'Your time off requests require manager or owner approval. New blocks will show as "Pending Review" until approved.')} +

+
+
+
+ )} + + {/* Business Blocks Info Banner */} + {myBlocksData?.business_blocks && myBlocksData.business_blocks.length > 0 && ( +
+
+ +
+

+ {t('myAvailability.businessBlocks', 'Business Closures')} +

+

+ {t('myAvailability.businessBlocksInfo', 'These blocks are set by your business and apply to everyone:')} +

+
+ {myBlocksData.business_blocks.map((block) => ( + + {block.title} + {renderRecurrenceBadge(block.recurrence_type)} + + ))} +
+
+
+
+ )} + + {/* Tabs */} +
+ +
+ + {/* Tab Content */} {isLoading ? (
) : ( -
- {/* Business Blocks (Read-only) */} - {myBlocksData?.business_blocks && myBlocksData.business_blocks.length > 0 && ( -
-

- - {t('myAvailability.businessBlocks', 'Business Closures')} -

-
-

- - {t('myAvailability.businessBlocksInfo', 'These blocks are set by your business and apply to everyone.')} +

+ {activeTab === 'blocks' && ( + <> + {/* Resource Info Banner */} +
+

+ + {t('myAvailability.resourceInfo', 'Managing blocks for:')} + {myBlocksData?.resource_name}

-
- - - {myBlocksData.business_blocks.map((block) => ( - - - - + + {/* My Blocks List */} + {myBlocksData?.my_blocks && myBlocksData.my_blocks.length === 0 ? ( +
+ +

+ {t('myAvailability.noBlocks', 'No Time Blocks')} +

+

+ {t('myAvailability.noBlocksDesc', 'Add time blocks for vacations, lunch breaks, or any time you need off.')} +

+ +
+ ) : ( +
+
- - {block.title} - - - {renderBlockTypeBadge(block.block_type)} - -
- {renderRecurrenceBadge(block.recurrence_type)} - {block.pattern_display && ( - - {block.pattern_display} - - )} -
-
+ + + + + + + - ))} - -
+ {t('myAvailability.titleCol', 'Title')} + + {t('myAvailability.typeCol', 'Type')} + + {t('myAvailability.patternCol', 'Pattern')} + + {t('myAvailability.statusCol', 'Status')} + + {t('myAvailability.actionsCol', 'Actions')} +
-
-
+ + + {myBlocksData?.my_blocks.map((block) => ( + + +
+ + {block.title} + + {!block.is_active && ( + + Inactive + + )} +
+ + + {renderBlockTypeBadge(block.block_type)} + + +
+ {renderRecurrenceBadge(block.recurrence_type)} + {block.pattern_display && ( + + {block.pattern_display} + + )} +
+ + +
+ {renderApprovalBadge((block as any).approval_status)} + {(block as any).review_notes && ( + + "{(block as any).review_notes}" + + )} +
+ + +
+ + + +
+ + + ))} + + +
+ )} + )} - {/* My Blocks (Editable) */} -
-

- - {t('myAvailability.myBlocks', 'My Time Blocks')} -

- - {myBlocksData?.my_blocks && myBlocksData.my_blocks.length === 0 ? ( -
- -

- {t('myAvailability.noBlocks', 'No Time Blocks')} -

-

- {t('myAvailability.noBlocksDesc', 'Add time blocks for vacations, lunch breaks, or any time you need off.')} -

- -
- ) : ( -
- - - - - - - - - - - {myBlocksData?.my_blocks.map((block) => ( - - - - - - - ))} - -
- {t('myAvailability.titleCol', 'Title')} - - {t('myAvailability.typeCol', 'Type')} - - {t('myAvailability.patternCol', 'Pattern')} - - {t('myAvailability.actionsCol', 'Actions')} -
- - {block.title} - - - {renderBlockTypeBadge(block.block_type)} - -
- {renderRecurrenceBadge(block.recurrence_type)} - {block.pattern_display && ( - - {block.pattern_display} - - )} -
-
-
- - - -
-
-
- )} -
- - {/* Yearly Calendar View */} -
- { - // Find the block and open edit modal if it's my block - const block = myBlocksData?.my_blocks.find(b => b.id === blockId); - if (block) { - openEditModal(block); - } - }} - /> -
+ {activeTab === 'calendar' && ( +
+ { + // Find the block and open edit modal if it's my block + const block = myBlocksData?.my_blocks.find(b => b.id === blockId); + if (block) { + openEditModal(block); + } + }} + /> +
+ )}
)} - {/* Create/Edit Modal */} - {isModalOpen && ( - -
-
-
-

- {editingBlock - ? t('myAvailability.editBlock', 'Edit Time Block') - : t('myAvailability.createBlock', 'Block Time Off')} -

- -
- -
- {/* Title */} -
- - handleFormChange('title', e.target.value)} - className="input-primary w-full" - placeholder="e.g., Vacation, Lunch Break, Doctor Appointment" - required - /> -
- - {/* Description */} -
- -