891 lines
29 KiB
TypeScript
891 lines
29 KiB
TypeScript
import React, { useEffect, useMemo, useState } from "react";
|
||
import {
|
||
ActivityIndicator,
|
||
Pressable,
|
||
TextInput,
|
||
StyleSheet,
|
||
View,
|
||
} from "react-native";
|
||
import { useSirouRouter } from "@sirou/react-native";
|
||
import { useColorScheme } from "nativewind";
|
||
|
||
import { api } from "@/lib/api";
|
||
import { AppRoutes } from "@/lib/routes";
|
||
import { toast } from "@/lib/toast-store";
|
||
|
||
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
||
import { FormFlow } from "@/components/FormFlow";
|
||
import { PickerModal, SelectOption } from "@/components/PickerModal";
|
||
import { CalendarGrid } from "@/components/CalendarGrid";
|
||
import { CustomerPicker } from "@/components/CustomerPicker";
|
||
import { Text } from "@/components/ui/text";
|
||
|
||
import {
|
||
Calendar,
|
||
CalendarSearch,
|
||
ChevronDown,
|
||
Plus,
|
||
Trash2,
|
||
} from "@/lib/icons";
|
||
|
||
type Item = { id: number; description: string; qty: string; price: string };
|
||
|
||
type Account = {
|
||
id: number;
|
||
bankName: string;
|
||
accountName: string;
|
||
accountNumber: string;
|
||
currency: string;
|
||
};
|
||
|
||
const S = StyleSheet.create({
|
||
input: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 12,
|
||
fontSize: 12,
|
||
fontWeight: "500",
|
||
borderRadius: 6,
|
||
borderWidth: 1,
|
||
textAlignVertical: "center",
|
||
},
|
||
inputCenter: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 10,
|
||
fontSize: 14,
|
||
fontWeight: "500",
|
||
borderRadius: 6,
|
||
borderWidth: 1,
|
||
textAlign: "center",
|
||
textAlignVertical: "center",
|
||
},
|
||
});
|
||
|
||
function useInputColors() {
|
||
const { colorScheme } = useColorScheme();
|
||
const dark = colorScheme === "dark";
|
||
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)",
|
||
};
|
||
}
|
||
|
||
function Field({
|
||
label,
|
||
value,
|
||
onChangeText,
|
||
placeholder,
|
||
numeric = false,
|
||
center = false,
|
||
flex,
|
||
multiline = false,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
onChangeText: (v: string) => void;
|
||
placeholder: string;
|
||
numeric?: boolean;
|
||
center?: boolean;
|
||
flex?: number;
|
||
multiline?: boolean;
|
||
}) {
|
||
const c = useInputColors();
|
||
return (
|
||
<View style={flex != null ? { flex } : undefined}>
|
||
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
||
{label}
|
||
</Text>
|
||
<TextInput
|
||
style={[
|
||
center ? S.inputCenter : S.input,
|
||
{ backgroundColor: c.bg, borderColor: c.border, color: c.text },
|
||
multiline
|
||
? { height: 80, paddingTop: 10, textAlignVertical: "top" }
|
||
: {},
|
||
]}
|
||
placeholder={placeholder}
|
||
placeholderTextColor={c.placeholder}
|
||
value={value}
|
||
onChangeText={onChangeText}
|
||
keyboardType={numeric ? "numeric" : "default"}
|
||
multiline={multiline}
|
||
autoCorrect={false}
|
||
autoCapitalize="none"
|
||
/>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function PickerField({
|
||
label,
|
||
value,
|
||
onPress,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
onPress: () => void;
|
||
}) {
|
||
const c = useInputColors();
|
||
return (
|
||
<View className="flex-1">
|
||
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
||
{label}
|
||
</Text>
|
||
<Pressable
|
||
onPress={onPress}
|
||
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 }}>
|
||
{value}
|
||
</Text>
|
||
<ChevronDown size={14} color={c.text} strokeWidth={3} />
|
||
</Pressable>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const CURRENCIES = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"];
|
||
|
||
const STEPS = [
|
||
{ key: "details", label: "Details" },
|
||
{ key: "customer", label: "Customer" },
|
||
{ key: "schedule", label: "Schedule" },
|
||
{ key: "items", label: "Items" },
|
||
{ key: "accounts", label: "Accounts" },
|
||
{ key: "totals", label: "Totals" },
|
||
{ key: "notes", label: "Notes" },
|
||
{ key: "summary", label: "Summary" },
|
||
];
|
||
|
||
export default function CreatePaymentRequestScreen() {
|
||
const nav = useSirouRouter<AppRoutes>();
|
||
const [step, setStep] = useState(0);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
|
||
const [paymentRequestNumber, setPaymentRequestNumber] = useState("");
|
||
const [customerName, setCustomerName] = useState("");
|
||
const [customerEmail, setCustomerEmail] = useState("");
|
||
const [customerPhone, setCustomerPhone] = useState("");
|
||
|
||
const [amount, setAmount] = useState("");
|
||
const [currency, setCurrency] = useState("ETB");
|
||
|
||
const [description, setDescription] = useState("");
|
||
const [notes, setNotes] = useState("");
|
||
|
||
const [taxAmount, setTaxAmount] = useState("0");
|
||
const [discountAmount, setDiscountAmount] = useState("0");
|
||
|
||
const [issueDate, setIssueDate] = useState(
|
||
new Date().toISOString().split("T")[0],
|
||
);
|
||
const [dueDate, setDueDate] = useState("");
|
||
|
||
const [status, setStatus] = useState("DRAFT");
|
||
|
||
const [items, setItems] = useState<Item[]>([
|
||
{ id: 1, description: "", qty: "1", price: "" },
|
||
]);
|
||
|
||
const [accounts, setAccounts] = useState<Account[]>([
|
||
{
|
||
id: 1,
|
||
bankName: "",
|
||
accountName: "",
|
||
accountNumber: "",
|
||
currency: "ETB",
|
||
},
|
||
]);
|
||
|
||
const c = useInputColors();
|
||
|
||
const [showCurrency, setShowCurrency] = useState(false);
|
||
const [showIssueDate, setShowIssueDate] = useState(false);
|
||
const [showDueDate, setShowDueDate] = useState(false);
|
||
const [showStatus, setShowStatus] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const year = new Date().getFullYear();
|
||
const random = Math.floor(1000 + Math.random() * 9000);
|
||
setPaymentRequestNumber(`PAYREQ-${year}-${random}`);
|
||
|
||
const d = new Date();
|
||
d.setDate(d.getDate() + 30);
|
||
setDueDate(d.toISOString().split("T")[0]);
|
||
}, []);
|
||
|
||
const updateItem = (id: number, field: keyof Item, value: string) =>
|
||
setItems((prev) =>
|
||
prev.map((it) => (it.id === id ? { ...it, [field]: value } : it)),
|
||
);
|
||
|
||
const addItem = () =>
|
||
setItems((prev) => [
|
||
...prev,
|
||
{ id: Date.now(), description: "", qty: "1", price: "" },
|
||
]);
|
||
|
||
const removeItem = (id: number) => {
|
||
if (items.length > 1) setItems((prev) => prev.filter((it) => it.id !== id));
|
||
};
|
||
|
||
const updateAccount = (id: number, field: keyof Account, value: string) =>
|
||
setAccounts((prev) =>
|
||
prev.map((acc) => (acc.id === id ? { ...acc, [field]: value } : acc)),
|
||
);
|
||
|
||
const addAccount = () =>
|
||
setAccounts((prev) => [
|
||
...prev,
|
||
{
|
||
id: Date.now(),
|
||
bankName: "",
|
||
accountName: "",
|
||
accountNumber: "",
|
||
currency: "ETB",
|
||
},
|
||
]);
|
||
|
||
const removeAccount = (id: number) => {
|
||
if (accounts.length > 1)
|
||
setAccounts((prev) => prev.filter((acc) => acc.id !== id));
|
||
};
|
||
|
||
const subtotal = useMemo(
|
||
() =>
|
||
items.reduce(
|
||
(sum, item) =>
|
||
sum + (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0),
|
||
0,
|
||
),
|
||
[items],
|
||
);
|
||
|
||
const computedTotal = useMemo(
|
||
() =>
|
||
subtotal +
|
||
(parseFloat(taxAmount) || 0) -
|
||
(parseFloat(discountAmount) || 0),
|
||
[subtotal, taxAmount, discountAmount],
|
||
);
|
||
|
||
const handleNext = () => {
|
||
if (step === 0 && !paymentRequestNumber.trim()) {
|
||
toast.error("Validation", "Payment request number is required");
|
||
return;
|
||
}
|
||
if (step === 1 && !customerName.trim()) {
|
||
toast.error("Validation", "Customer name is required");
|
||
return;
|
||
}
|
||
setStep(step + 1);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
if (!customerName) {
|
||
toast.error("Validation Error", "Please enter a customer name");
|
||
return;
|
||
}
|
||
|
||
const formattedPhone = customerPhone ? `+251${customerPhone}` : "";
|
||
|
||
const body = {
|
||
paymentRequestNumber,
|
||
customerName,
|
||
customerEmail,
|
||
customerPhone: formattedPhone,
|
||
amount: amount ? Number(amount) : Number(computedTotal.toFixed(2)),
|
||
currency,
|
||
issueDate: new Date(issueDate).toISOString(),
|
||
dueDate: new Date(dueDate).toISOString(),
|
||
description: description || `Payment request for ${customerName}`,
|
||
notes,
|
||
taxAmount: parseFloat(taxAmount) || 0,
|
||
discountAmount: parseFloat(discountAmount) || 0,
|
||
status,
|
||
accounts: accounts.map((a) => ({
|
||
bankName: a.bankName,
|
||
accountName: a.accountName,
|
||
accountNumber: a.accountNumber,
|
||
currency: a.currency,
|
||
})),
|
||
items: items.map((i) => ({
|
||
description: i.description || "Item",
|
||
quantity: parseFloat(i.qty) || 0,
|
||
unitPrice: parseFloat(i.price) || 0,
|
||
total: Number(
|
||
((parseFloat(i.qty) || 0) * (parseFloat(i.price) || 0)).toFixed(2),
|
||
),
|
||
})),
|
||
};
|
||
|
||
try {
|
||
setSubmitting(true);
|
||
await api.paymentRequests.create({
|
||
body,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
});
|
||
toast.success("Success", "Payment request created successfully!");
|
||
nav.back();
|
||
} catch (err: any) {
|
||
console.error("[PaymentRequestCreate] Error:", err);
|
||
toast.error("Error", err?.message || "Failed to create payment request");
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<ScreenWrapper className="bg-background">
|
||
<FormFlow
|
||
steps={STEPS}
|
||
currentStep={step}
|
||
onNext={handleNext}
|
||
onBack={() => setStep(step - 1)}
|
||
onComplete={handleSubmit}
|
||
loading={submitting}
|
||
completeLabel="Create Request"
|
||
>
|
||
{step === 0 && (
|
||
<View className="gap-5">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Request Details
|
||
</Text>
|
||
<View className="bg-card rounded-[6px] gap-4">
|
||
<Field
|
||
label="Request Number"
|
||
value={paymentRequestNumber}
|
||
onChangeText={setPaymentRequestNumber}
|
||
placeholder="e.g. PAYREQ-2024-001"
|
||
/>
|
||
<Field
|
||
label="Description"
|
||
value={description}
|
||
onChangeText={setDescription}
|
||
placeholder="e.g. Payment request for services"
|
||
/>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{step === 1 && (
|
||
<View className="gap-5">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Customer Information
|
||
</Text>
|
||
<View className="gap-4">
|
||
<View>
|
||
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
||
Customer Name
|
||
</Text>
|
||
<CustomerPicker
|
||
value={customerName}
|
||
onSelect={(c) => {
|
||
setCustomerName(c.name);
|
||
setCustomerEmail(c.email);
|
||
setCustomerPhone(c.phone.replace("+251", ""));
|
||
}}
|
||
placeholder="Select or search for a customer"
|
||
/>
|
||
</View>
|
||
<View className="flex-row gap-4">
|
||
<Field
|
||
label="Email"
|
||
value={customerEmail}
|
||
onChangeText={setCustomerEmail}
|
||
placeholder="billing@acme.com"
|
||
flex={1}
|
||
/>
|
||
</View>
|
||
<View>
|
||
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
||
Phone
|
||
</Text>
|
||
<View className="flex-row items-center h-11 px-3 border border-border rounded-[6px]" style={{ backgroundColor: c.bg, borderColor: c.border }}>
|
||
<Text className="text-foreground font-sans-bold text-xs">+251</Text>
|
||
<TextInput
|
||
className="flex-1 ml-2 text-foreground text-xs font-sans-medium"
|
||
placeholder="912345678"
|
||
placeholderTextColor={c.placeholder}
|
||
value={customerPhone}
|
||
onChangeText={setCustomerPhone}
|
||
keyboardType="phone-pad"
|
||
maxLength={9}
|
||
style={{ textAlignVertical: "center" }}
|
||
/>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{step === 2 && (
|
||
<View className="gap-5">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Schedule & Currency
|
||
</Text>
|
||
<View className="bg-card rounded-[6px] gap-4">
|
||
<View className="flex-row gap-4">
|
||
<PickerField
|
||
label="Issue Date"
|
||
value={issueDate}
|
||
onPress={() => setShowIssueDate(true)}
|
||
/>
|
||
<PickerField
|
||
label="Due Date"
|
||
value={dueDate || "Select"}
|
||
onPress={() => setShowDueDate(true)}
|
||
/>
|
||
</View>
|
||
<View className="flex-row gap-4">
|
||
<PickerField
|
||
label="Currency"
|
||
value={currency}
|
||
onPress={() => setShowCurrency(true)}
|
||
/>
|
||
<PickerField
|
||
label="Status"
|
||
value={status}
|
||
onPress={() => setShowStatus(true)}
|
||
/>
|
||
</View>
|
||
<Field
|
||
label="Amount"
|
||
value={amount}
|
||
onChangeText={setAmount}
|
||
placeholder="1500"
|
||
numeric
|
||
/>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{step === 3 && (
|
||
<View className="gap-5">
|
||
<View className="flex-row items-center justify-between">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Items
|
||
</Text>
|
||
<Pressable
|
||
onPress={addItem}
|
||
className="flex-row items-center gap-1 px-3 py-1.5 rounded-[6px] bg-primary/10 border border-primary/20"
|
||
>
|
||
<Plus color="#ea580c" size={14} strokeWidth={2.5} />
|
||
<Text className="text-primary text-[10px] font-sans-bold">
|
||
Add
|
||
</Text>
|
||
</Pressable>
|
||
</View>
|
||
<View className="gap-3">
|
||
{items.map((item, index) => (
|
||
<View
|
||
key={item.id}
|
||
className={`bg-card pb-4 ${index < items.length - 1 ? "border-b border-border" : ""}`}
|
||
>
|
||
<View className="flex-row justify-between items-center mb-3">
|
||
<Text className="text-[16px] font-sans-bold text-foreground">
|
||
Item {index + 1}
|
||
</Text>
|
||
<Pressable onPress={() => removeItem(item.id)} hitSlop={8}>
|
||
<Trash2 color="#ef4444" size={16} />
|
||
</Pressable>
|
||
</View>
|
||
<Field
|
||
label="Description"
|
||
placeholder="e.g. Web Development"
|
||
value={item.description}
|
||
onChangeText={(v) => updateItem(item.id, "description", v)}
|
||
/>
|
||
<View className="flex-row gap-3 mt-4">
|
||
<Field
|
||
label="Qty"
|
||
placeholder="1"
|
||
numeric
|
||
center
|
||
value={item.qty}
|
||
onChangeText={(v) => updateItem(item.id, "qty", v)}
|
||
flex={1}
|
||
/>
|
||
<Field
|
||
label="Price"
|
||
placeholder="0.00"
|
||
numeric
|
||
value={item.price}
|
||
onChangeText={(v) => updateItem(item.id, "price", v)}
|
||
flex={3}
|
||
/>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{step === 4 && (
|
||
<View className="gap-5">
|
||
<View className="flex-row items-center justify-between">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Accounts
|
||
</Text>
|
||
<Pressable
|
||
onPress={addAccount}
|
||
className="flex-row items-center gap-1 px-3 py-1.5 rounded-[6px] bg-primary/10 border border-primary/20"
|
||
>
|
||
<Plus color="#ea580c" size={14} strokeWidth={2.5} />
|
||
<Text className="text-primary text-[12px] font-sans-bold">
|
||
Add
|
||
</Text>
|
||
</Pressable>
|
||
</View>
|
||
<View className="gap-3">
|
||
{accounts.map((acc, index) => (
|
||
<View
|
||
key={acc.id}
|
||
className={`bg-card pb-4 ${index < accounts.length - 1 ? "border-b border-border" : ""}`}
|
||
>
|
||
<View className="flex-row justify-between items-center mb-3">
|
||
<Text className="text-[16px] font-sans-bold text-foreground">
|
||
Account {index + 1}
|
||
</Text>
|
||
<Pressable
|
||
onPress={() => removeAccount(acc.id)}
|
||
hitSlop={8}
|
||
>
|
||
<Trash2 color="#ef4444" size={16} />
|
||
</Pressable>
|
||
</View>
|
||
<Field
|
||
label="Bank Name"
|
||
value={acc.bankName}
|
||
onChangeText={(v) => updateAccount(acc.id, "bankName", v)}
|
||
placeholder="e.g. Yaltopia Bank"
|
||
/>
|
||
<View className="flex-row gap-4 mt-4">
|
||
<Field
|
||
label="Account Name"
|
||
value={acc.accountName}
|
||
onChangeText={(v) =>
|
||
updateAccount(acc.id, "accountName", v)
|
||
}
|
||
placeholder="e.g. Yaltopia Tech PLC"
|
||
flex={1}
|
||
/>
|
||
</View>
|
||
<View className="flex-row gap-4 mt-4">
|
||
<Field
|
||
label="Account Number"
|
||
value={acc.accountNumber}
|
||
onChangeText={(v) =>
|
||
updateAccount(acc.id, "accountNumber", v)
|
||
}
|
||
placeholder="123456789"
|
||
flex={2}
|
||
/>
|
||
<Field
|
||
label="Currency"
|
||
value={acc.currency}
|
||
onChangeText={(v) => updateAccount(acc.id, "currency", v)}
|
||
placeholder="ETB"
|
||
flex={1}
|
||
/>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{step === 5 && (
|
||
<View className="gap-5">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Totals
|
||
</Text>
|
||
<View className="bg-card rounded-[6px] pb-4 gap-4">
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-foreground font-sans-medium text-sm">
|
||
Subtotal
|
||
</Text>
|
||
<Text className="text-foreground font-sans-bold">
|
||
{currency}{" "}
|
||
{subtotal.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})}
|
||
</Text>
|
||
</View>
|
||
<View className="flex-row gap-4">
|
||
<Field
|
||
label="Tax Amount"
|
||
value={taxAmount}
|
||
onChangeText={setTaxAmount}
|
||
placeholder="0"
|
||
numeric
|
||
flex={1}
|
||
/>
|
||
<Field
|
||
label="Discount"
|
||
value={discountAmount}
|
||
onChangeText={setDiscountAmount}
|
||
placeholder="0"
|
||
numeric
|
||
flex={1}
|
||
/>
|
||
</View>
|
||
<View className="border-t border-border/40 pt-4 flex-row justify-between">
|
||
<Text className="text-foreground font-sans-bold text-[16px]">
|
||
Total
|
||
</Text>
|
||
<Text className="text-primary font-sans-bold text-[16px]">
|
||
{currency}{" "}
|
||
{(amount ? Number(amount) : computedTotal).toLocaleString(
|
||
"en-US",
|
||
{ minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
||
)}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
{step === 6 && (
|
||
<View className="bg-card rounded-[6px]">
|
||
<Field
|
||
label="Notes"
|
||
value={notes}
|
||
onChangeText={setNotes}
|
||
placeholder="e.g. Payment terms: Net 30"
|
||
multiline
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
{step === 7 && (
|
||
<>
|
||
<View className="gap-5 pb-4">
|
||
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
||
Summary
|
||
</Text>
|
||
<View className="bg-card rounded-[6px] gap-3">
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Request Number
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4">
|
||
{paymentRequestNumber}
|
||
</Text>
|
||
</View>
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Customer
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4">
|
||
{customerName}
|
||
</Text>
|
||
</View>
|
||
{customerEmail ? (
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Email
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4">
|
||
{customerEmail}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
{customerPhone ? (
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Phone
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4">
|
||
+251{customerPhone}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
{description ? (
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Description
|
||
</Text>
|
||
<Text
|
||
className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4"
|
||
numberOfLines={2}
|
||
>
|
||
{description}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Issue Date
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold">
|
||
{issueDate}
|
||
</Text>
|
||
</View>
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Due Date
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold">
|
||
{dueDate || "Not set"}
|
||
</Text>
|
||
</View>
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Status
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold">
|
||
{status}
|
||
</Text>
|
||
</View>
|
||
{items.length > 0 && (
|
||
<View className="border-t border-border/40 pt-3">
|
||
<Text className="text-[12px] text-muted-foreground font-sans-bold uppercase tracking-widest mb-2">
|
||
Items ({items.length})
|
||
</Text>
|
||
{items.map((item, i) => (
|
||
<View
|
||
key={item.id}
|
||
className="flex-row justify-between py-1"
|
||
>
|
||
<Text
|
||
className="text-[13px] text-foreground font-sans-medium flex-1"
|
||
numberOfLines={1}
|
||
>
|
||
{item.description || `Item ${i + 1}`}
|
||
</Text>
|
||
<Text className="text-[13px] text-foreground font-sans-bold">
|
||
{item.qty} × {currency}{" "}
|
||
{parseFloat(item.price || "0").toFixed(2)}
|
||
</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
{notes ? (
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Notes
|
||
</Text>
|
||
<Text
|
||
className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4"
|
||
numberOfLines={2}
|
||
>
|
||
{notes}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
{parseFloat(taxAmount) > 0 && (
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Tax
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold">
|
||
{currency} {parseFloat(taxAmount).toFixed(2)}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
{parseFloat(discountAmount) > 0 && (
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[14px] text-foreground font-sans-medium">
|
||
Discount
|
||
</Text>
|
||
<Text className="text-[14px] text-foreground font-sans-bold">
|
||
-{currency} {parseFloat(discountAmount).toFixed(2)}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
<View className="border-t border-border/40 my-1" />
|
||
<View className="flex-row justify-between">
|
||
<Text className="text-[16px] font-sans-bold text-foreground">
|
||
Total Amount
|
||
</Text>
|
||
<Text className="text-[16px] font-sans-bold text-primary">
|
||
{currency}{" "}
|
||
{(amount ? Number(amount) : computedTotal).toLocaleString(
|
||
"en-US",
|
||
{ minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
||
)}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</>
|
||
)}
|
||
</FormFlow>
|
||
|
||
<PickerModal
|
||
visible={showCurrency}
|
||
onClose={() => setShowCurrency(false)}
|
||
title="Select Currency"
|
||
>
|
||
{CURRENCIES.map((curr) => (
|
||
<SelectOption
|
||
key={curr}
|
||
label={curr}
|
||
value={curr}
|
||
selected={currency === curr}
|
||
onSelect={(v) => {
|
||
setCurrency(v);
|
||
setShowCurrency(false);
|
||
}}
|
||
/>
|
||
))}
|
||
</PickerModal>
|
||
|
||
<PickerModal
|
||
visible={showStatus}
|
||
onClose={() => setShowStatus(false)}
|
||
title="Select Status"
|
||
>
|
||
{["DRAFT", "SENT", "OPENED", "PAID", "EXPIRED", "CANCELLED"].map(
|
||
(s) => (
|
||
<SelectOption
|
||
key={s}
|
||
label={s}
|
||
value={s}
|
||
selected={status === s}
|
||
onSelect={(v) => {
|
||
setStatus(v);
|
||
setShowStatus(false);
|
||
}}
|
||
/>
|
||
),
|
||
)}
|
||
</PickerModal>
|
||
|
||
<PickerModal
|
||
visible={showIssueDate}
|
||
onClose={() => setShowIssueDate(false)}
|
||
title="Select Issue Date"
|
||
>
|
||
<CalendarGrid
|
||
selectedDate={issueDate}
|
||
onSelect={(v) => {
|
||
setIssueDate(v);
|
||
setShowIssueDate(false);
|
||
}}
|
||
/>
|
||
</PickerModal>
|
||
|
||
<PickerModal
|
||
visible={showDueDate}
|
||
onClose={() => setShowDueDate(false)}
|
||
title="Select Due Date"
|
||
>
|
||
<CalendarGrid
|
||
selectedDate={dueDate}
|
||
onSelect={(v) => {
|
||
setDueDate(v);
|
||
setShowDueDate(false);
|
||
}}
|
||
/>
|
||
</PickerModal>
|
||
</ScreenWrapper>
|
||
);
|
||
}
|