vkashti / app / admin / tasks / UpcomingTasksKanban.tsx
UpcomingTasksKanban.tsx
Raw
import React from 'react';
import { useTasks, Task } from './TasksProvider';
import { useDrag, useDrop } from 'react-dnd';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import TaskDialog from './TaskDialog';

const dayInMs = 24 * 60 * 60 * 1000;

function getNextDueDate(task: Task): Date {
  const start = new Date(task.start_date);
  const localStart = new Date(
    start.getFullYear(),
    start.getMonth(),
    start.getDate()
  );
  const today = new Date();
  const localToday = new Date(
    today.getFullYear(),
    today.getMonth(),
    today.getDate()
  );
  if (localStart >= localToday) return localStart;
  const diffDays = Math.ceil(
    (localToday.getTime() - localStart.getTime()) / dayInMs
  );
  const cycles = Math.ceil(diffDays / task.interval);
  return new Date(localStart.getTime() + cycles * task.interval * dayInMs);
}

// Move TaskItem out as a separate component to fix the rules-of-hooks issue
type TaskItemProps = {
  task: Task;
  isToday: boolean;
  getTaskStatus: (task: Task, isToday: boolean) => 'past' | 'imminent' | 'upcoming';
  setSelectedTask: (task: Task) => void;
  setIsDialogOpen: (isOpen: boolean) => void;
  dividerIndex?: number;
  index?: number;
};

const TaskItem: React.FC<TaskItemProps> = ({ 
  task, 
  isToday, 
  getTaskStatus, 
  setSelectedTask,
  setIsDialogOpen,
  dividerIndex,
  index
}) => {
  const [{ opacity }, dragRef] = useDrag({
    type: 'TASK',
    item: { id: task.id, currentStartDate: task.start_date },
    collect: (monitor) => ({ opacity: monitor.isDragging() ? 0.5 : 1 })
  });

  const status = getTaskStatus(task, isToday);
  const showDivider = isToday && typeof index === 'number' && typeof dividerIndex === 'number' && index === dividerIndex;

  if (showDivider) {
    return (
      <React.Fragment>
        <div className="divider text-sm text-gray-500">Now</div>
        <div
          // @ts-ignore
          ref={dragRef}
          onClick={() => {
            setSelectedTask(task);
            setIsDialogOpen(true);
          }}
          className={`card p-2 shadow transition-all ${
            status === 'imminent' ? 'bg-gray-100 animate-pulse' : ''
          } hover:bg-orange-50 cursor-pointer`}
          style={{
            opacity: status === 'past' ? 0.5 : (typeof opacity === 'number' ? opacity : 1)
          }}
        >
          <p className="text-sm font-medium">{task.name}</p>
          <p className="text-xs text-gray-500">
            {task.time}  {task.interval} days {' '}
            {`${task.difficulty}🔥`}
          </p>
        </div>
      </React.Fragment>
    );
  }

  return (
    <div
      // @ts-ignore
      ref={dragRef}
      onClick={() => {
        setSelectedTask(task);
        setIsDialogOpen(true);
      }}
      className={`relative p-2 rounded shadow-sm transition-all hover:shadow-lg
        ${status === 'imminent' ? 'bg-amber-50 border-amber-200' : 'bg-white'}
        hover:bg-orange-50 border-gray-200 cursor-pointer`}
      style={{
        opacity: status === 'past' ? 0.5 : (typeof opacity === 'number' ? opacity : 1)
      }}
    >
      {/* Badge for difficulty */}
      <div className="absolute top-1 right-1 inline-flex items-center gap-1 text-xs font-medium text-gray-700 bg-white/80 rounded-full px-1.5 py-0.5 border shadow-sm">
        🔥 {task.difficulty}
      </div>

      {/* Main content */}
      <div className="px-1">
        <div className="font-medium text-sm truncate">{task.name}</div>
        <div className="flex items-center gap-1 text-xs text-gray-600">
          <span className="inline-block w-3 h-3"></span>
          <span className="truncate">
            {task.time}  {task.interval} days
          </span>
        </div>
      </div>
    </div>
  );
};

export default function UpcomingTasksKanban() {
  const { tasks, updateTask, deleteTask, setIsDialogOpen, setSelectedTask, isDialogOpen, selectedTask } = useTasks();

  const today = new Date();
  today.setHours(0, 0, 0, 0);
  
  // Simply create an array of the next 7 days starting from today
  const columns = Array.from(
    { length: 7 },
    (_, i) => new Date(today.getTime() + i * dayInMs)
  );

  // Initialize columns by due date key (YYYY-MM-DD)
  const tasksByDate: Record<string, Task[]> = {};
  columns.forEach((date) => {
    const key = date.toISOString().slice(0, 10);
    tasksByDate[key] = [];
  });

  // Group tasks by all upcoming due dates in the displayed range
  tasks.forEach((task) => {
    let due = getNextDueDate(task);
    const lastDate = columns[columns.length - 1];
    while (due <= lastDate) {
      const key = due.toISOString().slice(0, 10);
      if (tasksByDate[key]) tasksByDate[key].push(task);
      due = new Date(due.getTime() + task.interval * dayInMs);
    }
  });

  // Update the sorting to consider time only
  const sortTasks = (a: Task, b: Task) => {
    return a.time.localeCompare(b.time);
  };

  // Add this helper function before the Column component
  const getTaskStatus = (task: Task, isToday: boolean) => {
    if (!isToday) return 'upcoming';

    const now = new Date();
    const [hours, minutes] = task.time.split(':').map(Number);
    const taskTime = new Date();
    taskTime.setHours(hours, minutes, 0, 0);

    if (taskTime < now) return 'past';

    // Check if task is within the next hour
    const nextHour = new Date(now.getTime() + 60 * 60 * 1000);
    if (taskTime <= nextHour) return 'imminent';

    return 'upcoming';
  };

  const handleSave = (updatedFields: Partial<Task>) => {
    if (selectedTask?.id) {
      updateTask(selectedTask.id, updatedFields);
    }
  };

  // Modify the Column component
  const Column = ({
    date,
    tasksForDate
  }: {
    date: Date;
    tasksForDate: Task[];
  }) => {
    const [, drop] = useDrop(() => ({
      accept: 'TASK',
      drop: (item: { id: string; currentStartDate: string }) => {
        updateTask(item.id, { start_date: date.toISOString() });
      }
    }));

    const isToday = date.toDateString() === new Date().toDateString();
    const now = new Date();
    const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;

    // Sort tasks and find the insertion point for the "Now" divider
    const sortedTasks = [...tasksForDate].sort((a, b) =>
      a.time.localeCompare(b.time)
    );
    const dividerIndex = isToday
      ? sortedTasks.findIndex((task) => task.time > currentTime)
      : -1;

    return (
      // @ts-ignore
      <div ref={drop} className="flex-1 max-w-[300px]">
        <div className="text-center font-bold mb-2">
          <div className="flex flex-col items-center">
            <span className="text-gray-600 text-xs">{date.toLocaleDateString('bg-BG', { weekday: 'short' }).toLowerCase()}</span>
            <span className="text-gray-900 text-sm">
              {date.toLocaleDateString('bg-BG', { day: '2-digit', month: '2-digit' }).replace('/', '.')}
            </span>
          </div>
        </div>
        <div className="space-y-2">
          {sortedTasks.map((task, index) => {
            // Create a component to use the hook correctly
            return <TaskItem key={task.id} task={task} isToday={isToday} getTaskStatus={getTaskStatus} setSelectedTask={setSelectedTask} setIsDialogOpen={setIsDialogOpen} dividerIndex={dividerIndex} index={index} />;
          })}
        </div>
      </div>
    );
  };

  return (
    <DndProvider backend={HTML5Backend}>
      <div className="overflow-x-auto w-full">
        <div className="flex space-x-4 mb-4 min-w-max">
          {columns.map((date) => {
            const key = date.toISOString().slice(0, 10);
            let tasksForDate = tasksByDate[key] || [];
            tasksForDate = tasksForDate.sort(sortTasks);
            return <Column key={key} date={date} tasksForDate={tasksForDate} />;
          })}
        </div>
      </div>

      <TaskDialog
        isOpen={isDialogOpen}
        onClose={() => {
          setIsDialogOpen(false);
          setSelectedTask(undefined);
        }}
        onSave={handleSave}
        onDelete={deleteTask}
        task={selectedTask}
      />
    </DndProvider>
  );
}