feat(staff): Restrict staff permissions and add schedule view

- Backend: Restrict staff from accessing resources, customers, services, and tasks APIs
- Frontend: Hide management sidebar links from staff members
- Add StaffSchedule page with vertical timeline view of appointments
- Add StaffHelp page with staff-specific documentation
- Return linked_resource_id and can_edit_schedule in user profile for staff

🤖 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 02:23:00 -05:00
parent 61882b300f
commit 01020861c7
48 changed files with 6156 additions and 148 deletions

View File

@@ -6,6 +6,7 @@ import { useResources, useCreateResource, useUpdateResource } from '../hooks/use
import { useAppointments } from '../hooks/useAppointments';
import { useStaff, StaffMember } from '../hooks/useStaff';
import ResourceCalendar from '../components/ResourceCalendar';
import ResourceDetailModal from '../components/ResourceDetailModal';
import Portal from '../components/Portal';
import { getOverQuotaResourceIds } from '../utils/quotaUtils';
import {
@@ -18,7 +19,8 @@ import {
Settings,
X,
Pencil,
AlertTriangle
AlertTriangle,
MapPin
} from 'lucide-react';
const ResourceIcon: React.FC<{ type: ResourceType }> = ({ type }) => {
@@ -46,6 +48,7 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [editingResource, setEditingResource] = React.useState<Resource | null>(null);
const [calendarResource, setCalendarResource] = React.useState<{ id: string; name: string } | null>(null);
const [detailResource, setDetailResource] = React.useState<Resource | null>(null);
// Calculate over-quota resources (will be auto-archived when grace period ends)
const overQuotaResourceIds = useMemo(
@@ -60,6 +63,7 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
const [formMaxConcurrent, setFormMaxConcurrent] = React.useState(1);
const [formMultilaneEnabled, setFormMultilaneEnabled] = React.useState(false);
const [formSavedLaneCount, setFormSavedLaneCount] = React.useState<number | undefined>(undefined);
const [formUserCanEditSchedule, setFormUserCanEditSchedule] = React.useState(false);
// Staff selection state
const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);
@@ -181,6 +185,7 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
setFormMaxConcurrent(editingResource.maxConcurrentEvents);
setFormMultilaneEnabled(editingResource.maxConcurrentEvents > 1);
setFormSavedLaneCount(editingResource.savedLaneCount);
setFormUserCanEditSchedule(editingResource.userCanEditSchedule ?? false);
// Pre-fill staff if editing a STAFF resource
if (editingResource.type === 'STAFF' && editingResource.userId) {
setSelectedStaffId(editingResource.userId);
@@ -197,6 +202,7 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
setFormMaxConcurrent(1);
setFormMultilaneEnabled(false);
setFormSavedLaneCount(undefined);
setFormUserCanEditSchedule(false);
setSelectedStaffId(null);
setStaffSearchQuery('');
setDebouncedSearchQuery('');
@@ -258,6 +264,7 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
maxConcurrentEvents: number;
savedLaneCount: number | undefined;
userId?: string;
userCanEditSchedule?: boolean;
} = {
name: formName,
type: formType,
@@ -267,6 +274,7 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
if (formType === 'STAFF' && selectedStaffId) {
resourceData.userId = selectedStaffId;
resourceData.userCanEditSchedule = formUserCanEditSchedule;
}
if (editingResource) {
@@ -409,6 +417,15 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
{resource.type === 'STAFF' && resource.userId && (
<button
onClick={() => setDetailResource(resource)}
className="text-green-600 hover:text-green-500 dark:text-green-400 dark:hover:text-green-300 font-medium text-xs inline-flex items-center gap-1 px-3 py-1 border border-green-200 dark:border-green-800 rounded-lg hover:bg-green-50 dark:hover:bg-green-900/30 transition-colors"
title={t('resources.trackLocation', 'Track Location')}
>
<MapPin size={14} /> {t('resources.trackLocation', 'Track')}
</button>
)}
<button
onClick={() => setCalendarResource({ id: resource.id, name: resource.name })}
className="text-brand-600 hover:text-brand-500 dark:text-brand-400 dark:hover:text-brand-300 font-medium text-xs inline-flex items-center gap-1 px-3 py-1 border border-brand-200 dark:border-brand-800 rounded-lg hover:bg-brand-50 dark:hover:bg-brand-900/30 transition-colors"
@@ -646,6 +663,35 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
</div>
)}
{/* Allow User to Edit Schedule Toggle (only for STAFF type) */}
{formType === 'STAFF' && selectedStaffId && (
<div className="flex items-center justify-between py-2">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('resources.allowEditSchedule', 'Allow User to Edit Schedule')}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{t('resources.allowEditScheduleDescription', 'Let this staff member reschedule and resize their own appointments in the mobile app')}
</p>
</div>
<button
type="button"
onClick={() => setFormUserCanEditSchedule(!formUserCanEditSchedule)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 ${
formUserCanEditSchedule ? 'bg-brand-600' : 'bg-gray-200 dark:bg-gray-600'
}`}
role="switch"
aria-checked={formUserCanEditSchedule}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
formUserCanEditSchedule ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
)}
{/* Submit Buttons */}
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
@@ -682,6 +728,14 @@ const Resources: React.FC<ResourcesProps> = ({ onMasquerade, effectiveUser }) =>
onClose={() => setCalendarResource(null)}
/>
)}
{/* Resource Detail Modal (with location tracking) */}
{detailResource && (
<ResourceDetailModal
resource={detailResource}
onClose={() => setDetailResource(null)}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,460 @@
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
DndContext,
DragEndEvent,
useSensor,
useSensors,
PointerSensor,
DragOverlay,
} from '@dnd-kit/core';
import {
format,
startOfDay,
endOfDay,
addDays,
subDays,
differenceInMinutes,
addMinutes,
isSameDay,
parseISO,
} from 'date-fns';
import {
ChevronLeft,
ChevronRight,
Calendar,
Clock,
User,
GripVertical,
} from 'lucide-react';
import apiClient from '../api/client';
import { User as UserType } from '../types';
import toast from 'react-hot-toast';
interface StaffScheduleProps {
user: UserType;
}
interface Job {
id: number;
title: string;
start_time: string;
end_time: string;
status: string;
notes?: string;
customer_name?: string;
service_name?: string;
}
const HOUR_HEIGHT = 60; // pixels per hour
const START_HOUR = 6; // 6 AM
const END_HOUR = 22; // 10 PM
const StaffSchedule: React.FC<StaffScheduleProps> = ({ user }) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [currentDate, setCurrentDate] = useState(new Date());
const [draggedJob, setDraggedJob] = useState<Job | null>(null);
const canEditSchedule = user.can_edit_schedule ?? false;
// Get the resource ID linked to this user (from the user object)
const userResourceId = user.linked_resource_id ?? null;
// Fetch appointments for the current staff member's resource
const { data: jobs = [], isLoading } = useQuery({
queryKey: ['staff-jobs', format(currentDate, 'yyyy-MM-dd'), userResourceId],
queryFn: async () => {
if (!userResourceId) return [];
const start = startOfDay(currentDate).toISOString();
const end = endOfDay(currentDate).toISOString();
const response = await apiClient.get('/appointments/', {
params: {
resource: userResourceId,
start_date: start,
end_date: end,
},
});
// Transform to Job format
return response.data.map((apt: any) => ({
id: apt.id,
title: apt.title || apt.service_name || 'Appointment',
start_time: apt.start_time,
end_time: apt.end_time,
status: apt.status,
notes: apt.notes,
customer_name: apt.customer_name,
service_name: apt.service_name,
}));
},
enabled: !!userResourceId,
});
// Mutation for rescheduling
const rescheduleMutation = useMutation({
mutationFn: async ({ jobId, newStart }: { jobId: number; newStart: Date }) => {
const job = jobs.find((j) => j.id === jobId);
if (!job) throw new Error('Job not found');
const duration = differenceInMinutes(parseISO(job.end_time), parseISO(job.start_time));
const newEnd = addMinutes(newStart, duration);
await apiClient.patch(`/appointments/${jobId}/`, {
start_time: newStart.toISOString(),
end_time: newEnd.toISOString(),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['staff-jobs'] });
queryClient.invalidateQueries({ queryKey: ['appointments'] });
toast.success(t('staff.jobRescheduled', 'Job rescheduled successfully'));
},
onError: () => {
toast.error(t('staff.rescheduleError', 'Failed to reschedule job'));
},
});
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
})
);
// Generate time slots
const timeSlots = useMemo(() => {
const slots = [];
for (let hour = START_HOUR; hour <= END_HOUR; hour++) {
slots.push({
hour,
label: format(new Date().setHours(hour, 0, 0, 0), 'h a'),
});
}
return slots;
}, []);
// Calculate job positions
const jobsWithPositions = useMemo(() => {
return jobs
.filter((job) => {
const jobDate = parseISO(job.start_time);
return isSameDay(jobDate, currentDate);
})
.map((job) => {
const startTime = parseISO(job.start_time);
const endTime = parseISO(job.end_time);
const startHour = startTime.getHours() + startTime.getMinutes() / 60;
const endHour = endTime.getHours() + endTime.getMinutes() / 60;
const top = (startHour - START_HOUR) * HOUR_HEIGHT;
const height = (endHour - startHour) * HOUR_HEIGHT;
return {
...job,
top: Math.max(0, top),
height: Math.max(30, height),
};
});
}, [jobs, currentDate]);
const handleDragStart = (event: any) => {
const jobId = parseInt(event.active.id.toString().replace('job-', ''));
const job = jobs.find((j) => j.id === jobId);
setDraggedJob(job || null);
};
const handleDragEnd = (event: DragEndEvent) => {
setDraggedJob(null);
if (!canEditSchedule) return;
const { active, delta } = event;
if (!active || Math.abs(delta.y) < 10) return;
const jobId = parseInt(active.id.toString().replace('job-', ''));
const job = jobs.find((j) => j.id === jobId);
if (!job) return;
// Calculate new time based on drag delta
const minutesDelta = Math.round((delta.y / HOUR_HEIGHT) * 60);
const snappedMinutes = Math.round(minutesDelta / 15) * 15; // Snap to 15-minute intervals
const originalStart = parseISO(job.start_time);
const newStart = addMinutes(originalStart, snappedMinutes);
// Validate new time is within bounds
const newHour = newStart.getHours();
if (newHour < START_HOUR || newHour >= END_HOUR) {
toast.error(t('staff.timeOutOfBounds', 'Cannot schedule outside business hours'));
return;
}
rescheduleMutation.mutate({ jobId, newStart });
};
const getStatusColor = (status: string) => {
switch (status.toUpperCase()) {
case 'SCHEDULED':
case 'CONFIRMED':
return 'bg-blue-100 border-blue-500 text-blue-800 dark:bg-blue-900/30 dark:border-blue-400 dark:text-blue-300';
case 'IN_PROGRESS':
return 'bg-yellow-100 border-yellow-500 text-yellow-800 dark:bg-yellow-900/30 dark:border-yellow-400 dark:text-yellow-300';
case 'COMPLETED':
return 'bg-green-100 border-green-500 text-green-800 dark:bg-green-900/30 dark:border-green-400 dark:text-green-300';
case 'CANCELLED':
case 'NO_SHOW':
return 'bg-red-100 border-red-500 text-red-800 dark:bg-red-900/30 dark:border-red-400 dark:text-red-300';
default:
return 'bg-gray-100 border-gray-500 text-gray-800 dark:bg-gray-700 dark:border-gray-500 dark:text-gray-300';
}
};
const navigateDate = (direction: 'prev' | 'next') => {
setCurrentDate((d) => (direction === 'prev' ? subDays(d, 1) : addDays(d, 1)));
};
const goToToday = () => {
setCurrentDate(new Date());
};
// Show message if no resource is linked
if (!userResourceId) {
return (
<div className="flex flex-col h-full bg-gray-50 dark:bg-gray-900">
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t('staff.mySchedule', 'My Schedule')}
</h1>
</div>
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center max-w-md">
<Calendar size={48} className="mx-auto text-gray-300 dark:text-gray-600 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{t('staff.noResourceLinked', 'No Schedule Available')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t(
'staff.noResourceLinkedDesc',
'Your account is not linked to a resource yet. Please contact your manager to set up your schedule.'
)}
</p>
</div>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full bg-gray-50 dark:bg-gray-900">
{/* Header */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t('staff.mySchedule', 'My Schedule')}
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
{canEditSchedule
? t('staff.dragToReschedule', 'Drag jobs to reschedule them')
: t('staff.viewOnlySchedule', 'View your scheduled jobs for the day')}
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => navigateDate('prev')}
className="p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<ChevronLeft size={20} />
</button>
<button
onClick={goToToday}
className="px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
{t('common.today', 'Today')}
</button>
<div className="flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
<Calendar size={16} className="text-gray-500 dark:text-gray-400" />
<span className="font-medium text-gray-900 dark:text-white">
{format(currentDate, 'EEEE, MMMM d, yyyy')}
</span>
</div>
<button
onClick={() => navigateDate('next')}
className="p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<ChevronRight size={20} />
</button>
</div>
</div>
</div>
{/* Timeline Content */}
<div className="flex-1 overflow-auto p-6">
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="flex">
{/* Time Column */}
<div className="w-20 flex-shrink-0 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
{timeSlots.map((slot) => (
<div
key={slot.hour}
className="border-b border-gray-100 dark:border-gray-700/50 text-right pr-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400"
style={{ height: HOUR_HEIGHT }}
>
{slot.label}
</div>
))}
</div>
{/* Events Column */}
<div
className="flex-1 relative"
style={{ height: (END_HOUR - START_HOUR + 1) * HOUR_HEIGHT }}
>
{/* Hour Grid Lines */}
{timeSlots.map((slot) => (
<div
key={slot.hour}
className="absolute left-0 right-0 border-b border-gray-100 dark:border-gray-700/50"
style={{ top: (slot.hour - START_HOUR) * HOUR_HEIGHT, height: HOUR_HEIGHT }}
/>
))}
{/* Current Time Line */}
{isSameDay(currentDate, new Date()) && (
<div
className="absolute left-0 right-0 border-t-2 border-red-500 z-20"
style={{
top:
(new Date().getHours() +
new Date().getMinutes() / 60 -
START_HOUR) *
HOUR_HEIGHT,
}}
>
<div className="absolute -left-1 -top-1.5 w-3 h-3 bg-red-500 rounded-full" />
</div>
)}
{/* Jobs */}
{jobsWithPositions.map((job) => (
<div
key={job.id}
id={`job-${job.id}`}
className={`absolute left-2 right-2 rounded-lg border-l-4 p-3 transition-shadow ${getStatusColor(job.status)} ${
canEditSchedule ? 'cursor-grab active:cursor-grabbing hover:shadow-lg' : ''
}`}
style={{
top: job.top,
height: job.height,
minHeight: 60,
}}
draggable={canEditSchedule}
>
<div className="flex items-start justify-between h-full">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{canEditSchedule && (
<GripVertical
size={14}
className="text-gray-400 flex-shrink-0"
/>
)}
<h3 className="font-semibold text-sm truncate">
{job.title || job.service_name || 'Appointment'}
</h3>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-xs opacity-80">
<Clock size={12} />
<span>
{format(parseISO(job.start_time), 'h:mm a')} -{' '}
{format(parseISO(job.end_time), 'h:mm a')}
</span>
</div>
{job.customer_name && (
<div className="flex items-center gap-1.5 text-xs opacity-80">
<User size={12} />
<span className="truncate">{job.customer_name}</span>
</div>
)}
</div>
</div>
<span
className={`text-xs px-2 py-0.5 rounded-full font-medium ${
job.status === 'IN_PROGRESS'
? 'bg-yellow-200 text-yellow-800'
: 'bg-white/50 dark:bg-gray-900/30'
}`}
>
{job.status.replace('_', ' ')}
</span>
</div>
</div>
))}
{/* Empty State */}
{jobsWithPositions.length === 0 && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<Calendar
size={48}
className="mx-auto text-gray-300 dark:text-gray-600 mb-4"
/>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
{t('staff.noJobsToday', 'No jobs scheduled')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t(
'staff.noJobsDescription',
'You have no jobs scheduled for this day'
)}
</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Drag Overlay */}
<DragOverlay>
{draggedJob ? (
<div className="p-3 bg-white dark:bg-gray-700 border-l-4 border-blue-500 rounded-lg shadow-xl opacity-90 w-64">
<div className="font-semibold text-sm text-gray-900 dark:text-white">
{draggedJob.title || draggedJob.service_name || 'Appointment'}
</div>
<div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 mt-1">
<Clock size={12} />
<span>
{format(parseISO(draggedJob.start_time), 'h:mm a')} -{' '}
{format(parseISO(draggedJob.end_time), 'h:mm a')}
</span>
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
)}
</div>
</div>
);
};
export default StaffSchedule;

View File

@@ -0,0 +1,285 @@
/**
* Staff Help Guide
*
* Simplified documentation for staff members.
* Only covers features that staff have access to.
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
ArrowLeft,
BookOpen,
LayoutDashboard,
Calendar,
CalendarOff,
HelpCircle,
CheckCircle,
Clock,
GripVertical,
Ticket,
} from 'lucide-react';
import { User } from '../../types';
interface StaffHelpProps {
user: User;
}
const StaffHelp: React.FC<StaffHelpProps> = ({ user }) => {
const navigate = useNavigate();
const { t } = useTranslation();
const canAccessTickets = user.can_access_tickets ?? false;
const canEditSchedule = user.can_edit_schedule ?? false;
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */}
<div className="sticky top-0 z-20 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
<div className="flex items-center gap-2">
<BookOpen size={24} className="text-brand-600 dark:text-brand-400" />
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
{t('staffHelp.title', 'Staff Guide')}
</h1>
</div>
</div>
</div>
</div>
<div className="max-w-4xl mx-auto px-4 py-8">
{/* Introduction */}
<section className="mb-12">
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
{t('staffHelp.welcome', 'Welcome to SmoothSchedule')}
</h2>
<p className="text-gray-700 dark:text-gray-300">
{t(
'staffHelp.intro',
'This guide covers everything you need to know as a staff member. You can view your schedule, manage your availability, and stay updated on your assignments.'
)}
</p>
</div>
</section>
{/* Dashboard Section */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<LayoutDashboard size={20} className="text-blue-600 dark:text-blue-400" />
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
{t('staffHelp.dashboard.title', 'Dashboard')}
</h2>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t(
'staffHelp.dashboard.description',
"Your dashboard provides a quick overview of your day. Here you can see today's summary and any important updates."
)}
</p>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>{t('staffHelp.dashboard.feature1', 'View daily summary and stats')}</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>{t('staffHelp.dashboard.feature2', 'Quick access to your schedule')}</span>
</li>
</ul>
</div>
</section>
{/* My Schedule Section */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<Calendar size={20} className="text-green-600 dark:text-green-400" />
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
{t('staffHelp.schedule.title', 'My Schedule')}
</h2>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t(
'staffHelp.schedule.description',
'The My Schedule page shows a vertical timeline of all your jobs for the day. You can navigate between days to see past and future appointments.'
)}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
{t('staffHelp.schedule.features', 'Features')}
</h3>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-4">
<li className="flex items-center gap-2">
<Clock size={16} className="text-brand-500" />
<span>
{t('staffHelp.schedule.feature1', 'See all your jobs in a vertical timeline')}
</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>
{t(
'staffHelp.schedule.feature2',
'View customer name and appointment details'
)}
</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>
{t('staffHelp.schedule.feature3', 'Navigate between days using arrows')}
</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>
{t('staffHelp.schedule.feature4', 'See current time indicator on today\'s view')}
</span>
</li>
</ul>
{canEditSchedule ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<GripVertical size={18} className="text-green-500" />
{t('staffHelp.schedule.rescheduleTitle', 'Drag to Reschedule')}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
{t(
'staffHelp.schedule.rescheduleDesc',
'You have permission to reschedule your jobs. Simply drag a job up or down on the timeline to move it to a different time slot. Changes will be saved automatically.'
)}
</p>
</div>
) : (
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-300">
{t(
'staffHelp.schedule.viewOnly',
'Your schedule is view-only. Contact a manager if you need to reschedule an appointment.'
)}
</p>
</div>
)}
</div>
</section>
{/* My Availability Section */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center">
<CalendarOff size={20} className="text-rose-600 dark:text-rose-400" />
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
{t('staffHelp.availability.title', 'My Availability')}
</h2>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t(
'staffHelp.availability.description',
'Use the My Availability page to set times when you are not available for bookings. This helps managers and the booking system know when not to schedule you.'
)}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
{t('staffHelp.availability.howTo', 'How to Block Time')}
</h3>
<ol className="space-y-2 text-sm text-gray-600 dark:text-gray-300 list-decimal list-inside mb-4">
<li>{t('staffHelp.availability.step1', 'Click "Add Time Block" button')}</li>
<li>{t('staffHelp.availability.step2', 'Select the date and time range')}</li>
<li>{t('staffHelp.availability.step3', 'Add an optional reason (e.g., "Vacation", "Doctor appointment")')}</li>
<li>{t('staffHelp.availability.step4', 'Choose if it repeats (one-time, weekly, etc.)')}</li>
<li>{t('staffHelp.availability.step5', 'Save your time block')}</li>
</ol>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<p className="text-sm text-gray-600 dark:text-gray-300">
<strong>{t('staffHelp.availability.note', 'Note:')}</strong>{' '}
{t(
'staffHelp.availability.noteDesc',
'Time blocks you create will prevent new bookings during those times. Existing appointments are not affected.'
)}
</p>
</div>
</div>
</section>
{/* Tickets Section - Only if user has access */}
{canAccessTickets && (
<section className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<Ticket size={20} className="text-purple-600 dark:text-purple-400" />
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
{t('staffHelp.tickets.title', 'Tickets')}
</h2>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t(
'staffHelp.tickets.description',
'You have access to the ticketing system. Use tickets to communicate with customers, report issues, or track requests.'
)}
</p>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>{t('staffHelp.tickets.feature1', 'View and respond to tickets')}</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>{t('staffHelp.tickets.feature2', 'Create new tickets for customer issues')}</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span>{t('staffHelp.tickets.feature3', 'Track ticket status and history')}</span>
</li>
</ul>
</div>
</section>
)}
{/* Help Footer */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t('staffHelp.footer.title', 'Need More Help?')}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t(
'staffHelp.footer.description',
"If you have questions or need assistance, please contact your manager or supervisor."
)}
</p>
{canAccessTickets && (
<button
onClick={() => navigate('/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
{t('staffHelp.footer.openTicket', 'Open a Ticket')}
</button>
)}
</section>
</div>
</div>
);
};
export default StaffHelp;