vkashti / app / admin / reservations / ReservationsProvider.tsx
ReservationsProvider.tsx
Raw
'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;
}