Files
smoothschedule/frontend/src/components/Schedule/DraggableEvent.tsx
poduck 01020861c7 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>
2025-12-07 02:23:00 -05:00

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>
);
};