988 lines
34 KiB
TypeScript
988 lines
34 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
|
import {
|
|
View,
|
|
ScrollView,
|
|
TextInput,
|
|
ActivityIndicator,
|
|
Pressable,
|
|
Linking,
|
|
Modal,
|
|
StyleSheet,
|
|
Alert,
|
|
Platform,
|
|
PermissionsAndroid,
|
|
} from "react-native";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { AppRoutes } from "@/lib/routes";
|
|
import { Stack, useLocalSearchParams, useFocusEffect } from "expo-router";
|
|
import { Text } from "@/components/ui/text";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { EmptyState } from "@/components/EmptyState";
|
|
import { FormFlow } from "@/components/FormFlow";
|
|
import {
|
|
Wallet,
|
|
Link2,
|
|
Clock,
|
|
AlertTriangle,
|
|
User,
|
|
Building2,
|
|
Hash,
|
|
Eye,
|
|
Trash2,
|
|
Info,
|
|
Search,
|
|
ChevronDown,
|
|
X,
|
|
Scan,
|
|
} from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { StandardHeader } from "@/components/StandardHeader";
|
|
import { api, BASE_URL } from "@/lib/api";
|
|
import { useColorScheme } from "nativewind";
|
|
import { toast } from "@/lib/toast-store";
|
|
import { ActionModal } from "@/components/ActionModal";
|
|
import { PickerModal, SelectOption } from "@/components/PickerModal";
|
|
import { CalendarGrid } from "@/components/CalendarGrid";
|
|
import { getPlaceholderColor } from "@/lib/colors";
|
|
|
|
let SmsAndroid: any = null;
|
|
if (Platform.OS === "android") {
|
|
try {
|
|
const smsModule = require("react-native-get-sms-android");
|
|
SmsAndroid = smsModule.default || smsModule;
|
|
} catch (e) {
|
|
console.log("[PaymentDetail] SMS module unavailable");
|
|
}
|
|
}
|
|
|
|
const S = StyleSheet.create({
|
|
input: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
fontSize: 12,
|
|
fontWeight: "500",
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
textAlignVertical: "center",
|
|
},
|
|
});
|
|
|
|
const CURRENCIES = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"];
|
|
const PAYMENT_METHODS = [
|
|
"Telebirr",
|
|
"CBE",
|
|
"Dashen",
|
|
"DECSI",
|
|
"Bank Transfer",
|
|
"Cash",
|
|
"Other",
|
|
];
|
|
|
|
export default function PaymentDetailScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const { id } = useLocalSearchParams();
|
|
const { colorScheme } = useColorScheme();
|
|
const isDark = colorScheme === "dark";
|
|
|
|
const [payment, setPayment] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [matching, setMatching] = useState(false);
|
|
const [scanningSms, setScanningSms] = useState(false);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [showInvoicePicker, setShowInvoicePicker] = useState(false);
|
|
const [editSaving, setEditSaving] = useState(false);
|
|
|
|
// Edit form state
|
|
const [editTxnId, setEditTxnId] = useState("");
|
|
const [editAmount, setEditAmount] = useState("");
|
|
const [editCurrency, setEditCurrency] = useState("ETB");
|
|
const [editPaymentMethod, setEditPaymentMethod] = useState("Telebirr");
|
|
const [editPaymentDate, setEditPaymentDate] = useState("");
|
|
const [editNotes, setEditNotes] = useState("");
|
|
|
|
const [editStep, setEditStep] = useState(0);
|
|
const [showEditCurrency, setShowEditCurrency] = useState(false);
|
|
const [showEditMethod, setShowEditMethod] = useState(false);
|
|
const [showEditDate, setShowEditDate] = useState(false);
|
|
|
|
// Invoice list for matching
|
|
const [invoices, setInvoices] = useState<any[]>([]);
|
|
const [invoiceSearch, setInvoiceSearch] = useState("");
|
|
|
|
const paymentId = Array.isArray(id) ? id[0] : id;
|
|
|
|
function useInputColors() {
|
|
const dark = isDark;
|
|
return {
|
|
bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)",
|
|
border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)",
|
|
text: dark ? "#f1f5f9" : "#0f172a",
|
|
placeholder: "rgba(100,116,139,0.45)",
|
|
};
|
|
}
|
|
const c = useInputColors();
|
|
|
|
const fetchPayment = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
if (!paymentId) throw new Error("No ID provided");
|
|
const response = await api.payments.getById({
|
|
params: { id: paymentId },
|
|
});
|
|
setPayment(response);
|
|
} catch (error) {
|
|
console.error("[PaymentDetail] Error fetching payment:", error);
|
|
toast.error("Error", "Failed to fetch payment details.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [paymentId]);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchPayment();
|
|
}, [fetchPayment]),
|
|
);
|
|
|
|
const handleDelete = () => setShowDeleteModal(true);
|
|
|
|
const confirmDelete = async () => {
|
|
setDeleting(true);
|
|
try {
|
|
if (!paymentId) return;
|
|
await api.payments.delete({ params: { id: paymentId } });
|
|
toast.success("Deleted", "Payment record has been removed.");
|
|
setShowDeleteModal(false);
|
|
nav.back();
|
|
} catch (err: any) {
|
|
toast.error("Error", err.message || "Failed to delete payment.");
|
|
setShowDeleteModal(false);
|
|
} finally {
|
|
setDeleting(false);
|
|
}
|
|
};
|
|
|
|
const handleScanSms = async () => {
|
|
if (Platform.OS !== "android") {
|
|
toast.error(
|
|
"Not Supported",
|
|
"SMS scanning is only available on Android.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
setScanningSms(true);
|
|
try {
|
|
const granted = await PermissionsAndroid.request(
|
|
PermissionsAndroid.PERMISSIONS.READ_SMS,
|
|
);
|
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
toast.error(
|
|
"Permission Denied",
|
|
"SMS access is required to scan payment messages.",
|
|
);
|
|
setScanningSms(false);
|
|
return;
|
|
}
|
|
|
|
if (!SmsAndroid) {
|
|
toast.error("Not Available", "SMS scanning module is not available.");
|
|
setScanningSms(false);
|
|
return;
|
|
}
|
|
|
|
const fiveMinsAgo = Date.now() - 5 * 60 * 1000;
|
|
const filter = { box: "inbox", minDate: fiveMinsAgo, maxCount: 30 };
|
|
|
|
SmsAndroid.list(
|
|
JSON.stringify(filter),
|
|
(fail: string) => {
|
|
toast.error("Scan Failed", fail);
|
|
setScanningSms(false);
|
|
},
|
|
(_count: number, smsList: string) => {
|
|
const messages = JSON.parse(smsList);
|
|
const match = messages.find((m: any) => {
|
|
const addr = (m.address || "").toUpperCase();
|
|
const body = (m.body || "").toUpperCase();
|
|
return (
|
|
addr === "127" ||
|
|
addr === "CBE" ||
|
|
body.includes("TELEBIRR") ||
|
|
body.includes("CBE")
|
|
);
|
|
});
|
|
|
|
if (!match) {
|
|
toast.error(
|
|
"No Match",
|
|
"No CBE or Telebirr SMS found in the last 5 minutes.",
|
|
);
|
|
setScanningSms(false);
|
|
return;
|
|
}
|
|
|
|
const text = match.body;
|
|
const lines = [
|
|
`From: ${match.address}`,
|
|
`Date: ${new Date(match.date).toLocaleString()}`,
|
|
"",
|
|
`"${text}"`,
|
|
"",
|
|
"Send this SMS to verify the payment?",
|
|
];
|
|
|
|
Alert.alert("SMS Found", lines.filter(Boolean).join("\n"), [
|
|
{
|
|
text: "Cancel",
|
|
style: "cancel",
|
|
onPress: () => setScanningSms(false),
|
|
},
|
|
{
|
|
text: "Verify",
|
|
onPress: async () => {
|
|
try {
|
|
await api.payments.verifySms({
|
|
params: { id: paymentId },
|
|
body: { smsContent: text },
|
|
});
|
|
toast.success(
|
|
"Verified",
|
|
"SMS has been sent for verification.",
|
|
);
|
|
fetchPayment();
|
|
} catch (err: any) {
|
|
toast.error("Error", err.message || "Verification failed.");
|
|
} finally {
|
|
setScanningSms(false);
|
|
}
|
|
},
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
} catch (err) {
|
|
toast.error("Error", "Something went wrong during SMS scan.");
|
|
setScanningSms(false);
|
|
}
|
|
};
|
|
|
|
const openEdit = () => {
|
|
const amt = Number(
|
|
typeof payment.amount === "object"
|
|
? payment.amount.value
|
|
: payment.amount,
|
|
);
|
|
setEditTxnId(payment.transactionId || "");
|
|
setEditAmount(String(amt));
|
|
setEditCurrency(payment.currency || "ETB");
|
|
setEditPaymentMethod(payment.paymentMethod || "Telebirr");
|
|
setEditPaymentDate(
|
|
payment.paymentDate
|
|
? new Date(payment.paymentDate).toISOString().split("T")[0]
|
|
: new Date().toISOString().split("T")[0],
|
|
);
|
|
setEditNotes(payment.notes || "");
|
|
setEditStep(0);
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
const handleEditSave = async () => {
|
|
if (!editAmount || parseFloat(editAmount) <= 0) {
|
|
toast.error("Validation Error", "Amount must be greater than 0");
|
|
return;
|
|
}
|
|
|
|
setEditSaving(true);
|
|
try {
|
|
await api.payments.update({
|
|
params: { id: paymentId },
|
|
body: {
|
|
transactionId: editTxnId,
|
|
amount: parseFloat(editAmount),
|
|
currency: editCurrency,
|
|
paymentDate: new Date(editPaymentDate).toISOString(),
|
|
paymentMethod: editPaymentMethod,
|
|
notes: editNotes,
|
|
},
|
|
});
|
|
toast.success("Success", "Payment updated successfully.");
|
|
setShowEditModal(false);
|
|
fetchPayment();
|
|
} catch (err: any) {
|
|
const msg =
|
|
err?.response?.data?.message ||
|
|
err?.data?.message ||
|
|
err?.message ||
|
|
"Failed to update payment";
|
|
toast.error("Error", msg);
|
|
} finally {
|
|
setEditSaving(false);
|
|
}
|
|
};
|
|
|
|
const openMatchPicker = async () => {
|
|
if (!payment || matching || !paymentId) return;
|
|
|
|
setMatching(true);
|
|
try {
|
|
const response = await api.invoices.getAll();
|
|
const list = Array.isArray(response)
|
|
? response
|
|
: (response as any).data || [];
|
|
setInvoices(list);
|
|
setInvoiceSearch("");
|
|
setShowInvoicePicker(true);
|
|
} catch (err: any) {
|
|
toast.error("Error", "Failed to fetch invoices.");
|
|
} finally {
|
|
setMatching(false);
|
|
}
|
|
};
|
|
|
|
const handleInvoiceSelect = async (inv: any) => {
|
|
setShowInvoicePicker(false);
|
|
try {
|
|
await api.payments.associate({
|
|
params: { id: paymentId },
|
|
body: { invoiceId: inv.id },
|
|
});
|
|
toast.success(
|
|
"Success",
|
|
`Payment associated with invoice #${inv.invoiceNumber || inv.id}.`,
|
|
);
|
|
fetchPayment();
|
|
} catch (err: any) {
|
|
toast.error("Error", err.message || "Failed to associate.");
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Payment Details" showBack />
|
|
<View className="flex-1 justify-center items-center">
|
|
<ActivityIndicator color="#ea580c" size="large" />
|
|
<Text
|
|
variant="muted"
|
|
className="mt-4 font-sans-bold text-[10px] uppercase tracking-widest"
|
|
>
|
|
Retrieving Transaction...
|
|
</Text>
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
if (!payment) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Payment Details" showBack />
|
|
<View className="flex-1 justify-center items-center p-6">
|
|
<AlertTriangle color="#ef4444" size={48} strokeWidth={1.5} />
|
|
<Text variant="h4" className="mt-4 text-foreground font-sans-black">
|
|
Transaction Not Found
|
|
</Text>
|
|
<Text variant="muted" className="mt-2 text-center">
|
|
The requested payment record could not be retrieved.
|
|
</Text>
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
const amountValue = Number(
|
|
typeof payment.amount === "object" ? payment.amount.value : payment.amount,
|
|
);
|
|
|
|
const paymentDate = payment.paymentDate
|
|
? new Date(payment.paymentDate)
|
|
: new Date(payment.createdAt);
|
|
|
|
const isFlagged = payment.isFlagged === true;
|
|
|
|
const filteredInvoices = invoices.filter((inv) => {
|
|
if (!invoiceSearch) return true;
|
|
const q = invoiceSearch.toLowerCase();
|
|
return (
|
|
(inv.invoiceNumber || "").toLowerCase().includes(q) ||
|
|
(inv.customerName || "").toLowerCase().includes(q)
|
|
);
|
|
});
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader
|
|
title="Payment Details"
|
|
showBack
|
|
rightAction="edit"
|
|
onRightActionPress={openEdit}
|
|
/>
|
|
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ paddingBottom: 10, paddingHorizontal: 16 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Flagged Alert */}
|
|
{isFlagged && (
|
|
<View className="mt-5 mb-4 bg-red-500/10 border border-red-500/20 rounded-[6px] p-4 flex-row items-start">
|
|
<View className="bg-red-500/20 p-2 rounded-full mr-3">
|
|
<AlertTriangle color="#ef4444" size={18} />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-red-600 font-sans-black text-[10px] uppercase tracking-[2px] mb-1">
|
|
Security Flag ({payment.flagReason || "Audit Needed"})
|
|
</Text>
|
|
<Text className="text-foreground/80 font-sans-medium text-xs leading-5">
|
|
{payment.flagNotes || "System flagged this for manual review."}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Hero Section */}
|
|
<View className="pt-6">
|
|
<Text className="text-[10px] font-sans-black uppercase tracking-[3px] text-muted-foreground mb-2">
|
|
Total Amount
|
|
</Text>
|
|
<View className="flex-row items-end gap-3 mb-6">
|
|
<Text className="text-4xl font-sans-black text-foreground tracking-tighter">
|
|
{amountValue.toLocaleString()}
|
|
</Text>
|
|
<Text className="text-2xl font-sans-black text-primary mb-1">
|
|
{payment.currency || "USD"}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex-row gap-3 mb-6">
|
|
<Card
|
|
className="rounded-[6px] overflow-hidden border border-border"
|
|
style={{ flex: 1 }}
|
|
>
|
|
<CardContent className="p-4">
|
|
<Building2 size={18} color="#ea580c" />
|
|
<Text className="text-[9px] uppercase font-sans-black tracking-widest text-muted-foreground mt-3 mb-1">
|
|
Sender
|
|
</Text>
|
|
<Text
|
|
className="text-foreground font-sans-black text-sm"
|
|
numberOfLines={1}
|
|
>
|
|
{payment.senderName || "Unknown"}
|
|
</Text>
|
|
</CardContent>
|
|
</Card>
|
|
<Card
|
|
className="rounded-[6px] overflow-hidden border border-border"
|
|
style={{ flex: 1 }}
|
|
>
|
|
<CardContent className="p-4">
|
|
<Wallet size={18} color="#ea580c" />
|
|
<Text className="text-[9px] uppercase font-sans-black tracking-widest text-muted-foreground mt-3 mb-1">
|
|
Method
|
|
</Text>
|
|
<Text
|
|
className="text-foreground font-sans-black text-sm"
|
|
numberOfLines={1}
|
|
>
|
|
{payment.paymentMethod || "Direct"}
|
|
</Text>
|
|
</CardContent>
|
|
</Card>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Details Section */}
|
|
<Card className="rounded-[6px] overflow-hidden mb-6 border border-border">
|
|
<CardContent className="p-4">
|
|
<Text className="font-sans-bold text-xs uppercase tracking-widest text-muted-foreground mb-4">
|
|
Transaction Details
|
|
</Text>
|
|
|
|
<View className="gap-4">
|
|
<View className="flex-row items-center gap-3">
|
|
<Hash size={15} color="#64748b" />
|
|
<View className="flex-1">
|
|
<Text className="text-[10px] uppercase tracking-wider text-muted-foreground font-sans-semibold">
|
|
Transaction ID
|
|
</Text>
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{payment.transactionId || "—"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className="flex-row items-center gap-3">
|
|
<User size={15} color="#64748b" />
|
|
<View className="flex-1">
|
|
<Text className="text-[10px] uppercase tracking-wider text-muted-foreground font-sans-semibold">
|
|
Receiver
|
|
</Text>
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{payment.receiverName || "—"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className="flex-row items-center gap-3">
|
|
<Clock size={15} color="#64748b" />
|
|
<View className="flex-1">
|
|
<Text className="text-[10px] uppercase tracking-wider text-muted-foreground font-sans-semibold">
|
|
Payment Date
|
|
</Text>
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{paymentDate.toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{payment.invoiceId && (
|
|
<Pressable
|
|
onPress={() =>
|
|
nav.go("invoices/[id]", { id: payment.invoiceId })
|
|
}
|
|
className="flex-row items-center gap-3 active:opacity-60"
|
|
>
|
|
<Link2 size={15} color="#64748b" />
|
|
<View className="flex-1">
|
|
<Text className="text-[10px] uppercase tracking-wider text-muted-foreground font-sans-semibold">
|
|
Linked Invoice
|
|
</Text>
|
|
<Text className="text-foreground font-sans-bold text-sm underline">
|
|
{payment.invoiceId}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
)}
|
|
|
|
<View className="flex-row items-center gap-3">
|
|
<Info size={15} color="#64748b" />
|
|
<View className="flex-1">
|
|
<Text className="text-[10px] uppercase tracking-wider text-muted-foreground font-sans-semibold">
|
|
Created
|
|
</Text>
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{new Date(payment.createdAt).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Notes */}
|
|
{payment.notes && (
|
|
<View className="mb-6">
|
|
<Text className="font-sans-bold text-xs uppercase tracking-widest text-muted-foreground mb-3">
|
|
Notes
|
|
</Text>
|
|
<Card className="rounded-[6px] overflow-hidden border border-border">
|
|
<CardContent className="p-4">
|
|
<Text className="text-foreground font-sans-medium italic opacity-70 leading-6">
|
|
"{payment.notes}"
|
|
</Text>
|
|
</CardContent>
|
|
</Card>
|
|
</View>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<View className="gap-3 mb-10">
|
|
<View className="flex-row gap-3">
|
|
{payment.receiptPath && (
|
|
<Button
|
|
className="flex-1 h-10 rounded-[6px] border border-border bg-card"
|
|
onPress={() =>
|
|
Linking.openURL(
|
|
`${BASE_URL}${payment.receiptPath.replace(/^\//, "")}`,
|
|
)
|
|
}
|
|
>
|
|
<Eye
|
|
color={isDark ? "#f1f5f9" : "#0f172a"}
|
|
size={18}
|
|
strokeWidth={2.5}
|
|
/>
|
|
<Text className="ml-2 text-foreground font-sans-black text-xs uppercase tracking-widest">
|
|
View Receipt
|
|
</Text>
|
|
</Button>
|
|
)}
|
|
<Button
|
|
className="flex-1 h-10 rounded-[6px] border border-border bg-card"
|
|
onPress={handleScanSms}
|
|
disabled={scanningSms}
|
|
>
|
|
{scanningSms ? (
|
|
<ActivityIndicator color="#ea580c" />
|
|
) : (
|
|
<>
|
|
<Scan color="#ea580c" size={18} strokeWidth={2.5} />
|
|
<Text className="ml-2 text-foreground font-sans-black text-xs uppercase tracking-widest">
|
|
Scan SMS
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Button>
|
|
</View>
|
|
<View className="flex-row gap-3">
|
|
{!payment.invoiceId && (
|
|
<Button
|
|
className="flex-1 h-10 rounded-[6px] bg-primary shadow-lg shadow-primary/20"
|
|
onPress={openMatchPicker}
|
|
disabled={matching}
|
|
>
|
|
{matching ? (
|
|
<ActivityIndicator color="white" />
|
|
) : (
|
|
<>
|
|
<Link2 size={18} color="white" strokeWidth={2.5} />
|
|
<Text className="ml-2 text-white font-sans-black text-xs uppercase tracking-widest">
|
|
Link Invoice
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Button>
|
|
)}
|
|
</View>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
className="h-10 rounded-[6px] bg-red-500"
|
|
onPress={handleDelete}
|
|
disabled={deleting}
|
|
>
|
|
{deleting ? (
|
|
<ActivityIndicator color="#ef4444" />
|
|
) : (
|
|
<>
|
|
<Trash2 color="#fff" size={18} />
|
|
<Text className="ml-2 text-white font-sans-bold text-xs uppercase tracking-widest">
|
|
Delete Payment
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Button>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Edit Modal */}
|
|
<Modal
|
|
visible={showEditModal}
|
|
animationType="slide"
|
|
presentationStyle="pageSheet"
|
|
onRequestClose={() => setShowEditModal(false)}
|
|
>
|
|
<ScreenWrapper className="bg-background">
|
|
<View className="flex-1 flex-col">
|
|
<View className="flex-row items-center justify-between px-4 pt-4 pb-2">
|
|
<Text variant="h4" className="text-foreground font-sans-semibold">
|
|
Edit Payment
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowEditModal(false)}
|
|
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
|
|
>
|
|
<X color={isDark ? "#f1f5f9" : "#0f172a"} size={18} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<FormFlow
|
|
steps={[
|
|
{ key: "details", label: "Details" },
|
|
{ key: "schedule", label: "Schedule" },
|
|
{ key: "notes", label: "Notes" },
|
|
]}
|
|
currentStep={editStep}
|
|
onNext={() => setEditStep(editStep + 1)}
|
|
onBack={() => setEditStep(editStep - 1)}
|
|
onComplete={handleEditSave}
|
|
loading={editSaving}
|
|
completeLabel="Save Changes"
|
|
hideHeader
|
|
>
|
|
{editStep === 0 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Payment Details
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-4">
|
|
<View>
|
|
<Text className="font-sans-semibold text-[10px] uppercase tracking-wider mb-1.5 ml-1">
|
|
Transaction ID
|
|
</Text>
|
|
<TextInput
|
|
style={[
|
|
S.input,
|
|
{
|
|
backgroundColor: c.bg,
|
|
borderColor: c.border,
|
|
color: c.text,
|
|
},
|
|
]}
|
|
placeholder="TXN-2024-001"
|
|
placeholderTextColor={c.placeholder}
|
|
value={editTxnId}
|
|
onChangeText={setEditTxnId}
|
|
autoCorrect={false}
|
|
/>
|
|
</View>
|
|
<View className="flex-row gap-4">
|
|
<View style={{ flex: 1 }}>
|
|
<Text className="font-sans-semibold text-[10px] uppercase tracking-wider mb-1.5 ml-1">
|
|
Amount
|
|
</Text>
|
|
<TextInput
|
|
style={[
|
|
S.input,
|
|
{
|
|
backgroundColor: c.bg,
|
|
borderColor: c.border,
|
|
color: c.text,
|
|
},
|
|
]}
|
|
placeholder="0.00"
|
|
placeholderTextColor={c.placeholder}
|
|
value={editAmount}
|
|
onChangeText={setEditAmount}
|
|
keyboardType="numeric"
|
|
/>
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text className="font-sans-semibold text-[10px] uppercase tracking-wider mb-1.5 ml-1">
|
|
Currency
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowEditCurrency(true)}
|
|
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
|
|
style={{
|
|
backgroundColor: c.bg,
|
|
borderColor: c.border,
|
|
}}
|
|
>
|
|
<Text
|
|
className="text-xs font-sans-bold"
|
|
style={{ color: c.text }}
|
|
>
|
|
{editCurrency}
|
|
</Text>
|
|
<ChevronDown
|
|
size={14}
|
|
color={c.text}
|
|
strokeWidth={3}
|
|
/>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{editStep === 1 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Schedule
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-4">
|
|
<View style={{ flex: 1 }}>
|
|
<Text className="font-sans-semibold text-[10px] uppercase tracking-wider mb-1.5 ml-1">
|
|
Payment Date
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowEditDate(true)}
|
|
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
|
|
style={{ backgroundColor: c.bg, borderColor: c.border }}
|
|
>
|
|
<Text
|
|
className="text-xs font-sans-medium"
|
|
style={{ color: c.text }}
|
|
>
|
|
{editPaymentDate || "Select Date"}
|
|
</Text>
|
|
<Clock size={14} color="#ea580c" strokeWidth={2.5} />
|
|
</Pressable>
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text className="font-sans-semibold text-[10px] uppercase tracking-wider mb-1.5 ml-1">
|
|
Method
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowEditMethod(true)}
|
|
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
|
|
style={{ backgroundColor: c.bg, borderColor: c.border }}
|
|
>
|
|
<Text
|
|
className="text-xs font-sans-bold"
|
|
style={{ color: c.text }}
|
|
>
|
|
{editPaymentMethod}
|
|
</Text>
|
|
<ChevronDown size={14} color={c.text} strokeWidth={3} />
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{editStep === 2 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Notes
|
|
</Text>
|
|
<View className="bg-card rounded-[6px]">
|
|
<Text className="font-sans-semibold text-[10px] uppercase tracking-wider mb-1.5 ml-1">
|
|
Notes
|
|
</Text>
|
|
<TextInput
|
|
style={[
|
|
S.input,
|
|
{
|
|
backgroundColor: c.bg,
|
|
borderColor: c.border,
|
|
color: c.text,
|
|
height: 120,
|
|
textAlignVertical: "top",
|
|
paddingTop: 10,
|
|
},
|
|
]}
|
|
placeholder="Optional notes"
|
|
placeholderTextColor={c.placeholder}
|
|
value={editNotes}
|
|
onChangeText={setEditNotes}
|
|
multiline
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</FormFlow>
|
|
</View>
|
|
|
|
<PickerModal
|
|
visible={showEditCurrency}
|
|
onClose={() => setShowEditCurrency(false)}
|
|
title="Select Currency"
|
|
>
|
|
{CURRENCIES.map((curr) => (
|
|
<SelectOption
|
|
key={curr}
|
|
label={curr}
|
|
value={curr}
|
|
selected={editCurrency === curr}
|
|
onSelect={(v) => {
|
|
setEditCurrency(v);
|
|
setShowEditCurrency(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
|
|
<PickerModal
|
|
visible={showEditMethod}
|
|
onClose={() => setShowEditMethod(false)}
|
|
title="Select Payment Method"
|
|
>
|
|
{PAYMENT_METHODS.map((method) => (
|
|
<SelectOption
|
|
key={method}
|
|
label={method}
|
|
value={method}
|
|
selected={editPaymentMethod === method}
|
|
onSelect={(v) => {
|
|
setEditPaymentMethod(v);
|
|
setShowEditMethod(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
|
|
<PickerModal
|
|
visible={showEditDate}
|
|
onClose={() => setShowEditDate(false)}
|
|
title="Select Payment Date"
|
|
>
|
|
<CalendarGrid
|
|
selectedDate={editPaymentDate}
|
|
onSelect={(v) => {
|
|
setEditPaymentDate(v);
|
|
setShowEditDate(false);
|
|
}}
|
|
/>
|
|
</PickerModal>
|
|
</ScreenWrapper>
|
|
</Modal>
|
|
|
|
{/* Invoice Picker Modal */}
|
|
<PickerModal
|
|
visible={showInvoicePicker}
|
|
onClose={() => setShowInvoicePicker(false)}
|
|
title="Select Invoice"
|
|
>
|
|
<View className="px-4 pb-3">
|
|
<View className="flex-row items-center rounded-xl px-3 border border-border h-10">
|
|
<Search size={16} color={isDark ? "#94a3b8" : "#64748b"} />
|
|
<TextInput
|
|
className="flex-1 ml-2 text-foreground py-0 text-sm"
|
|
placeholder="Search by number or customer..."
|
|
placeholderTextColor={getPlaceholderColor(isDark)}
|
|
value={invoiceSearch}
|
|
onChangeText={setInvoiceSearch}
|
|
autoCorrect={false}
|
|
style={{ textAlignVertical: "center" }}
|
|
/>
|
|
</View>
|
|
</View>
|
|
<ScrollView className="max-h-96" keyboardShouldPersistTaps="handled">
|
|
{filteredInvoices.length > 0 ? (
|
|
filteredInvoices.map((inv) => (
|
|
<Pressable
|
|
key={inv.id}
|
|
onPress={() => handleInvoiceSelect(inv)}
|
|
className="px-4 py-3 border-b border-border/40 flex-row items-center"
|
|
>
|
|
<View className="flex-1">
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{inv.customerName || "Unknown"}
|
|
</Text>
|
|
<Text className="text-muted-foreground text-[10px] font-sans-semibold uppercase tracking-wider mt-0.5">
|
|
#{inv.invoiceNumber || inv.id} · {inv.currency || "ETB"}{" "}
|
|
{Number(inv.amount || 0).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
<Link2 size={16} color="#ea580c" strokeWidth={2.5} />
|
|
</Pressable>
|
|
))
|
|
) : (
|
|
<EmptyState
|
|
title={
|
|
invoiceSearch
|
|
? "No invoices match your search"
|
|
: "No invoices available"
|
|
}
|
|
/>
|
|
)}
|
|
</ScrollView>
|
|
</PickerModal>
|
|
|
|
<ActionModal
|
|
visible={showDeleteModal}
|
|
onClose={() => setShowDeleteModal(false)}
|
|
onConfirm={confirmDelete}
|
|
title="Delete Payment"
|
|
description="Are you sure you want to permanently delete this payment record? This action cannot be reversed."
|
|
confirmText="Delete"
|
|
confirmVariant="destructive"
|
|
icon={Trash2}
|
|
iconColor="#ef4444"
|
|
loading={deleting}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|