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:
99
frontend/src/components/Timeline/ResourceRow.tsx
Normal file
99
frontend/src/components/Timeline/ResourceRow.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { clsx } from 'clsx';
|
||||
import { differenceInHours } from 'date-fns';
|
||||
import { calculateLayout, Event } from '../../lib/layoutAlgorithm';
|
||||
import { DraggableEvent } from './DraggableEvent';
|
||||
import { getPosition } from '../../lib/timelineUtils';
|
||||
|
||||
interface ResourceRowProps {
|
||||
resourceId: number;
|
||||
resourceName: string;
|
||||
events: Event[];
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
hourWidth: number;
|
||||
eventHeight: number;
|
||||
onResizeStart: (e: React.MouseEvent, direction: 'left' | 'right', id: number) => void;
|
||||
}
|
||||
|
||||
const ResourceRow: React.FC<ResourceRowProps> = ({
|
||||
resourceId,
|
||||
resourceName,
|
||||
events,
|
||||
startTime,
|
||||
endTime,
|
||||
hourWidth,
|
||||
eventHeight,
|
||||
onResizeStart,
|
||||
}) => {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: `resource-${resourceId}`,
|
||||
data: { resourceId },
|
||||
});
|
||||
|
||||
const eventsWithLanes = useMemo(() => calculateLayout(events), [events]);
|
||||
const maxLane = Math.max(0, ...eventsWithLanes.map(e => e.laneIndex || 0));
|
||||
const rowHeight = (maxLane + 1) * eventHeight + 20;
|
||||
|
||||
const totalWidth = getPosition(endTime, startTime, hourWidth);
|
||||
|
||||
// Calculate total hours for grid lines
|
||||
const totalHours = Math.ceil(differenceInHours(endTime, startTime));
|
||||
|
||||
return (
|
||||
<div className="flex border-b border-gray-200 group">
|
||||
<div
|
||||
className="w-48 flex-shrink-0 p-4 border-r border-gray-200 bg-gray-50 font-medium flex items-center sticky left-0 z-10 group-hover:bg-gray-100 transition-colors"
|
||||
style={{ height: rowHeight }}
|
||||
>
|
||||
{resourceName}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={clsx(
|
||||
"relative flex-grow transition-colors",
|
||||
isOver ? "bg-blue-50" : ""
|
||||
)}
|
||||
style={{ height: rowHeight, width: totalWidth }}
|
||||
>
|
||||
{/* Grid Lines */}
|
||||
<div className="absolute inset-0 pointer-events-none flex">
|
||||
{Array.from({ length: totalHours }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-r border-gray-100 h-full"
|
||||
style={{ width: hourWidth }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Events */}
|
||||
{eventsWithLanes.map((event) => {
|
||||
const left = getPosition(event.start, startTime, hourWidth);
|
||||
const width = getPosition(event.end, startTime, hourWidth) - left;
|
||||
const top = (event.laneIndex || 0) * eventHeight + 10;
|
||||
|
||||
return (
|
||||
<DraggableEvent
|
||||
key={event.id}
|
||||
id={event.id}
|
||||
title={event.title}
|
||||
start={event.start}
|
||||
end={event.end}
|
||||
laneIndex={event.laneIndex || 0}
|
||||
height={eventHeight - 4}
|
||||
left={left}
|
||||
width={width}
|
||||
top={top}
|
||||
onResizeStart={onResizeStart}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceRow;
|
||||
Reference in New Issue
Block a user