Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes: - Django backend with multi-tenancy (django-tenants) - React + TypeScript frontend with Vite - Platform administration API with role-based access control - Authentication system with token-based auth - Quick login dev tools for testing different user roles - CORS and CSRF configuration for local development - Docker development environment setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
162
frontend/src/components/Schedule/DraggableEvent.tsx
Normal file
162
frontend/src/components/Schedule/DraggableEvent.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
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';
|
||||
|
||||
export type AppointmentStatus = 'PENDING' | 'CONFIRMED' | 'COMPLETED' | 'CANCELLED' | 'NO_SHOW';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user