492 lines
16 KiB
TypeScript
492 lines
16 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { View, Text, Image, ScrollView, TouchableOpacity } from "react-native";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import { Icons } from "~/assets/icons";
|
|
import TopBar from "~/components/ui/topBar";
|
|
import BottomSheet from "~/components/ui/bottomSheet";
|
|
import { Button } from "~/components/ui/button";
|
|
import Skeleton from "~/components/ui/skeleton";
|
|
import { router } from "expo-router";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
|
|
import { doc, FieldValue } from "~/lib/firebase";
|
|
|
|
type RequestStatus = "pending" | "accepted" | "completed" | "rejected";
|
|
|
|
type AgentRequest = {
|
|
id: string;
|
|
clientName: string;
|
|
datetimeLabel: string;
|
|
amount?: string;
|
|
currency?: string;
|
|
status: RequestStatus;
|
|
acceptance: RequestStatus;
|
|
rawDate: string;
|
|
time: string;
|
|
notes?: string;
|
|
repeatLabel?: string;
|
|
destinationAccount?: string;
|
|
};
|
|
|
|
const MOCK_REQUESTS: AgentRequest[] = [];
|
|
|
|
const getStatusPillClasses = (status: RequestStatus) => {
|
|
switch (status) {
|
|
case "pending":
|
|
return "bg-yellow-100 text-yellow-700";
|
|
case "accepted":
|
|
return "bg-blue-100 text-blue-700";
|
|
case "rejected":
|
|
return "bg-red-100 text-red-700";
|
|
case "completed":
|
|
default:
|
|
return "bg-green-100 text-green-700";
|
|
}
|
|
};
|
|
|
|
const isSameDay = (a: Date, b: Date) => {
|
|
return (
|
|
a.getFullYear() === b.getFullYear() &&
|
|
a.getMonth() === b.getMonth() &&
|
|
a.getDate() === b.getDate()
|
|
);
|
|
};
|
|
|
|
const formatAppointmentDatetime = (item: any): string => {
|
|
const today = new Date();
|
|
const tomorrow = new Date();
|
|
tomorrow.setDate(today.getDate() + 1);
|
|
|
|
let dateObj: Date | null = null;
|
|
|
|
if (item.nextRunDate) {
|
|
const d = new Date(item.nextRunDate);
|
|
if (!isNaN(d.getTime())) {
|
|
dateObj = d;
|
|
}
|
|
}
|
|
|
|
if (!dateObj && item.date) {
|
|
const d = new Date(item.date);
|
|
if (!isNaN(d.getTime())) {
|
|
dateObj = d;
|
|
}
|
|
}
|
|
|
|
const timeLabel = item.time || "";
|
|
|
|
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",
|
|
});
|
|
}
|
|
} else if (item.date) {
|
|
dayLabel = item.date;
|
|
}
|
|
|
|
if (dayLabel && timeLabel) {
|
|
return `${dayLabel} · ${timeLabel}`;
|
|
}
|
|
|
|
return dayLabel || timeLabel || "";
|
|
};
|
|
|
|
const buildRepeatLabel = (
|
|
repeatType?: string,
|
|
interval?: number
|
|
): string | undefined => {
|
|
if (!repeatType || repeatType === "none") {
|
|
return "One-time";
|
|
}
|
|
|
|
if (repeatType === "every_x_days" && interval) {
|
|
return `Every ${interval} days`;
|
|
}
|
|
|
|
return repeatType;
|
|
};
|
|
|
|
export default function EventsScreen() {
|
|
const [requests, setRequests] = useState<AgentRequest[]>(MOCK_REQUESTS);
|
|
const [selectedRequest, setSelectedRequest] = useState<AgentRequest | null>(
|
|
null
|
|
);
|
|
const [loading, setLoading] = useState(true);
|
|
const { user } = useAuthWithProfile();
|
|
|
|
useEffect(() => {
|
|
const fetchAgentAppointments = async () => {
|
|
try {
|
|
if (!user || typeof (user as any).getIdToken !== "function") {
|
|
setLoading(false);
|
|
console.log(
|
|
"[Requests] /api/agents/my-appointments skipping fetch - no auth user or token method"
|
|
);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
const token = await (user as any).getIdToken();
|
|
|
|
const response = await fetch(
|
|
"https://referralapi-fclnigvupq-uc.a.run.app/api/agents/my-appointments",
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
console.log("[Requests] raw fetch Response", response);
|
|
|
|
console.log(
|
|
"[Requests] /api/agents/my-appointments status",
|
|
response.status
|
|
);
|
|
|
|
const text = await response.text();
|
|
try {
|
|
const json = JSON.parse(text);
|
|
console.log("[Requests] /api/agents/my-appointments json", json);
|
|
console.log(
|
|
"[Requests] /api/agents/my-appointments json data ",
|
|
json?.data
|
|
);
|
|
|
|
const payload = json?.data;
|
|
const items = Array.isArray(payload?.data)
|
|
? payload.data
|
|
: Array.isArray(json?.data)
|
|
? json.data
|
|
: [];
|
|
|
|
if (items.length > 0) {
|
|
const mapped: AgentRequest[] = items.map((item: any) => {
|
|
const acceptance: RequestStatus =
|
|
item.acceptance === "accepted" || item.acceptance === "rejected"
|
|
? item.acceptance
|
|
: "pending";
|
|
|
|
const status: RequestStatus = acceptance;
|
|
const datetimeLabel = formatAppointmentDatetime(item);
|
|
|
|
const repeatLabel = buildRepeatLabel(
|
|
item.repeatType,
|
|
item.interval
|
|
);
|
|
|
|
return {
|
|
id: String(item.id),
|
|
clientName:
|
|
item.fullName ||
|
|
item.email ||
|
|
item.phoneNumber ||
|
|
"Unknown client",
|
|
datetimeLabel,
|
|
rawDate: item.date || "",
|
|
time: item.time || "",
|
|
amount:
|
|
typeof item.amount === "number"
|
|
? item.amount.toString()
|
|
: undefined,
|
|
currency: "ETB",
|
|
status,
|
|
acceptance,
|
|
notes: item.notes,
|
|
repeatLabel,
|
|
};
|
|
});
|
|
|
|
setRequests(mapped);
|
|
}
|
|
} catch {
|
|
console.log("[Requests] /api/agents/my-appointments raw", text);
|
|
}
|
|
} catch (error) {
|
|
console.error("[Requests] /api/agents/my-appointments error", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAgentAppointments();
|
|
}, [user]);
|
|
|
|
const handleUpdateStatus = async (id: string, next: RequestStatus) => {
|
|
try {
|
|
const ref = doc("appointments", id);
|
|
await ref.update({
|
|
acceptance: next,
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
});
|
|
console.log(
|
|
"[Requests] updated appointments acceptance in Firestore",
|
|
id,
|
|
next
|
|
);
|
|
|
|
// Only update local UI once backend write succeeds
|
|
setRequests((prev) =>
|
|
prev.map((req) =>
|
|
req.id === id ? { ...req, status: next, acceptance: next } : req
|
|
)
|
|
);
|
|
setSelectedRequest((prev) =>
|
|
prev && prev.id === id
|
|
? { ...prev, status: next, acceptance: next }
|
|
: prev
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
"[Requests] failed to update appointments acceptance",
|
|
id,
|
|
error
|
|
);
|
|
}
|
|
};
|
|
|
|
const handlePayNow = (req: AgentRequest) => {
|
|
// UI-only: navigate to the main pay/send screen with no real prefill logic
|
|
router.push(ROUTES.SEND_OR_REQUEST_MONEY);
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<View className="flex-1 bg-white">
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ paddingBottom: 32 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<TopBar />
|
|
|
|
<View className="px-5 pt-6">
|
|
<Text className="text-lg font-dmsans-bold text-primary mb-1">
|
|
Requests
|
|
</Text>
|
|
<Text className="text-sm font-dmsans text-gray-500 mb-4">
|
|
Every "Book Now" request for you appears here.
|
|
</Text>
|
|
|
|
{loading && (
|
|
<View className="mb-6">
|
|
{[1, 2, 3].map((i) => (
|
|
<View className=" py-2">
|
|
<Skeleton width="100%" height={120} radius={24} />
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{!loading && requests.length === 0 && (
|
|
<View className="items-center justify-center py-10">
|
|
<Text className="text-sm font-dmsans text-gray-400">
|
|
No requests right now.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!loading && requests.length > 0 && (
|
|
<View className="space-y-4 mb-6">
|
|
{[...requests]
|
|
// Hide accepted requests completely
|
|
.filter((req) => req.status !== "accepted")
|
|
// Keep rejected requests at the bottom
|
|
.sort((a, b) => {
|
|
if (a.status === "rejected" && b.status !== "rejected")
|
|
return 1;
|
|
if (a.status !== "rejected" && b.status === "rejected")
|
|
return -1;
|
|
return 0;
|
|
})
|
|
.map((req) => {
|
|
const pillClasses = getStatusPillClasses(req.status);
|
|
|
|
return (
|
|
<View
|
|
key={req.id}
|
|
className="bg-white rounded-3xl mb-2 border border-gray-100"
|
|
style={{
|
|
shadowColor: "#000",
|
|
shadowOpacity: 0.02,
|
|
shadowRadius: 40,
|
|
shadowOffset: { width: 0, height: 8 },
|
|
elevation: 2,
|
|
}}
|
|
>
|
|
<View className="px-4 py-4">
|
|
<View className="flex-row items-center justify-between mb-1.5">
|
|
<Text className="text-base font-dmsans-bold text-gray-900">
|
|
{req.clientName}
|
|
</Text>
|
|
<View
|
|
className={`px-2 py-0.5 rounded-full ${pillClasses}`}
|
|
>
|
|
<Text className="text-[10px] font-dmsans-medium">
|
|
{req.status === "pending"
|
|
? "Pending"
|
|
: req.status === "accepted"
|
|
? "Accepted"
|
|
: req.status === "rejected"
|
|
? "Rejected"
|
|
: "Completed"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Text className="text-[12px] font-dmsans text-gray-500 mb-1">
|
|
{req.datetimeLabel}
|
|
</Text>
|
|
|
|
{req.amount && req.currency && (
|
|
<Text className="text-sm font-dmsans-medium text-gray-900 mb-2">
|
|
{req.currency} {req.amount}
|
|
</Text>
|
|
)}
|
|
|
|
<View className="flex-row gap-3 mt-2">
|
|
<TouchableOpacity
|
|
activeOpacity={0.9}
|
|
className="flex-1 border border-gray-200 rounded-2xl py-2.5 items-center justify-center bg-white"
|
|
onPress={() => setSelectedRequest(req)}
|
|
>
|
|
<Text className="text-sm font-dmsans-medium text-gray-800">
|
|
View Details
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
<BottomSheet
|
|
visible={!!selectedRequest}
|
|
onClose={() => setSelectedRequest(null)}
|
|
maxHeightRatio={0.5}
|
|
>
|
|
{selectedRequest && (
|
|
<View className="w-full pt-4 ">
|
|
<Text className="text-base font-dmsans-bold text-primary mb-2 text-center">
|
|
Request Details
|
|
</Text>
|
|
<Text className="text-lg font-dmsans-bold text-gray-900 text-center mb-1">
|
|
{selectedRequest.clientName}
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans text-gray-500 text-center mb-3">
|
|
{selectedRequest.datetimeLabel}
|
|
</Text>
|
|
|
|
<View className="space-y-2 mb-5">
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[12px] font-dmsans text-gray-500">
|
|
Date
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans-medium text-gray-900">
|
|
{selectedRequest.datetimeLabel}
|
|
</Text>
|
|
</View>
|
|
{selectedRequest.amount && selectedRequest.currency && (
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[12px] font-dmsans text-gray-500">
|
|
Amount
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans-medium text-gray-900">
|
|
{selectedRequest.currency} {selectedRequest.amount}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{selectedRequest.destinationAccount && (
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[12px] font-dmsans text-gray-500">
|
|
Destination account
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans-medium text-gray-900">
|
|
{selectedRequest.destinationAccount}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[12px] font-dmsans text-gray-500">
|
|
Status
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans-medium text-gray-900">
|
|
{selectedRequest.acceptance === "pending"
|
|
? "Pending"
|
|
: selectedRequest.acceptance === "accepted"
|
|
? "Accepted"
|
|
: selectedRequest.acceptance === "rejected"
|
|
? "Rejected"
|
|
: "Completed"}
|
|
</Text>
|
|
</View>
|
|
{selectedRequest.notes && (
|
|
<View className="mt-2">
|
|
<Text className="text-[12px] font-dmsans text-gray-500 mb-1">
|
|
Notes
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans text-gray-800">
|
|
{selectedRequest.notes}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{selectedRequest.repeatLabel && (
|
|
<View className="mt-2">
|
|
<Text className="text-[12px] font-dmsans text-gray-500 mb-1">
|
|
Repeat
|
|
</Text>
|
|
<Text className="text-[12px] font-dmsans text-gray-800">
|
|
{selectedRequest.repeatLabel}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View className="flex-row gap-3 mt-2">
|
|
{selectedRequest.status === "pending" && (
|
|
<>
|
|
<TouchableOpacity
|
|
activeOpacity={0.8}
|
|
className="flex-1 bg-white border border-[#E5E7EB] rounded-2xl py-3 items-center justify-center"
|
|
onPress={() =>
|
|
handleUpdateStatus(selectedRequest.id, "accepted")
|
|
}
|
|
>
|
|
<Text className="text-sm font-dmsans-medium text-gray-800">
|
|
Mark as Accepted
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
activeOpacity={0.8}
|
|
className="flex-1 bg-red-50 border border-red-200 rounded-2xl py-3 items-center justify-center"
|
|
onPress={() =>
|
|
handleUpdateStatus(selectedRequest.id, "rejected")
|
|
}
|
|
>
|
|
<Text className="text-sm font-dmsans-medium text-red-600">
|
|
Reject Request
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</BottomSheet>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|