'use client';
import {
createContext,
useContext,
useState,
useEffect,
useMemo,
useCallback
} from 'react';
import { createClient } from '@/utils/supabase/client';
import { Reservation } from '@/types/types';
import { useSyncedDate } from '@/hooks/useSyncedDate';
type ReservationsContextType = {
reservations: Reservation[];
selectedDate: Date | null;
setSelectedDate: (date: Date | null) => void;
updateReservation: (id: number, updatedFields: Partial<Reservation>) => void;
createReservation: (timeParams?: {
from_date: string;
to_date: string;
}) => Promise<void>;
deleteReservation: (id: number) => void;
pendingReservations: Reservation[];
startDate: Date;
setStartDate: (date: Date) => void;
interval: number;
setInterval: (days: number) => void;
view: 'grid' | 'list';
setView: (view: 'grid' | 'list') => void;
};
const ReservationsContext = createContext<ReservationsContextType | null>(null);
export function ReservationsProvider({
children
}: {
children: React.ReactNode;
}) {
const supabase = createClient();
const [allReservations, setAllReservations] = useState<Reservation[]>([]);
const [pendingReservations, setPendingReservations] = useState<Reservation[]>(
[]
);
const [selectedDate, setSelectedDate] = useSyncedDate();
const [startDate, setStartDate] = useState<Date>(() => {
const today = new Date();
// Ensure we're working with the start of the day in local timezone
today.setHours(0, 0, 0, 0);
// Adjust for timezone offset
today.setMinutes(today.getMinutes() - today.getTimezoneOffset());
return today;
});
const [interval, setInterval] = useState<number>(() => {
if (typeof window !== 'undefined') {
return window.innerWidth < 768 ? 1 : 7;
}
return 7;
});
const [view, setView] = useState<'grid' | 'list'>('list');
// Wrap setStartDate to ensure consistent date handling
const handleSetStartDate = useCallback((date: Date) => {
const newDate = new Date(date);
newDate.setHours(0, 0, 0, 0);
// Adjust for timezone offset
newDate.setMinutes(newDate.getMinutes() - newDate.getTimezoneOffset());
setStartDate(newDate);
}, []);
// Wrap getEndDate to ensure consistent date handling
const getEndDate = useCallback(() => {
const end = new Date(startDate);
end.setDate(end.getDate() + interval);
return end;
}, [startDate, interval]);
// Modified to return reservations for current view (either selected date or 7-day range)
const reservations = useMemo(() => {
const endDate = getEndDate();
return allReservations
.filter((r) => {
const date = new Date(r.from_date);
return date >= startDate && date <= endDate;
})
.sort((a, b) => {
// Sort by approved (false first) then by date
if (a.approved === b.approved) {
return (
new Date(a.from_date).getTime() - new Date(b.from_date).getTime()
);
}
return a.approved ? 1 : -1;
});
}, [allReservations, startDate, getEndDate]);
const fetchReservations = useCallback(
async (startDate: Date, endDate: Date) => {
const { data, error } = await supabase
.from('reservations')
.select('*')
.gte('from_date', startDate.toISOString())
.lte('from_date', endDate.toISOString())
.order('from_date', { ascending: true });
if (error) {
console.error('Error fetching reservations:', error.message);
return;
}
// @ts-ignore
setAllReservations(data || []);
},
[supabase]
);
// Initial fetch and real-time subscriptions
useEffect(() => {
const fetchInitialData = async () => {
try {
// Fetch ALL pending reservations
const { data: pendingData, error: pendingError } = await supabase
.from('reservations')
.select('*')
.eq('approved', false)
.order('from_date', { ascending: true });
if (pendingError) throw pendingError;
setPendingReservations((pendingData || []) as Reservation[]);
} catch (error) {
console.error('Error fetching initial pending data:', error);
}
};
fetchInitialData();
// Subscribe to ALL pending reservations changes
const pendingChannel = supabase
.channel('pending_reservations')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'reservations'
},
async (payload) => {
// For DELETE events
if (payload.eventType === 'DELETE') {
setPendingReservations((prev) =>
prev.filter((r) => r.id.toString() !== payload.old.id.toString())
);
return;
}
// For INSERT events
if (payload.eventType === 'INSERT' && !payload.new.approved) {
setPendingReservations((prev) => [
...prev,
payload.new as Reservation
]);
return;
}
// For UPDATE events
if (payload.eventType === 'UPDATE') {
setPendingReservations((prev) => {
// If the reservation was approved, remove it from pending
if (payload.new.approved) {
return prev.filter(
(r) => r.id.toString() !== payload.new.id.toString()
);
}
// If it was unapproved, add it to pending
if (!payload.new.approved && payload.old.approved) {
return [...prev, payload.new as Reservation];
}
// If it's still pending, update it
if (!payload.new.approved) {
return prev.map((r) =>
r.id.toString() === payload.new.id.toString()
? (payload.new as Reservation)
: r
);
}
return prev;
});
}
}
)
.subscribe();
// Subscribe to date range reservations
const rangeChannel = supabase
.channel('range_reservations')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'reservations'
},
async (payload) => {
const endDate = getEndDate();
if (payload.eventType === 'DELETE') {
setAllReservations((prev) =>
prev.filter((r) => r.id.toString() !== payload.old.id.toString())
);
return;
}
const payloadDate = new Date(
(payload.new as Record<string, any>)?.from_date || (payload.old as Record<string, any>)?.from_date
);
// Only update if the change is within our current date range
if (payloadDate >= startDate && payloadDate <= endDate) {
if (payload.eventType === 'INSERT') {
setAllReservations((prev) => [
...prev,
payload.new as Reservation
]);
} else if (payload.eventType === 'UPDATE') {
setAllReservations((prev) =>
prev.map((r) =>
r.id.toString() === payload.new.id.toString()
? (payload.new as Reservation)
: r
)
);
}
}
}
)
.subscribe();
return () => {
supabase.removeChannel(pendingChannel);
supabase.removeChannel(rangeChannel);
};
}, [startDate, interval, supabase, getEndDate]);
useEffect(() => {
const endDate = getEndDate();
fetchReservations(startDate, endDate);
}, [startDate, interval, fetchReservations, getEndDate]);
const updateReservation = async (
id: number,
updatedFields: Partial<Reservation>
) => {
try {
const { error } = await supabase
.from('reservations')
.update(updatedFields)
.eq('id', id);
if (error) {
console.error(
'Error updating reservation:',
error instanceof Error ? error.message : String(error)
);
return;
}
const oldReservation = allReservations.find((r) => r.id === id);
// Send SMS only if the reservation is being approved and has a phone number
if (
oldReservation &&
!oldReservation.approved &&
updatedFields.approved &&
oldReservation.phone
) {
const message = `Вкъщи Бар 🌟 \nРезервацията ви за ${oldReservation.persons} гости е потвърдена! Очакваме ви ${new Date(oldReservation.from_date).toLocaleDateString('bg-BG', { weekday: 'short', day: '2-digit', month: 'short' })} в ${new Date(oldReservation.from_date).toLocaleTimeString('bg-BG', { hour: '2-digit', minute: '2-digit' })}.`;
fetch('/api/sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: oldReservation.phone,
message
})
}).catch((smsError) => {
console.error('Error sending SMS:', smsError.message);
});
}
// Update local state to reflect the change
setAllReservations(prev => prev.map(res =>
res.id === id ? { ...res, ...updatedFields } : res
));
} catch (error) {
console.error(
'Error updating reservation:',
error instanceof Error ? error.message : String(error)
);
}
};
const createReservation = async (timeParams?: {
from_date: string;
to_date: string;
}) => {
if (!selectedDate && !timeParams) return;
let fromDate: Date, toDate: Date;
if (timeParams) {
fromDate = new Date(timeParams.from_date);
toDate = new Date(timeParams.to_date);
} else {
fromDate = new Date(selectedDate!);
fromDate.setHours(20, 0, 0, 0);
toDate = new Date(fromDate);
toDate.setHours(fromDate.getHours() + 2, 0, 0, 0);
}
const newReservation: Partial<Reservation> = {
person_name: 'Клиент',
phone: '',
from_date: fromDate.toISOString(),
to_date: toDate.toISOString(),
persons: 2,
caparo: 0,
description: 'Няма описание'
};
try {
const { error } = await supabase
.from('reservations')
.insert([newReservation]);
if (error) throw error;
} catch (error) {
console.error('Error creating reservation:', error);
}
};
const deleteReservation = async (id: number) => {
try {
const { error } = await supabase
.from('reservations')
.delete()
.eq('id', id);
if (error) {
console.error(
'Error deleting reservation:',
error instanceof Error ? error.message : String(error)
);
return;
}
} catch (error) {
console.error(
'Error deleting reservation:',
error instanceof Error ? error.message : String(error)
);
}
};
return (
<ReservationsContext.Provider
value={{
reservations,
selectedDate,
setSelectedDate,
updateReservation,
createReservation,
deleteReservation,
pendingReservations,
startDate,
setStartDate: handleSetStartDate,
interval,
setInterval,
view,
setView
}}
>
{children}
</ReservationsContext.Provider>
);
}
export function useReservations() {
const context = useContext(ReservationsContext);
if (context === null) {
throw new Error(
'useReservations must be used within a ReservationsProvider'
);
}
return context;
}