import React, { useState, useRef, useEffect } from "react"; import { View, ScrollView, TouchableOpacity, Platform } from "react-native"; import { Text } from "~/components/ui/text"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import BackButton from "~/components/ui/backButton"; import BottomSheet from "~/components/ui/bottomSheet"; import { Button } from "~/components/ui/button"; import { router } from "expo-router"; import { ROUTES } from "~/lib/routes"; import * as Calendar from "expo-calendar"; import ModalToast from "~/components/ui/toast"; import Skeleton from "~/components/ui/skeleton"; import { collection, doc, FieldValue } from "~/lib/firebase"; import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; type ScheduleStatus = "active" | "paused" | "stopped"; type Schedule = { id: string; recipientName: string; label: string; amount: string; currency: string; nextRunDate: string; nextRunLabel: string; frequency: string; status: ScheduleStatus; }; const toISODate = (date: Date) => date.toISOString().split("T")[0]; const TODAY = new Date(); const addDays = (date: Date, days: number) => { const d = new Date(date); d.setDate(date.getDate() + days); return d; }; const START_OF_WEEK = new Date(TODAY); START_OF_WEEK.setDate(TODAY.getDate() - TODAY.getDay()); const WEEK_DAYS = Array.from({ length: 7 }).map((_, index) => { const day = new Date(START_OF_WEEK); day.setDate(START_OF_WEEK.getDate() + index); const labels = ["S", "M", "T", "W", "T", "F", "S"]; return { key: toISODate(day), label: labels[day.getDay()], dateNumber: day.getDate(), isToday: day.toDateString() === TODAY.toDateString(), }; }); const MOCK_SCHEDULES: Schedule[] = [ { id: "sch-1", recipientName: "Abebe Kebede", label: "Monthly support", amount: "4,500", currency: "ETB", nextRunDate: toISODate(TODAY), nextRunLabel: "Today · 4:00 PM", frequency: "Every month", status: "active", }, { id: "sch-2", recipientName: "Sara Alemu", label: "Weekly allowance", amount: "1,000", currency: "ETB", nextRunDate: toISODate(addDays(TODAY, 1)), nextRunLabel: "Tomorrow · 9:00 AM", frequency: "Every week", status: "paused", }, { id: "sch-3", recipientName: "Hope Community Fund", label: "Quarterly disbursement", amount: "32,000", currency: "ETB", nextRunDate: toISODate(addDays(TODAY, 2)), nextRunLabel: "In 2 days · 2:15 PM", frequency: "Every 3 months", status: "stopped", }, ]; const getStatusPillClasses = (status: ScheduleStatus) => { switch (status) { case "active": return "bg-emerald-100 text-emerald-700"; case "paused": return "bg-yellow-100 text-yellow-700"; case "stopped": default: return "bg-gray-100 text-gray-600"; } }; const isSameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); const buildFrequencyLabel = ( repeatType?: string, interval?: number ): string => { if (!repeatType || repeatType === "none") return "One-time"; if (repeatType === "every_x_days" && interval) { return `Every ${interval} days`; } return repeatType; }; const buildNextRunLabel = (dateValue?: any, time?: string): string => { if (!dateValue && !time) return ""; const today = TODAY; const tomorrow = addDays(TODAY, 1); let dateObj: Date | null = null; if (dateValue instanceof Date) { dateObj = dateValue; } else if (typeof dateValue === "string") { const parsed = new Date(dateValue); if (!isNaN(parsed.getTime())) { dateObj = parsed; } } let dayLabel = ""; if (dateObj) { if (isSameDay(dateObj, today)) dayLabel = "Today"; else if (isSameDay(dateObj, tomorrow)) dayLabel = "Tomorrow"; else { dayLabel = dateObj.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } } if (dayLabel && time) return `${dayLabel} · ${time}`; return dayLabel || time || ""; }; const parseTimeFromLabel = (label: string) => { const parts = label.split("·"); const rawTime = parts[1]?.trim(); if (!rawTime) { return { hours: 9, minutes: 0 }; } const match = rawTime.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); if (!match) { return { hours: 9, minutes: 0 }; } let hours = parseInt(match[1], 10); const minutes = parseInt(match[2], 10); const period = match[3].toUpperCase(); if (period === "PM" && hours < 12) hours += 12; if (period === "AM" && hours === 12) hours = 0; return { hours, minutes }; }; const getDateTimesForSchedule = (schedule: Schedule) => { const [year, month, day] = schedule.nextRunDate .split("-") .map((v) => parseInt(v, 10)); const { hours, minutes } = parseTimeFromLabel(schedule.nextRunLabel); const startDate = new Date(year, month - 1, day, hours, minutes); const endDate = new Date(startDate.getTime() + 60 * 60 * 1000); return { startDate, endDate }; }; const syncScheduleToCalendar = async ( schedule: Schedule, showToast: ( title: string, description?: string, variant?: "success" | "error" | "warning" | "info" ) => void ) => { try { const { status } = await Calendar.requestCalendarPermissionsAsync(); if (status !== "granted") { showToast( "Calendar permission needed", "Allow calendar access to sync schedules.", "warning" ); return; } let calendarId: string | null = null; if (Platform.OS === "ios") { const defaultCal = await Calendar.getDefaultCalendarAsync(); calendarId = defaultCal?.id ?? null; } else { const calendars = await Calendar.getCalendarsAsync( Calendar.EntityTypes.EVENT ); const editable = calendars.find( (c: Calendar.Calendar) => c.allowsModifications ); calendarId = editable?.id ?? calendars[0]?.id ?? null; } if (!calendarId) { showToast( "No calendar found", "We couldn't find an editable calendar on this device.", "error" ); return; } const { startDate, endDate } = getDateTimesForSchedule(schedule); await Calendar.createEventAsync(calendarId, { title: schedule.label || "Scheduled payment", notes: `${schedule.recipientName} • ${schedule.currency} ${schedule.amount}`, startDate, endDate, timeZone: undefined, }); showToast( "Added to calendar", "This schedule was added as an event.", "success" ); } catch (error) { console.warn("[Schedules] Failed to sync schedule to calendar", error); showToast( "Calendar error", "We couldn't add this schedule to your calendar.", "error" ); } }; export default function SchedulesScreen() { const [schedules, setSchedules] = useState([]); const [selected, setSelected] = useState(null); const [statusFilter, setStatusFilter] = useState<"all" | ScheduleStatus>( "all" ); const [selectedDateKey, setSelectedDateKey] = useState( WEEK_DAYS.find((d) => d.isToday)?.key || WEEK_DAYS[0].key ); const [loading, setLoading] = useState(true); const [toastVisible, setToastVisible] = useState(false); const [toastTitle, setToastTitle] = useState(""); const [toastDescription, setToastDescription] = useState( undefined ); const [toastVariant, setToastVariant] = useState< "success" | "error" | "warning" | "info" >("info"); const toastTimeoutRef = useRef | null>(null); const { user } = useAuthWithProfile(); useEffect(() => { const loadSchedules = async () => { if (!user?.uid) { setSchedules([]); setLoading(false); return; } try { setLoading(true); const appointmentsCol: any = collection("appointments"); const snap = await appointmentsCol .where("agentId", "==", user.uid) .where("acceptance", "==", "accepted") .get(); const loaded: Schedule[] = snap.docs.map((docSnap: any) => { const data = docSnap.data() || {}; const nextRunSource = data.nextRunDate || (data.date && data.time ? `${data.date}T${data.time}:00` : undefined); const nextRun = nextRunSource ? new Date(nextRunSource) : TODAY; const nextRunDateKey = toISODate(nextRun); const nextRunLabel = buildNextRunLabel(nextRun, data.time); const frequency = buildFrequencyLabel(data.repeatType, data.interval); const status: ScheduleStatus = data.scheduleStatus === "paused" || data.scheduleStatus === "stopped" ? data.scheduleStatus : "active"; return { id: String(docSnap.id), recipientName: data.fullName || data.email || data.phoneNumber || "Unknown", label: data.notes || "", amount: typeof data.amount === "number" ? data.amount.toString() : String(data.amount ?? ""), currency: "ETB", nextRunDate: nextRunDateKey, nextRunLabel, frequency, status, }; }); setSchedules(loaded); } catch (error) { console.error( "[Schedules] Failed to load appointments as schedules", error ); } finally { setLoading(false); } }; loadSchedules(); }, [user]); const showToast = ( title: string, description?: string, variant: "success" | "error" | "warning" | "info" = "info" ) => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } setToastTitle(title); setToastDescription(description); setToastVariant(variant); setToastVisible(true); toastTimeoutRef.current = setTimeout(() => { setToastVisible(false); toastTimeoutRef.current = null; }, 2500); }; const handleUpdateStatus = async (id: string, next: ScheduleStatus) => { try { const ref = doc("appointments", id); await ref.update({ scheduleStatus: next, updatedAt: FieldValue.serverTimestamp(), }); setSchedules((prev) => prev.map((sch) => (sch.id === id ? { ...sch, status: next } : sch)) ); setSelected((prev) => prev && prev.id === id ? { ...prev, status: next } : prev ); } catch (error) { console.error("[Schedules] Failed to update scheduleStatus", id, error); } }; const activeCount = schedules.filter((s) => s.status === "active").length; const pausedCount = schedules.filter((s) => s.status === "paused").length; const stoppedCount = schedules.filter((s) => s.status === "stopped").length; const dateFiltered = schedules.filter( (s) => s.nextRunDate === selectedDateKey ); const filteredSchedules = statusFilter === "all" ? dateFiltered : dateFiltered.filter((s) => s.status === statusFilter); return ( Schedules View and manage your recurring payment schedules. {/* Week day selector */} {WEEK_DAYS.map((day) => { const isSelected = day.key === selectedDateKey; const hasSchedulesForDay = schedules.some( (s) => s.nextRunDate === day.key ); return ( setSelectedDateKey(day.key)} > {day.label} {day.dateNumber} {hasSchedulesForDay && ( )} ); })} {loading && ( {[1, 2, 3].map((i) => ( ))} )} {!loading && filteredSchedules.length === 0 && ( No schedules match your filters. )} {!loading && filteredSchedules.length > 0 && ( {filteredSchedules.map((sch) => { return ( {sch.recipientName} {sch.label} Next run {sch.nextRunLabel} Amount {sch.currency} {sch.amount} Frequency {sch.frequency} setSelected(sch)} > View details ); })} )} setSelected(null)} maxHeightRatio={0.45} > {selected && ( Schedule Details {selected.recipientName} {selected.label} Next run {selected.nextRunLabel} Amount {selected.currency} {selected.amount} Frequency {selected.frequency} router.push(ROUTES.SEND_OR_REQUEST_MONEY)} > Pay now )} ); }