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

729 lines
26 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
View,
ScrollView,
Keyboard,
TouchableOpacity,
Platform,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { ArrowLeftIcon, ChevronLeft, ChevronRight } from "lucide-react-native";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Text } from "~/components/ui/text";
import Dropdown from "~/components/ui/dropdown";
import { router } from "expo-router";
import BackButton from "~/components/ui/backButton";
import { useRecipientsStore } from "~/lib/stores";
import { useTabStore } from "~/lib/stores";
import { ROUTES } from "~/lib/routes";
import { formatPhoneNumber } from "~/lib/utils/phoneUtils";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
import { awardPoints } from "~/lib/services/pointsService";
import BottomSheet from "~/components/ui/bottomSheet";
import { Picker } from "react-native-wheel-pick";
const PROFILE_BANK_OPTIONS: { id: string; name: string }[] = [
{ id: "cbe", name: "Commercial Bank of Ethiopia" },
{ id: "dashen", name: "Dashen Bank" },
{ id: "abay", name: "Abay Bank" },
{ id: "awash", name: "Awash Bank" },
{ id: "hibret", name: "Hibret Bank" },
{ id: "telebirr", name: "Ethio Telecom (Telebirr)" },
{ id: "safaricom", name: "Safaricom M-PESA" },
];
export default function AddRecpi() {
const { addRecipient, loading, addError, clearAddError } =
useRecipientsStore();
const { setLastVisitedTab } = useTabStore();
const [fullName, setFullName] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
const [clientType, setClientType] = useState<"Individual" | "Business">(
"Individual"
);
const [selectedBank, setSelectedBank] = useState<string | null>(null);
const [accountNumberInput, setAccountNumberInput] = useState("");
const [accountSheetVisible, setAccountSheetVisible] = useState(false);
const [scheduleSheetVisible, setScheduleSheetVisible] = useState(false);
const [scheduleFrequency, setScheduleFrequency] = useState("");
const [scheduleHour, setScheduleHour] = useState("00");
const [scheduleMinute, setScheduleMinute] = useState("00");
const [schedulePeriod, setSchedulePeriod] = useState<"AM" | "PM">("AM");
const [scheduleTime, setScheduleTime] = useState("");
const [scheduleDate, setScheduleDate] = useState("");
const { t } = useTranslation();
const isTelecomWallet =
selectedBank === "telebirr" || selectedBank === "safaricom";
const accountLabel = isTelecomWallet ? "Phone Number" : "Account Number";
const accountPlaceholder = isTelecomWallet
? "Enter phone number"
: "Enter account number";
// Time wheel options
const HOURS = Array.from({ length: 24 }, (_, i) =>
String(i).padStart(2, "0")
);
const MINUTES = Array.from({ length: 60 }, (_, i) =>
String(i).padStart(2, "0")
);
const PERIODS: ("AM" | "PM")[] = ["AM", "PM"];
// Calendar state for month navigation (UI-only)
const today = new Date();
const [calendarCursor, setCalendarCursor] = useState(() => {
const start = new Date();
start.setDate(1);
return start;
});
const cursorYear = calendarCursor.getFullYear();
const cursorMonth = calendarCursor.getMonth();
const firstOfMonth = new Date(cursorYear, cursorMonth, 1);
const lastOfMonth = new Date(cursorYear, cursorMonth + 1, 0);
const firstWeekday = firstOfMonth.getDay(); // 0 = Sun
const calendarDays = [
// Leading empty slots before the 1st
...Array.from({ length: firstWeekday }, () => null),
// Actual days
...Array.from({ length: lastOfMonth.getDate() }, (_, idx) => {
const day = idx + 1;
const key = `${cursorYear}-${cursorMonth + 1}-${day}`;
const isToday =
today.getFullYear() === cursorYear &&
today.getMonth() === cursorMonth &&
today.getDate() === day;
return { key, label: String(day), isToday };
}),
];
const MONTH_NAMES = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const [toastVisible, setToastVisible] = useState(false);
const [toastTitle, setToastTitle] = useState("");
const [toastDescription, setToastDescription] = useState<string | undefined>(
undefined
);
const [toastVariant, setToastVariant] = useState<
"success" | "error" | "warning" | "info"
>("info");
const toastTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(
null
);
// Keep a combined time string for potential future summary usage
useEffect(() => {
setScheduleTime(`${scheduleHour}:${scheduleMinute} ${schedulePeriod}`);
}, [scheduleHour, scheduleMinute, schedulePeriod]);
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);
};
// Set the tab state when component mounts
useEffect(() => {
setLastVisitedTab("/(tabs)/listrecipient");
}, [setLastVisitedTab]);
useEffect(() => {
return () => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
const handlePhoneChange = (value: string) => {
const formatted = formatPhoneNumber(value);
setPhoneNumber(formatted);
};
const validateForm = () => {
if (!fullName.trim()) {
showToast(
t("addrecipient.validationErrorTitle"),
t("addrecipient.validationFullNameRequired"),
"error"
);
return false;
}
if (!phoneNumber.trim()) {
showToast(
t("addrecipient.validationErrorTitle"),
t("addrecipient.validationPhoneRequired"),
"error"
);
return false;
}
const cleanPhone = phoneNumber.replace(/[^+\d]/g, "");
if (cleanPhone.length < 7) {
showToast(
t("addrecipient.validationErrorTitle"),
t("addrecipient.validationPhoneInvalid"),
"error"
);
return false;
}
return true;
};
const handleAddRecipient = async () => {
Keyboard.dismiss();
if (!validateForm()) return;
setIsSubmitting(true);
setHasAttemptedSubmit(true);
await addRecipient({
fullName: fullName.trim(),
phoneNumber: phoneNumber.trim(),
});
setIsSubmitting(false);
};
// Show error if any
useEffect(() => {
if (addError) {
showToast(
t("addrecipient.toastErrorTitle"),
addError || t("addrecipient.toastAddError"),
"error"
);
// Clear the error after showing it
clearAddError();
// Reset submission state
setHasAttemptedSubmit(false);
}
}, [addError, clearAddError]);
// Show success when operation completes without error
useEffect(() => {
if (hasAttemptedSubmit && !loading && !addError && !isSubmitting) {
// Operation completed successfully
router.replace(ROUTES.RECIPIENT_ADDED);
awardPoints("add_recipient").catch((error) => {
console.warn(
"[AddRecipient] Failed to award add recipient points",
error
);
});
}
}, [loading, addError, isSubmitting, hasAttemptedSubmit]);
const isFormValid =
fullName.trim() &&
phoneNumber.trim() &&
phoneNumber.replace(/[^+\d]/g, "").length >= 7;
return (
<ScreenWrapper edges={[]}>
<View className="flex-1 w-full justify-between">
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingBottom: 32 }}
>
{/* Header */}
<View className="">
<BackButton />
</View>
<View className="px-5 pt-4">
<Text
className="text-xl font-dmsans-bold text-primary"
numberOfLines={1}
>
{t("addrecipient.title")}
</Text>
<Text className="text-sm font-dmsans text-gray-500 mt-1">
{t("addrecipient.sectionSubtitle")}
</Text>
</View>
{/* Client details card */}
<View className="px-5 pt-6">
<View className="bg-white rounded-md py-5 space-y-5">
{/* Client type toggle */}
<View className="flex-row items-center justify-between mb-1">
<Text className="text-xs font-dmsans text-gray-500">
{t("addrecipient.clientTypeLabel", "Client type")}
</Text>
<View className="flex-row bg-[#F3F4F6] rounded-full p-[2px]">
{(["Individual", "Business"] as const).map((type) => {
const isActive = clientType === type;
return (
<TouchableOpacity
key={type}
className={`px-3 py-1 rounded-full ${
isActive ? "bg-primary" : "bg-transparent"
}`}
onPress={() => setClientType(type)}
>
<Text
className={`text-[11px] font-dmsans-medium ${
isActive ? "text-white" : "text-gray-500"
}`}
>
{type}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
<View>
<Label className="text-xs font-dmsans text-gray-500 mb-2">
{t("addrecipient.fullNameLabel")}
</Label>
<Input
placeholder={t("addrecipient.fullNamePlaceholder")}
value={fullName}
onChangeText={setFullName}
editable={!isSubmitting}
containerClassName="w-full"
borderClassName="border-[#D9DBE9] bg-white"
placeholderColor="#7E7E7E"
textClassName="text-[#000] text-sm"
/>
</View>
<View>
<Label className="text-xs font-dmsans text-gray-500 mb-2">
{t("addrecipient.phoneLabel")}
</Label>
<Input
placeholder={t("addrecipient.phonePlaceholder")}
value={phoneNumber}
onChangeText={handlePhoneChange}
keyboardType="phone-pad"
editable={!isSubmitting}
containerClassName="w-full"
borderClassName="border-[#D9DBE9] bg-white"
placeholderColor="#7E7E7E"
textClassName="text-[#000] text-sm"
/>
</View>
{/* Actions for account & schedule (UI-only) */}
<View className="pt-2 flex-row gap-3">
<Button
className="flex-1 bg-white border border-primary rounded-2xl"
onPress={() => setAccountSheetVisible(true)}
>
<Text className="text-primary font-dmsans-medium text-sm">
{t("addrecipient.addAccountButton", "Add Account")}
</Text>
</Button>
<Button
className="flex-1 bg-primary/5 border border-primary rounded-2xl"
onPress={() => setScheduleSheetVisible(true)}
>
<Text className="text-primary font-dmsans-medium text-sm">
{t("addrecipient.setScheduleButton", "Set Schedule")}
</Text>
</Button>
</View>
</View>
</View>
</ScrollView>
<View className="w-full px-5 pb-8">
<Button
className="bg-primary rounded-3xl"
onPress={handleAddRecipient}
disabled={!isFormValid || isSubmitting || loading}
>
<Text className="font-dmsans text-white">
{isSubmitting || loading
? t("addrecipient.addButtonLoading")
: t("addrecipient.addButton")}
</Text>
</Button>
</View>
</View>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
{/* Bottom sheet: Add Account (UI-only, matches Edit Profile add account) */}
<BottomSheet
visible={accountSheetVisible}
onClose={() => setAccountSheetVisible(false)}
maxHeightRatio={0.9}
>
<View className="mb-4">
<Text className="text-xl font-dmsans-bold text-primary text-center">
{t("addrecipient.accountSheetTitle", "Add Account")}
</Text>
</View>
<View className="mb-4 ">
<Text className="text-base font-dmsans text-black mb-2">
{t("addrecipient.accountBankLabel", "Bank")}
</Text>
<View className="flex-row flex-wrap justify-between">
{PROFILE_BANK_OPTIONS.map((bank) => {
const isSelected = selectedBank === bank.id;
const initials = bank.name
.split(" ")
.map((part) => part[0])
.join("")
.toUpperCase()
.slice(0, 2);
return (
<TouchableOpacity
key={bank.id}
activeOpacity={0.8}
onPress={() => setSelectedBank(bank.id)}
className={`items-center justify-between px-3 py-4 mb-3 rounded-2xl border ${
isSelected
? "border-primary bg-primary/5"
: "border-gray-200 bg-white"
}`}
style={{ width: "30%" }}
>
<View className="w-10 h-10 mb-2 rounded-full bg-primary/10 items-center justify-center">
<Text className="text-primary font-dmsans-bold text-sm">
{initials}
</Text>
</View>
<Text
className="text-center text-xs font-dmsans text-gray-800"
numberOfLines={2}
>
{bank.name}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
<View className="mb-4">
<Text className="text-base font-dmsans text-black mb-2">
{accountLabel}
</Text>
<Input
placeholder={accountPlaceholder}
value={accountNumberInput}
onChangeText={(text) =>
setAccountNumberInput(text.replace(/[^0-9]/g, ""))
}
containerClassName="w-full mb-4"
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
placeholderColor="#9CA3AF"
textClassName="text-[#111827] text-sm"
keyboardType="number-pad"
/>
</View>
<View className="mb-4">
<Button
className="bg-primary rounded-3xl w-full"
onPress={() => setAccountSheetVisible(false)}
disabled={!selectedBank || !accountNumberInput.trim()}
>
<Text className="font-dmsans text-white">
{t("common.save", "Save")}
</Text>
</Button>
</View>
</BottomSheet>
{/* Bottom sheet: Set Schedule (UI-only with dropdowns & calendar) */}
<BottomSheet
visible={scheduleSheetVisible}
onClose={() => setScheduleSheetVisible(false)}
maxHeightRatio={0.95}
>
<View className="w-full px-5 pt-4 pb-6">
<Text className="text-base font-dmsans-bold text-primary mb-1">
{t("addrecipient.scheduleSheetTitle", "Set Schedule")}
</Text>
<Text className="text-xs font-dmsans text-gray-500 mb-4">
{t(
"addrecipient.scheduleSheetSubtitle",
"Choose a simple reminder schedule for this client. This is UI-only for now."
)}
</Text>
<View className="space-y-4">
{/* Frequency dropdown */}
<View>
<Label className="text-[11px] font-dmsans text-gray-500 mb-1">
{t("addrecipient.scheduleFrequencyLabel", "Frequency")}
</Label>
<Dropdown
value={scheduleFrequency || null}
options={[
{ label: "Daily", value: "daily" },
{ label: "Weekly", value: "weekly" },
{ label: "Monthly", value: "monthly" },
{ label: "Custom", value: "custom" },
]}
onSelect={(val) => setScheduleFrequency(val)}
placeholder={t(
"addrecipient.scheduleFrequencyPlaceholder",
"Daily / Weekly / Monthly / Custom"
)}
/>
</View>
{/* Time-of-day selector: 3-column wheel (hour, minute, AM/PM) */}
<View>
<Label className="text-[11px] font-dmsans text-gray-500 mt-3 mb-1">
{t("addrecipient.scheduleTimeLabel", "Time of day")}
</Label>
<View className="flex-row bg-white rounded-2xl border border-[#E5E7EB] px-3 py-3">
{/* Hour wheel */}
<View className="flex-1 items-center">
<Text className="text-[10px] font-dmsans text-gray-400 mb-1">
{t("addrecipient.scheduleHourLabel", "Hour")}
</Text>
<Picker
style={{
width: "100%",
height: 150,
backgroundColor: "transparent",
}}
pickerData={HOURS}
selectedValue={scheduleHour}
onValueChange={(val: string) => setScheduleHour(val)}
textColor="#9CA3AF"
selectTextColor="#16A34A"
isCyclic
/>
</View>
{/* Minute wheel */}
<View className="flex-1 items-center">
<Text className="text-[10px] font-dmsans text-gray-400 mb-1">
{t("addrecipient.scheduleMinuteLabel", "Min")}
</Text>
<Picker
style={{
width: "100%",
height: 150,
backgroundColor: "transparent",
}}
pickerData={MINUTES}
selectedValue={scheduleMinute}
onValueChange={(val: string) => setScheduleMinute(val)}
textColor="#9CA3AF"
selectTextColor="#16A34A"
isCyclic
/>
</View>
{/* AM/PM wheel */}
<View className="w-16 items-center">
<Text className="text-[10px] font-dmsans text-gray-400 mb-1">
{t("addrecipient.schedulePeriodLabel", "AM/PM")}
</Text>
<Picker
style={{
width: "100%",
height: 150,
backgroundColor: "transparent",
}}
pickerData={PERIODS}
selectedValue={schedulePeriod}
onValueChange={(val: string) =>
setSchedulePeriod(val as "AM" | "PM")
}
textColor="#9CA3AF"
selectTextColor="#16A34A"
/>
</View>
</View>
</View>
{/* Date selector - calendar with header & weekdays */}
<View>
<Label className="text-[11px] font-dmsans mt-3 text-gray-500 mb-2">
{t("addrecipient.scheduleDateLabel", "Date")}
</Label>
{/* Month header */}
<View className="flex-row items-center justify-between mb-2">
<TouchableOpacity
onPress={() => {
setCalendarCursor(
(prev) =>
new Date(prev.getFullYear(), prev.getMonth() - 1, 1)
);
}}
className="p-1"
>
<ChevronLeft size={18} color="#6B7280" />
</TouchableOpacity>
<Text className="text-sm font-dmsans-medium text-gray-800">
{MONTH_NAMES[cursorMonth]} {cursorYear}
</Text>
<TouchableOpacity
onPress={() => {
setCalendarCursor(
(prev) =>
new Date(prev.getFullYear(), prev.getMonth() + 1, 1)
);
}}
className="p-1"
>
<ChevronRight size={18} color="#6B7280" />
</TouchableOpacity>
</View>
{/* Weekday labels */}
<View className="flex-row mb-1">
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((d) => (
<View key={d} className="flex-1 items-center">
<Text className="text-[10px] font-dmsans text-gray-400">
{d}
</Text>
</View>
))}
</View>
{/* Day grid: 7 columns (Sun-Sat) */}
<View className="space-y-[4px]">
{Array.from(
{ length: Math.ceil(calendarDays.length / 7) },
(_, rowIndex) => {
const row = calendarDays.slice(
rowIndex * 7,
rowIndex * 7 + 7
);
return (
<View key={rowIndex} className="flex-row">
{row.map((day, idx) => {
if (!day) {
return (
<View
key={`empty-${rowIndex}-${idx}`}
className="flex-1 items-center justify-center"
/>
);
}
const safeDay = day as {
key: string;
label: string;
isToday: boolean;
};
const isSelected = scheduleDate === safeDay.key;
const isToday = safeDay.isToday;
return (
<View
key={safeDay.key}
className="flex-1 items-center justify-center"
>
<TouchableOpacity
activeOpacity={0.8}
onPress={() => setScheduleDate(safeDay.key)}
className={`items-center justify-center rounded-full border ${
isSelected
? "bg-primary border-primary"
: "bg-white border-transparent"
}`}
style={{ width: 32, height: 32 }}
>
<Text
className={`text-xs font-dmsans-medium ${
isSelected
? "text-white"
: isToday
? "text-primary"
: "text-gray-800"
}`}
>
{safeDay.label}
</Text>
</TouchableOpacity>
{isToday && !isSelected && (
<Text className="text-[8px] font-dmsans text-primary mt-0.5">
{t(
"addrecipient.scheduleTodayLabel",
"Today"
)}
</Text>
)}
</View>
);
})}
</View>
);
}
)}
</View>
</View>
</View>
<View className="flex-row gap-3 mt-5">
<Button
className="flex-1 bg-white border border-[#E5E7EB] rounded-2xl"
onPress={() => setScheduleSheetVisible(false)}
>
<Text className="text-gray-700 font-dmsans-medium text-sm">
{t("common.cancel", "Cancel")}
</Text>
</Button>
<Button
className="flex-1 bg-primary rounded-2xl"
onPress={() => setScheduleSheetVisible(false)}
>
<Text className="text-white font-dmsans-medium text-sm">
{t("common.save", "Save")}
</Text>
</Button>
</View>
</View>
</BottomSheet>
</ScreenWrapper>
);
}