feat(time-blocks): Add comprehensive time blocking system with contracts

- Add TimeBlock and Holiday models with recurrence support (one-time, weekly, monthly, yearly, holiday)
- Implement business-level and resource-level blocking with hard/soft block types
- Add multi-select holiday picker for bulk holiday blocking
- Add calendar overlay visualization with distinct colors:
  - Business blocks: Red (hard) / Yellow (soft)
  - Resource blocks: Purple (hard) / Cyan (soft)
- Add month view resource indicators showing 1/n width per resource
- Add yearly calendar view for block overview
- Add My Availability page for staff self-service
- Add contracts module with templates, signing flow, and PDF generation
- Update scheduler with click-to-day navigation in week view

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-04 17:19:12 -05:00
parent cf91bae24f
commit 8d0cc1e90a
63 changed files with 11863 additions and 61 deletions

View File

@@ -0,0 +1,789 @@
/**
* My Availability Page
*
* Staff-facing page to view and manage their own time blocks.
* Shows business-level blocks (read-only) and personal blocks (editable).
*/
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import {
TimeBlockListItem,
BlockType,
RecurrenceType,
RecurrencePattern,
User,
} from '../types';
import {
useMyBlocks,
useCreateTimeBlock,
useUpdateTimeBlock,
useDeleteTimeBlock,
useToggleTimeBlock,
useHolidays,
CreateTimeBlockData,
} from '../hooks/useTimeBlocks';
import Portal from '../components/Portal';
import YearlyBlockCalendar from '../components/time-blocks/YearlyBlockCalendar';
import {
Calendar,
Building2,
User as UserIcon,
Plus,
Pencil,
Trash2,
X,
AlertTriangle,
Clock,
CalendarDays,
Ban,
AlertCircle,
Power,
PowerOff,
Info,
} from 'lucide-react';
const RECURRENCE_TYPE_LABELS: Record<RecurrenceType, string> = {
NONE: 'One-time',
WEEKLY: 'Weekly',
MONTHLY: 'Monthly',
YEARLY: 'Yearly',
HOLIDAY: 'Holiday',
};
const BLOCK_TYPE_LABELS: Record<BlockType, string> = {
HARD: 'Hard Block',
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;
}
const MyAvailability: React.FC<MyAvailabilityProps> = (props) => {
const { t } = useTranslation();
const contextUser = useOutletContext<{ user?: User }>()?.user;
const user = props.user || contextUser;
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingBlock, setEditingBlock] = useState<TimeBlockListItem | null>(null);
const [formData, setFormData] = useState<TimeBlockFormData>(defaultFormData);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
// Fetch data
const { data: myBlocksData, isLoading } = useMyBlocks();
const { data: holidays = [] } = useHolidays('US');
// Mutations
const createBlock = useCreateTimeBlock();
const updateBlock = useUpdateTimeBlock();
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) => {
try {
await deleteBlock.mutateAsync(id);
setDeleteConfirmId(null);
} catch (error) {
console.error('Failed to delete time block:', error);
}
};
const handleToggle = async (id: string) => {
try {
await toggleBlock.mutateAsync(id);
} catch (error) {
console.error('Failed to toggle time block:', error);
}
};
// Render block type badge
const renderBlockTypeBadge = (type: BlockType) => (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
type === 'HARD'
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300'
}`}
>
{type === 'HARD' ? <Ban size={12} className="mr-1" /> : <AlertCircle size={12} className="mr-1" />}
{BLOCK_TYPE_LABELS[type]}
</span>
);
// Render recurrence badge
const renderRecurrenceBadge = (type: RecurrenceType) => (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{type === 'HOLIDAY' ? (
<CalendarDays size={12} className="mr-1" />
) : type === 'NONE' ? (
<Clock size={12} className="mr-1" />
) : (
<Calendar size={12} className="mr-1" />
)}
{RECURRENCE_TYPE_LABELS[type]}
</span>
);
// Handle no linked resource
if (!isLoading && !myBlocksData?.resource_id) {
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t('myAvailability.title', 'My Availability')}
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{t('myAvailability.subtitle', 'Manage your time off and unavailability')}
</p>
</div>
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<AlertTriangle size={48} className="mx-auto text-yellow-500 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{t('myAvailability.noResource', 'No Resource Linked')}
</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
{t('myAvailability.noResourceDesc', 'Your account is not linked to a resource. Please contact your manager to set up your availability.')}
</p>
</div>
</div>
);
}
return (
<div className="space-y-6 p-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t('myAvailability.title', 'My Availability')}
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{myBlocksData?.resource_name && (
<span className="flex items-center gap-1">
<UserIcon size={14} />
{myBlocksData.resource_name}
</span>
)}
</p>
</div>
<button onClick={openCreateModal} className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">
<Plus size={18} />
{t('myAvailability.addBlock', 'Block Time')}
</button>
</div>
{isLoading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
</div>
) : (
<div className="space-y-6">
{/* Business Blocks (Read-only) */}
{myBlocksData?.business_blocks && myBlocksData.business_blocks.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Building2 size={20} />
{t('myAvailability.businessBlocks', 'Business Closures')}
</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 mb-3">
<p className="text-sm text-blue-800 dark:text-blue-300 flex items-center gap-2">
<Info size={16} />
{t('myAvailability.businessBlocksInfo', 'These blocks are set by your business and apply to everyone.')}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{myBlocksData.business_blocks.map((block) => (
<tr key={block.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td className="px-6 py-4 whitespace-nowrap">
<span className="font-medium text-gray-900 dark:text-white">
{block.title}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{renderBlockTypeBadge(block.block_type)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
{renderRecurrenceBadge(block.recurrence_type)}
{block.pattern_display && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{block.pattern_display}
</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* My Blocks (Editable) */}
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<UserIcon size={20} />
{t('myAvailability.myBlocks', 'My Time Blocks')}
</h2>
{myBlocksData?.my_blocks && myBlocksData.my_blocks.length === 0 ? (
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<Calendar size={48} className="mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{t('myAvailability.noBlocks', 'No Time Blocks')}
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-4">
{t('myAvailability.noBlocksDesc', 'Add time blocks for vacations, lunch breaks, or any time you need off.')}
</p>
<button onClick={openCreateModal} className="inline-flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">
<Plus size={18} />
{t('myAvailability.addFirstBlock', 'Add First Block')}
</button>
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('myAvailability.titleCol', 'Title')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('myAvailability.typeCol', 'Type')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('myAvailability.patternCol', 'Pattern')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('myAvailability.actionsCol', 'Actions')}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{myBlocksData?.my_blocks.map((block) => (
<tr key={block.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td className="px-6 py-4 whitespace-nowrap">
<span className="font-medium text-gray-900 dark:text-white">
{block.title}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{renderBlockTypeBadge(block.block_type)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
{renderRecurrenceBadge(block.recurrence_type)}
{block.pattern_display && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{block.pattern_display}
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => handleToggle(block.id)}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title={block.is_active ? 'Deactivate' : 'Activate'}
>
{block.is_active ? <Power size={16} /> : <PowerOff size={16} />}
</button>
<button
onClick={() => openEditModal(block)}
className="p-2 text-gray-400 hover:text-blue-600"
title="Edit"
>
<Pencil size={16} />
</button>
<button
onClick={() => setDeleteConfirmId(block.id)}
className="p-2 text-gray-400 hover:text-red-600"
title="Delete"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Yearly Calendar View */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<YearlyBlockCalendar
resourceId={myBlocksData?.resource_id}
onBlockClick={(blockId) => {
// 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);
}
}}
/>
</div>
</div>
)}
{/* Create/Edit Modal */}
{isModalOpen && (
<Portal>
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{editingBlock
? t('myAvailability.editBlock', 'Edit Time Block')
: t('myAvailability.createBlock', 'Block Time Off')}
</h2>
<button
onClick={closeModal}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X size={24} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
{/* Title */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('myAvailability.form.title', 'Title')} *
</label>
<input
type="text"
value={formData.title}
onChange={(e) => handleFormChange('title', e.target.value)}
className="input-primary w-full"
placeholder="e.g., Vacation, Lunch Break, Doctor Appointment"
required
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('myAvailability.form.description', 'Description')}
</label>
<textarea
value={formData.description}
onChange={(e) => handleFormChange('description', e.target.value)}
className="input-primary w-full"
rows={2}
placeholder="Optional reason"
/>
</div>
{/* Block Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('myAvailability.form.blockType', 'Block Type')}
</label>
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="block_type"
value="SOFT"
checked={formData.block_type === 'SOFT'}
onChange={() => handleFormChange('block_type', 'SOFT')}
className="text-brand-500"
/>
<AlertCircle size={16} className="text-yellow-500" />
<span className="text-sm">Soft Block</span>
<span className="text-xs text-gray-500">(shows warning, can be overridden)</span>
</label>
<label className={`flex items-center gap-2 ${canCreateHardBlocks ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="radio"
name="block_type"
value="HARD"
checked={formData.block_type === 'HARD'}
onChange={() => canCreateHardBlocks && handleFormChange('block_type', 'HARD')}
className="text-brand-500"
disabled={!canCreateHardBlocks}
/>
<Ban size={16} className="text-red-500" />
<span className="text-sm">Hard Block</span>
<span className="text-xs text-gray-500">(prevents booking)</span>
{!canCreateHardBlocks && (
<span className="text-xs text-red-500">(requires permission)</span>
)}
</label>
</div>
</div>
{/* Recurrence Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('myAvailability.form.recurrenceType', 'Recurrence')}
</label>
<select
value={formData.recurrence_type}
onChange={(e) => handleFormChange('recurrence_type', e.target.value as RecurrenceType)}
className="input-primary w-full"
>
<option value="NONE">One-time (specific date/range)</option>
<option value="WEEKLY">Weekly (e.g., every Monday)</option>
<option value="MONTHLY">Monthly (e.g., 1st of month)</option>
</select>
</div>
{/* Recurrence Pattern - NONE */}
{formData.recurrence_type === 'NONE' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Start Date *
</label>
<input
type="date"
value={formData.start_date}
onChange={(e) => handleFormChange('start_date', e.target.value)}
className="input-primary w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
End Date
</label>
<input
type="date"
value={formData.end_date}
onChange={(e) => handleFormChange('end_date', e.target.value)}
className="input-primary w-full"
min={formData.start_date}
/>
</div>
</div>
)}
{/* Recurrence Pattern - WEEKLY */}
{formData.recurrence_type === 'WEEKLY' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Days of Week *
</label>
<div className="flex flex-wrap gap-2">
{DAY_ABBREVS.map((day, index) => {
const isSelected = (formData.recurrence_pattern.days_of_week || []).includes(index);
return (
<button
key={day}
type="button"
onClick={() => handleDayOfWeekToggle(index)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
isSelected
? 'bg-brand-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300'
}`}
>
{day}
</button>
);
})}
</div>
</div>
)}
{/* Recurrence Pattern - MONTHLY */}
{formData.recurrence_type === 'MONTHLY' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Days of Month *
</label>
<div className="flex flex-wrap gap-1">
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => {
const isSelected = (formData.recurrence_pattern.days_of_month || []).includes(day);
return (
<button
key={day}
type="button"
onClick={() => {
const current = formData.recurrence_pattern.days_of_month || [];
const newDays = current.includes(day)
? current.filter((d) => d !== day)
: [...current, day].sort((a, b) => a - b);
handlePatternChange('days_of_month', newDays);
}}
className={`w-8 h-8 rounded text-sm font-medium transition-colors ${
isSelected
? 'bg-brand-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300'
}`}
>
{day}
</button>
);
})}
</div>
</div>
)}
{/* All Day Toggle */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="all_day"
checked={formData.all_day}
onChange={(e) => handleFormChange('all_day', e.target.checked)}
className="rounded text-brand-500"
/>
<label htmlFor="all_day" className="text-sm text-gray-700 dark:text-gray-300">
{t('myAvailability.form.allDay', 'All day')}
</label>
</div>
{/* Time Range (if not all day) */}
{!formData.all_day && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Start Time *
</label>
<input
type="time"
value={formData.start_time}
onChange={(e) => handleFormChange('start_time', e.target.value)}
className="input-primary w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
End Time *
</label>
<input
type="time"
value={formData.end_time}
onChange={(e) => handleFormChange('end_time', e.target.value)}
className="input-primary w-full"
required
/>
</div>
</div>
)}
{/* Submit Button */}
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="button" onClick={closeModal} className="btn-secondary">
{t('common.cancel', 'Cancel')}
</button>
<button
type="submit"
className="btn-primary"
disabled={createBlock.isPending || updateBlock.isPending}
>
{(createBlock.isPending || updateBlock.isPending) ? (
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
{t('common.saving', 'Saving...')}
</span>
) : editingBlock ? (
t('common.save', 'Save Changes')
) : (
t('myAvailability.create', 'Block Time')
)}
</button>
</div>
</form>
</div>
</div>
</Portal>
)}
{/* Delete Confirmation Modal */}
{deleteConfirmId && (
<Portal>
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-red-100 dark:bg-red-900/30 rounded-full">
<AlertTriangle size={24} className="text-red-600 dark:text-red-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('myAvailability.deleteConfirmTitle', 'Delete Time Block?')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('myAvailability.deleteConfirmDesc', 'This action cannot be undone.')}
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<button onClick={() => setDeleteConfirmId(null)} className="btn-secondary">
{t('common.cancel', 'Cancel')}
</button>
<button
onClick={() => handleDelete(deleteConfirmId)}
className="btn-danger"
disabled={deleteBlock.isPending}
>
{deleteBlock.isPending ? (
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
{t('common.deleting', 'Deleting...')}
</span>
) : (
t('common.delete', 'Delete')
)}
</button>
</div>
</div>
</div>
</Portal>
)}
</div>
);
};
export default MyAvailability;