Amba-Agent-App/app/(root)/(tabs)/requests.tsx
2026-01-16 00:22:35 +03:00

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>
);
}