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>
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
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;
|