'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 'admin' or 'team' 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>
);
}