vkashti / app / admin / schedule / page.tsx
page.tsx
Raw
'use client';

import { useState, useEffect, Suspense } from 'react';
import { MdDeleteOutline } from 'react-icons/md';
import domtoimage from 'dom-to-image-more';
import { createClient } from '@/utils/supabase/client';
import ScheduleFilters from './ScheduleFilters';

type Schedule = {
  id: number;
  created_at: string;
  start_time: string;
  end_time: string;
  user_id: string;
  position?: string | null;
};

type Employee = {
  name: string;
  tag: string;
  id: string;
  firstName?: string;
  lastName?: string;
};

type ScheduleState = {
  name: string;
  hours: {
    timeRange: string;
    position: string;
  }[][];
};

const supabase = createClient();

// Update the getDayName function
const getDayName = (date: Date) => {
  return date.toLocaleDateString('bg-BG', { weekday: 'short' });
};

function getWeekDates(baseDate = new Date()) {
  const week = [];
  const start = new Date(baseDate);
  
  // Ensure we're working with just the date part (reset time to midnight)
  start.setHours(0, 0, 0, 0);

  for (let i = 0; i < 7; i++) {
    const date = new Date(start);
    date.setDate(start.getDate() + i);
    week.push({
      fullDate: new Date(date),
      dayName: getDayName(date)
    });
  }
  return week;
}

const fetchScheduleForTimeRange = async (startTime: string, endTime: string): Promise<Schedule[]> => {
  const { data, error } = await supabase
    // @ts-ignore
    .from('schedules')
    .select('*')
    .gte('start_time', startTime)
    .lte('end_time', endTime);

  if (error) {
    console.error('Error fetching schedule:', error.message);
    return [];
  }

  // @ts-ignore
  return data || [];
};

const assignShift = async (employeeId: string, startTime: string, endTime: string, position: string = 'бар', isUpdate: boolean = false, oldStartTime?: string, oldEndTime?: string) => {
  if (isUpdate && oldStartTime && oldEndTime) {
    // First delete the old shift
    const { error: deleteError } = await supabase
      .from('schedules')
      .delete()
      .match({ 
        user_id: employeeId,
        start_time: oldStartTime,
        end_time: oldEndTime 
      });

    if (deleteError) {
      console.error('Error deleting old shift:', deleteError.message);
      return null;
    }
  }

  // Then insert the new/updated shift
  const { data, error } = await supabase
    .from('schedules')
    .insert([{ user_id: employeeId, start_time: startTime, end_time: endTime, position }]);

  if (error) {
    console.error('Error assigning shift:', error.message);
    return null;
  }

  return data;
};

const deleteShift = async (employeeId: string, startTime: string, endTime: string) => {
  const { error } = await supabase
    // @ts-ignore
    .from('schedules')
    .delete()
    .match({ 
      user_id: employeeId,
      start_time: startTime,
      end_time: endTime 
    });

  if (error) {
    console.error('Error deleting shift:', error.message);
    return false;
  }

  return true;
};

// Function to check if a person is currently working
const isCurrentlyWorking = (employee: ScheduleState, weekDates: { fullDate: Date; dayName: string }[]): number => {
  const now = new Date();
  const currentDay = now.getDay();
  // Adjust to match weekDates index (0 = Sunday in JS Date, but our week might start differently)
  const todayIndex = weekDates.findIndex(date => 
    date.fullDate.getDate() === now.getDate() && 
    date.fullDate.getMonth() === now.getMonth() && 
    date.fullDate.getFullYear() === now.getFullYear()
  );
  
  if (todayIndex === -1) return -1;
  
  const shifts = employee.hours[todayIndex];
  if (!shifts || shifts.length === 0) return -1;
  
  const currentTime = now.toLocaleTimeString('bg-BG', {
    hour: '2-digit',
    minute: '2-digit',
    hour12: false
  });
  
  return shifts.some(shift => {
    const [start, end] = shift.timeRange.split(' - ');
    
    // Handle overnight shifts (e.g., 22:00 - 03:00)
    if (start > end) {
      // Current time is after shift start OR before shift end
      return currentTime >= start || currentTime <= end;
    }
    
    // Normal shift (e.g., 09:00 - 17:00)
    return currentTime >= start && currentTime <= end;
  }) ? todayIndex : -1;
};

function ScheduleContent() {
  const [weekStart, setWeekStart] = useState(new Date());
  const [employees, setEmployees] = useState<Employee[]>([]);
  const [selectedEmployees, setSelectedEmployees] = useState<string[]>([]);
  const [schedule, setSchedule] = useState<ScheduleState[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  const [modalData, setModalData] = useState({
    employeeIndex: -1,
    dayIndex: -1,
    start: '',
    end: '',
    editingRangeIndex: -1,
    position: 'бар'
  });

  // Fetch team members with 'admin' or 'team' roles
  useEffect(() => {
    const fetchTeamMembers = async () => {
      setIsLoading(true);
      try {
        // Approach: First try to get users with admin/team roles, fall back to all profiles if needed
        
        // Get all user_roles entries
        const { data: roleEntries, error: rolesError } = await supabase
          .from('user_roles')
          .select('user_id, role');
          
        if (rolesError) {
          console.error('Error fetching roles:', rolesError.message);
        }
        
        // Filter for admin and team roles, case-insensitive
        const adminTeamUserIds = roleEntries
          ?.filter(entry => ['admin', 'team'].includes(entry.role.toLowerCase()))
          .map(entry => entry.user_id) || [];
          
        console.log(`Found ${adminTeamUserIds.length} users with admin/team roles`);
        
        // Fetch profiles - either filtered by roles or all as fallback
        const query = supabase.from('profiles').select('*');
        
        // Only filter by user_ids if we found admin/team roles
        if (adminTeamUserIds.length > 0) {
          query.in('user_id', adminTeamUserIds);
        }
        
        const { data: profiles, error: profilesError } = await query;
        
        if (profilesError) {
          console.error('Error fetching profiles:', profilesError.message);
          setIsLoading(false);
          return;
        }
        
        if (!profiles || profiles.length === 0) {
          console.warn('No profiles found for schedule');
          setIsLoading(false);
          return;
        }
        
        console.log(`Found ${profiles.length} profiles for schedule`);
        
        // Transform profiles to employee format
        const teamMembers = profiles.map((profile: any) => ({
          name: `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Unknown',
          firstName: profile.first_name || '',
          lastName: profile.last_name || '',
          tag: profile.instagram_handle || '',
          id: profile.user_id
        }));
        
        // Update state
        setEmployees(teamMembers);
        setSelectedEmployees(teamMembers.map(emp => emp.id));
        setSchedule(teamMembers.map(employee => ({
          name: employee.name,
          hours: Array(7).fill(null).map(() => [])
        })));
        
      } catch (error) {
        console.error('Error in fetchTeamMembers:', error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchTeamMembers();
  }, []);

  useEffect(() => {
    const fetchScheduleForWeek = async () => {
      if (employees.length === 0) return;
      
      // Get start and end of the week
      const weekDates = getWeekDates(weekStart);
      const weekStartDate = weekDates[0].fullDate;
      const weekEndDate = new Date(weekDates[6].fullDate);
      weekEndDate.setHours(23, 59, 59, 999);

      // Fetch all schedules for the week
      const scheduleData = await fetchScheduleForTimeRange(
        weekStartDate.toISOString(),
        weekEndDate.toISOString()
      );

      // Transform the data into the schedule format
      const formattedSchedule = employees.map(employee => {
        const employeeShifts = scheduleData.filter(shift => shift.user_id === employee.id);
        
        // Initialize empty week
        const weekSchedule = Array(7).fill(null).map(() => []);
        
        // Fill in shifts for each day
        employeeShifts.forEach(shift => {
          const shiftStart = new Date(shift.start_time);
          // Calculate the day index based on the difference from week start
          const daysDiff = Math.floor(
            (shiftStart.getTime() - weekStartDate.getTime()) / (24 * 60 * 60 * 1000)
          );
          
          if (daysDiff >= 0 && daysDiff < 7) {
            const startTime = shiftStart.toLocaleTimeString('bg-BG', { 
              hour: '2-digit', 
              minute: '2-digit',
              hour12: false 
            });
            
            const endTime = new Date(shift.end_time).toLocaleTimeString('bg-BG', {
              hour: '2-digit',
              minute: '2-digit',
              hour12: false
            });
            
            // Store both time range and position
            (weekSchedule[daysDiff] as any).push({
              timeRange: `${startTime} - ${endTime}`,
              position: shift.position || 'бар'
            });
          }
        });

        return {
          name: employee.name,
          hours: weekSchedule
        };
      });

      setSchedule(formattedSchedule);
    };

    if (weekStart && employees.length > 0) {
      fetchScheduleForWeek();
    }
  }, [weekStart, employees]); // Re-fetch when week changes or employees change

  // ▼▼▼ 1) Edit existing time slots ▼▼▼
  const handleRangeEdit = (
    e: React.MouseEvent,
    employeeIndex: number,
    dayIndex: number,
    rangeIndex: number,
    shiftData: { timeRange: string; position: string }
  ) => {
    e.stopPropagation();
    const [start, end] = shiftData.timeRange.split(' - ');
    setModalData({
      employeeIndex,
      dayIndex,
      start,
      end,
      editingRangeIndex: rangeIndex,
      position: shiftData.position
    });
  };

  const handleCellClick = (employeeIndex: number, dayIndex: number) => {
    setModalData({
      employeeIndex,
      dayIndex,
      start: '',
      end: '',
      editingRangeIndex: -1,
      position: 'бар'
    });
  };

  const handleRangeSubmit = async () => {
    const { employeeIndex, dayIndex, start, end, editingRangeIndex, position } = modalData;
    if (!start || !end) return;

    const employeeId = employees[employeeIndex].id;
    
    // Get the date for the selected day
    const selectedDate = weekDates[dayIndex].fullDate;
    
    // Create start datetime
    const startDateTime = new Date(selectedDate);
    const [startHours, startMinutes] = start.split(':').map(Number);
    startDateTime.setHours(startHours, startMinutes, 0, 0);

    // Create end datetime
    const endDateTime = new Date(selectedDate);
    const [endHours, endMinutes] = end.split(':').map(Number);
    endDateTime.setHours(endHours, endMinutes, 0, 0);

    // Handle case where end time is on the next day (e.g., shift ends at 2 AM)
    if (endDateTime < startDateTime) {
      endDateTime.setDate(endDateTime.getDate() + 1);
    }

    // If editing an existing shift, get the old start and end times
    let oldStartTime, oldEndTime;
    if (editingRangeIndex >= 0 && schedule[employeeIndex]?.hours[dayIndex][editingRangeIndex]) {
      const shiftData = schedule[employeeIndex].hours[dayIndex][editingRangeIndex];
      const [oldStart, oldEnd] = shiftData.timeRange.split(' - ');
      
      // Create old start datetime
      const oldStartDateTime = new Date(selectedDate);
      const [oldStartHours, oldStartMinutes] = oldStart.split(':').map(Number);
      oldStartDateTime.setHours(oldStartHours, oldStartMinutes, 0, 0);
      
      // Create old end datetime
      const oldEndDateTime = new Date(selectedDate);
      const [oldEndHours, oldEndMinutes] = oldEnd.split(':').map(Number);
      oldEndDateTime.setHours(oldEndHours, oldEndMinutes, 0, 0);
      
      // Handle case where old end time is on the next day
      if (oldEndDateTime < oldStartDateTime) {
        oldEndDateTime.setDate(oldEndDateTime.getDate() + 1);
      }
      
      oldStartTime = oldStartDateTime.toISOString();
      oldEndTime = oldEndDateTime.toISOString();
    }

    await assignShift(
      employeeId, 
      startDateTime.toISOString(), 
      endDateTime.toISOString(), 
      position,
      editingRangeIndex >= 0,
      oldStartTime,
      oldEndTime
    );

    // Refresh the schedule data
    const weekStartDate = weekDates[0].fullDate;
    const weekEndDate = new Date(weekDates[6].fullDate);
    weekEndDate.setHours(23, 59, 59, 999);

    const scheduleData = await fetchScheduleForTimeRange(
      weekStartDate.toISOString(),
      weekEndDate.toISOString()
    );

    // Update the schedule state with the new data
    setSchedule(prevSchedule => {
      return prevSchedule.map(employee => {
        if (employee.name === employees[employeeIndex].name) {
          const employeeShifts = scheduleData.filter(shift => 
            shift.user_id === employees[employeeIndex].id
          );
          
          const updatedHours = [...employee.hours];
          const dayShifts = employeeShifts
            .filter(shift => {
              const shiftDate = new Date(shift.start_time);
              return shiftDate.getDay() === weekDates[dayIndex].fullDate.getDay();
            })
            .map(shift => {
              const start = new Date(shift.start_time).toLocaleTimeString('bg-BG', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false
              });
              const end = new Date(shift.end_time).toLocaleTimeString('bg-BG', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false
              });
              return {
                timeRange: `${start} - ${end}`,
                position: shift.position || 'бар'
              };
            });
            
          updatedHours[dayIndex] = dayShifts;
          return { ...employee, hours: updatedHours };
        }
        return employee;
      });
    });

    setModalData({
      employeeIndex: -1,
      dayIndex: -1,
      start: '',
      end: '',
      editingRangeIndex: -1,
      position: 'бар'
    });
  };

  const handleRangeDelete = async (
    e: React.MouseEvent,
    employeeIndex: number,
    dayIndex: number,
    rangeIndex: number
  ) => {
    e.stopPropagation();
    if (employeeIndex < 0 || dayIndex < 0 || rangeIndex < 0) return;

    const employeeId = employees[employeeIndex].id;
    const shiftData = schedule[employeeIndex].hours[dayIndex][rangeIndex];
    const [start, end] = shiftData.timeRange.split(' - ');

    // Get the date for the selected day
    const selectedDate = weekDates[dayIndex].fullDate;
    
    // Create start datetime
    const startDateTime = new Date(selectedDate);
    const [startHours, startMinutes] = start.split(':').map(Number);
    startDateTime.setHours(startHours, startMinutes, 0, 0);

    // Create end datetime
    const endDateTime = new Date(selectedDate);
    const [endHours, endMinutes] = end.split(':').map(Number);
    endDateTime.setHours(endHours, endMinutes, 0, 0);

    // Handle case where end time is on the next day
    if (endDateTime < startDateTime) {
      endDateTime.setDate(endDateTime.getDate() + 1);
    }

    const success = await deleteShift(
      employeeId,
      startDateTime.toISOString(),
      endDateTime.toISOString()
    );

    if (success) {
      // Update local state only after successful deletion
      const updatedSchedule = [...schedule];
      const dayRanges = [...updatedSchedule[employeeIndex].hours[dayIndex]];
      dayRanges.splice(rangeIndex, 1);
      updatedSchedule[employeeIndex].hours[dayIndex] = dayRanges;
      setSchedule(updatedSchedule);
    } else {
      alert('Failed to delete shift. Please try again.');
    }
  };

  const weekDates = getWeekDates(weekStart);

  // ▼▼▼ 2) SUBMIT: Capture table and send to Discord as an image ▼▼▼

  const handleSubmit = async () => {
    try {
      const tableElement = document.getElementById('schedule-table');
      if (!tableElement) return;

      // Style fixes for clean rendering
      tableElement.querySelectorAll('*').forEach((child) => {
        (child as HTMLElement).style.backgroundColor = '#ffffff';
      });

      // 1) Generate PNG from table
      const dataUrl = await domtoimage.toPng(tableElement);

      // 2) Send base64 image to our Next.js API route
      const response = await fetch('/api/schedule', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ image: dataUrl })
      });

      if (!response.ok) {
        console.error('Failed to send screenshot to Discord.');
        return;
      }

      alert('Таблицата е изпратена към Discord успешно!');
    } catch (error) {
      console.error('Error sending schedule to Discord:', error);
    }
  };

  // Filter employees based on selection
  const filteredSchedule = employees.length > 0 
    ? schedule.filter((_, index) => 
        selectedEmployees.includes(employees[index]?.id || '')
      )
    : [];

  return (
    <div className="mx-auto">
      
      <ScheduleFilters
        selectedEmployees={selectedEmployees}
        setSelectedEmployees={setSelectedEmployees}
        weekStart={weekStart}
        setWeekStart={setWeekStart}
        employees={employees}
      />

      {isLoading ? (
        <div className="flex justify-center items-center h-64">
          <div className="loading loading-spinner loading-lg text-primary"></div>
        </div>
      ) : employees.length === 0 ? (
        <div className="alert alert-warning">
          <div>
            <span className="font-bold">No team members found!</span>
            <p className="text-sm mt-1">Please add users with &apos;admin&apos; or &apos;team&apos; roles in the Profiles section.</p>
          </div>
        </div>
      ) : (
        <>
          <div className="overflow-x-auto w-full rounded-lg">
            <table
              id="schedule-table"
              className="w-full table-fixed min-w-[1000px] text-sm"
            >
              <thead>
                <tr className="bg-gray-50">
                  <th className="text-neutral py-2 px-3 w-[100px] text-center font-medium"></th>
                  {weekDates.map((dateInfo, idx) => (
                    <th key={idx} className="text-neutral py-2 px-3 w-[120px] text-center font-medium">
                      <div className="flex flex-col items-center">
                        <span className="text-gray-600 text-xs">{dateInfo.dayName}</span>
                        <span className="text-gray-900 text-sm">
                          {dateInfo.fullDate.toLocaleDateString('bg-BG', { 
                            day: '2-digit', 
                            month: '2-digit' 
                          }).replace('/', '.')}
                        </span>
                      </div>
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {filteredSchedule.map((employee, employeeIndex) => {
                  const workingDayIndex = isCurrentlyWorking(employee, weekDates);
                  return (
                  <tr 
                    key={employee.name}
                    className="hover:bg-gray-50/50 transition-colors duration-150"
                  >
                    <td className={`py-2 px-3 ${workingDayIndex !== -1 ? 'font-medium' : ''}`}>
                      <div className="flex items-center gap-1.5">
                        <span className="text-gray-900">
                          {employees[employeeIndex]?.firstName}
                        </span>
                        {workingDayIndex !== -1 && (
                          <span className="text-[10px] bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-medium">
                            Active
                          </span>
                        )}
                      </div>
                    </td>
                    {employee.hours.map((ranges, dayIndex) => (
                      <td
                        key={dayIndex}
                        className={`py-2 px-3 text-center cursor-pointer transition-colors duration-150 ${
                          dayIndex === workingDayIndex 
                            ? 'bg-green-50 border-l border-r border-green-200' 
                            : ''
                        }`}
                        onClick={() => handleCellClick(employeeIndex, dayIndex)}
                      >
                        {ranges.length
                          ? ranges.map((shiftData, idx) => (
                              <div key={idx} className="relative group my-0.5">
                                <span
                                  onClick={(e) =>
                                    handleRangeEdit(
                                      e,
                                      employeeIndex,
                                      dayIndex,
                                      idx,
                                      shiftData
                                    )
                                  }
                                  className="cursor-pointer mr-4 hover:bg-gray-100 rounded px-1.5 py-0.5 transition-colors duration-150 inline-flex items-center gap-1"
                                >
                                  <span className="text-gray-700">{shiftData.timeRange}</span>
                                  {shiftData.position === 'кухня' && (
                                    <span className="text-[10px] bg-amber-100 text-amber-700 px-1 py-0.5 rounded-full font-medium">
                                      К
                                    </span>
                                  )}
                                </span>
                                <button
                                  className="absolute right-0 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-red-500 hidden group-hover:block transition-colors duration-150"
                                  onClick={(e) =>
                                    handleRangeDelete(
                                      e,
                                      employeeIndex,
                                      dayIndex,
                                      idx
                                    )
                                  }
                                >
                                  <MdDeleteOutline size={16} />
                                </button>
                              </div>
                            ))
                          : <span className="text-gray-300">Х</span>}
                      </td>
                    ))}
                  </tr>
                  );
                })}
              </tbody>
            </table>
          </div>

          {/* Buttons below the table */}
          <div className="flex gap-4 mt-4">
            <button
              onClick={handleSubmit}
              className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-150 font-medium shadow-sm text-sm"
            >
              Изпрати в #team
            </button>
          </div>
        </>
      )}

      {/* Modal for Add/Edit time slot */}
      {modalData.employeeIndex >= 0 && (
        <div 
          className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
          onClick={() => setModalData({
            employeeIndex: -1,
            dayIndex: -1,
            start: '',
            end: '',
            editingRangeIndex: -1,
            position: 'бар'
          })}
        >
          <div 
            className="bg-white p-6 rounded shadow-lg max-w-sm w-full"
            onClick={e => e.stopPropagation()}
          >
            <h2 className="text-xl font-bold mb-4">
              {modalData.editingRangeIndex >= 0 ? 'Редакция' : 'Добавете'}{' '}
              диапазон
            </h2>
            <form className="grid grid-cols-2 gap-4">
              <div>
                <label
                  htmlFor="start-time"
                  className="block mb-2 text-sm font-medium text-gray-900"
                >
                  Начален час:
                </label>
                <input
                  type="time"
                  id="start-time"
                  step="900"
                  autoFocus
                  className="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
                  value={modalData.start}
                  onChange={(e) =>
                    setModalData({ ...modalData, start: e.target.value })
                  }
                />
              </div>
              <div>
                <label
                  htmlFor="end-time"
                  className="block mb-2 text-sm font-medium text-gray-900"
                >
                  Краен час:
                </label>
                <input
                  type="time"
                  step="900"
                  id="end-time"
                  className="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
                  value={modalData.end}
                  onChange={(e) =>
                    setModalData({ ...modalData, end: e.target.value })
                  }
                />
              </div>
              <div>
                <label
                  htmlFor="position"
                  className="block mb-2 text-sm font-medium text-gray-900"
                >
                  Позиция:
                </label>
                <select
                  id="position"
                  className="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
                  value={modalData.position}
                  onChange={(e) =>
                    setModalData({ ...modalData, position: e.target.value })
                  }
                >
                  <option value="бар">Бар</option>
                  <option value="кухня">Кухня</option>
                </select>
              </div>
            </form>
            <div className="mt-4 flex gap-4">
              <button
                onClick={handleRangeSubmit}
                className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
              >
                Запазете
              </button>
              <button
                onClick={() =>
                  setModalData({
                    employeeIndex: -1,
                    dayIndex: -1,
                    start: '',
                    end: '',
                    editingRangeIndex: -1,
                    position: 'бар'
                  })
                }
                className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
              >
                Затвори
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

export default function SchedulePage() {
  return (
    <Suspense fallback={<></>}>
      <ScheduleContent />
    </Suspense>
  );
}