- 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>
165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
import React from 'react';
|
|
import { useDraggable } from '@dnd-kit/core';
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
import { clsx } from 'clsx';
|
|
import { Clock, DollarSign } from 'lucide-react';
|
|
|
|
// Import from types.ts for consistency
|
|
import type { AppointmentStatus } from '../../types';
|
|
export type { AppointmentStatus };
|
|
|
|
export interface DraggableEventProps {
|
|
id: number;
|
|
title: string;
|
|
serviceName?: string;
|
|
start: Date;
|
|
end: Date;
|
|
status?: AppointmentStatus;
|
|
isPaid?: boolean;
|
|
height: number;
|
|
left: number;
|
|
width: number;
|
|
top: number;
|
|
onResizeStart: (e: React.MouseEvent, direction: 'left' | 'right', id: number) => void;
|
|
}
|
|
|
|
export const DraggableEvent: React.FC<DraggableEventProps> = ({
|
|
id,
|
|
title,
|
|
serviceName,
|
|
start,
|
|
end,
|
|
status = 'CONFIRMED',
|
|
isPaid = false,
|
|
height,
|
|
left,
|
|
width,
|
|
top,
|
|
onResizeStart,
|
|
}) => {
|
|
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
|
id: `event-${id}`,
|
|
data: {
|
|
type: 'event',
|
|
title,
|
|
duration: (end.getTime() - start.getTime()) / 60000
|
|
},
|
|
});
|
|
|
|
const style: React.CSSProperties = {
|
|
transform: CSS.Translate.toString(transform),
|
|
left,
|
|
width,
|
|
top,
|
|
height,
|
|
position: 'absolute',
|
|
zIndex: isDragging ? 50 : 10,
|
|
};
|
|
|
|
// Status Logic matching legacy OwnerScheduler.tsx exactly
|
|
const getStatusStyles = () => {
|
|
const now = new Date();
|
|
|
|
// Legacy: if (status === 'COMPLETED' || status === 'NO_SHOW')
|
|
if (status === 'COMPLETED' || status === 'NO_SHOW') {
|
|
return {
|
|
container: 'bg-gray-100 border-gray-400 text-gray-600 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400',
|
|
accent: 'bg-gray-400'
|
|
};
|
|
}
|
|
|
|
// Legacy: if (status === 'CANCELLED')
|
|
if (status === 'CANCELLED') {
|
|
return {
|
|
container: 'bg-gray-100 border-gray-400 text-gray-500 opacity-75 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400',
|
|
accent: 'bg-gray-400'
|
|
};
|
|
}
|
|
|
|
// Legacy: if (now > endTime) (Overdue)
|
|
if (now > end) {
|
|
return {
|
|
container: 'bg-red-100 border-red-500 text-red-900 dark:bg-red-900/50 dark:border-red-500 dark:text-red-200',
|
|
accent: 'bg-red-500'
|
|
};
|
|
}
|
|
|
|
// Legacy: if (now >= startTime && now <= endTime) (In Progress)
|
|
if (now >= start && now <= end) {
|
|
return {
|
|
container: 'bg-yellow-100 border-yellow-500 text-yellow-900 dark:bg-yellow-900/50 dark:border-yellow-500 dark:text-yellow-200',
|
|
accent: 'bg-yellow-500 animate-pulse'
|
|
};
|
|
}
|
|
|
|
// Legacy: Default (Future)
|
|
return {
|
|
container: 'bg-blue-100 border-blue-500 text-blue-900 dark:bg-blue-900/50 dark:border-blue-500 dark:text-blue-200',
|
|
accent: 'bg-blue-500'
|
|
};
|
|
};
|
|
|
|
const styles = getStatusStyles();
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...listeners}
|
|
{...attributes}
|
|
className={clsx(
|
|
"rounded-md border shadow-sm text-xs overflow-hidden cursor-pointer group transition-all select-none flex",
|
|
styles.container,
|
|
isDragging ? "opacity-50 ring-2 ring-blue-500 ring-offset-2 z-50 shadow-xl" : "hover:shadow-md"
|
|
)}
|
|
>
|
|
{/* Colored Status Strip */}
|
|
<div className={clsx("w-1.5 shrink-0", styles.accent)} />
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 p-1.5 min-w-0 flex flex-col justify-center">
|
|
<div className="flex items-center justify-between gap-1">
|
|
<span className="font-semibold truncate">
|
|
{title}
|
|
</span>
|
|
{isPaid && (
|
|
<DollarSign size={10} className="text-emerald-600 dark:text-emerald-400 shrink-0" />
|
|
)}
|
|
</div>
|
|
|
|
{serviceName && width > 100 && (
|
|
<div className="text-[10px] opacity-80 truncate">
|
|
{serviceName}
|
|
</div>
|
|
)}
|
|
|
|
{/* Time (only show if wide enough) */}
|
|
{width > 60 && (
|
|
<div className="flex items-center gap-1 mt-0.5 text-[10px] opacity-70">
|
|
<Clock size={8} />
|
|
<span className="truncate">
|
|
{start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Resize Handles */}
|
|
<div
|
|
className="absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize hover:bg-blue-400/50 z-20 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onMouseDown={(e) => {
|
|
e.stopPropagation();
|
|
onResizeStart(e, 'left', id);
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute right-0 top-0 bottom-0 w-1 cursor-ew-resize hover:bg-blue-400/50 z-20 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onMouseDown={(e) => {
|
|
e.stopPropagation();
|
|
onResizeStart(e, 'right', id);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|