From b6bc3d2d9cabf47b7c7cf8af88a6e8da717ac8fc Mon Sep 17 00:00:00 2001 From: elnatansamuel25 Date: Wed, 17 Jun 2026 15:16:40 +0300 Subject: [PATCH] ui --- app.json | 5 +- app/(tabs)/index.tsx | 39 +- app/(tabs)/invoices.tsx | 18 +- app/(tabs)/payments.tsx | 53 +- app/(tabs)/scan.tsx | 15 +- app/add-receipt.tsx | 887 --------------------- app/company-details.tsx | 12 + app/customers/[id].tsx | 327 ++------ app/customers/edit.tsx | 525 +++++++++++++ app/declarations/create.tsx | 90 ++- app/declarations/index.tsx | 20 +- app/declarations/scan.tsx | 289 +++++++ app/edit-profile.tsx | 178 +++-- app/invoices/[id].tsx | 349 +++++---- app/invoices/create.tsx | 169 ++-- app/invoices/edit.tsx | 21 +- app/notifications/index.tsx | 163 +++- app/payment-requests/[id].tsx | 197 ++++- app/payment-requests/create.tsx | 692 +++++++---------- app/payment-requests/edit.tsx | 781 +++++++++++++++++++ app/payments/[id].tsx | 1195 +++++++++++++++++++++++------ app/payments/create.tsx | 237 ++++-- app/pin-lock.tsx | 8 +- app/profile.tsx | 24 - app/proforma-requests/[id].tsx | 622 +++++++++++++++ app/proforma-requests/create.tsx | 793 +++++++++++++++++++ app/proforma.tsx | 602 ++++++++++----- app/proforma/[id].tsx | 839 +++++++++++++------- app/proforma/create.tsx | 21 +- app/proforma/edit.tsx | 21 +- app/team/[id]/details.tsx | 8 +- app/team/create.tsx | 291 +++++++ app/team/index.tsx | 12 +- assets/{cbe.png => cbe1.png} | Bin assets/ticket.png | Bin 0 -> 105111 bytes components/CommandPalette.tsx | 4 +- components/ConfirmSubmitModal.tsx | 102 +++ components/CreateMethodSheet.tsx | 102 +++ components/CustomerPicker.tsx | 75 +- components/ModalToast.tsx | 130 ++-- components/Toast.tsx | 175 ----- lib/api.ts | 17 +- lib/icons.tsx | 5 + lib/payment-providers.ts | 15 - lib/routes.ts | 43 +- lib/scan-cache.ts | 16 +- package-lock.json | 574 +++++++------- package.json | 3 + 48 files changed, 7389 insertions(+), 3375 deletions(-) delete mode 100644 app/add-receipt.tsx create mode 100644 app/customers/edit.tsx create mode 100644 app/declarations/scan.tsx create mode 100644 app/payment-requests/edit.tsx create mode 100644 app/proforma-requests/[id].tsx create mode 100644 app/proforma-requests/create.tsx create mode 100644 app/team/create.tsx rename assets/{cbe.png => cbe1.png} (100%) create mode 100644 assets/ticket.png create mode 100644 components/ConfirmSubmitModal.tsx create mode 100644 components/CreateMethodSheet.tsx delete mode 100644 components/Toast.tsx delete mode 100644 lib/payment-providers.ts diff --git a/app.json b/app.json index 169e41b..479adf0 100644 --- a/app.json +++ b/app.json @@ -22,7 +22,10 @@ "com.googleusercontent.apps.1028200585478-cnp6lpmp75mj4b79pnijiefv5rl1etqi" ] } - ] + ], + "NSAppTransportSecurity": { + "NSAllowsArbitraryLoads": true + } } }, "android": { diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index c4cd1ca..5075619 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -19,18 +19,16 @@ import { DollarSign, FileText, ShieldCheck, - Receipt, Wallet, ChevronRight, AlertTriangle, - Banknote, FileCheck, + Building2, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { EmptyState } from "@/components/EmptyState"; import { useAuthStore } from "@/lib/auth-store"; -import { getProviderLogo, isCash } from "@/lib/payment-providers"; interface NewsItem { id: string; @@ -49,6 +47,8 @@ interface Payment { currency: string; paymentDate: string; paymentMethod: string; + financialInstitution?: string; + financialInstitutionLogoUrl?: string; isFlagged: boolean; senderName?: string; receiverName?: string; @@ -222,11 +222,6 @@ export default function HomeScreen() { label="Proforma" onPress={() => nav.go("proforma")} /> - } - label="Receipt" - onPress={() => nav.go("add-receipt")} - /> @@ -241,6 +236,13 @@ export default function HomeScreen() { label="Declaration" onPress={() => nav.go("declarations/index")} /> + + } + label="Company" + onPress={() => nav.go("company-details")} + /> @@ -273,8 +275,7 @@ export default function HomeScreen() { const dateStr = new Date( pay.paymentDate, ).toLocaleDateString(); - const logo = getProviderLogo(pay.paymentMethod); - const cash = isCash(pay.paymentMethod); + const logoUrl = pay.financialInstitutionLogoUrl; const hasFlag = pay.isFlagged; return ( - {logo ? ( - + {logoUrl ? ( + @@ -294,11 +295,7 @@ export default function HomeScreen() { ) : ( {hasFlag ? ( @@ -307,12 +304,6 @@ export default function HomeScreen() { size={18} strokeWidth={2} /> - ) : cash ? ( - ) : ( { try { @@ -171,7 +173,7 @@ export default function InvoicesTabScreen() { {/* Create button */} ); diff --git a/app/customers/[id].tsx b/app/customers/[id].tsx index f73039e..7e8e48a 100644 --- a/app/customers/[id].tsx +++ b/app/customers/[id].tsx @@ -1,12 +1,10 @@ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState, useCallback } from "react"; import { View, ScrollView, ActivityIndicator, Pressable, - TextInput, Modal, - Dimensions, } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; @@ -22,19 +20,13 @@ import { Tag, ShieldCheck, BookOpen, - FileText, - Wallet, - Plus, - Search, - X, + Pencil, + Trash2, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { api } from "@/lib/api"; import { toast } from "@/lib/toast-store"; -import { useColorScheme } from "nativewind"; - -const { height: SCREEN_HEIGHT } = Dimensions.get("window"); export default function CustomerDetailScreen() { const nav = useSirouRouter(); @@ -42,14 +34,8 @@ export default function CustomerDetailScreen() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - - const [proformaItems, setProformaItems] = useState([]); - const [paymentRequestItems, setPaymentRequestItems] = useState([]); - const [showProformaSheet, setShowProformaSheet] = useState(false); - const [showPaymentRequestSheet, setShowPaymentRequestSheet] = useState(false); - const [sheetLoading, setSheetLoading] = useState(false); - const [proformaSearch, setProformaSearch] = useState(""); - const [paymentReqSearch, setPaymentReqSearch] = useState(""); + const [deleting, setDeleting] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); const fetch = useCallback(async () => { try { @@ -68,56 +54,21 @@ export default function CustomerDetailScreen() { useFocusEffect(useCallback(() => { fetch(); }, [fetch])); - const openProformaSheet = async () => { - setSheetLoading(true); - setShowProformaSheet(true); - setProformaSearch(""); + const handleDelete = async () => { try { - const res = await api.proforma.getAll({ query: { page: 1, limit: 50 } }); - setProformaItems(res?.data || []); - } catch { - setProformaItems([]); + setDeleting(true); + const cId = Array.isArray(id) ? id[0] : id; + await api.customers.delete({ params: { id: cId } }); + toast.success("Deleted", "Customer has been deleted"); + setShowDeleteModal(false); + nav.back(); + } catch (err: any) { + toast.error("Error", err?.message || "Failed to delete customer"); } finally { - setSheetLoading(false); + setDeleting(false); } }; - const openPaymentRequestSheet = async () => { - setSheetLoading(true); - setShowPaymentRequestSheet(true); - setPaymentReqSearch(""); - try { - const res = await api.paymentRequests.getAll({ query: { page: 1, limit: 50 } }); - setPaymentRequestItems(res?.data || []); - } catch { - setPaymentRequestItems([]); - } finally { - setSheetLoading(false); - } - }; - - const filteredProformas = useMemo(() => { - if (!proformaSearch.trim()) return proformaItems; - const q = proformaSearch.toLowerCase(); - return proformaItems.filter( - (p: any) => - (p.proformaNumber || "")?.toLowerCase().includes(q) || - (p.customerName || "")?.toLowerCase().includes(q) || - (String(p.amount || "")).includes(q), - ); - }, [proformaItems, proformaSearch]); - - const filteredPaymentRequests = useMemo(() => { - if (!paymentReqSearch.trim()) return paymentRequestItems; - const q = paymentReqSearch.toLowerCase(); - return paymentRequestItems.filter( - (r: any) => - (r.paymentRequestNumber || "")?.toLowerCase().includes(q) || - (r.customerName || "")?.toLowerCase().includes(q) || - (String(r.amount || "")).includes(q), - ); - }, [paymentRequestItems, paymentReqSearch]); - if (loading) { return ( @@ -148,9 +99,6 @@ export default function CustomerDetailScreen() { const isCompany = data?.type === "COMPANY"; const d = data || {}; - const goProformaCreate = () => nav.go("proforma/create"); - const goPaymentRequestCreate = () => nav.go("payment-requests/create"); - return ( @@ -283,218 +231,87 @@ export default function CustomerDetailScreen() { {/* Action Buttons */} { + const cId = Array.isArray(id) ? id[0] : id; + nav.go("customers/edit", { id: cId }); + }} + className="bg-primary h-11 rounded-[6px] flex-row items-center justify-center gap-2" > - + - Proformas + Edit Customer setShowDeleteModal(true)} + className="bg-red-500 h-11 rounded-[6px] flex-row items-center justify-center gap-2" > - + - Payment Requests + Delete Customer - {/* Proforma Bottom Sheet */} - setShowProformaSheet(false)} - loading={sheetLoading} - items={filteredProformas} - search={proformaSearch} - onSearchChange={setProformaSearch} - onCreateNew={goProformaCreate} - onSelectItem={(id: string) => { - setShowProformaSheet(false); - nav.go("proforma/[id]", { id }); - }} - /> - - {/* Payment Request Bottom Sheet */} - setShowPaymentRequestSheet(false)} - loading={sheetLoading} - items={filteredPaymentRequests} - search={paymentReqSearch} - onSearchChange={setPaymentReqSearch} - onCreateNew={goPaymentRequestCreate} - onSelectItem={(id: string) => { - setShowPaymentRequestSheet(false); - nav.go("payment-requests/[id]", { id }); - }} - type="payment" - /> - - ); -} - -function ProformaSheet({ - visible, - onClose, - loading, - items, - search, - onSearchChange, - onCreateNew, - onSelectItem, - type = "proforma", -}: { - visible: boolean; - onClose: () => void; - loading: boolean; - items: any[]; - search: string; - onSearchChange: (v: string) => void; - onCreateNew: () => void; - onSelectItem: (id: string) => void; - type?: "proforma" | "payment"; -}) { - const { colorScheme } = useColorScheme(); - const isDark = colorScheme === "dark"; - const label = type === "proforma" ? "Proforma" : "Payment Request"; - - return ( - - - + {/* Delete Confirmation Modal */} + setShowDeleteModal(false)} + > + setShowDeleteModal(false)} + > e.stopPropagation()} > - {/* Header */} - - - - {label}s - - - - - - - {/* Search */} - - - - - {search.length > 0 && ( - onSearchChange("")}> - - - )} + + + + + Delete Customer? + + + This will permanently delete{" "} + + {d.displayName} + {" "} + and all associated data. This action cannot be undone. + - {/* Create New */} - + - - - Create New {label} + {deleting ? ( + + ) : ( + + Yes, Delete + + )} + + setShowDeleteModal(false)} + className="bg-secondary h-12 rounded-[6px] items-center justify-center border border-border" + > + + Cancel - - {/* List */} - - {loading ? ( - - - - ) : items.length === 0 ? ( - - - {search ? `No ${label}s match your search` : `No ${label}s found`} - - - ) : ( - items.map((item: any) => { - const num = type === "proforma" - ? item.proformaNumber - : item.paymentRequestNumber; - const status = (item.status || "DRAFT").toUpperCase(); - const st: Record = { - PAID: { label: "Paid", bg: "bg-emerald-500/10", text: "text-emerald-600" }, - PENDING: { label: "Pending", bg: "bg-amber-500/10", text: "text-amber-600" }, - DRAFT: { label: "Draft", bg: "bg-blue-500/10", text: "text-blue-600" }, - CANCELLED: { label: "Cancelled", bg: "bg-slate-500/10", text: "text-slate-600" }, - }; - const s = st[status] || st.DRAFT; - return ( - onSelectItem(item.id)} - className="bg-card rounded-[6px] border border-border p-4 mb-3" - > - - - - {num || item.id?.slice(0, 8) || "—"} - - - {item.customerName || "—"} - - - - {item.amount != null ? Number(item.amount).toLocaleString("en-US", { minimumFractionDigits: 2 }) : "—"} - - - - - - {s.label} - - - {item.issueDate && ( - - {new Date(item.issueDate).toLocaleDateString()} - - )} - - - ); - }) - )} - - - - + + + ); } diff --git a/app/customers/edit.tsx b/app/customers/edit.tsx new file mode 100644 index 0000000..5f229df --- /dev/null +++ b/app/customers/edit.tsx @@ -0,0 +1,525 @@ +import React, { useState, useEffect } from "react"; +import { View, Pressable, TextInput, StyleSheet, ActivityIndicator } from "react-native"; +import { useSirouRouter } from "@sirou/react-native"; +import { useColorScheme } from "nativewind"; +import { useLocalSearchParams } from "expo-router"; + +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 { Text } from "@/components/ui/text"; + +const S = StyleSheet.create({ + input: { + paddingHorizontal: 12, + paddingVertical: 12, + fontSize: 12, + fontWeight: "500", + borderRadius: 6, + borderWidth: 1, + 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, + flex, + multiline = false, +}: { + label: string; + value: string; + onChangeText: (v: string) => void; + placeholder: string; + numeric?: boolean; + flex?: number; + multiline?: boolean; +}) { + const c = useInputColors(); + return ( + + + {label} + + + + ); +} + +const TYPES = ["INDIVIDUAL", "COMPANY"] as const; + +const STEPS = [ + { key: "type", label: "Type" }, + { key: "details", label: "Details" }, + { key: "documents", label: "Documents" }, + { key: "notes", label: "Notes" }, + { key: "summary", label: "Summary" }, +]; + +function stripPhone(p?: string | null): string { + if (!p) return ""; + return p.replace(/^\+?251/, ""); +} + +export default function EditCustomerScreen() { + const nav = useSirouRouter(); + const { id } = useLocalSearchParams<{ id: string }>(); + + const [step, setStep] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [loadingData, setLoadingData] = useState(true); + const c = useInputColors(); + + const [type, setType] = useState<"INDIVIDUAL" | "COMPANY">("INDIVIDUAL"); + const [displayName, setDisplayName] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [companyName, setCompanyName] = useState(""); + const [email, setEmail] = useState(""); + const [phone, setPhone] = useState(""); + const [tin, setTin] = useState(""); + const [vatReg, setVatReg] = useState(""); + const [businessLicense, setBusinessLicense] = useState(""); + const [address, setAddress] = useState(""); + const [notes, setNotes] = useState(""); + + useEffect(() => { + (async () => { + try { + setLoadingData(true); + const cId = Array.isArray(id) ? id[0] : id; + if (!cId) return; + const data = await api.customers.getById({ params: { id: cId } }); + setType(data.type || "INDIVIDUAL"); + setDisplayName(data.displayName || ""); + setFirstName(data.firstName || ""); + setLastName(data.lastName || ""); + setCompanyName(data.companyName || ""); + setEmail(data.email || ""); + setPhone(stripPhone(data.phone)); + setTin(data.tin || ""); + setVatReg(data.vatRegistrationNumber || ""); + setBusinessLicense(data.businessLicenseNumber || ""); + setAddress(data.address || ""); + setNotes(data.notes || ""); + } catch { + toast.error("Error", "Failed to load customer"); + } finally { + setLoadingData(false); + } + })(); + }, [id]); + + const handleNext = () => { + if (step === 0 && !displayName.trim()) { + toast.error("Validation", "Display name is required"); + return; + } + if (step === 1 && type === "INDIVIDUAL" && !firstName.trim()) { + toast.error("Validation", "First name is required"); + return; + } + if (step === 1 && type === "COMPANY" && !companyName.trim()) { + toast.error("Validation", "Company name is required"); + return; + } + setStep(step + 1); + }; + + const handleSubmit = async () => { + const body: Record = { + type, + displayName, + email: email || undefined, + phone: phone ? `+251${phone.replace(/^\+/, "")}` : undefined, + tin: tin || undefined, + vatRegistrationNumber: vatReg || undefined, + businessLicenseNumber: businessLicense || undefined, + address: address || undefined, + firstName: firstName || undefined, + lastName: lastName || undefined, + companyName: companyName || undefined, + notes: notes || undefined, + }; + + Object.keys(body).forEach((k) => body[k] === undefined && delete body[k]); + + try { + setSubmitting(true); + const cId = Array.isArray(id) ? id[0] : id; + await api.customers.update({ params: { id: cId }, body }); + toast.success("Success", "Customer updated successfully!"); + nav.back(); + } catch (err: any) { + toast.error("Error", err?.message || "Failed to update customer"); + } finally { + setSubmitting(false); + } + }; + + if (loadingData) { + return ( + + + + + + ); + } + + return ( + + setStep(step - 1)} + onComplete={handleSubmit} + loading={submitting} + completeLabel="Update Customer" + > + {step === 0 && ( + + + Customer Type + + + + {TYPES.map((t) => ( + setType(t)} + className={`flex-1 py-3 rounded-[6px] items-center border ${ + type === t + ? "bg-primary border-primary" + : "bg-card border-border" + }`} + > + + {t === "INDIVIDUAL" ? "Individual" : "Company"} + + + ))} + + + {type === "INDIVIDUAL" && ( + + )} + + + )} + + {step === 1 && ( + + + {type === "INDIVIDUAL" ? "Personal Details" : "Company Details"} + + + {type === "INDIVIDUAL" ? ( + <> + + + + + + + Phone + + + +251 + + + + + ) : ( + <> + + + + + + + Phone + + + +251 + + + + + )} + + + + )} + + {step === 2 && ( + + + Documents + + + + + + + + )} + + {step === 3 && ( + + + + + + )} + + {step === 4 && ( + + + Summary + + + + + Type + + + {type === "INDIVIDUAL" ? "Individual" : "Company"} + + + + + Display Name + + + {displayName} + + + {firstName ? ( + + + First Name + + + {firstName} + + + ) : null} + {lastName ? ( + + + Last Name + + + {lastName} + + + ) : null} + {companyName ? ( + + + Company + + + {companyName} + + + ) : null} + {email ? ( + + + Email + + + {email} + + + ) : null} + {phone ? ( + + + Phone + + + +251{phone} + + + ) : null} + {tin ? ( + + + TIN + + + {tin} + + + ) : null} + {vatReg ? ( + + + VAT Reg + + + {vatReg} + + + ) : null} + {businessLicense ? ( + + + Business License + + + {businessLicense} + + + ) : null} + {address ? ( + + + Address + + + {address} + + + ) : null} + {notes ? ( + + + Notes + + + {notes} + + + ) : null} + + + )} + + + ); +} diff --git a/app/declarations/create.tsx b/app/declarations/create.tsx index b73efa6..24e03a3 100644 --- a/app/declarations/create.tsx +++ b/app/declarations/create.tsx @@ -30,6 +30,8 @@ import { useAuthStore } from "@/lib/auth-store"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { FormFlow } from "@/components/FormFlow"; import { getPlaceholderColor } from "@/lib/colors"; +import { CalendarGrid } from "@/components/CalendarGrid"; +import { getScanData } from "@/lib/scan-cache"; const { height: SCREEN_HEIGHT } = Dimensions.get("window"); @@ -187,6 +189,9 @@ export default function CreateDeclarationScreen() { // Pickers & Modals const [showTypePicker, setShowTypePicker] = useState(false); const [showPeriodPicker, setShowPeriodPicker] = useState(false); + const [showPeriodStart, setShowPeriodStart] = useState(false); + const [showPeriodEnd, setShowPeriodEnd] = useState(false); + const [showDueDate, setShowDueDate] = useState(false); const [showInvoicePicker, setShowInvoicePicker] = useState(false); const [invoices, setInvoices] = useState([]); const [invoiceSearch, setInvoiceSearch] = useState(""); @@ -200,6 +205,29 @@ export default function CreateDeclarationScreen() { { key: "review", label: "Review" }, ]; + useEffect(() => { + const scan = getScanData(); + if (scan?.type === "declaration" && scan.data) { + const d = scan.data; + if (d.type) setType(d.type); + if (d.header?.declarationNumber) setDeclarationNumber(d.header.declarationNumber); + if (d.header?.title) setTitle(d.header.title); + if (d.suggestedTitle) setTitle(d.suggestedTitle); + if (d.suggestedPeriodStart) { + const start = new Date(d.suggestedPeriodStart).toISOString().split("T")[0]; + setPeriodStart(start); + } + if (d.suggestedPeriodEnd) { + const end = new Date(d.suggestedPeriodEnd).toISOString().split("T")[0]; + setPeriodEnd(end); + } + if (d.header?.tin) setTin(d.header.tin); + if (d.header?.taxAccountNumber) setTaxAccountNumber(d.header.taxAccountNumber); + if (d.suggestedFilename) setSuggestedFilename(d.suggestedFilename); + toast.success("Scan Complete", "Declaration data extracted from scan."); + } + }, []); + const openInvoicePicker = async () => { setLoadingInvoices(true); try { @@ -411,28 +439,23 @@ export default function CreateDeclarationScreen() { required /> - setShowPeriodStart(true)} required - flex={1} /> - setShowPeriodEnd(true)} required - flex={1} /> - setShowDueDate(true)} /> @@ -785,6 +808,45 @@ export default function CreateDeclarationScreen() { /> ))} + setShowPeriodStart(false)} + title="Period Start" + > + { + setPeriodStart(v); + setShowPeriodStart(false); + }} + /> + + setShowPeriodEnd(false)} + title="Period End" + > + { + setPeriodEnd(v); + setShowPeriodEnd(false); + }} + /> + + setShowDueDate(false)} + title="Due Date" + > + { + setDueDate(v); + setShowDueDate(false); + }} + /> + ); } diff --git a/app/declarations/index.tsx b/app/declarations/index.tsx index 60580d3..4765f7f 100644 --- a/app/declarations/index.tsx +++ b/app/declarations/index.tsx @@ -14,12 +14,13 @@ import { api } from "@/lib/api"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Plus, Search, FileText, Calendar, ChevronRight } from "@/lib/icons"; +import { Plus, Search, FileText } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { EmptyState } from "@/components/EmptyState"; import { useColorScheme } from "nativewind"; import { getPlaceholderColor } from "@/lib/colors"; +import { CreateMethodSheet } from "@/components/CreateMethodSheet"; const TYPE_OPTIONS = ["All", "VAT", "WITHHOLDING_TAX"]; const STATUS_OPTIONS = ["All", "DRAFT", "SUBMITTED", "PAID", "CANCELLED"]; @@ -69,6 +70,7 @@ export default function DeclarationsScreen() { const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState("All"); const [statusFilter, setStatusFilter] = useState("All"); + const [showCreateSheet, setShowCreateSheet] = useState(false); const fetchPage = useCallback( async (pageNum: number, replace = false) => { @@ -178,7 +180,7 @@ export default function DeclarationsScreen() { + nav.back()} + className="mt-4 border border-border w-3/4 rounded-[12px] py-3 flex-row justify-center items-center" + > + Go Back + + + ); + } + + return ( + + {previewUri ? ( + + + + + + Preview + + setPreviewUri(null)} + className="h-12 w-12 rounded-full bg-black/40 items-center justify-center border border-white/20" + > + + + + + + + + + + + + ) : ( + + + {/* Top bar */} + + setTorch(!torch)} + className={`h-12 w-12 rounded-full items-center justify-center border border-white/20 ${torch ? "bg-primary" : "bg-black/40"}`} + > + + + + nav.back()} + className="h-12 w-12 rounded-full bg-black/40 items-center justify-center border border-white/20" + > + + + + + {/* Scan Frame */} + + + + + + + {/* Capture Button */} + + + + + + + Tap to Capture + + + + + + )} + + ); +} diff --git a/app/edit-profile.tsx b/app/edit-profile.tsx index 81c58b7..74c80b2 100644 --- a/app/edit-profile.tsx +++ b/app/edit-profile.tsx @@ -2,31 +2,57 @@ import React, { useState } from "react"; import { View, ScrollView, - Pressable, TextInput, ActivityIndicator, - useColorScheme, + StyleSheet, } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; +import { useColorScheme } from "nativewind"; import { AppRoutes } from "@/lib/routes"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { ScreenWrapper } from "@/components/ScreenWrapper"; -import { ArrowLeft, User, Mail, Check } from "@/lib/icons"; +import { StandardHeader } from "@/components/StandardHeader"; import { useAuthStore } from "@/lib/auth-store"; import { api } from "@/lib/api"; import { toast } from "@/lib/toast-store"; +const S = StyleSheet.create({ + input: { + paddingHorizontal: 14, + paddingVertical: 13, + fontSize: 15, + fontWeight: "500", + borderRadius: 10, + borderWidth: 1, + 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)", + }; +} + export default function EditProfileScreen() { const nav = useSirouRouter(); const { user, updateUser } = useAuthStore(); - const isDark = useColorScheme() === "dark"; - const iconColor = isDark ? "#94a3b8" : "#64748b"; + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + const c = useInputColors(); const [loading, setLoading] = useState(false); const [firstName, setFirstName] = useState(user?.firstName || ""); const [lastName, setLastName] = useState(user?.lastName || ""); + const initials = `${user?.firstName?.[0] || ""}${user?.lastName?.[0] || ""}`.toUpperCase(); + const handleSave = async () => { if (!firstName.trim() || !lastName.trim()) { toast.error("Error", "First and last name are required"); @@ -53,66 +79,57 @@ export default function EditProfileScreen() { return ( - - nav.back()} - className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" - > - - - - Edit Profile - - - + - + + + + {initials || "?"} + + + + {user?.firstName} {user?.lastName} + + + {user?.email} + + + + First Name - - - - {firstName.trim().length > 0 && ( - - )} - + Last Name - - - - {lastName.trim().length > 0 && ( - - )} - + {user?.email && ( @@ -120,40 +137,47 @@ export default function EditProfileScreen() { Email - - - - {user.email} - + + {user.email} )} + - - - - nav.back()} - className="h-12 rounded-[8px] border border-border items-center justify-center" - disabled={loading} - > - - Cancel + + + + diff --git a/app/invoices/[id].tsx b/app/invoices/[id].tsx index f58f2ff..abbe7f1 100644 --- a/app/invoices/[id].tsx +++ b/app/invoices/[id].tsx @@ -8,6 +8,7 @@ import { Pressable, Modal, Dimensions, + Image, } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; @@ -28,11 +29,12 @@ import { X, MoreVertical, FileText, - Receipt, CreditCard, ChevronRight, Check, Edit, + Camera, + ArrowUpRight, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; @@ -40,7 +42,10 @@ import { api, BASE_URL } from "@/lib/api"; import { toast } from "@/lib/toast-store"; import { useAuthStore } from "@/lib/auth-store"; import { ActionModal } from "@/components/ActionModal"; +import { EmptyState } from "@/components/EmptyState"; +import { WebView } from "react-native-webview"; import { UploadIcon } from "lucide-react-native"; +import ticketImage from "@/assets/ticket.png"; const { height: SCREEN_HEIGHT } = Dimensions.get("window"); @@ -56,7 +61,13 @@ export default function InvoiceDetailScreen() { const [showShareSheet, setShowShareSheet] = useState(false); const [showMoreSheet, setShowMoreSheet] = useState(false); const [sharing, setSharing] = useState(false); - const [activeTab, setActiveTab] = useState<"details" | "activity">("details"); + const [activeTab, setActiveTab] = useState<"details" | "items" | "image">( + "details", + ); + const [showImageFullScreen, setShowImageFullScreen] = useState(false); + const [imageLoading, setImageLoading] = useState(false); + + const token = useAuthStore((state) => state.token); useFocusEffect( useCallback(() => { @@ -163,7 +174,11 @@ export default function InvoiceDetailScreen() { // Robust data extraction const originalData = invoice.scannedData?.originalData || {}; const items = - (invoice.items?.length > 0 ? invoice.items : originalData.items) || []; + (invoice.items?.length > 0 + ? invoice.items + : invoice.scannedData?.items?.length > 0 + ? invoice.scannedData.items + : originalData.items) || []; const taxAmountValue = Number( typeof invoice.taxAmount === "object" @@ -208,6 +223,25 @@ export default function InvoiceDetailScreen() { CANCELLED: "Cancelled", }; + // Scanned image URL — try several common fields + const scannedImageRaw = + invoice.scannedData?.imageUrl || + invoice.scannedData?.image || + invoice.scannedData?.imagePath || + invoice.scannedData?.originalData?.imageUrl || + invoice.imageUrl || + invoice.imagePath || + invoice.receiptPath || + null; + + const scannedImageUrl = scannedImageRaw + ? scannedImageRaw.startsWith("http") + ? scannedImageRaw + : `${BASE_URL}${scannedImageRaw.replace(/^\//, "")}` + : null; + + const hasScannedImage = Boolean(invoice?.isScanned && scannedImageUrl); + const customerName = ( invoice.customerName?.replace("Customer Name: ", "") || "Walking Client" ).trim(); @@ -218,6 +252,8 @@ export default function InvoiceDetailScreen() { ? new Date(invoice.issueDate) : new Date(invoice.createdAt); + const paidDate = invoice.paidDate ? new Date(invoice.paidDate) : null; + const formatLongDate = (d: Date) => d.toLocaleDateString("en-US", { day: "numeric", @@ -250,96 +286,13 @@ export default function InvoiceDetailScreen() { - - - - - - - - - - $ - - - - + - {isPaid ? "Payment Date" : "Due Date"} + Due Date - {isPaid - ? `Paid at ${formatLongDate(paymentDate)}` - : formatLongDate(paymentDate)} + {formatLongDate(paymentDate)} + {paidDate && ( + + + Paid Date + + + + + {formatLongDate(paidDate)} + + + + )} @@ -428,23 +392,39 @@ export default function InvoiceDetailScreen() { )} - setActiveTab("activity")} - className="pb-2.5" - > + setActiveTab("items")} className="pb-2.5"> - Activity Log + Items - {activeTab === "activity" && ( + {activeTab === "items" && ( )} + {hasScannedImage && ( + setActiveTab("image")} + className="pb-2.5" + > + + Image + + {activeTab === "image" && ( + + )} + + )} @@ -479,17 +459,18 @@ export default function InvoiceDetailScreen() { - - - Notes - - - {invoice.notes || "-"} - - + {invoice.notes && ( + + + Note + + + + {invoice.notes} + + + + )} {items.length > 0 ? ( @@ -527,7 +508,7 @@ export default function InvoiceDetailScreen() { className="text-foreground text-[14px] font-sans-bold" numberOfLines={1} > - {item.description || `Item ${idx + 1}`} + {item.description || "No item"} {qty} ×{" "} @@ -619,79 +600,76 @@ export default function InvoiceDetailScreen() { - ) : ( + ) : activeTab === "image" ? ( - {items.length > 0 ? ( + {hasScannedImage ? ( - Items + Scanned Document - - {items.map((item: any, idx: number) => { - const qty = Number( - item.quantity?.value || item.quantity || 1, - ); - const unitPrice = Number( - item.unitPrice?.value || item.unitPrice || 0, - ); - const lineTotal = Number( - item.total?.value || item.total || qty * unitPrice, - ); - return ( - - - - - - - {item.description || `Item ${idx + 1}`} - - - {qty} ×{" "} - {unitPrice.toLocaleString("en-US", { - minimumFractionDigits: 2, - })}{" "} - {invoice.currency || "USD"} - - - - {lineTotal.toLocaleString("en-US", { - minimumFractionDigits: 2, - })}{" "} - {invoice.currency || "USD"} + setShowImageFullScreen(true)} + className="rounded-[10px] overflow-hidden border border-border bg-card active:opacity-80" + > + setImageLoading(true)} + onLoadEnd={() => setImageLoading(false)} + onError={() => { + setImageLoading(false); + toast.error( + "Image Error", + "Failed to load scanned image.", + ); + }} + renderError={() => ( + + + + Failed to load image - ); - })} - + )} + /> + {imageLoading && ( + + + + )} + + + + + + Tap to view full screen + ) : ( - + - No activity yet + No image available - Events related to this invoice will show up here. + The scanned image could not be found. )} - )} + ) : null} + {/* Full screen image viewer */} + setShowImageFullScreen(false)} + > + setShowImageFullScreen(false)} + > + + {scannedImageUrl && ( + + )} + + setShowImageFullScreen(false)} + className="absolute top-12 right-5 h-10 w-10 rounded-full bg-black/60 items-center justify-center border border-white/20" + > + + + + + setShowDeleteModal(false)} diff --git a/app/invoices/create.tsx b/app/invoices/create.tsx index de9bb89..5e9d7bf 100644 --- a/app/invoices/create.tsx +++ b/app/invoices/create.tsx @@ -29,6 +29,7 @@ import * as ImagePicker from "expo-image-picker"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { CalendarGrid } from "@/components/CalendarGrid"; import { CustomerPicker } from "@/components/CustomerPicker"; +import { ConfirmSubmitModal } from "@/components/ConfirmSubmitModal"; import { getPlaceholderColor } from "@/lib/colors"; import { getScanData } from "@/lib/scan-cache"; @@ -155,11 +156,15 @@ export default function CreateInvoiceScreen() { const [step, setStep] = useState(0); const [submitting, setSubmitting] = useState(false); const [scanFailures, setScanFailures] = useState(0); + const [showConfirm, setShowConfirm] = useState(false); + const [scanRecordId, setScanRecordId] = useState(null); const [invoiceNumber, setInvoiceNumber] = useState(""); + const [customerId, setCustomerId] = useState(""); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); + const [selectedCustomers, setSelectedCustomers] = useState([]); const [description, setDescription] = useState(""); const [currency, setCurrency] = useState("ETB "); const [type, setType] = useState("SALES"); @@ -198,41 +203,42 @@ export default function CreateInvoiceScreen() { d.setDate(d.getDate() + 30); setDueDate(d.toISOString().split("T")[0]); - const scanData = getScanData(); - if (scanData) { - if (scanData.invoiceNumber) setInvoiceNumber(scanData.invoiceNumber); - const name = - scanData.customerName - ?.trim() - ?.replace(/^(Customer Name:|Bill To:)\s*/i, "") || ""; - if (name) setCustomerName(name); - if (scanData.customerEmail) setCustomerEmail(scanData.customerEmail); - if (scanData.customerPhone) setCustomerPhone(scanData.customerPhone.replace(/^\+251/, "")); - if (scanData.description) setDescription(scanData.description); - if (scanData.currency) setCurrency(scanData.currency); - if (scanData.taxAmount != null) setTaxAmount(String(scanData.taxAmount)); - if (scanData.issueDate) { - try { - setIssueDate( - new Date(scanData.issueDate).toISOString().split("T")[0], - ); - } catch (_) {} - } - if (scanData.dueDate) { - try { - setDueDate(new Date(scanData.dueDate).toISOString().split("T")[0]); - } catch (_) {} - } - if (scanData.items && scanData.items.length > 0) { - setItems( - scanData.items.map((item: any, idx: number) => ({ - id: idx + 1, - description: item.description || "", - qty: String(item.quantity || "1"), - price: String(item.unitPrice || item.total || ""), - })), + const payload = getScanData(); + if (!payload || payload.type !== "invoice" || !payload.id) return; + setScanRecordId(payload.id); + const scanData = payload.data || {}; + if (scanData.invoiceNumber) setInvoiceNumber(scanData.invoiceNumber); + const name = + scanData.customerName + ?.trim() + ?.replace(/^(Customer Name:|Bill To:)\s*/i, "") || ""; + if (name) setCustomerName(name); + if (scanData.customerEmail) setCustomerEmail(scanData.customerEmail); + if (scanData.customerPhone) setCustomerPhone(scanData.customerPhone.replace(/^\+251/, "")); + if (scanData.description) setDescription(scanData.description); + if (scanData.currency) setCurrency(scanData.currency); + if (scanData.taxAmount != null) setTaxAmount(String(scanData.taxAmount)); + if (scanData.issueDate) { + try { + setIssueDate( + new Date(scanData.issueDate).toISOString().split("T")[0], ); - } + } catch (_) {} + } + if (scanData.dueDate) { + try { + setDueDate(new Date(scanData.dueDate).toISOString().split("T")[0]); + } catch (_) {} + } + if (scanData.items && scanData.items.length > 0) { + setItems( + scanData.items.map((item: any, idx: number) => ({ + id: idx + 1, + description: item.description || "", + qty: String(item.quantity || "1"), + price: String(item.unitPrice || item.total || ""), + })), + ); } }, []); @@ -290,6 +296,7 @@ export default function CreateInvoiceScreen() { throw new Error(scanResult.message || "Extraction failed"); toast.success("Success!", "Data extracted."); const ocr = scanResult.data || {}; + if (scanResult.invoiceId) setScanRecordId(scanResult.invoiceId); if (ocr.invoiceNumber) setInvoiceNumber(ocr.invoiceNumber); const name = (ocr.customerName?.trim() || "").replace( /^(Customer Name:|Bill To:)\s*/i, @@ -379,44 +386,51 @@ export default function CreateInvoiceScreen() { } setSubmitting(true); try { - await api.invoices.create({ - body: { - invoiceNumber, - customerName, - customerEmail, - customerPhone: customerPhone ? `+251${customerPhone}` : "", - amount: Number(total.toFixed(2)), - currency, - type, - status, - issueDate: new Date(issueDate).toISOString(), - dueDate: new Date(dueDate).toISOString(), - description, - notes, - taxAmount: parseFloat(taxAmount) || 0, - discountAmount: parseFloat(discountAmount) || 0, - isScanned: false, - scannedData: { sellerTIN: "123456", items: [] }, - items: validItems.map((item) => ({ - description: item.description.trim(), - quantity: parseFloat(item.qty) || 0, - unitPrice: parseFloat(item.price) || 0, - total: Number( - ( - (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0) - ).toFixed(2), - ), - })), - }, - }); - toast.success("Success", "Invoice created!"); + const body = { + invoiceNumber, + customerName, + customerEmail, + customerPhone: customerPhone ? `+251${customerPhone}` : "", + amount: Number(total.toFixed(2)), + currency, + type, + status, + issueDate: new Date(issueDate).toISOString(), + dueDate: new Date(dueDate).toISOString(), + description, + notes, + taxAmount: parseFloat(taxAmount) || 0, + discountAmount: parseFloat(discountAmount) || 0, + isScanned: !!scanRecordId, + scannedData: scanRecordId + ? undefined + : { sellerTIN: "123456", items: [] }, + items: validItems.map((item) => ({ + description: item.description.trim(), + quantity: parseFloat(item.qty) || 0, + unitPrice: parseFloat(item.price) || 0, + total: Number( + ( + (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0) + ).toFixed(2), + ), + })), + }; + + if (scanRecordId) { + await api.invoices.update({ params: { id: scanRecordId }, body }); + toast.success("Success", "Invoice updated!"); + } else { + await api.invoices.create({ body }); + toast.success("Success", "Invoice created!"); + } nav.back(); } catch (error: any) { const msg = error?.response?.data?.message || error?.data?.message || error?.message || - "Failed to create invoice"; + (scanRecordId ? "Failed to update invoice" : "Failed to create invoice"); toast.error("Error", msg); throw error; } finally { @@ -435,9 +449,9 @@ export default function CreateInvoiceScreen() { currentStep={step} onNext={handleNext} onBack={() => setStep(step - 1)} - onComplete={handleSubmit} + onComplete={() => setShowConfirm(true)} loading={submitting} - completeLabel="Create Invoice" + completeLabel={scanRecordId ? "Update Invoice" : "Create Invoice"} > {step === 0 && ( @@ -470,11 +484,20 @@ export default function CreateInvoiceScreen() { Customer Name { - setCustomerName(c.name); - setCustomerEmail(c.email); - setCustomerPhone(c.phone.replace("+251", "")); + selectedIds={customerId ? [customerId] : []} + selectedCustomers={selectedCustomers} + onSelect={(ids, customers) => { + setCustomerId(ids[0] || ""); + setSelectedCustomers(customers); + if (customers[0]) { + setCustomerName(customers[0].name); + setCustomerEmail(customers[0].email); + setCustomerPhone(customers[0].phone?.replace("+251", "") || ""); + } else { + setCustomerName(""); + setCustomerEmail(""); + setCustomerPhone(""); + } }} placeholder="Select or search for a customer" /> diff --git a/app/invoices/edit.tsx b/app/invoices/edit.tsx index 76efed4..bc35bac 100644 --- a/app/invoices/edit.tsx +++ b/app/invoices/edit.tsx @@ -160,9 +160,11 @@ export default function EditInvoiceScreen() { const [step, setStep] = useState(0); const [invoiceNumber, setInvoiceNumber] = useState(""); + const [customerId, setCustomerId] = useState(""); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); + const [selectedCustomers, setSelectedCustomers] = useState([]); const [currency, setCurrency] = useState("ETB"); const [type, setType] = useState("SALES"); const [notes, setNotes] = useState(""); @@ -383,11 +385,20 @@ export default function EditInvoiceScreen() { Customer Name { - setCustomerName(c.name); - setCustomerEmail(c.email); - setCustomerPhone(c.phone.replace("+251", "")); + selectedIds={customerId ? [customerId] : []} + selectedCustomers={selectedCustomers} + onSelect={(ids, customers) => { + setCustomerId(ids[0] || ""); + setSelectedCustomers(customers); + if (customers[0]) { + setCustomerName(customers[0].name); + setCustomerEmail(customers[0].email); + setCustomerPhone(customers[0].phone?.replace("+251", "") || ""); + } else { + setCustomerName(""); + setCustomerEmail(""); + setCustomerPhone(""); + } }} placeholder="Select or search for a customer" /> diff --git a/app/notifications/index.tsx b/app/notifications/index.tsx index d525cbf..23e4432 100644 --- a/app/notifications/index.tsx +++ b/app/notifications/index.tsx @@ -1,19 +1,58 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { View, ActivityIndicator, FlatList, RefreshControl } from "react-native"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; +import { View, ActivityIndicator, FlatList, RefreshControl, Pressable } from "react-native"; import { Text } from "@/components/ui/text"; -import { Card, CardContent } from "@/components/ui/card"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { api } from "@/lib/api"; import { EmptyState } from "@/components/EmptyState"; +import { Bell, Clock } from "@/lib/icons"; type NotificationItem = { id: string; title?: string; body?: string; - message?: string; + icon?: string; + url?: string; + sentAt?: string; createdAt?: string; - read?: boolean; + isSent?: boolean; +}; + +function formatRelativeTime(dateString: string): string { + const now = new Date(); + const date = new Date(dateString); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return "Just now"; + if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? "s" : ""} ago`; + if (diffHours < 24) return `${diffHours} hr${diffHours > 1 ? "s" : ""} ago`; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays} days ago`; + return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); +} + +function getDateGroup(dateString: string): string { + const now = new Date(); + const date = new Date(dateString); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + const itemDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + + if (itemDate.getTime() === today.getTime()) return "Today"; + if (itemDate.getTime() === yesterday.getTime()) return "Yesterday"; + if (now.getTime() - itemDate.getTime() < 7 * 86400000) return "This Week"; + return date.toLocaleDateString("en-US", { month: "long", year: "numeric" }); +} + +type SectionItem = { + type: "header" | "item"; + key: string; + item?: NotificationItem; + isLast?: boolean; }; export default function NotificationsScreen() { @@ -64,26 +103,85 @@ export default function NotificationsScreen() { if (!loading && !loadingMore && hasMore) fetchNotifications(page + 1, "more"); }; - const renderItem = ({ item }: { item: NotificationItem }) => { - const message = item.body ?? item.message ?? ""; - const time = item.createdAt - ? new Date(item.createdAt).toLocaleString() + const grouped = useMemo(() => { + const groups: Record = {}; + for (const item of items) { + const dateStr = item.sentAt || item.createdAt; + const group = dateStr ? getDateGroup(dateStr) : "Other"; + if (!groups[group]) groups[group] = []; + groups[group].push(item); + } + return Object.entries(groups); + }, [items]); + + const sections = useMemo(() => { + const data: SectionItem[] = []; + for (const [title, groupItems] of grouped) { + data.push({ type: "header", key: `header-${title}` }); + groupItems.forEach((item, idx) => { + data.push({ + type: "item", + key: item.id, + item, + isLast: idx === groupItems.length - 1, + }); + }); + } + return data; + }, [grouped]); + + const renderSectionHeader = (title: string) => ( + + + {title} + + + ); + + const renderItem = ({ item, isLast }: { item: NotificationItem; isLast: boolean }) => { + const time = item.sentAt || item.createdAt + ? formatRelativeTime(item.sentAt || item.createdAt!) : ""; + const iconName = item.icon || "bell"; + return ( - - - - {item.title ?? "Notification"} - - {message ? ( - {message} - ) : null} - {time ? ( - {time} - ) : null} - - + + + {/* Icon */} + + + + + {/* Content */} + + + {item.title || "Notification"} + + {item.body ? ( + + {item.body} + + ) : null} + + + {/* Time + Unread dot */} + + {time ? ( + + {time} + + ) : null} + + + + ); }; @@ -97,24 +195,29 @@ export default function NotificationsScreen() { {loading ? ( - + ) : ( i.id} - renderItem={renderItem} - contentContainerStyle={{ padding: 16, paddingBottom: 32 }} + data={sections} + keyExtractor={(i) => i.key} + renderItem={({ item }) => { + if (item.type === "header") { + return renderSectionHeader(item.key.replace("header-", "")); + } + return renderItem({ item: item.item!, isLast: item.isLast! }); + }} + contentContainerStyle={{ paddingBottom: 32 }} onEndReached={onEndReached} onEndReachedThreshold={0.4} refreshControl={ } ListEmptyComponent={ - + @@ -122,7 +225,7 @@ export default function NotificationsScreen() { ListFooterComponent={ loadingMore ? ( - + ) : null } diff --git a/app/payment-requests/[id].tsx b/app/payment-requests/[id].tsx index e44fc3b..d204d79 100644 --- a/app/payment-requests/[id].tsx +++ b/app/payment-requests/[id].tsx @@ -1,17 +1,28 @@ import React, { useState, useCallback } from "react"; -import { View, ScrollView, ActivityIndicator } from "react-native"; +import { View, ScrollView, ActivityIndicator, Pressable, TextInput, Modal } 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 { User, Calendar, Clock, Building2, Hash, Send } from "@/lib/icons"; +import { User, Calendar, Clock, Building2, Send, Pencil, ChevronRight } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { api } from "@/lib/api"; import { useColorScheme } from "nativewind"; import { toast } from "@/lib/toast-store"; +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)", + }; +} + const STATUS_THEME: Record< string, { label: string; bg: string; text: string; dot: string } @@ -69,11 +80,16 @@ export default function PaymentRequestDetailScreen() { const { id } = useLocalSearchParams(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; + const c = useInputColors(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); + const [showSendModal, setShowSendModal] = useState(false); + const [sendChannel, setSendChannel] = useState("EMAIL"); + const [sendRecipient, setSendRecipient] = useState(""); + const fetch = useCallback(async () => { try { setLoading(true); @@ -97,14 +113,33 @@ export default function PaymentRequestDetailScreen() { }, [fetch]), ); - const handleSendEmail = async () => { + const openSendModal = () => { + setSendChannel("EMAIL"); + setSendRecipient(data?.customerEmail || ""); + setShowSendModal(true); + }; + + const handleSend = async () => { + if (!sendRecipient.trim()) { + toast.error("Validation", "Recipient is required"); + return; + } try { setSending(true); const reqId = Array.isArray(id) ? id[0] : id; - await api.paymentRequests.sendEmail({ params: { id: reqId } }); - toast.success("Sent", "Payment request emailed to customer"); + await api.paymentRequests.send({ + params: { id: reqId }, + body: { + channel: sendChannel, + recipient: sendRecipient.trim(), + }, + headers: { "Content-Type": "application/json" }, + }); + toast.success("Sent", `Payment request sent via ${sendChannel.toLowerCase()}`); + setShowSendModal(false); + fetch(); } catch (err: any) { - toast.error("Error", err?.message || "Failed to send email"); + toast.error("Error", err?.message || "Failed to send payment request"); } finally { setSending(false); } @@ -142,8 +177,20 @@ export default function PaymentRequestDetailScreen() { contentContainerStyle={{ paddingBottom: 120 }} showsVerticalScrollIndicator={false} > + {/* Status Badge */} + + + + + + {theme.label} + + + + + {/* Customer + Dates cluster */} - + @@ -346,28 +393,128 @@ export default function PaymentRequestDetailScreen() { {/* Actions */} - {!data.customerEmail && ( - - No customer email on file + + + Send - )} + + + + {/* Send Modal */} + setShowSendModal(false)} + > + setShowSendModal(false)} + > + + e.stopPropagation()} + > + + + Send Payment Request + + setShowSendModal(false)} + className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center" + > + + + + + {/* Channel Toggle */} + + {(["EMAIL", "PHONE"] as const).map((ch) => ( + { + setSendChannel(ch); + if (ch === "EMAIL") { + setSendRecipient(data?.customerEmail || ""); + } else { + setSendRecipient(data?.customerPhone || ""); + } + }} + className={`flex-1 h-11 rounded-[6px] items-center justify-center border ${ + sendChannel === ch + ? "bg-primary border-primary" + : "bg-card border-border" + }`} + > + + {ch === "EMAIL" ? "Email" : "SMS"} + + + ))} + + + {/* Recipient Input */} + + + {sendChannel === "EMAIL" ? "Email Address" : "Phone Number"} + + + + + + + + + ); } diff --git a/app/payment-requests/create.tsx b/app/payment-requests/create.tsx index a1f72e6..9feed1e 100644 --- a/app/payment-requests/create.tsx +++ b/app/payment-requests/create.tsx @@ -20,23 +20,9 @@ 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"; +import { 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; -}; +type Item = { id: number; description: string; quantity: string; unitPrice: string }; const S = StyleSheet.create({ input: { @@ -80,6 +66,7 @@ function Field({ center = false, flex, multiline = false, + keyboardType, }: { label: string; value: string; @@ -89,6 +76,7 @@ function Field({ center?: boolean; flex?: number; multiline?: boolean; + keyboardType?: "default" | "numeric" | "email-address" | "phone-pad"; }) { const c = useInputColors(); return ( @@ -108,7 +96,7 @@ function Field({ placeholderTextColor={c.placeholder} value={value} onChangeText={onChangeText} - keyboardType={numeric ? "numeric" : "default"} + keyboardType={keyboardType || (numeric ? "numeric" : "default")} multiline={multiline} autoCorrect={false} autoCapitalize="none" @@ -147,15 +135,14 @@ function PickerField({ } const CURRENCIES = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"]; +const CHANNELS = ["EMAIL", "PHONE"]; 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: "paymentMethod", label: "Payment" }, { key: "summary", label: "Summary" }, ]; @@ -165,38 +152,29 @@ export default function CreatePaymentRequestScreen() { const [submitting, setSubmitting] = useState(false); const [paymentRequestNumber, setPaymentRequestNumber] = useState(""); + const [description, setDescription] = useState(""); + const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); - const [customerPhone, setCustomerPhone] = useState(""); + const [customerId, setCustomerId] = useState(""); + + const [channel, setChannel] = useState("EMAIL"); + const [recipient, setRecipient] = 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 [companyPaymentMethodId, setCompanyPaymentMethodId] = useState(""); + const [paymentMethods, setPaymentMethods] = useState([]); + const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false); const [items, setItems] = useState([ - { id: 1, description: "", qty: "1", price: "" }, - ]); - - const [accounts, setAccounts] = useState([ - { - id: 1, - bankName: "", - accountName: "", - accountNumber: "", - currency: "ETB", - }, + { id: 1, description: "", quantity: "1", unitPrice: "" }, ]); const c = useInputColors(); @@ -204,7 +182,8 @@ export default function CreatePaymentRequestScreen() { const [showCurrency, setShowCurrency] = useState(false); const [showIssueDate, setShowIssueDate] = useState(false); const [showDueDate, setShowDueDate] = useState(false); - const [showStatus, setShowStatus] = useState(false); + const [showChannel, setShowChannel] = useState(false); + const [showPaymentMethod, setShowPaymentMethod] = useState(false); useEffect(() => { const year = new Date().getFullYear(); @@ -216,6 +195,27 @@ export default function CreatePaymentRequestScreen() { setDueDate(d.toISOString().split("T")[0]); }, []); + useEffect(() => { + (async () => { + setLoadingPaymentMethods(true); + try { + const res = await api.company.paymentMethods(); + const list = Array.isArray(res) ? res : res?.data || []; + setPaymentMethods(list); + } catch { + setPaymentMethods([]); + } finally { + setLoadingPaymentMethods(false); + } + })(); + }, []); + + useEffect(() => { + if (channel === "EMAIL" && customerEmail && !recipient) { + setRecipient(customerEmail); + } + }, [channel, customerEmail]); + const updateItem = (id: number, field: keyof Item, value: string) => setItems((prev) => prev.map((it) => (it.id === id ? { ...it, [field]: value } : it)), @@ -224,53 +224,24 @@ export default function CreatePaymentRequestScreen() { const addItem = () => setItems((prev) => [ ...prev, - { id: Date.now(), description: "", qty: "1", price: "" }, + { id: Date.now(), description: "", quantity: "1", unitPrice: "" }, ]); 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), + sum + + (parseFloat(item.quantity) || 0) * (parseFloat(item.unitPrice) || 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"); @@ -285,38 +256,29 @@ export default function CreatePaymentRequestScreen() { const handleSubmit = async () => { if (!customerName) { - toast.error("Validation Error", "Please enter a customer name"); + toast.error("Validation", "Please select a customer"); return; } - const formattedPhone = customerPhone ? `+251${customerPhone}` : ""; - const body = { paymentRequestNumber, customerName, - customerEmail, - customerPhone: formattedPhone, - amount: amount ? Number(amount) : Number(computedTotal.toFixed(2)), + customerEmail: customerEmail || undefined, + channel, + recipient, + amount: amount ? Number(amount) : Number(subtotal.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, - })), + companyPaymentMethodId: companyPaymentMethodId || undefined, + customerId: customerId || undefined, + ...(description ? { description } : {}), items: items.map((i) => ({ description: i.description || "Item", - quantity: parseFloat(i.qty) || 0, - unitPrice: parseFloat(i.price) || 0, + quantity: parseFloat(i.quantity) || 0, + unitPrice: parseFloat(i.unitPrice) || 0, total: Number( - ((parseFloat(i.qty) || 0) * (parseFloat(i.price) || 0)).toFixed(2), + ((parseFloat(i.quantity) || 0) * (parseFloat(i.unitPrice) || 0)).toFixed(2), ), })), }; @@ -339,6 +301,12 @@ export default function CreatePaymentRequestScreen() { } }; + const paymentMethodLabel = companyPaymentMethodId + ? paymentMethods.find((pm: any) => pm.id === companyPaymentMethodId) + ?.label || paymentMethods.find((pm: any) => pm.id === companyPaymentMethodId) + ?.providerName || "Selected" + : "Select"; + return ( @@ -377,48 +346,49 @@ export default function CreatePaymentRequestScreen() { Customer Information - + - Customer Name + Customer { - setCustomerName(c.name); - setCustomerEmail(c.email); - setCustomerPhone(c.phone.replace("+251", "")); + selectedIds={customerId ? [customerId] : []} + selectedCustomers={ + customerId + ? [ + { + id: customerId, + name: customerName, + email: customerEmail, + phone: "", + }, + ] + : [] + } + onSelect={(ids, customers) => { + setCustomerId(ids[0] || ""); + setCustomerName(customers[0]?.name || ""); + setCustomerEmail(customers[0]?.email || ""); }} - placeholder="Select or search for a customer" + placeholder="Select a customer" /> - - - - - - Phone - - - +251 - - - + setShowChannel(true)} + /> + )} @@ -447,17 +417,12 @@ export default function CreatePaymentRequestScreen() { value={currency} onPress={() => setShowCurrency(true)} /> - setShowStatus(true)} - /> 0 ? `${currency} ${subtotal.toFixed(2)}` : "Enter amount"} numeric /> @@ -506,142 +471,144 @@ export default function CreatePaymentRequestScreen() { placeholder="1" numeric center - value={item.qty} - onChangeText={(v) => updateItem(item.id, "qty", v)} + value={item.quantity} + onChangeText={(v) => updateItem(item.id, "quantity", v)} flex={1} /> updateItem(item.id, "price", v)} + value={item.unitPrice} + onChangeText={(v) => updateItem(item.id, "unitPrice", v)} flex={3} /> + {parseFloat(item.quantity) > 0 && parseFloat(item.unitPrice) > 0 && ( + + + = {currency}{" "} + {( + (parseFloat(item.quantity) || 0) * + (parseFloat(item.unitPrice) || 0) + ).toFixed(2)} + + + )} ))} + {subtotal > 0 && ( + + + Subtotal + + + {currency} {subtotal.toFixed(2)} + + + )} )} {step === 4 && ( - - - Accounts - - - - - Add + + Payment Method + + + + + Company Payment Method - - - - {accounts.map((acc, index) => ( - { + if (paymentMethods.length > 0) { + setShowPaymentMethod(true); + } else { + toast.error( + "No Methods", + "No payment methods available. Please configure one in company settings.", + ); + } + }} + className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" + style={{ backgroundColor: c.bg, borderColor: c.border }} > - - - Account {index + 1} - - removeAccount(acc.id)} - hitSlop={8} - > - - - - updateAccount(acc.id, "bankName", v)} - placeholder="e.g. Yaltopia Bank" - /> - - - updateAccount(acc.id, "accountName", v) - } - placeholder="e.g. Yaltopia Tech PLC" - flex={1} - /> - - - - updateAccount(acc.id, "accountNumber", v) - } - placeholder="123456789" - flex={2} - /> - updateAccount(acc.id, "currency", v)} - placeholder="ETB" - flex={1} - /> - - - ))} + {loadingPaymentMethods ? ( + + ) : ( + <> + + {paymentMethodLabel} + + + + )} + + )} {step === 5 && ( - + - Totals + Summary - - - - Subtotal + + + {description ? ( + + ) : null} + + + + + + + + {items.length > 0 && ( + + + Items ({items.length}) + + {items.map((item, i) => ( + + + {item.description || `Item ${i + 1}`} + + + {item.quantity} × {currency}{" "} + {parseFloat(item.unitPrice || "0").toFixed(2)} + + + ))} + + )} + {paymentMethodLabel !== "Select" && ( + + )} + + + + Total Amount - + {currency}{" "} - {subtotal.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} - - - - - - - - - Total - - - {currency}{" "} - {(amount ? Number(amount) : computedTotal).toLocaleString( + {(amount ? Number(amount) : subtotal).toLocaleString( "en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }, )} @@ -650,172 +617,6 @@ export default function CreatePaymentRequestScreen() { )} - {step === 6 && ( - - - - )} - - {step === 7 && ( - <> - - - Summary - - - - - Request Number - - - {paymentRequestNumber} - - - - - Customer - - - {customerName} - - - {customerEmail ? ( - - - Email - - - {customerEmail} - - - ) : null} - {customerPhone ? ( - - - Phone - - - +251{customerPhone} - - - ) : null} - {description ? ( - - - Description - - - {description} - - - ) : null} - - - Issue Date - - - {issueDate} - - - - - Due Date - - - {dueDate || "Not set"} - - - - - Status - - - {status} - - - {items.length > 0 && ( - - - Items ({items.length}) - - {items.map((item, i) => ( - - - {item.description || `Item ${i + 1}`} - - - {item.qty} × {currency}{" "} - {parseFloat(item.price || "0").toFixed(2)} - - - ))} - - )} - {notes ? ( - - - Notes - - - {notes} - - - ) : null} - {parseFloat(taxAmount) > 0 && ( - - - Tax - - - {currency} {parseFloat(taxAmount).toFixed(2)} - - - )} - {parseFloat(discountAmount) > 0 && ( - - - Discount - - - -{currency} {parseFloat(discountAmount).toFixed(2)} - - - )} - - - - Total Amount - - - {currency}{" "} - {(amount ? Number(amount) : computedTotal).toLocaleString( - "en-US", - { minimumFractionDigits: 2, maximumFractionDigits: 2 }, - )} - - - - - - )} setShowStatus(false)} - title="Select Status" + visible={showChannel} + onClose={() => setShowChannel(false)} + title="Invite Channel" > - {["DRAFT", "SENT", "OPENED", "PAID", "EXPIRED", "CANCELLED"].map( - (s) => ( - { - setStatus(v); - setShowStatus(false); - }} - /> - ), - )} + {CHANNELS.map((ch) => ( + { + setChannel(v); + setShowChannel(false); + }} + /> + ))} + + + setShowPaymentMethod(false)} + title="Payment Method" + > + {paymentMethods.map((pm: any) => ( + { + setCompanyPaymentMethodId(v); + setShowPaymentMethod(false); + }} + /> + ))} ); } + +function SummaryRow({ + label, + value, + multiline, +}: { + label: string; + value: string; + multiline?: boolean; +}) { + return ( + + + {label} + + + {value} + + + ); +} diff --git a/app/payment-requests/edit.tsx b/app/payment-requests/edit.tsx new file mode 100644 index 0000000..653ea2b --- /dev/null +++ b/app/payment-requests/edit.tsx @@ -0,0 +1,781 @@ +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 { useLocalSearchParams } from "expo-router"; + +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 { ChevronDown, Plus, Trash2 } from "@/lib/icons"; + +type Item = { id: number; description: string; quantity: string; unitPrice: 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, + keyboardType, +}: { + label: string; + value: string; + onChangeText: (v: string) => void; + placeholder: string; + numeric?: boolean; + center?: boolean; + flex?: number; + multiline?: boolean; + keyboardType?: "default" | "numeric" | "email-address" | "phone-pad"; +}) { + const c = useInputColors(); + return ( + + + {label} + + + + ); +} + +function PickerField({ + label, + value, + onPress, +}: { + label: string; + value: string; + onPress: () => void; +}) { + const c = useInputColors(); + return ( + + + {label} + + + + {value} + + + + + ); +} + +const CURRENCIES = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"]; +const CHANNELS = ["EMAIL", "PHONE"]; + +const STEPS = [ + { key: "details", label: "Details" }, + { key: "customer", label: "Customer" }, + { key: "schedule", label: "Schedule" }, + { key: "items", label: "Items" }, + { key: "paymentMethod", label: "Payment" }, + { key: "summary", label: "Summary" }, +]; + +function formatDate(d: string | null | undefined): string { + if (!d) return ""; + try { + return new Date(d).toISOString().split("T")[0]; + } catch { + return ""; + } +} + +export default function EditPaymentRequestScreen() { + const nav = useSirouRouter(); + const { id } = useLocalSearchParams<{ id: string }>(); + const [step, setStep] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [loadingData, setLoadingData] = useState(true); + + const [paymentRequestNumber, setPaymentRequestNumber] = useState(""); + const [description, setDescription] = useState(""); + + const [customerName, setCustomerName] = useState(""); + const [customerEmail, setCustomerEmail] = useState(""); + const [customerId, setCustomerId] = useState(""); + + const [channel, setChannel] = useState("EMAIL"); + const [recipient, setRecipient] = useState(""); + + const [amount, setAmount] = useState(""); + const [currency, setCurrency] = useState("ETB"); + + const [issueDate, setIssueDate] = useState( + new Date().toISOString().split("T")[0], + ); + const [dueDate, setDueDate] = useState(""); + + const [companyPaymentMethodId, setCompanyPaymentMethodId] = useState(""); + const [paymentMethods, setPaymentMethods] = useState([]); + const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false); + + const [items, setItems] = useState([ + { id: 1, description: "", quantity: "1", unitPrice: "" }, + ]); + + const c = useInputColors(); + + const [showCurrency, setShowCurrency] = useState(false); + const [showIssueDate, setShowIssueDate] = useState(false); + const [showDueDate, setShowDueDate] = useState(false); + const [showChannel, setShowChannel] = useState(false); + const [showPaymentMethod, setShowPaymentMethod] = useState(false); + + useEffect(() => { + (async () => { + try { + setLoadingData(true); + const reqId = Array.isArray(id) ? id[0] : id; + if (!reqId) return; + const data = await api.paymentRequests.getById({ params: { id: reqId } }); + setPaymentRequestNumber(data.paymentRequestNumber || ""); + setDescription(data.description || ""); + setCustomerName(data.customerName || ""); + setCustomerEmail(data.customerEmail || ""); + setCustomerId(data.customerId || ""); + setChannel(data.channel || "EMAIL"); + setRecipient(data.recipient || data.customerEmail || ""); + setAmount(data.amount != null ? String(data.amount) : ""); + setCurrency(data.currency || "ETB"); + setIssueDate(formatDate(data.issueDate) || new Date().toISOString().split("T")[0]); + setDueDate(formatDate(data.dueDate)); + setCompanyPaymentMethodId(data.companyPaymentMethodId || ""); + if (data.items?.length) { + setItems( + data.items.map((it: any, idx: number) => ({ + id: idx + 1, + description: it.description || "", + quantity: String(it.quantity ?? 1), + unitPrice: String(it.unitPrice ?? ""), + })), + ); + } + } catch { + toast.error("Error", "Failed to load payment request"); + } finally { + setLoadingData(false); + } + })(); + }, [id]); + + useEffect(() => { + (async () => { + setLoadingPaymentMethods(true); + try { + const res = await api.company.paymentMethods(); + const list = Array.isArray(res) ? res : res?.data || []; + setPaymentMethods(list); + } catch { + setPaymentMethods([]); + } finally { + setLoadingPaymentMethods(false); + } + })(); + }, []); + + useEffect(() => { + if (channel === "EMAIL" && customerEmail && !recipient) { + setRecipient(customerEmail); + } + }, [channel, customerEmail]); + + 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: "", quantity: "1", unitPrice: "" }, + ]); + + const removeItem = (id: number) => { + if (items.length > 1) setItems((prev) => prev.filter((it) => it.id !== id)); + }; + + const subtotal = useMemo( + () => + items.reduce( + (sum, item) => + sum + + (parseFloat(item.quantity) || 0) * (parseFloat(item.unitPrice) || 0), + 0, + ), + [items], + ); + + 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", "Please select a customer"); + return; + } + + const body = { + paymentRequestNumber, + customerName, + customerEmail: customerEmail || undefined, + channel, + recipient, + amount: amount ? Number(amount) : Number(subtotal.toFixed(2)), + currency, + issueDate: new Date(issueDate).toISOString(), + dueDate: dueDate ? new Date(dueDate).toISOString() : undefined, + companyPaymentMethodId: companyPaymentMethodId || undefined, + customerId: customerId || undefined, + ...(description ? { description } : {}), + items: items.map((i) => ({ + description: i.description || "Item", + quantity: parseFloat(i.quantity) || 0, + unitPrice: parseFloat(i.unitPrice) || 0, + total: Number( + ((parseFloat(i.quantity) || 0) * (parseFloat(i.unitPrice) || 0)).toFixed(2), + ), + })), + }; + + try { + setSubmitting(true); + const reqId = Array.isArray(id) ? id[0] : id; + await api.paymentRequests.update({ + params: { id: reqId }, + body, + headers: { "Content-Type": "application/json" }, + }); + toast.success("Success", "Payment request updated successfully!"); + nav.back(); + } catch (err: any) { + console.error("[PaymentRequestEdit] Error:", err); + toast.error("Error", err?.message || "Failed to update payment request"); + } finally { + setSubmitting(false); + } + }; + + const paymentMethodLabel = companyPaymentMethodId + ? paymentMethods.find((pm: any) => pm.id === companyPaymentMethodId) + ?.label || paymentMethods.find((pm: any) => pm.id === companyPaymentMethodId) + ?.providerName || "Selected" + : "Select"; + + if (loadingData) { + return ( + + + + + + ); + } + + return ( + + setStep(step - 1)} + onComplete={handleSubmit} + loading={submitting} + completeLabel="Update Request" + > + {step === 0 && ( + + + Request Details + + + + + + + )} + + {step === 1 && ( + + + Customer Information + + + + + Customer + + { + setCustomerId(ids[0] || ""); + setCustomerName(customers[0]?.name || ""); + setCustomerEmail(customers[0]?.email || ""); + }} + placeholder="Select a customer" + /> + + setShowChannel(true)} + /> + + + + )} + + {step === 2 && ( + + + Schedule & Currency + + + + setShowIssueDate(true)} + /> + setShowDueDate(true)} + /> + + + setShowCurrency(true)} + /> + + 0 ? `${currency} ${subtotal.toFixed(2)}` : "Enter amount"} + numeric + /> + + + )} + + {step === 3 && ( + + + + Items + + + + + Add + + + + + {items.map((item, index) => ( + + + + Item {index + 1} + + removeItem(item.id)} hitSlop={8}> + + + + updateItem(item.id, "description", v)} + /> + + updateItem(item.id, "quantity", v)} + flex={1} + /> + updateItem(item.id, "unitPrice", v)} + flex={3} + /> + + {parseFloat(item.quantity) > 0 && parseFloat(item.unitPrice) > 0 && ( + + + = {currency}{" "} + {( + (parseFloat(item.quantity) || 0) * + (parseFloat(item.unitPrice) || 0) + ).toFixed(2)} + + + )} + + ))} + + {subtotal > 0 && ( + + + Subtotal + + + {currency} {subtotal.toFixed(2)} + + + )} + + )} + + {step === 4 && ( + + + Payment Method + + + + + Company Payment Method + + { + if (paymentMethods.length > 0) { + setShowPaymentMethod(true); + } else { + toast.error( + "No Methods", + "No payment methods available. Please configure one in company settings.", + ); + } + }} + className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" + style={{ backgroundColor: c.bg, borderColor: c.border }} + > + {loadingPaymentMethods ? ( + + ) : ( + <> + + {paymentMethodLabel} + + + + )} + + + + + )} + + {step === 5 && ( + + + Summary + + + + {description ? ( + + ) : null} + + + + + + + + {items.length > 0 && ( + + + Items ({items.length}) + + {items.map((item, i) => ( + + + {item.description || `Item ${i + 1}`} + + + {item.quantity} × {currency}{" "} + {parseFloat(item.unitPrice || "0").toFixed(2)} + + + ))} + + )} + {paymentMethodLabel !== "Select" && ( + + )} + + + + Total Amount + + + {currency}{" "} + {(amount ? Number(amount) : subtotal).toLocaleString( + "en-US", + { minimumFractionDigits: 2, maximumFractionDigits: 2 }, + )} + + + + + )} + + + setShowCurrency(false)} + title="Select Currency" + > + {CURRENCIES.map((curr) => ( + { + setCurrency(v); + setShowCurrency(false); + }} + /> + ))} + + + setShowChannel(false)} + title="Invite Channel" + > + {CHANNELS.map((ch) => ( + { + setChannel(v); + setShowChannel(false); + }} + /> + ))} + + + setShowPaymentMethod(false)} + title="Payment Method" + > + {paymentMethods.map((pm: any) => ( + { + setCompanyPaymentMethodId(v); + setShowPaymentMethod(false); + }} + /> + ))} + + + setShowIssueDate(false)} + title="Select Issue Date" + > + { + setIssueDate(v); + setShowIssueDate(false); + }} + /> + + + setShowDueDate(false)} + title="Select Due Date" + > + { + setDueDate(v); + setShowDueDate(false); + }} + /> + + + ); +} + +function SummaryRow({ + label, + value, + multiline, +}: { + label: string; + value: string; + multiline?: boolean; +}) { + return ( + + + {label} + + + {value} + + + ); +} diff --git a/app/payments/[id].tsx b/app/payments/[id].tsx index 9dbd9a7..4449d1e 100644 --- a/app/payments/[id].tsx +++ b/app/payments/[id].tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useEffect } from "react"; import { View, ScrollView, @@ -11,7 +11,10 @@ import { Alert, Platform, PermissionsAndroid, + Dimensions, + Image, } from "react-native"; +import { WebView } from "react-native-webview"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Stack, useLocalSearchParams, useFocusEffect } from "expo-router"; @@ -33,8 +36,20 @@ import { Info, Search, ChevronDown, + ChevronRight, X, Scan, + MoreVertical, + Edit, + Package, + Share2, + Mail, + MessageSquare, + Calendar, + Check, + Camera, + ArrowUpRight, + Flag, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; @@ -45,6 +60,8 @@ import { ActionModal } from "@/components/ActionModal"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { CalendarGrid } from "@/components/CalendarGrid"; import { getPlaceholderColor } from "@/lib/colors"; +import ticketImage from "@/assets/ticket.png"; +import { CheckCircle2, CreditCardIcon } from "lucide-react-native"; let SmsAndroid: any = null; if (Platform.OS === "android") { @@ -56,6 +73,8 @@ if (Platform.OS === "android") { } } +const { height: SCREEN_HEIGHT } = Dimensions.get("window"); + const S = StyleSheet.create({ input: { paddingHorizontal: 12, @@ -94,6 +113,19 @@ export default function PaymentDetailScreen() { const [showEditModal, setShowEditModal] = useState(false); const [showInvoicePicker, setShowInvoicePicker] = useState(false); const [editSaving, setEditSaving] = useState(false); + const [showMoreSheet, setShowMoreSheet] = useState(false); + const [activeTab, setActiveTab] = useState<"details" | "items" | "image">( + "details", + ); + const [linkedInvoice, setLinkedInvoice] = useState(null); + const [showImageFullScreen, setShowImageFullScreen] = useState(false); + const [imageLoading, setImageLoading] = useState(false); + const [showFlagModal, setShowFlagModal] = useState(false); + const [flagReason, setFlagReason] = useState<"FAKE" | "SCAM" | "OTHER">( + "FAKE", + ); + const [flagNotes, setFlagNotes] = useState(""); + const [flagging, setFlagging] = useState(false); // Edit form state const [editTxnId, setEditTxnId] = useState(""); @@ -147,6 +179,25 @@ export default function PaymentDetailScreen() { }, [fetchPayment]), ); + useEffect(() => { + const fetchLinked = async () => { + if (!payment?.invoiceId) { + setLinkedInvoice(null); + return; + } + try { + const inv = await api.invoices.getById({ + params: { id: payment.invoiceId }, + }); + setLinkedInvoice(inv); + } catch (err) { + console.log("[PaymentDetail] could not load linked invoice", err); + setLinkedInvoice(null); + } + }; + fetchLinked(); + }, [payment?.invoiceId]); + const handleDelete = () => setShowDeleteModal(true); const confirmDelete = async () => { @@ -360,6 +411,48 @@ export default function PaymentDetailScreen() { } }; + const openFlagModal = () => { + if (!payment) return; + setFlagReason(payment.flagReason || "FAKE"); + setFlagNotes(payment.flagNotes || ""); + setShowMoreSheet(false); + setShowFlagModal(true); + }; + + const handleLinkInvoicePress = () => { + if (payment?.invoiceId) { + toast.warning( + "Already Linked", + "This payment is already linked to an invoice.", + ); + return; + } + openMatchPicker(); + }; + + const handleFlag = async () => { + if (!paymentId) return; + setFlagging(true); + try { + await api.payments.flag({ + params: { id: paymentId }, + body: { flagReason, flagNotes }, + }); + toast.success("Flagged", "Payment has been flagged for review."); + setShowFlagModal(false); + fetchPayment(); + } catch (err: any) { + const msg = + err?.response?.data?.message || + err?.data?.message || + err?.message || + "Failed to flag payment"; + toast.error("Error", msg); + } finally { + setFlagging(false); + } + }; + if (loading) { return ( @@ -406,6 +499,37 @@ export default function PaymentDetailScreen() { const isFlagged = payment.isFlagged === true; + const scannedImageRaw = + payment.scannedData?.imageUrl || + payment.scannedData?.image || + payment.scannedData?.imagePath || + payment.scannedData?.originalData?.imageUrl || + payment.imageUrl || + payment.imagePath || + payment.receiptPath || + null; + + const scannedImageUrl = scannedImageRaw + ? scannedImageRaw.startsWith("http") + ? scannedImageRaw + : `${BASE_URL}${scannedImageRaw.replace(/^\//, "")}` + : null; + + const hasScannedImage = Boolean(payment?.isScanned && scannedImageUrl); + + const hasSms = Boolean( + payment.smsContent || + payment.smsVerified || + payment.smsBody || + payment.verifiedAt || + payment.smsId, + ); + + const linkedItems = + (linkedInvoice?.items?.length > 0 + ? linkedInvoice.items + : linkedInvoice?.scannedData?.originalData?.items) || []; + const filteredInvoices = invoices.filter((inv) => { if (!invoiceSearch) return true; const q = invoiceSearch.toLowerCase(); @@ -415,264 +539,751 @@ export default function PaymentDetailScreen() { ); }); + const formatLongDate = (d: Date) => + d.toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }); + + const ActionOption = ({ + icon, + label, + description, + onPress, + destructive, + }: { + icon: React.ReactNode; + label: string; + description: string; + onPress: () => void; + destructive?: boolean; + }) => ( + + + {icon} + + + + {label} + + + {description} + + + + ); + return ( setShowMoreSheet(true)} + className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" + > + + + } /> - {/* Flagged Alert */} - {isFlagged && ( - - - - - - - Security Flag ({payment.flagReason || "Audit Needed"}) - - - {payment.flagNotes || "System flagged this for manual review."} - - - - )} - - {/* Hero Section */} - - - Total Amount - - - - {amountValue.toLocaleString()} - - - {payment.currency || "USD"} - + {/* Hero Card — illustration overflows the top */} + + + - - - - - + + + {amountValue.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{" "} + + {payment.currency || "USD"} + + + + {payment.transactionId + ? `Txn ${payment.transactionId}` + : payment.paymentMethod || "Direct"} + + + + + + Method + + + {payment.financialInstitutionLogoUrl ? ( + + ) : ( + + )} + + {payment.financialInstitution || + payment.paymentMethod || + "Direct"} + + + + + Sender {payment.senderName || "Unknown"} - - - - - - - Method + + + + Date - - {payment.paymentMethod || "Direct"} + + {formatLongDate(paymentDate)} - - + + + {isFlagged && ( + + + Flagged + + + {payment.flagReason || "Audit Needed"} + + + )} + {payment.isReferenceVerified && ( + + + Reference Verified + + + Yes + + + )} + - {/* Details Section */} - - - - Transaction Details - - - - - - - - Transaction ID - - - {payment.transactionId || "—"} - - - - - - - - - Receiver - - - {payment.receiverName || "—"} - - - - - - - - - Payment Date - - - {paymentDate.toLocaleString()} - - - - - {payment.invoiceId && ( - - nav.go("invoices/[id]", { id: payment.invoiceId }) - } - className="flex-row items-center gap-3 active:opacity-60" + {/* Tabs */} + + + setActiveTab("details")} + className="pb-2.5" + > + + Details + + {activeTab === "details" && ( + + )} + + {hasScannedImage && ( + setActiveTab("image")} + className="pb-2.5" + > + - + Image + + {activeTab === "image" && ( + + )} + + )} + + + + {/* Tab content */} + {activeTab === "details" ? ( + + {/* Transaction Details */} + + + + - Linked Invoice + Transaction ID - - {payment.invoiceId} + + {payment.transactionId || "—"} - - )} + - - - - - Created - - - {new Date(payment.createdAt).toLocaleString()} - + + + + + Payment Method + + + {payment.paymentMethod || "—"} + + + + + + {payment.financialInstitutionLogoUrl ? ( + + ) : ( + + )} + + + Financial Institution + + + {payment.financialInstitution || "—"} + + + + + + + + + Sender + + + {payment.senderName || "—"} + + + + + + + + + Receiver + + + {payment.receiverName || "—"} + + + + + + + + + Payment Date + + + {paymentDate.toLocaleString()} + + + + + + + + + Reference + + + {payment.isReferenceVerified + ? "Verified" + : "Not verified"} + + + + + {isFlagged && ( + + + + + Flagged for review + + + {payment.flagReason ? ( + + + Reason + + + {payment.flagReason} + + + ) : null} + {payment.flagNotes ? ( + + {payment.flagNotes} + + ) : null} + + )} + + {payment.invoiceId ? ( + + nav.go("invoices/[id]", { id: payment.invoiceId }) + } + className="flex-row items-center gap-3 active:opacity-60" + > + + + + Linked Invoice + + + {payment.invoiceId} + + + + + ) : ( + + + Linked Invoice + + + {matching ? ( + + ) : ( + <> + + + Link Invoice + + + )} + + + )} + + + + + + Created + + + {new Date(payment.createdAt).toLocaleString()} + + - - - {/* Notes */} - {payment.notes && ( - - - Notes - - - - - "{payment.notes}" + {/* Note */} + {payment.notes && ( + + + Note - - + + + {payment.notes} + + + + )} + + ) : activeTab === "image" ? ( + + {hasScannedImage ? ( + + + Scanned Document + + setShowImageFullScreen(true)} + className="rounded-[10px] overflow-hidden border border-border bg-card active:opacity-80" + > + setImageLoading(true)} + onLoadEnd={() => setImageLoading(false)} + onError={() => { + setImageLoading(false); + toast.error( + "Image Error", + "Failed to load scanned image.", + ); + }} + renderError={() => ( + + + + Failed to load image + + + )} + /> + {imageLoading && ( + + + + )} + + + + + + Tap to view full screen + + + ) : ( + + + + + + No image available + + + The scanned image could not be found. + + + )} + + ) : ( + + {linkedItems.length > 0 ? ( + + + Items + + + {linkedItems.map((item: any, idx: number) => { + const qty = Number( + item.quantity?.value || item.quantity || 1, + ); + const unitPrice = Number( + item.unitPrice?.value || item.unitPrice || 0, + ); + const lineTotal = Number( + item.total?.value || item.total || qty * unitPrice, + ); + return ( + + + + + + + {item.description || `Item ${idx + 1}`} + + + {qty} ×{" "} + {unitPrice.toLocaleString("en-US", { + minimumFractionDigits: 2, + })}{" "} + {payment.currency || "USD"} + + + + {lineTotal.toLocaleString("en-US", { + minimumFractionDigits: 2, + })}{" "} + {payment.currency || "USD"} + + + ); + })} + + + ) : payment.invoiceId ? ( + + + + Loading items from linked invoice... + + + ) : ( + + + + + + No items linked + + + Link an invoice to see its items here. + + {!payment.invoiceId && ( + + )} + + )} )} - {/* Actions */} - - - {payment.receiptPath && ( - - )} - - - - {!payment.invoiceId && ( - - )} - - - + + + View Receipt + + + )} + {/* Sticky bottom bar — Scan SMS + Link Invoice side by side (like invoice detail) */} + + + {scanningSms ? ( + + ) : ( + <> + + + {hasSms ? "SMS Verified" : "Scan SMS"} + + + )} + + + {matching ? ( + + ) : ( + <> + + + {payment.invoiceId ? "Linked" : "Link Invoice"} + + + )} + + + + {/* More bottom sheet */} + setShowMoreSheet(false)} + > + setShowMoreSheet(false)} + > + + e.stopPropagation()} + > + + + + Payment + + setShowMoreSheet(false)} + className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" + > + + + + + + } + label="Edit Payment" + description="Update amount, method, or dates" + onPress={() => { + setShowMoreSheet(false); + openEdit(); + }} + /> + } + label={isFlagged ? "Update Flag" : "Flag Payment"} + description="Mark this payment for manual review" + onPress={openFlagModal} + /> + } + label="Delete Payment" + description="Permanently remove this record" + onPress={() => { + setShowMoreSheet(false); + handleDelete(); + }} + destructive + /> + + + + + + {/* Edit Modal */} + + {/* Flag Modal */} + setShowFlagModal(false)} + > + setShowFlagModal(false)} + > + e.stopPropagation()} + className="bg-card rounded-t-[36px] overflow-hidden border-t-[3px] border-border/20" + style={{ maxHeight: SCREEN_HEIGHT * 0.8 }} + > + + + + Flag Payment + + setShowFlagModal(false)} + className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" + > + + + + + + + Mark this payment as suspicious. The reason will be visible to + your team and recorded in the audit log. + + + + Reason + + + {(["FAKE", "SCAM", "OTHER"] as const).map((r) => ( + setFlagReason(r)} + className={`h-11 px-3 border rounded-[6px] flex-row items-center justify-between ${ + flagReason === r + ? "border-primary bg-primary/10" + : "border-border" + }`} + style={{ + backgroundColor: + flagReason === r + ? isDark + ? "rgba(234,88,12,0.15)" + : "rgba(234,88,12,0.08)" + : c.bg, + }} + > + + {r === "FAKE" + ? "Fake Payment" + : r === "SCAM" + ? "Scam / Phishing" + : "Other"} + + {flagReason === r && ( + + )} + + ))} + + + + Notes (optional) + + + + + {flagging ? ( + + ) : ( + <> + + + {isFlagged ? "Update Flag" : "Submit Flag"} + + + )} + + + + + + + {/* Full screen image viewer */} + setShowImageFullScreen(false)} + > + setShowImageFullScreen(false)} + > + + {scannedImageUrl && ( + + )} + + setShowImageFullScreen(false)} + className="absolute top-12 right-5 h-10 w-10 rounded-full bg-black/60 items-center justify-center border border-white/20" + > + + + + ); } diff --git a/app/payments/create.tsx b/app/payments/create.tsx index 7a80c05..5cbdf31 100644 --- a/app/payments/create.tsx +++ b/app/payments/create.tsx @@ -8,6 +8,7 @@ import { ActivityIndicator, Platform, PermissionsAndroid, + Switch, } from "react-native"; import { useColorScheme } from "nativewind"; import { Text } from "@/components/ui/text"; @@ -21,8 +22,8 @@ import { EmptyState } from "@/components/EmptyState"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { CalendarGrid } from "@/components/CalendarGrid"; import { FormFlow } from "@/components/FormFlow"; -import { CustomerPicker } from "@/components/CustomerPicker"; import { getPlaceholderColor } from "@/lib/colors"; +import { getScanData } from "@/lib/scan-cache"; let SmsAndroid: any = null; if (Platform.OS === "android") { @@ -118,7 +119,7 @@ function PickerField({ @@ -138,8 +139,24 @@ const PAYMENT_METHODS = [ "DECSI", "Bank Transfer", "Cash", + "Credit Card", "Other", ]; +const FINANCIAL_INSTITUTIONS = ["CBE", "ABYSSINIA", "TELE", "DASHEN"]; + +const PROVIDER_ALIASES: Record = { + cbe: "CBE", + telebirr: "TELE", + tele: "TELE", + dashen: "DASHEN", + abyssinia: "ABYSSINIA", +}; + +function normalizeFinancialInstitution(input: string | undefined | null): string | null { + if (!input) return null; + const key = String(input).trim().toLowerCase(); + return PROVIDER_ALIASES[key] ?? null; +} function parseSmsMessage(body: string) { const text = body.toUpperCase(); @@ -168,23 +185,25 @@ export default function CreatePaymentScreen() { const nav = useSirouRouter(); const [step, setStep] = useState(0); const [submitting, setSubmitting] = useState(false); + const [scanRecordId, setScanRecordId] = useState(null); const [transactionId, setTransactionId] = useState(""); const [amount, setAmount] = useState(""); const [currency, setCurrency] = useState("ETB"); const [paymentMethod, setPaymentMethod] = useState("Telebirr"); + const [financialInstitution, setFinancialInstitution] = useState("CBE"); + const [isReferenceVerified, setIsReferenceVerified] = useState(false); const [paymentDate, setPaymentDate] = useState( new Date().toISOString().split("T")[0], ); const [notes, setNotes] = useState(""); const [selectedInvoice, setSelectedInvoice] = useState(null); - - const [customerName, setCustomerName] = useState(""); - const [customerEmail, setCustomerEmail] = useState(""); - const [customerPhone, setCustomerPhone] = useState(""); + const [customerId, setCustomerId] = useState(""); const [showCurrency, setShowCurrency] = useState(false); const [showPaymentMethod, setShowPaymentMethod] = useState(false); + const [showFinancialInstitution, setShowFinancialInstitution] = + useState(false); const [showPaymentDate, setShowPaymentDate] = useState(false); const [showInvoicePicker, setShowInvoicePicker] = useState(false); @@ -249,6 +268,46 @@ export default function CreatePaymentScreen() { })(); }, []); + useEffect(() => { + const payload = getScanData(); + if (!payload) return; + if (payload.type !== "payment" || !payload.id) return; + setScanRecordId(payload.id); + const scanData = payload.data || {}; + if (scanData.transactionId) + setTransactionId(String(scanData.transactionId)); + if (scanData.amount != null) setAmount(String(scanData.amount)); + if (scanData.currency) setCurrency(scanData.currency); + if (scanData.paymentMethod) setPaymentMethod(scanData.paymentMethod); + if (scanData.provider) { + const normalized = normalizeFinancialInstitution(scanData.provider); + if (normalized) setFinancialInstitution(normalized); + } + if (scanData.paymentDate) { + try { + setPaymentDate( + new Date(scanData.paymentDate).toISOString().split("T")[0], + ); + } catch (_) {} + } + if ( + scanData.referenceNumber || + scanData.merchantName || + scanData.merchantId + ) { + const parts: string[] = []; + if (scanData.referenceNumber) + parts.push(`Ref: ${scanData.referenceNumber}`); + if (scanData.merchantName) + parts.push(`Merchant: ${scanData.merchantName}`); + if (scanData.merchantId) + parts.push(`Merchant ID: ${scanData.merchantId}`); + setNotes((prev) => + prev ? `${prev}\n${parts.join(" · ")}` : parts.join(" · "), + ); + } + }, []); + const openInvoicePicker = async () => { setLoadingInvoices(true); try { @@ -278,8 +337,8 @@ export default function CreatePaymentScreen() { const STEPS = [ { key: "details", label: "Payment Details" }, - { key: "invoice", label: "Invoice" }, - { key: "info", label: "Customer Info" }, + { key: "schedule", label: "Schedule" }, + { key: "method", label: "Method" }, { key: "summary", label: "Summary" }, ]; @@ -321,15 +380,23 @@ export default function CreatePaymentScreen() { currency, paymentDate: new Date(paymentDate).toISOString(), paymentMethod, - notes, - customerName: customerName.trim() || undefined, - customerEmail: customerEmail.trim() || undefined, - customerPhone: customerPhone.trim() ? `+251${customerPhone.trim()}` : undefined, + financialInstitution, + isReferenceVerified, + notes: notes.trim() || undefined, ...(selectedInvoice?.id ? { invoiceId: selectedInvoice.id } : {}), + ...(customerId ? { customerId } : {}), }; - await api.payments.create({ body: payload }); - toast.success("Success", "Payment created successfully!"); + if (scanRecordId) { + await api.payments.update({ + params: { id: scanRecordId }, + body: payload, + }); + toast.success("Success", "Payment updated successfully!"); + } else { + await api.payments.create({ body: payload }); + toast.success("Success", "Payment created successfully!"); + } nav.back(); } catch (error: any) { console.error("[CreatePayment] Error:", error); @@ -337,7 +404,9 @@ export default function CreatePaymentScreen() { error?.response?.data?.message || error?.data?.message || error?.message || - "Failed to create payment"; + (scanRecordId + ? "Failed to update payment" + : "Failed to create payment"); toast.error("Error", msg); } finally { setSubmitting(false); @@ -353,7 +422,7 @@ export default function CreatePaymentScreen() { onBack={() => setStep(step - 1)} onComplete={handleSubmit} loading={submitting} - completeLabel="Create Payment" + completeLabel={scanRecordId ? "Update Payment" : "Create Payment"} > {step === 0 && ( @@ -382,11 +451,6 @@ export default function CreatePaymentScreen() { onPress={() => setShowCurrency(true)} /> - setShowPaymentMethod(true)} - /> - Invoice + Link Invoice @@ -446,49 +510,37 @@ export default function CreatePaymentScreen() { {step === 2 && ( - Customer Info + Method - - - - Customer Name - - { - setCustomerName(c.name); - setCustomerEmail(c.email); - setCustomerPhone(c.phone.replace("+251", "")); - }} - placeholder="Select or search for a customer" - /> - - - - - - - Phone - - - +251 - + + setShowPaymentMethod(true)} + /> + setShowFinancialInstitution(true)} + /> + + + + + + + Reference verified + + + Transaction reference has been confirmed + + @@ -508,6 +560,18 @@ export default function CreatePaymentScreen() { {transactionId} + + + Amount + + + {currency}{" "} + {(parseFloat(amount) || 0).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + Payment Method @@ -516,6 +580,22 @@ export default function CreatePaymentScreen() { {paymentMethod} + + + Financial Institution + + + {financialInstitution} + + + + + Reference Verified + + + {isReferenceVerified ? "Yes" : "No"} + + Payment Date @@ -524,6 +604,16 @@ export default function CreatePaymentScreen() { {paymentDate} + {selectedInvoice?.customerName ? ( + + + Customer + + + {selectedInvoice?.customerName} + + + ) : null} Linked Invoice @@ -603,6 +693,25 @@ export default function CreatePaymentScreen() { ))} + setShowFinancialInstitution(false)} + title="Select Financial Institution" + > + {FINANCIAL_INSTITUTIONS.map((fi) => ( + { + setFinancialInstitution(v); + setShowFinancialInstitution(false); + }} + /> + ))} + + setShowPaymentDate(false)} @@ -645,6 +754,7 @@ export default function CreatePaymentScreen() { { setSelectedInvoice(null); + setCustomerId(""); setShowInvoicePicker(false); }} className="px-4 py-3 border-b border-border/40 flex-row items-center" @@ -659,6 +769,7 @@ export default function CreatePaymentScreen() { key={inv.id} onPress={() => { setSelectedInvoice(inv); + setCustomerId(inv.customerId || ""); setShowInvoicePicker(false); }} className="px-4 py-3 border-b border-border/40 flex-row items-center" diff --git a/app/pin-lock.tsx b/app/pin-lock.tsx index 6d89631..b763385 100644 --- a/app/pin-lock.tsx +++ b/app/pin-lock.tsx @@ -10,6 +10,7 @@ import { api } from "@/lib/api"; import { usePinStore } from "@/lib/pin-store"; import { useAuthStore } from "@/lib/auth-store"; import { toast } from "@/lib/toast-store"; +import bcrypt from "react-native-bcrypt"; const LOCKOUT_THRESHOLD = 5; @@ -34,7 +35,11 @@ export default function PinLockScreen() { if (loading || value.length < 6) return; setLoading(true); try { - await api.auth.verifyPin({ query: { pin: value } }); + const res = await api.auth.verifyPin({ query: { pin: value } }); + const match = bcrypt.compareSync(value, res.pin); + if (!match) { + throw new Error("PIN mismatch"); + } unlock(); nav.go("(tabs)"); } catch (err: any) { @@ -90,7 +95,6 @@ export default function PinLockScreen() { {user?.firstName ?? "User"} - r diff --git a/app/profile.tsx b/app/profile.tsx index 9f302ff..88943cb 100644 --- a/app/profile.tsx +++ b/app/profile.tsx @@ -13,8 +13,6 @@ import { User, Lock, Globe, - Building2, - Users, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { useColorScheme } from "nativewind"; @@ -212,28 +210,6 @@ export default function ProfileScreen() { /> */} - - } - label="Overview" - sublabel="View company details" - onPress={() => nav.go("company-details")} - /> - {/* } - label="Edit Company Info" - sublabel="Update business details" - onPress={() => nav.go("company/edit")} - /> */} - } - label="Workers" - sublabel="Manage team members" - onPress={() => nav.go("team/index")} - isLast - /> - - } diff --git a/app/proforma-requests/[id].tsx b/app/proforma-requests/[id].tsx new file mode 100644 index 0000000..1df077e --- /dev/null +++ b/app/proforma-requests/[id].tsx @@ -0,0 +1,622 @@ +import React, { useState, useCallback } from "react"; +import { + View, + ScrollView, + ActivityIndicator, + Linking, + Pressable, + Share, + Modal, +} 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 { + Inbox, + Calendar, + Clock, + Hash, + Share2, + Package, + X, + Mail, + Link2, + Info, + Truck, + Send, + AlertCircle, + CheckCircle2, + XCircle, + Hourglass, +} from "@/lib/icons"; +import { ScreenWrapper } from "@/components/ScreenWrapper"; +import { StandardHeader } from "@/components/StandardHeader"; +import { api } from "@/lib/api"; +import { useColorScheme } from "nativewind"; +import { toast } from "@/lib/toast-store"; + +const STATUS_THEME: Record< + string, + { label: string; bg: string; text: string; dot: string; pillBg: string } +> = { + DRAFT: { + label: "Draft", + bg: "bg-slate-500/10", + text: "text-slate-600", + dot: "bg-slate-500", + pillBg: "#6b728015", + }, + OPEN: { + label: "Open", + bg: "bg-primary/10", + text: "text-primary", + dot: "bg-primary", + pillBg: "#E4621215", + }, + UNDER_REVIEW: { + label: "Under Review", + bg: "bg-blue-500/10", + text: "text-blue-600", + dot: "bg-blue-500", + pillBg: "#2563eb15", + }, + REVISION_REQUESTED: { + label: "Revision Requested", + bg: "bg-red-500/10", + text: "text-red-600", + dot: "bg-red-500", + pillBg: "#dc262615", + }, + CLOSED: { + label: "Closed", + bg: "bg-emerald-500/10", + text: "text-emerald-600", + dot: "bg-emerald-500", + pillBg: "#16a34a15", + }, + CANCELLED: { + label: "Cancelled", + bg: "bg-slate-500/10", + text: "text-slate-600", + dot: "bg-slate-500", + pillBg: "#6b728015", + }, +}; + +const CATEGORY_THEME: Record = { + EQUIPMENT: { color: "#2563eb", bg: "#2563eb15" }, + SERVICE: { color: "#16a34a", bg: "#16a34a15" }, + MIXED: { color: "#E46212", bg: "#E4621215" }, +}; + +const INVITE_STATUS_ICON: Record< + string, + { Icon: React.ComponentType; color: string; label: string } +> = { + PENDING: { Icon: Hourglass, color: "#94a3b8", label: "Pending" }, + SENT: { Icon: CheckCircle2, color: "#16a34a", label: "Sent" }, + FAILED: { Icon: XCircle, color: "#dc2626", label: "Failed" }, +}; + +function fmtDate(d?: string) { + if (!d) return "—"; + return new Date(d).toLocaleDateString(); +} + +export default function ProformaRequestDetailScreen() { + const nav = useSirouRouter(); + const { id } = useLocalSearchParams(); + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [showShareSheet, setShowShareSheet] = useState(false); + + const fetch = useCallback(async () => { + try { + setLoading(true); + const reqId = Array.isArray(id) ? id[0] : id; + if (!reqId) return; + const result = await api.proformaRequests.getById({ + params: { id: reqId }, + }); + setData(result); + } catch (err: any) { + toast.error("Error", "Failed to load proforma request"); + } finally { + setLoading(false); + } + }, [id]); + + useFocusEffect( + useCallback(() => { + fetch(); + }, [fetch]), + ); + + const handleShare = async (channel: "system" | "email") => { + if (!data?.inviteUrl) { + toast.error("No invite link", "This request has no invite URL yet"); + return; + } + try { + if (channel === "email") { + await Linking.openURL( + `mailto:?subject=${encodeURIComponent( + data.title || "Proforma Request", + )}&body=${encodeURIComponent(data.inviteUrl)}`, + ); + } else { + await Share.share({ + message: `${data.title || "Proforma Request"}\n${data.inviteUrl}`, + }); + } + setShowShareSheet(false); + } catch (err: any) { + toast.error("Error", err?.message || "Failed to share"); + } + }; + + if (loading || !data) { + return ( + + + + + + + ); + } + + const statusKey = (data.status || "DRAFT").toUpperCase(); + const theme = STATUS_THEME[statusKey] || STATUS_THEME.DRAFT; + const categoryKey = (data.category || "MIXED").toUpperCase(); + const categoryTheme = CATEGORY_THEME[categoryKey] || CATEGORY_THEME.MIXED; + const items: any[] = Array.isArray(data.items) ? data.items : []; + const invites: any[] = Array.isArray(data.invites) ? data.invites : []; + const submissionCount = data.submissionCount ?? 0; + const totalQuantity = items.reduce( + (s: number, i: any) => s + (Number(i.quantity) || 0), + 0, + ); + + return ( + + + + + + {/* Hero Card */} + + + + + + + + + {data.title || "Untitled request"} + + + + + {categoryKey} + + + + + + + + {theme.label} + + + + + {data.description ? ( + + {data.description} + + ) : null} + + + + + Deadline + + + + + {fmtDate(data.submissionDeadline)} + + + + + + + Submissions + + + + + {submissionCount} + + + + + + + + {/* Items */} + {items.length > 0 && ( + + + + Requested Items ({items.length}) + + + {totalQuantity} units + + + + {items.map((item: any, idx: number) => ( + + + + {item.itemName || `Item ${idx + 1}`} + + + {item.quantity || 0} {item.unitOfMeasure || "unit"} + + + {item.itemDescription ? ( + + {item.itemDescription} + + ) : null} + {item.technicalSpecifications && + Object.keys(item.technicalSpecifications).length > 0 ? ( + + {Object.entries( + item.technicalSpecifications as Record, + ).map(([k, v]) => ( + + + {k}: {String(v)} + + + ))} + + ) : null} + + ))} + + + )} + + {/* Commercial Terms */} + + + Commercial Terms + + + {data.paymentTerms ? ( + + ) : null} + {data.incoterms ? ( + + ) : null} + {data.validityPeriod != null ? ( + + ) : null} + + + {data.discountStructure ? ( + + ) : null} + + + + {/* Invite link */} + {data.inviteUrl ? ( + + + Invite Link + + + + + + {data.inviteUrl} + + + setShowShareSheet(true)} + className="h-9 rounded-[6px] bg-primary flex-row items-center justify-center gap-1.5" + > + + + Share Invite Link + + + + + ) : null} + + {/* Invites */} + {invites.length > 0 && ( + + + Invites ({invites.length}) + + + {invites.map((inv: any, idx: number) => { + const key = (inv.status || "PENDING").toUpperCase(); + const invTheme = INVITE_STATUS_ICON[key] || INVITE_STATUS_ICON.PENDING; + const { Icon, color, label } = invTheme; + return ( + + + + + + + + {inv.customerName || "Customer"} + + {inv.sentTo ? ( + + {inv.sentTo} + + ) : null} + + + + + {label} + + + + {inv.sendError ? ( + + {inv.sendError} + + ) : null} + + ); + })} + + + )} + + {/* Notes fallback / no-data state */} + {items.length === 0 && !data.description && ( + + + + + This request has no items or description yet. + + + + )} + + + setShowShareSheet(false)} + > + setShowShareSheet(false)} + className="flex-1 bg-black/40 justify-end" + > + {}} + className="bg-background rounded-t-3xl p-5 pb-8" + style={{ + borderTopWidth: 1, + borderColor: isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.05)", + }} + > + + + Share Invite Link + + setShowShareSheet(false)} hitSlop={8}> + + + + handleShare("system")} + /> + handleShare("email")} + /> + + + + + ); +} + +function TermRow({ + icon: Icon, + label, + value, + divider, + multiline, +}: { + icon: React.ComponentType; + label: string; + value: string; + divider?: boolean; + multiline?: boolean; +}) { + return ( + + + + + + + {label} + + + {value} + + + + ); +} + +function ShareOption({ + icon: Icon, + label, + description, + onPress, +}: { + icon: React.ComponentType; + label: string; + description: string; + onPress: () => void; +}) { + return ( + + + + + + {label} + + {description} + + + + ); +} diff --git a/app/proforma-requests/create.tsx b/app/proforma-requests/create.tsx new file mode 100644 index 0000000..cacc752 --- /dev/null +++ b/app/proforma-requests/create.tsx @@ -0,0 +1,793 @@ +import React, { useState, useEffect } from "react"; +import { View, Pressable, TextInput, StyleSheet, Switch } from "react-native"; +import { useColorScheme } from "nativewind"; +import { Text } from "@/components/ui/text"; +import { ChevronDown, Plus, Trash2, X } from "@/lib/icons"; +import { ScreenWrapper } from "@/components/ScreenWrapper"; +import { useSirouRouter } from "@sirou/react-native"; +import { AppRoutes } from "@/lib/routes"; +import { api } from "@/lib/api"; +import { toast } from "@/lib/toast-store"; +import { PickerModal, SelectOption } from "@/components/PickerModal"; +import { CalendarGrid } from "@/components/CalendarGrid"; +import { FormFlow } from "@/components/FormFlow"; +import { CustomerPicker } from "@/components/CustomerPicker"; + +type Item = { + id: number; + itemName: string; + itemDescription: string; + quantity: string; + unitOfMeasure: 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, + textAlignVertical: "center", + textAlign: "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 ( + + + {label} + + + + ); +} + +function PickerField({ + label, + value, + onPress, + flex, +}: { + label: string; + value: string; + onPress: () => void; + flex?: number; +}) { + const c = useInputColors(); + return ( + + + {label} + + + + {value || "Select"} + + + + + ); +} + +const CATEGORIES = ["EQUIPMENT", "SERVICE", "MIXED"]; +const INCOTERMS = ["EXW", "FOB", "CIF", "DAP", "DDP"]; +const INVITE_CHANNELS = ["EMAIL", "PHONE"]; +const UNITS = ["unit", "kg", "m", "m²", "m³", "litre", "hour", "service"]; + +const STEPS = [ + { key: "details", label: "Details" }, + { key: "items", label: "Items" }, + { key: "terms", label: "Terms" }, + { key: "schedule", label: "Schedule" }, + { key: "summary", label: "Summary" }, +]; + +export default function CreateProformaRequestScreen() { + const nav = useSirouRouter(); + const [step, setStep] = useState(0); + const [submitting, setSubmitting] = useState(false); + + // Step 1 — Details + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [category, setCategory] = useState("EQUIPMENT"); + + // Step 2 — Items + const [items, setItems] = useState([ + { + id: 1, + itemName: "", + itemDescription: "", + quantity: "1", + unitOfMeasure: "unit", + }, + ]); + + // Step 3 — Terms + const [paymentTerms, setPaymentTerms] = useState("Net 30 days"); + const [incoterms, setIncoterms] = useState("EXW"); + const [taxIncluded, setTaxIncluded] = useState(false); + const [discountStructure, setDiscountStructure] = useState(""); + const [validityPeriod, setValidityPeriod] = useState("30"); + const [allowRevisions, setAllowRevisions] = useState(true); + + // Step 4 — Schedule & Invite + const [submissionDeadline, setSubmissionDeadline] = useState(""); + const [inviteChannel, setInviteChannel] = useState("EMAIL"); + const [customerIds, setCustomerIds] = useState([]); + const [selectedCustomers, setSelectedCustomers] = useState< + { id: string; name: string; email: string; phone: string }[] + >([]); + + // Modal visibility + const [showCategory, setShowCategory] = useState(false); + const [showIncoterms, setShowIncoterms] = useState(false); + const [showInviteChannel, setShowInviteChannel] = useState(false); + const [showDeadline, setShowDeadline] = useState(false); + + const c = useInputColors(); + + useEffect(() => { + const d = new Date(); + d.setDate(d.getDate() + 14); + setSubmissionDeadline(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(), + itemName: "", + itemDescription: "", + quantity: "1", + unitOfMeasure: "unit", + }, + ]); + + const removeItem = (id: number) => { + if (items.length > 1) setItems((prev) => prev.filter((it) => it.id !== id)); + }; + + const setItemUnit = (id: number, unit: string) => + updateItem(id, "unitOfMeasure", unit); + + const handleNext = () => { + if (step === 0 && !title.trim()) { + toast.error("Validation", "Title is required"); + return; + } + if (step === 1) { + if (items.length === 0) { + toast.error("Validation", "Add at least one item"); + return; + } + if (items.every((it) => !it.itemName.trim())) { + toast.error("Validation", "At least one item must have a name"); + return; + } + } + setStep((s) => s + 1); + }; + + const handleSubmit = async () => { + if (!title.trim()) { + toast.error("Validation", "Title is required"); + return; + } + if (!submissionDeadline) { + toast.error("Validation", "Submission deadline is required"); + return; + } + + const payload = { + title: title.trim(), + description: description.trim() || undefined, + category, + submissionDeadline: new Date(submissionDeadline).toISOString(), + allowRevisions, + paymentTerms: paymentTerms.trim() || undefined, + incoterms: incoterms || undefined, + taxIncluded, + discountStructure: discountStructure.trim() || undefined, + validityPeriod: parseInt(validityPeriod, 10) || 30, + items: items + .filter((it) => it.itemName.trim()) + .map((it) => ({ + itemName: it.itemName.trim(), + itemDescription: it.itemDescription.trim() || undefined, + quantity: parseInt(it.quantity, 10) || 1, + unitOfMeasure: it.unitOfMeasure || "unit", + })), + customerIds: customerIds.length > 0 ? customerIds : undefined, + inviteChannel, + }; + + try { + setSubmitting(true); + await api.proformaRequests.create({ + body: payload, + headers: { "Content-Type": "application/json" }, + }); + toast.success("Success", "Proforma request created successfully!"); + nav.back(); + } catch (err: any) { + console.error("[ProformaRequestCreate] Error:", err); + toast.error( + "Error", + err?.response?.data?.message || + err?.message || + "Failed to create request", + ); + } finally { + setSubmitting(false); + } + }; + + const totalQuantity = items.reduce( + (s, it) => s + (parseInt(it.quantity, 10) || 0), + 0, + ); + const namedItemCount = items.filter((it) => it.itemName.trim()).length; + + return ( + + setStep(step - 1)} + onComplete={handleSubmit} + loading={submitting} + completeLabel="Create Request" + > + {step === 0 && ( + + + Request Details + + + + + setShowCategory(true)} + /> + + + )} + + {step === 1 && ( + + + + Requested Items + + + + + Add + + + + + {items.map((item, index) => ( + 1} + onUpdate={updateItem} + onRemove={removeItem} + onPickUnit={(u) => setItemUnit(item.id, u)} + /> + ))} + + + )} + + {step === 2 && ( + + + Commercial Terms + + + + + setShowIncoterms(true)} + flex={2} + /> + + + + + + + Options + + + + + + + + )} + + {step === 3 && ( + + + Deadline & Invitation + + + setShowDeadline(true)} + /> + setShowInviteChannel(true)} + /> + + + Customers + + { + setCustomerIds(ids); + setSelectedCustomers(customers); + }} + placeholder="Select customers to invite" + /> + {selectedCustomers.length > 0 && ( + + {selectedCustomers.map((cust) => ( + + + {cust.name} + + { + setCustomerIds((ids) => + ids.filter((id) => id !== cust.id), + ); + setSelectedCustomers((prev) => + prev.filter((c) => c.id !== cust.id), + ); + }} + hitSlop={6} + > + + + + ))} + + )} + + + + )} + + {step === 4 && ( + + + Summary + + + + {description ? ( + + ) : null} + + + {namedItemCount > 0 ? ( + + + Items ({namedItemCount}) + + {items + .filter((it) => it.itemName.trim()) + .map((it, i) => ( + + + {it.itemName} + + + {it.quantity} {it.unitOfMeasure} + + + ))} + + ) : null} + + + + + {discountStructure ? ( + + ) : null} + + + Tax Included + + + {taxIncluded ? "Yes" : "No"} + + + + + Allow Revisions + + + {allowRevisions ? "Yes" : "No"} + + + + + + {selectedCustomers.length > 0 ? ( + + + Customers ({selectedCustomers.length}) + + {selectedCustomers.map((cust) => ( + + {cust.name} + + ))} + + ) : null} + + + )} + + + setShowCategory(false)} + title="Select Category" + > + {CATEGORIES.map((cat) => ( + { + setCategory(v); + setShowCategory(false); + }} + /> + ))} + + + setShowIncoterms(false)} + title="Select Incoterms" + > + {INCOTERMS.map((t) => ( + { + setIncoterms(v); + setShowIncoterms(false); + }} + /> + ))} + + + setShowInviteChannel(false)} + title="Invite Channel" + > + {INVITE_CHANNELS.map((ch) => ( + { + setInviteChannel(v); + setShowInviteChannel(false); + }} + /> + ))} + + + setShowDeadline(false)} + title="Submission Deadline" + > + { + setSubmissionDeadline(v); + setShowDeadline(false); + }} + /> + + + ); +} + +function ToggleRow({ + title, + subtitle, + value, + onValueChange, +}: { + title: string; + subtitle: string; + value: boolean; + onValueChange: (v: boolean) => void; +}) { + return ( + + + + {title} + + + {subtitle} + + + + + ); +} + +function ItemRow({ + item, + index, + canRemove, + onUpdate, + onRemove, + onPickUnit, +}: { + item: Item; + index: number; + canRemove: boolean; + onUpdate: (id: number, field: keyof Item, value: string) => void; + onRemove: (id: number) => void; + onPickUnit: (u: string) => void; +}) { + const [showUnits, setShowUnits] = useState(false); + const c = useInputColors(); + return ( + + + + Item {index + 1} + + {canRemove && ( + onRemove(item.id)} hitSlop={8}> + + + )} + + onUpdate(item.id, "itemName", v)} + /> + + onUpdate(item.id, "itemDescription", v)} + multiline + /> + + + onUpdate(item.id, "quantity", v)} + flex={1} + /> + + + Unit + + setShowUnits(true)} + className="h-10 px-3 border border-border rounded-[6px] flex-row items-center justify-between" + style={{ backgroundColor: c.bg, borderColor: c.border }} + > + + {item.unitOfMeasure || "unit"} + + + + + + setShowUnits(false)} + title="Unit of Measure" + > + {UNITS.map((u) => ( + { + onPickUnit(v); + setShowUnits(false); + }} + /> + ))} + + + ); +} + +function Row({ + label, + value, + multiline, +}: { + label: string; + value: string; + multiline?: boolean; +}) { + return ( + + + {label} + + + {value} + + + ); +} diff --git a/app/proforma.tsx b/app/proforma.tsx index 1f245f9..8724a7c 100644 --- a/app/proforma.tsx +++ b/app/proforma.tsx @@ -1,24 +1,36 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { CommandPalette } from "@/components/CommandPalette"; import { View, + ScrollView, Pressable, - ActivityIndicator, - FlatList, - ListRenderItem, TextInput, + ActivityIndicator, } from "react-native"; import { Text } from "@/components/ui/text"; import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; import { useSirouRouter } from "@sirou/react-native"; +import { useFocusEffect } from "expo-router"; import { AppRoutes } from "@/lib/routes"; -import { Plus, FileText, Search } from "@/lib/icons"; +import { + Plus, + FileText, + Search, + ChevronRight, + Inbox, +} from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; -import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/EmptyState"; import { api } from "@/lib/api"; import { useAuthStore } from "@/lib/auth-store"; -import { PERMISSION_MAP, hasPermission } from "@/lib/permissions"; +import { toast } from "@/lib/toast-store"; +import { useColorScheme } from "nativewind"; +import { getPlaceholderColor } from "@/lib/colors"; +import { hasPermission, PERMISSION_MAP } from "@/lib/permissions"; + +type Tab = "proforma" | "request"; interface ProformaItem { id: string; @@ -41,170 +53,141 @@ interface ProformaItem { updatedAt: string; } -const dummyData: ProformaItem = { - id: "dummy-1", - proformaNumber: "PF-001", - customerName: "John Doe", - customerEmail: "john@example.com", - customerPhone: "+1234567890", - amount: { value: 1000, currency: "USD" }, - currency: "USD", - issueDate: "2026-03-10T11:51:36.134Z", - dueDate: "2026-03-10T11:51:36.134Z", - description: "Dummy proforma", - notes: "Test notes", - taxAmount: { value: 100, currency: "USD" }, - discountAmount: { value: 50, currency: "USD" }, - pdfPath: "dummy.pdf", - userId: "user-1", - items: [ - { - id: "item-1", - description: "Test item", - quantity: 1, - unitPrice: { value: 1000, currency: "USD" }, - total: { value: 1000, currency: "USD" }, - }, - ], - createdAt: "2026-03-10T11:51:36.134Z", - updatedAt: "2026-03-10T11:51:36.134Z", +interface ProformaRequest { + id: string; + title: string; + description: string; + category: "EQUIPMENT" | "SERVICE" | "MIXED"; + status: + | "DRAFT" + | "OPEN" + | "UNDER_REVIEW" + | "REVISION_REQUESTED" + | "CLOSED" + | "CANCELLED"; + submissionDeadline: string; + items: { id: string; itemName: string; quantity: number; unitOfMeasure: string }[]; + createdAt: string; + updatedAt: string; +} + +const REQUEST_STATUS_COLORS: Record = { + DRAFT: "#6b7280", + OPEN: "#E46212", + UNDER_REVIEW: "#2563eb", + REVISION_REQUESTED: "#dc2626", + CLOSED: "#16a34a", + CANCELLED: "#6b7280", +}; + +const REQUEST_STATUS_BG: Record = { + DRAFT: "#6b728015", + OPEN: "#E4621215", + UNDER_REVIEW: "#2563eb15", + REVISION_REQUESTED: "#dc262615", + CLOSED: "#16a34a15", + CANCELLED: "#6b728015", +}; + +const CATEGORY_COLORS: Record = { + EQUIPMENT: "#2563eb", + SERVICE: "#16a34a", + MIXED: "#E46212", }; export default function ProformaScreen() { const nav = useSirouRouter(); const permissions = useAuthStore((s) => s.permissions); + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + const [tab, setTab] = useState("proforma"); + const [searchOpen, setSearchOpen] = useState(false); + + // Proforma state const [proformas, setProformas] = useState([]); const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [search, setSearch] = useState(""); + // Request state + const [requests, setRequests] = useState([]); + const [requestsLoading, setRequestsLoading] = useState(false); + const [reqPage, setReqPage] = useState(1); + const [reqHasMore, setReqHasMore] = useState(true); + const [reqLoadingMore, setReqLoadingMore] = useState(false); + const canCreateProformas = hasPermission( permissions, PERMISSION_MAP["proforma:create"], ); - const fetchProformas = useCallback( - async (pageNum: number, isRefresh = false) => { - const { isAuthenticated } = useAuthStore.getState(); - if (!isAuthenticated) return; + const fetchProformas = useCallback(async (pageNum: number) => { + const { isAuthenticated } = useAuthStore.getState(); + if (!isAuthenticated) return; + try { + pageNum === 1 ? setLoading(true) : setLoadingMore(true); + const response = await api.proforma.getAll({ + query: { page: pageNum, limit: 10 }, + }); + const newProformas = response.data; + setProformas((prev) => + pageNum === 1 ? newProformas : [...prev, ...newProformas], + ); + setHasMore(response.meta.hasNextPage); + setPage(pageNum); + } catch (err: any) { + console.error("[Proforma] Fetch error:", err); + setHasMore(false); + } finally { + setLoading(false); + setLoadingMore(false); + } + }, []); - try { - if (!isRefresh) { - pageNum === 1 ? setLoading(true) : setLoadingMore(true); - } + const fetchRequests = useCallback(async (pageNum: number) => { + const { isAuthenticated } = useAuthStore.getState(); + if (!isAuthenticated) return; + try { + pageNum === 1 ? setRequestsLoading(true) : setReqLoadingMore(true); + const response = await api.proformaRequests.getAll({ + query: { page: pageNum, limit: 10 }, + }); + const newRequests = response.data; + setRequests((prev) => + pageNum === 1 ? newRequests : [...prev, ...newRequests], + ); + setReqHasMore(response.meta.hasNextPage); + setReqPage(pageNum); + } catch (err: any) { + console.error("[ProformaRequests] Fetch error:", err); + toast.error("Error", "Failed to fetch proforma requests."); + } finally { + setRequestsLoading(false); + setReqLoadingMore(false); + } + }, []); - const response = await api.proforma.getAll({ - query: { page: pageNum, limit: 10 }, - }); - - let newProformas = response.data; - - const newData = newProformas; - if (isRefresh) { - setProformas(newData); - } else { - setProformas((prev) => - pageNum === 1 ? newData : [...prev, ...newData], - ); - } - setHasMore(response.meta.hasNextPage); - setPage(pageNum); - } catch (err: any) { - console.error("[Proforma] Fetch error:", err); - setHasMore(false); - } finally { - setLoading(false); - setRefreshing(false); - setLoadingMore(false); - } - }, - [], + useFocusEffect( + useCallback(() => { + if (tab === "proforma") fetchProformas(1); + else fetchRequests(1); + }, [tab, fetchProformas, fetchRequests]), ); - useEffect(() => { - fetchProformas(1); - }, [fetchProformas]); - - const onRefresh = () => { - setRefreshing(true); - fetchProformas(1, true); - }; - const loadMore = () => { - if (hasMore && !loadingMore && !loading) { + if (tab === "proforma" && hasMore && !loadingMore && !loading) { fetchProformas(page + 1); } - }; - - const renderProformaItem: ListRenderItem = ({ item }) => { - const amountVal = - typeof item.amount === "object" ? item.amount.value : item.amount; - const issuedStr = item.issueDate - ? new Date(item.issueDate).toLocaleDateString() - : ""; - const dueStr = item.dueDate - ? new Date(item.dueDate).toLocaleDateString() - : ""; - const itemsCount = Array.isArray(item.items) ? item.items.length : 0; - - return ( - - nav.go("proforma/[id]", { id: item.id })} - className="mb-3" - > - - - - - - - - - - - - {item.proformaNumber || "Proforma"} - - - {item.customerName || "Customer"} - - - - - - {item.currency || "$"} - {amountVal?.toLocaleString?.() ?? amountVal ?? "0"} - - - - - - - Issued: {issuedStr} | Due: {dueStr} | {itemsCount} item - {itemsCount !== 1 ? "s" : ""} - - - - - - - - - ); + if ( + tab === "request" && + reqHasMore && + !reqLoadingMore && + !requestsLoading + ) { + fetchRequests(reqPage + 1); + } }; const filteredProformas = useMemo(() => { @@ -226,63 +209,288 @@ export default function ProformaScreen() { }); }, [proformas, search]); + const filteredRequests = useMemo(() => { + if (!search.trim()) return requests; + const q = search.toLowerCase(); + return requests.filter((r) => { + if (r.title?.toLowerCase().includes(q)) return true; + if (r.description?.toLowerCase().includes(q)) return true; + if (r.category?.toLowerCase().includes(q)) return true; + if (r.status?.toLowerCase().includes(q)) return true; + return false; + }); + }, [requests, search]); + + const renderProformaCard = (item: ProformaItem) => { + const amountVal = + typeof item.amount === "object" ? item.amount.value : item.amount; + const issuedStr = item.issueDate + ? new Date(item.issueDate).toLocaleDateString() + : ""; + const dueStr = item.dueDate ? new Date(item.dueDate).toLocaleDateString() : ""; + const itemsCount = Array.isArray(item.items) ? item.items.length : 0; + + return ( + nav.go("proforma/[id]", { id: item.id })} + className="mb-2" + > + + + + + + + + + {item.proformaNumber || "Proforma"} + + + {item.currency || "ETB"}{" "} + {(amountVal || 0).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + + + {item.customerName || "Customer"} · Issued {issuedStr} + {dueStr ? ` · Due ${dueStr}` : ""} · {itemsCount} item + {itemsCount !== 1 ? "s" : ""} + + + + + + + ); + }; + + const renderRequestCard = (req: ProformaRequest) => { + const statusColor = REQUEST_STATUS_COLORS[req.status] || "#6b7280"; + const statusBg = REQUEST_STATUS_BG[req.status] || "#6b728015"; + const categoryColor = CATEGORY_COLORS[req.category] || "#6b7280"; + const deadlineStr = req.submissionDeadline + ? new Date(req.submissionDeadline).toLocaleDateString() + : ""; + const itemsCount = Array.isArray(req.items) ? req.items.length : 0; + + return ( + nav.go("proforma-requests/[id]", { id: req.id })} + className="mb-2" + > + + + + + + + + + {req.title || "Untitled request"} + + + + {req.status.replace(/_/g, " ")} + + + + + {itemsCount} item{itemsCount !== 1 ? "s" : ""} · Deadline{" "} + {deadlineStr || "—"} + + + + + {req.category} + + + + + + + + ); + }; + + const isLoading = + tab === "proforma" ? loading && page === 1 : requestsLoading && reqPage === 1; + if (isLoading) { + return ( + + setSearchOpen(true)} + /> + + + + + ); + } + + const items = tab === "proforma" ? filteredProformas : filteredRequests; + return ( - item.id} + - - - - - - - - - - } - ListFooterComponent={ - loadingMore ? ( - - ) : null - } - ListEmptyComponent={ - !loading ? ( - { + const isCloseToBottom = + nativeEvent.layoutMeasurement.height + + nativeEvent.contentOffset.y >= + nativeEvent.contentSize.height - 20; + if (isCloseToBottom) loadMore(); + }} + scrollEventThrottle={400} + > + setSearchOpen(true)} + /> + + + + - ) : ( - - + + + + + {/* Tabs */} + + { + if (tab !== "proforma") { + setTab("proforma"); + setSearch(""); + } + }} + className={`flex-1 py-2 rounded-[8px] items-center ${ + tab === "proforma" ? "bg-primary" : "" + }`} + > + + Proforma + + + { + if (tab !== "request") { + setTab("request"); + setSearch(""); + } + }} + className={`flex-1 py-2 rounded-[8px] items-center ${ + tab === "request" ? "bg-primary" : "" + }`} + > + + Requests + + + + + + {items.length > 0 ? ( + tab === "proforma" + ? (items as ProformaItem[]).map(renderProformaCard) + : (items as ProformaRequest[]).map(renderRequestCard) + ) : ( + + )} + + + {(loadingMore || reqLoadingMore) && ( + + - ) - } + )} + + + + setSearchOpen(false)} /> ); diff --git a/app/proforma/[id].tsx b/app/proforma/[id].tsx index 49d4611..34e5c09 100644 --- a/app/proforma/[id].tsx +++ b/app/proforma/[id].tsx @@ -3,22 +3,21 @@ import { View, ScrollView, ActivityIndicator, - Alert, Linking, Pressable, Modal, Dimensions, + StyleSheet, + Image, } 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 { EmptyState } from "@/components/EmptyState"; import { FileText, Calendar, - Download, - Trash2, - Package, Clock, User, Hash, @@ -26,9 +25,15 @@ import { Edit, Mail, MessageSquare, - Globe, MoreVertical, X, + Package, + Share2, + Download, + TrendingUp, + TrendingDown, + Check, + Trash2, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; @@ -36,6 +41,9 @@ import { api, BASE_URL } from "@/lib/api"; import { toast } from "@/lib/toast-store"; import { useAuthStore } from "@/lib/auth-store"; import { useColorScheme } from "nativewind"; +import { ActionModal } from "@/components/ActionModal"; +import { SendHorizonal } from "lucide-react-native"; +import ticketImage from "@/assets/ticket.png"; const { height: SCREEN_HEIGHT } = Dimensions.get("window"); @@ -49,12 +57,40 @@ function fmt(v: number, currency = "ETB") { return `${currency} ${v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } -const STATUS_THEME: Record = { - PAID: { label: "Paid", bg: "bg-emerald-500/10", text: "text-emerald-600", dot: "bg-emerald-500" }, - PENDING: { label: "Pending", bg: "bg-amber-500/10", text: "text-amber-600", dot: "bg-amber-500" }, - DRAFT: { label: "Draft", bg: "bg-blue-500/10", text: "text-blue-600", dot: "bg-blue-500" }, - CANCELLED: { label: "Cancelled", bg: "bg-slate-500/10", text: "text-slate-600", dot: "bg-slate-500" }, - DEFAULT: { label: "Unknown", bg: "bg-slate-500/10", text: "text-slate-500", dot: "bg-slate-500" }, +const STATUS_THEME: Record< + string, + { label: string; bg: string; text: string; dot: string } +> = { + PAID: { + label: "Paid", + bg: "bg-emerald-500/10", + text: "text-emerald-600", + dot: "bg-emerald-500", + }, + PENDING: { + label: "Pending", + bg: "bg-amber-500/10", + text: "text-amber-600", + dot: "bg-amber-500", + }, + DRAFT: { + label: "Draft", + bg: "bg-blue-500/10", + text: "text-blue-600", + dot: "bg-blue-500", + }, + CANCELLED: { + label: "Cancelled", + bg: "bg-slate-500/10", + text: "text-slate-600", + dot: "bg-slate-500", + }, + DEFAULT: { + label: "Unknown", + bg: "bg-slate-500/10", + text: "text-slate-500", + dot: "bg-slate-500", + }, }; export default function ProformaDetailScreen() { @@ -65,9 +101,18 @@ export default function ProformaDetailScreen() { const [loading, setLoading] = useState(true); const [proforma, setProforma] = useState(null); - const [showActions, setShowActions] = useState(false); + const [activeTab, setActiveTab] = useState<"details" | "items">("details"); + const [showMoreSheet, setShowMoreSheet] = useState(false); + const [showSendSheet, setShowSendSheet] = useState(false); + const [sharing, setSharing] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [deleting, setDeleting] = useState(false); - useFocusEffect(useCallback(() => { fetchProforma(); }, [id])); + useFocusEffect( + useCallback(() => { + fetchProforma(); + }, [id]), + ); const fetchProforma = async () => { try { @@ -93,26 +138,38 @@ export default function ProformaDetailScreen() { } }; - const handleDelete = () => { - Alert.alert("Delete Proforma", "This cannot be undone.", [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - style: "destructive", - onPress: async () => { - try { - setLoading(true); - const pid = Array.isArray(id) ? id[0] : id; - await api.proforma.delete({ params: { id: pid } }); - toast.success("Success", "Proforma deleted"); - nav.back(); - } catch { - toast.error("Error", "Failed to delete proforma"); - setLoading(false); - } - }, - }, - ]); + const handleShare = async (channel: "email" | "sms") => { + try { + setSharing(true); + const pid = Array.isArray(id) ? id[0] : id; + await api.proforma.shareLink({ body: { proformaId: pid, channel } }); + toast.success( + "Sent", + `Proforma shared via ${channel === "email" ? "email" : "SMS"}`, + ); + setShowSendSheet(false); + } catch (err: any) { + toast.error("Error", err?.message || "Failed to share proforma"); + } finally { + setSharing(false); + } + }; + + const handleDelete = () => setShowDeleteModal(true); + + const confirmDelete = async () => { + try { + setDeleting(true); + const pid = Array.isArray(id) ? id[0] : id; + await api.proforma.delete({ params: { id: pid } }); + toast.success("Deleted", "Proforma has been removed."); + setShowDeleteModal(false); + nav.back(); + } catch (err: any) { + toast.error("Error", err?.message || "Failed to delete proforma"); + } finally { + setDeleting(false); + } }; if (loading) { @@ -152,249 +209,438 @@ export default function ProformaDetailScreen() { const statusKey = (proforma.status || "DRAFT").toUpperCase(); const theme = STATUS_THEME[statusKey] || STATUS_THEME.DEFAULT; + const issueDate = proforma.issueDate ? new Date(proforma.issueDate) : null; + const dueDate = proforma.dueDate ? new Date(proforma.dueDate) : null; + + const formatLongDate = (d: Date) => + d.toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }); + + const customerName = ( + proforma.customerName?.replace("Customer Name: ", "") || "Walking Client" + ).trim(); + + const ActionOption = ({ + icon, + label, + description, + onPress, + destructive, + }: { + icon: React.ReactNode; + label: string; + description: string; + onPress?: () => void; + destructive?: boolean; + }) => ( + + + {icon} + + + + {label} + + + {description} + + + + ); + return ( nav.go("proforma/edit", { id: proforma.id })} + right={ + setShowMoreSheet(true)} + className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" + > + + + } /> - {/* Hero — Amount + Status */} - - - - - Total Amount + {/* Hero Card — illustration overflows the top */} + + + + + + + + {amount.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{" "} + + {currency} - - - {amount.toLocaleString("en-US", { minimumFractionDigits: 2 })} - - - {currency} - - - - + + + {proforma.proformaNumber + ? `Proforma ${proforma.proformaNumber}` + : `Proforma #${(proforma.id || "").slice(0, 8).toUpperCase()}`} + + + {/* Status badge */} + - + {theme.label} - - - {/* Period Dates */} - - - - - + + + Issued - - - - {proforma.issueDate ? new Date(proforma.issueDate).toLocaleDateString() : "—"} - - + + {issueDate ? formatLongDate(issueDate) : "—"} + - - - + + Due - - - - {proforma.dueDate ? new Date(proforma.dueDate).toLocaleDateString() : "—"} - - - - - - - - {/* Customer */} - {proforma.customerName && ( - - - Customer - - - - - - - - - {proforma.customerName} - - {(proforma.customerEmail || proforma.customerPhone) && ( - - {[proforma.customerEmail, proforma.customerPhone].filter(Boolean).join(" · ")} - - )} - - - - - - #{proforma.id?.slice(0, 8) || "—"} + + {dueDate ? formatLongDate(dueDate) : "—"} - )} + - {/* Items */} - {items.length > 0 && ( - - - Items ({items.length}) - - - {items.map((item: any, idx: number) => ( - - - - {item.description || `Item ${idx + 1}`} + {/* Tabs */} + + + setActiveTab("details")} + className="pb-2.5" + > + + Details + + {activeTab === "details" && ( + + )} + + setActiveTab("items")} className="pb-2.5"> + + Items + + {activeTab === "items" && ( + + )} + + + + + {/* Tab content */} + {activeTab === "details" ? ( + + {/* Customer */} + {proforma.customerName && ( + + + Customer + + + + + + + + {customerName} + + {(proforma.customerEmail || proforma.customerPhone) && ( + + {proforma.customerEmail || proforma.customerPhone} + + )} + + + + )} + + {/* Proforma Details */} + + + Proforma Details + + + + + + + Proforma Number - {fmt(safeVal(item.total || item.unitPrice) * safeVal(item.quantity || 1), currency)} + {proforma.proformaNumber || + `PRF${(proforma.id || "").slice(0, 8).toUpperCase()}`} - - {safeVal(item.quantity)} × {fmt(safeVal(item.unitPrice), currency)} + + + + + + + Issue Date + + + {issueDate ? issueDate.toLocaleString() : "—"} + + + + + + + + + Due Date + + + {dueDate ? dueDate.toLocaleString() : "—"} + + + + + {proforma.description && ( + + + + + Description + + + {proforma.description} + + + + )} + + + + {/* Note (border only, no bg) */} + {proforma.notes && ( + + + Note + + + + {proforma.notes} - ))} - - - )} - - {items.length === 0 && ( - - - - No items - - - )} - - {/* Summary */} - - - Summary - - - - - Subtotal - - - {fmt(subtotal, currency)} - - - {tax > 0 && ( - - Tax - +{fmt(tax, currency)} )} - {discount > 0 && ( - - Discount - -{fmt(discount, currency)} + + ) : ( + + {items.length > 0 ? ( + + + Items + + + {items.map((item: any, idx: number) => { + const qty = safeVal(item.quantity || 1); + const unitPrice = safeVal(item.unitPrice); + const lineTotal = safeVal(item.total || qty * unitPrice); + return ( + + + + + + + {item.description || "No item"} + + + {qty} ×{" "} + {unitPrice.toLocaleString("en-US", { + minimumFractionDigits: 2, + })}{" "} + {currency} + + + + {lineTotal.toLocaleString("en-US", { + minimumFractionDigits: 2, + })}{" "} + {currency} + + + ); + })} + + + {/* Summary */} + + + + Subtotal + + + {fmt(subtotal, currency)} + + + {tax > 0 && ( + + + Tax + + + +{fmt(tax, currency)} + + + )} + {discount > 0 && ( + + + Discount + + + -{fmt(discount, currency)} + + + )} + + + Total + + + {fmt(amount, currency)} + + + + ) : ( + )} - - Total - - {fmt(amount, currency)} - - - - - {/* Description */} - {proforma.description ? ( - - - Description - - - - {proforma.description} - - - - ) : null} - - {/* Notes */} - {proforma.notes ? ( - - - Notes - - - - {proforma.notes} - - - - ) : null} - - {/* Actions Trigger */} - - setShowActions(true)} - className="bg-primary h-10 rounded-[6px] flex-row items-center justify-center gap-2" - > - - - Actions - - - + )} - {/* Actions Bottom Sheet */} + {/* Sticky bottom bar — Send + Download PDF (like invoice detail) */} + + setShowSendSheet(true)} + className="flex-1 h-12 rounded-[8px] border border-border items-center justify-center flex-row gap-2 bg-card" + > + + + Send + + + + + + Download PDF + + + + + {/* More bottom sheet */} setShowActions(false)} + onRequestClose={() => setShowMoreSheet(false)} > - setShowActions(false)}> + setShowMoreSheet(false)} + > e.stopPropagation()} > - {/* Header */} - Actions + + Proforma + setShowActions(false)} + onPress={() => setShowMoreSheet(false)} className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" > - {/* PDF */} } - label="Download PDF" - description="Save proforma as PDF document" - onPress={() => { setShowActions(false); handleGetPdf(); }} + icon={} + label="Edit Proforma" + description="Update details, items, or dates" + onPress={() => { + setShowMoreSheet(false); + nav.go("proforma/edit", { id: proforma.id }); + }} /> - - {/* Delete */} } label="Delete Proforma" - description="Permanently remove this proforma" - onPress={() => { setShowActions(false); handleDelete(); }} - danger - /> - - {/* Send as Email */} - - - Send - - - } - label="Send as Email" - description="Public accessible shortened link via yaltopia.com" - /> - } - label="Send as SMS" - description="Public accessible shortened link via yaltopia.com" + description="Permanently remove this record" + onPress={() => { + setShowMoreSheet(false); + handleDelete(); + }} + destructive /> + + {/* Send bottom sheet (Email / SMS) */} + setShowSendSheet(false)} + > + setShowSendSheet(false)} + > + + e.stopPropagation()} + > + + + + Send Proforma + + setShowSendSheet(false)} + className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" + > + + + + + + + Send a public, shortened link via yaltopia.com to your + customer's email or phone. + + } + label="Send as Email" + description="Public accessible shortened link via yaltopia.com" + onPress={() => handleShare("email")} + /> + + } + label="Send as SMS" + description="Public accessible shortened link via yaltopia.com" + onPress={() => handleShare("sms")} + /> + + + + + + + setShowDeleteModal(false)} + onConfirm={confirmDelete} + title="Delete Proforma" + description="Are you sure you want to permanently delete this proforma? This action cannot be reversed." + confirmText="Delete" + confirmVariant="destructive" + icon={Trash2} + iconColor="#ef4444" + loading={deleting} + /> ); } - -function ActionOption({ - icon, - label, - description, - onPress, - danger, -}: { - icon: React.ReactNode; - label: string; - description: string; - onPress?: () => void; - danger?: boolean; -}) { - return ( - - - {icon} - - - - {label} - - - {description} - - - - ); -} diff --git a/app/proforma/create.tsx b/app/proforma/create.tsx index 0fa4fcf..9a1e1e3 100644 --- a/app/proforma/create.tsx +++ b/app/proforma/create.tsx @@ -142,9 +142,11 @@ export default function CreateProformaScreen() { // Fields const [proformaNumber, setProformaNumber] = useState(""); + const [customerId, setCustomerId] = useState(""); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); + const [selectedCustomers, setSelectedCustomers] = useState([]); const [description, setDescription] = useState(""); const [currency, setCurrency] = useState("ETB"); const [taxAmount, setTaxAmount] = useState("0"); @@ -301,11 +303,20 @@ export default function CreateProformaScreen() { Customer Name { - setCustomerName(c.name); - setCustomerEmail(c.email); - setCustomerPhone(c.phone.replace("+251", "")); + selectedIds={customerId ? [customerId] : []} + selectedCustomers={selectedCustomers} + onSelect={(ids, customers) => { + setCustomerId(ids[0] || ""); + setSelectedCustomers(customers); + if (customers[0]) { + setCustomerName(customers[0].name); + setCustomerEmail(customers[0].email); + setCustomerPhone(customers[0].phone?.replace("+251", "") || ""); + } else { + setCustomerName(""); + setCustomerEmail(""); + setCustomerPhone(""); + } }} placeholder="Select or search for a customer" /> diff --git a/app/proforma/edit.tsx b/app/proforma/edit.tsx index 9d75024..4e90a31 100644 --- a/app/proforma/edit.tsx +++ b/app/proforma/edit.tsx @@ -160,9 +160,11 @@ export default function EditProformaScreen() { const [step, setStep] = useState(0); const [proformaNumber, setProformaNumber] = useState(""); + const [customerId, setCustomerId] = useState(""); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); + const [selectedCustomers, setSelectedCustomers] = useState([]); const [currency, setCurrency] = useState("USD"); const [description, setDescription] = useState(""); const [notes, setNotes] = useState(""); @@ -358,11 +360,20 @@ export default function EditProformaScreen() { Customer Name { - setCustomerName(c.name); - setCustomerEmail(c.email); - setCustomerPhone(c.phone.replace("+251", "")); + selectedIds={customerId ? [customerId] : []} + selectedCustomers={selectedCustomers} + onSelect={(ids, customers) => { + setCustomerId(ids[0] || ""); + setSelectedCustomers(customers); + if (customers[0]) { + setCustomerName(customers[0].name); + setCustomerEmail(customers[0].email); + setCustomerPhone(customers[0].phone?.replace("+251", "") || ""); + } else { + setCustomerName(""); + setCustomerEmail(""); + setCustomerPhone(""); + } }} placeholder="Select or search for a customer" /> diff --git a/app/team/[id]/details.tsx b/app/team/[id]/details.tsx index 0fbb5a4..0e476f7 100644 --- a/app/team/[id]/details.tsx +++ b/app/team/[id]/details.tsx @@ -52,7 +52,7 @@ export default function TeamMemberDetailsScreen() { if (loading) { return ( - + @@ -63,9 +63,9 @@ export default function TeamMemberDetailsScreen() { if (!member) { return ( - + - Worker not found + Team member not found ); @@ -73,7 +73,7 @@ export default function TeamMemberDetailsScreen() { return ( - + void; + placeholder: string; + icon?: React.ReactNode; + flex?: number; + numeric?: boolean; + secureTextEntry?: boolean; +}) { + const c = useInputColors(); + return ( + + + {label} + + + {icon} + + + + ); +} + +const ROLES = ["VIEWER", "EMPLOYEE", "ACCOUNTANT", "CUSTOMER_SERVICE"]; + +const STEPS = [ + { key: "name", label: "Name" }, + { key: "contact", label: "Contact" }, + { key: "access", label: "Access" }, +]; + +export default function CreateTeamMemberScreen() { + const nav = useSirouRouter(); + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + const iconColor = isDark ? "#94a3b8" : "#64748b"; + + const [step, setStep] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [showRolePicker, setShowRolePicker] = useState(false); + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [phone, setPhone] = useState(""); + const [role, setRole] = useState("VIEWER"); + const [password, setPassword] = useState(""); + + const handleNext = () => { + if (step === 0 && (!firstName.trim() || !lastName.trim())) { + toast.error("Validation", "First and last name are required"); + return; + } + if (step === 1 && (!email.trim() || !phone.trim())) { + toast.error("Validation", "Email and phone are required"); + return; + } + if (step < STEPS.length - 1) setStep(step + 1); + }; + + const handleBack = () => { + if (step > 0) setStep(step - 1); + }; + + const handleSubmit = async () => { + if (!password.trim()) { + toast.error("Validation", "Password is required"); + return; + } + + setSubmitting(true); + try { + const formattedPhone = `+251${phone.trim()}`; + await api.team.create({ + body: { + firstName: firstName.trim(), + lastName: lastName.trim(), + email: email.trim(), + phone: formattedPhone, + role, + password: password.trim(), + }, + }); + toast.success("Team Member Added", `${firstName} has been added to the team.`); + nav.back(); + } catch (err: any) { + toast.error("Creation Failed", err.message || "Failed to add team member"); + } finally { + setSubmitting(false); + } + }; + + return ( + + + {step === 0 && ( + + + + Personal Info + + + Enter the team member's full name + + + } + /> + } + /> + + )} + + {step === 1 && ( + + + + Contact Details + + + How to reach this team member + + + } + /> + + + Phone Number + + + + +251 + + + + + )} + + {step === 2 && ( + + + + System Access + + + Set role and initial password + + + + + + Role + + setShowRolePicker(true)} + style={[S.input, { backgroundColor: useInputColors().bg, borderColor: useInputColors().border, flexDirection: "row", alignItems: "center" }]} + > + + + {role.replace("_", " ")} + + + + + + } + secureTextEntry + /> + + )} + + + setShowRolePicker(false)} + title="Select Role" + > + {ROLES.map((r) => ( + { + setRole(v); + setShowRolePicker(false); + }} + /> + ))} + + + ); +} diff --git a/app/team/index.tsx b/app/team/index.tsx index dc85565..c6020a8 100644 --- a/app/team/index.tsx +++ b/app/team/index.tsx @@ -65,14 +65,14 @@ export default function TeamScreen() { return ( - + - Workers ({filteredWorkers?.length || 0}) + Members ({filteredWorkers?.length || 0}) @@ -159,7 +159,7 @@ export default function TeamScreen() { )) ) : ( )} diff --git a/assets/cbe.png b/assets/cbe1.png similarity index 100% rename from assets/cbe.png rename to assets/cbe1.png diff --git a/assets/ticket.png b/assets/ticket.png new file mode 100644 index 0000000000000000000000000000000000000000..8fbab9886e24e4d425bb53b12bee5887cf204f2a GIT binary patch literal 105111 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

(hWA`AgAF4WUBb>#a{dF-C)_dv|=F|x6*bybL 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

NQwGqNJIDft~GZT4~I9TS0f>e+! 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 Hze4>FaUKoZ literal 0 HcmV?d00001 diff --git a/components/CommandPalette.tsx b/components/CommandPalette.tsx index f67f550..cf9a29d 100644 --- a/components/CommandPalette.tsx +++ b/components/CommandPalette.tsx @@ -3,7 +3,7 @@ import { View, Pressable, TextInput, useColorScheme, Modal, ScrollView } from "r import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Text } from "@/components/ui/text"; -import { X, Search, FileText, ShieldCheck, Wallet, Receipt, Settings, User, HelpCircle, Briefcase, FolderOpen, BarChart3, DraftingCompass, Scan, Lock, Globe, History } from "@/lib/icons"; +import { X, Search, FileText, ShieldCheck, Wallet, Settings, User, HelpCircle, Briefcase, FolderOpen, BarChart3, DraftingCompass, Scan, Lock, Globe, History, Inbox } from "@/lib/icons"; const ICON_COLOR = "#E46212"; @@ -19,7 +19,6 @@ const FLOWS: Flow[] = [ { label: "Add Invoice", keywords: ["invoice", "create", "new", "bill"], route: "invoices/create", icon: }, { label: "Verify Payment", keywords: ["verify", "payment", "reference", "ft"], route: "verify-payment", icon: }, { label: "Create Payment", keywords: ["payment", "create", "new", "pay"], route: "payments/create", icon: }, - { label: "Add Receipt", keywords: ["receipt", "scan", "upload"], route: "add-receipt", icon: }, { label: "Settings", keywords: ["settings", "preferences", "theme"], route: "settings", icon: }, { label: "Profile", keywords: ["profile", "account", "user"], route: "profile", icon: }, { label: "Help & Support", keywords: ["help", "support", "ticket"], route: "help", icon: }, @@ -29,6 +28,7 @@ const FLOWS: Flow[] = [ { label: "Reports", keywords: ["reports", "analytics", "stats"], route: "reports/index", icon: }, { label: "Scan Receipt", keywords: ["scan", "camera", "receipt", "ocr"], route: "(tabs)/scan", icon: }, { label: "Proforma", keywords: ["proforma", "estimate", "quote"], route: "(tabs)/proforma", icon: }, + { label: "Proforma Requests", keywords: ["request", "rfq", "quote", "proforma", "inquiry"], route: "(tabs)/proforma", icon: }, { label: "News", keywords: ["news", "updates", "announcements"], route: "news/index", icon: }, { label: "Change PIN", keywords: ["pin", "password", "security", "change"], route: "set-pin", icon: }, { label: "Payment History", keywords: ["payments", "history", "transactions"], route: "history", icon: }, diff --git a/components/ConfirmSubmitModal.tsx b/components/ConfirmSubmitModal.tsx new file mode 100644 index 0000000..6a6e3ae --- /dev/null +++ b/components/ConfirmSubmitModal.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { Modal, Pressable, StyleSheet, View } from "react-native"; +import { Text } from "./ui/text"; +import { Button } from "./ui/button"; +import { CheckCircle2, X } from "@/lib/icons"; +import { useColorScheme } from "nativewind"; + +interface ConfirmSubmitModalProps { + visible: boolean; + onClose: () => void; + onConfirm: () => void; + title?: string; + description?: string; + confirmText?: string; + cancelText?: string; + loading?: boolean; +} + +export function ConfirmSubmitModal({ + visible, + onClose, + onConfirm, + title = "Confirm submission", + description = "Are you sure all the information is correct? Please review before proceeding.", + confirmText = "Yes, submit", + cancelText = "Review again", + loading = false, +}: ConfirmSubmitModalProps) { + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + + return ( + + + e.stopPropagation()} + > + + + + + + + {title} + + + + + + + + {description} + + + + + + + + + + + ); +} diff --git a/components/CreateMethodSheet.tsx b/components/CreateMethodSheet.tsx new file mode 100644 index 0000000..8b5a0d8 --- /dev/null +++ b/components/CreateMethodSheet.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { Modal, Pressable, View, Dimensions, ScrollView } from "react-native"; +import { Text } from "./ui/text"; +import { ScanLine, X, Edit } from "@/lib/icons"; +import { useColorScheme } from "nativewind"; + +const { height: SCREEN_HEIGHT } = Dimensions.get("window"); + +interface CreateMethodSheetProps { + visible: boolean; + onClose: () => void; + onSelectScan: () => void; + onSelectManual: () => void; + title?: string; +} + +export function CreateMethodSheet({ + visible, + onClose, + onSelectScan, + onSelectManual, + title = "Create", +}: CreateMethodSheetProps) { + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + + return ( + + + + e.stopPropagation()} + > + + + + {title} + + + + + + + + + + + + + + Scan + + + Capture a photo and auto-fill the form + + + + + + + + + + + Enter manually + + + Enter all the details yourself + + + + + + + + + ); +} diff --git a/components/CustomerPicker.tsx b/components/CustomerPicker.tsx index bedb1d2..71cd324 100644 --- a/components/CustomerPicker.tsx +++ b/components/CustomerPicker.tsx @@ -10,25 +10,27 @@ import { } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { Text } from "@/components/ui/text"; -import { Search, X, Plus, User, Building2, ChevronDown } from "@/lib/icons"; +import { Search, X, Plus, User, Building2, ChevronDown, Check } from "@/lib/icons"; import { api } from "@/lib/api"; import { useColorScheme } from "nativewind"; const { height: SCREEN_HEIGHT } = Dimensions.get("window"); interface CustomerData { + id: string; name: string; email: string; phone: string; } interface CustomerPickerProps { - value: string; - onSelect: (c: CustomerData) => void; + selectedIds: string[]; + selectedCustomers: CustomerData[]; + onSelect: (ids: string[], customers: CustomerData[]) => void; placeholder?: string; } -export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerProps) { +export function CustomerPicker({ selectedIds, selectedCustomers, onSelect, placeholder }: CustomerPickerProps) { const nav = useSirouRouter(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; @@ -38,8 +40,13 @@ export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerP const [loading, setLoading] = useState(false); const [search, setSearch] = useState(""); + const [tempIds, setTempIds] = useState(selectedIds); + const [tempCustomers, setTempCustomers] = useState(selectedCustomers); + const openPicker = async () => { setShow(true); + setTempIds(selectedIds); + setTempCustomers(selectedCustomers); setSearch(""); setLoading(true); try { @@ -52,6 +59,29 @@ export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerP } }; + const toggleCustomer = (c: any) => { + const id = String(c.id); + const name = c.displayName || `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.companyName || ""; + + let newIds: string[]; + let newCustomers: CustomerData[]; + + if (tempIds.includes(id)) { + newIds = tempIds.filter((i) => i !== id); + newCustomers = tempCustomers.filter((p) => p.id !== id); + } else { + newIds = [...tempIds, id]; + newCustomers = [ + ...tempCustomers, + { id, name, email: c.email || "", phone: c.phone || "" }, + ]; + } + + setTempIds(newIds); + setTempCustomers(newCustomers); + onSelect(newIds, newCustomers); + }; + const filtered = useMemo(() => { if (!search.trim()) return customers; const q = search.toLowerCase(); @@ -65,14 +95,25 @@ export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerP ); }, [customers, search]); + const triggerLabel = selectedIds.length === 0 + ? (placeholder || "Select customers") + : selectedIds.length === 1 + ? selectedCustomers[0]?.name || placeholder + : `${selectedIds.length} customers selected`; + return ( <> - - {value || placeholder || "Select a customer"} + 0 ? "text-foreground" : "text-muted-foreground" + }`} + numberOfLines={1} + > + {triggerLabel} @@ -119,7 +160,6 @@ export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerP - {/* Add New Customer */} { setShow(false); nav.go("customers/create"); }} @@ -148,20 +188,14 @@ export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerP ) : ( filtered.map((c: any) => { const isCompany = c.type === "COMPANY"; + const isSelected = tempIds.includes(String(c.id)); return ( { - setShow(false); - onSelect({ - name: c.displayName || `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.companyName || "", - email: c.email || "", - phone: c.phone || "", - }); - }} - className="bg-card rounded-[6px] border border-border p-4 mb-3" + onPress={() => toggleCustomer(c)} + className="bg-card rounded-[6px] border border-border p-4 mb-3 flex-row items-center" > - + {isCompany ? ( @@ -185,6 +219,13 @@ export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerP + + {isSelected && } + ); }) diff --git a/components/ModalToast.tsx b/components/ModalToast.tsx index ad28673..60d5c0d 100644 --- a/components/ModalToast.tsx +++ b/components/ModalToast.tsx @@ -1,52 +1,54 @@ import React, { useEffect, useRef } from "react"; import { View, StyleSheet, Animated } from "react-native"; -import { CheckCircle2, AlertCircle, AlertTriangle, Lightbulb, X } from "@/lib/icons"; +import { + CheckCircle2, + AlertCircle, + AlertTriangle, + Lightbulb, +} from "@/lib/icons"; import { Text } from "@/components/ui/text"; import { useToast, ToastType } from "@/lib/toast-store"; import { useColorScheme } from "nativewind"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; const VARIANT_CONFIG: Record< ToastType, - { iconColor: string; borderColor: string; icon: React.ReactNode } + { iconColor: string; icon: typeof CheckCircle2 } > = { success: { - iconColor: "#16a34a", - borderColor: "#16a34a", - icon: , + iconColor: "#4ADE80", + icon: CheckCircle2, }, error: { - iconColor: "#dc2626", - borderColor: "#dc2626", - icon: , + iconColor: "#F87171", + icon: AlertCircle, }, warning: { - iconColor: "#d97706", - borderColor: "#d97706", - icon: , + iconColor: "#FBBF24", + icon: AlertTriangle, }, info: { - iconColor: "#E46212", - borderColor: "#E46212", - icon: , + iconColor: "#60A5FA", + icon: Lightbulb, }, }; export function ModalToast() { const { visible, type, title, message, hide, duration } = useToast(); const isDark = useColorScheme() === "dark"; - const insets = useSafeAreaInsets(); - const translateX = useRef(new Animated.Value(-40)).current; + const translateY = useRef(new Animated.Value(-20)).current; const opacity = useRef(new Animated.Value(0)).current; + const hideRef = useRef(hide); + hideRef.current = hide; + useEffect(() => { if (visible) { - translateX.setValue(-40); + translateY.setValue(-20); opacity.setValue(0); Animated.parallel([ - Animated.spring(translateX, { + Animated.spring(translateY, { toValue: 0, useNativeDriver: true, speed: 20, @@ -59,57 +61,48 @@ export function ModalToast() { }), ]).start(); - const timer = setTimeout(hide, duration); + const timer = setTimeout(() => { + Animated.timing(opacity, { + toValue: 0, + duration: 180, + useNativeDriver: true, + }).start(() => hideRef.current()); + }, duration); return () => clearTimeout(timer); } - }, [visible]); + }, [visible, duration]); if (!visible) return null; const config = VARIANT_CONFIG[type]; + const Icon = config.icon; return ( - - + - {config.icon} - + - - - {title} - - {message ? ( - - {message} + + + {title} - ) : null} - - - - - - + + + ); } @@ -119,27 +112,26 @@ const styles = StyleSheet.create({ zIndex: 9999, elevation: 50, }, + wrapper: { + position: "absolute", + top: 60, + left: 0, + right: 0, + alignItems: "center", + paddingHorizontal: 20, + }, toast: { - marginHorizontal: 16, - borderRadius: 12, - paddingHorizontal: 16, - paddingVertical: 12, + width: "100%", + maxWidth: 400, + borderRadius: 14, + paddingHorizontal: 18, + paddingVertical: 16, flexDirection: "row", alignItems: "center", - shadowColor: "#000", - shadowOpacity: 0.18, - shadowRadius: 8, - shadowOffset: { width: 0, height: 4 }, - }, - iconContainer: { - width: 32, - height: 32, - borderRadius: 16, - alignItems: "center", - justifyContent: "center", + borderWidth: 1, + gap: 12, }, textContainer: { flex: 1, - marginLeft: 12, }, }); diff --git a/components/Toast.tsx b/components/Toast.tsx deleted file mode 100644 index 8b48c02..0000000 --- a/components/Toast.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useEffect } from "react"; -import { View, Dimensions, Pressable } from "react-native"; -import { Text } from "@/components/ui/text"; -import { useToast, ToastType } from "@/lib/toast-store"; -import { - CheckCircle2, - AlertCircle, - AlertTriangle, - Lightbulb, - X, -} from "@/lib/icons"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import Animated, { - useSharedValue, - useAnimatedStyle, - withSpring, - withTiming, - runOnJS, -} from "react-native-reanimated"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; - -const { width: SCREEN_WIDTH } = Dimensions.get("window"); -const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.35; - -const TOAST_VARIANTS: Record< - ToastType, - { - accent: string; - iconBg: string; - icon: React.ReactNode; - } -> = { - success: { - accent: "#16a34a", - iconBg: "#16a34a15", - icon: , - }, - info: { - accent: "#E46212", - iconBg: "#E4621215", - icon: , - }, - warning: { - accent: "#d97706", - iconBg: "#d9770615", - icon: , - }, - error: { - accent: "#dc2626", - iconBg: "#dc262615", - icon: , - }, -}; - -export function Toast() { - const { visible, type, title, message, hide, duration } = useToast(); - const insets = useSafeAreaInsets(); - - const opacity = useSharedValue(0); - const scale = useSharedValue(0.85); - const translateY = useSharedValue(-60); - const translateX = useSharedValue(0); - - useEffect(() => { - if (visible) { - opacity.value = withTiming(1, { duration: 200 }); - scale.value = withSpring(1, { damping: 14, stiffness: 160 }); - translateY.value = withSpring(0, { damping: 16, stiffness: 140 }); - translateX.value = 0; - - const timer = setTimeout(handleHide, duration); - return () => clearTimeout(timer); - } - }, [visible]); - - const handleHide = () => { - opacity.value = withTiming(0, { duration: 180 }); - scale.value = withTiming(0.92, { duration: 180 }); - translateY.value = withTiming(-40, { duration: 180 }, () => { - runOnJS(hide)(); - }); - }; - - const swipeGesture = Gesture.Pan() - .onUpdate((event) => { - translateX.value = event.translationX; - }) - .onEnd((event) => { - if (Math.abs(event.translationX) > SWIPE_THRESHOLD) { - translateX.value = withTiming( - event.translationX > 0 ? SCREEN_WIDTH : -SCREEN_WIDTH, - { duration: 200 }, - () => runOnJS(handleHide)(), - ); - } else { - translateX.value = withSpring(0); - } - }); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - transform: [ - { translateY: translateY.value }, - { translateX: translateX.value }, - { scale: scale.value }, - ], - })); - - if (!visible) return null; - - const variant = TOAST_VARIANTS[type]; - - return ( - - - - - - - {variant.icon} - - - - - {title} - - {message ? ( - - {message} - - ) : null} - - - - - - - - - ); -} diff --git a/lib/api.ts b/lib/api.ts index 855229e..ec96f91 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -81,6 +81,7 @@ export const api = createApi({ endpoints: { get: { method: "GET", path: "company" }, update: { method: "PUT", path: "company" }, + paymentMethods: { method: "GET", path: "company/payment-methods" }, }, }, team: { @@ -115,6 +116,7 @@ export const api = createApi({ update: { method: "PUT", path: "payments/:id" }, associate: { method: "POST", path: "payments/:id/associate" }, verifySms: { method: "POST", path: "payments/:id/verify-sms" }, + flag: { method: "POST", path: "payments/:id/flag" }, delete: { method: "DELETE", path: "payments/:id" }, }, }, @@ -124,8 +126,9 @@ export const api = createApi({ getAll: { method: "GET", path: "payment-requests" }, getById: { method: "GET", path: "payment-requests/:id" }, create: { method: "POST", path: "payment-requests" }, + update: { method: "PUT", path: "payment-requests/:id" }, open: { method: "POST", path: "payment-requests/:id/open" }, - sendEmail: { method: "POST", path: "payment-requests/:id/send-email" }, + send: { method: "POST", path: "payment-requests/:id/send" }, }, }, proforma: { @@ -137,6 +140,15 @@ export const api = createApi({ update: { method: "PUT", path: "proforma/:id" }, delete: { method: "DELETE", path: "proforma/:id" }, getPdf: { method: "GET", path: "proforma/:id/pdf" }, + shareLink: { method: "POST", path: "proforma/share/link" }, + }, + }, + proformaRequests: { + middleware: [authMiddleware], + endpoints: { + getAll: { method: "GET", path: "proforma-requests" }, + getById: { method: "GET", path: "proforma-requests/:id" }, + create: { method: "POST", path: "proforma-requests" }, }, }, rbac: { @@ -165,6 +177,8 @@ export const api = createApi({ getAll: { method: "GET", path: "customers" }, getById: { method: "GET", path: "customers/:id" }, create: { method: "POST", path: "customers" }, + update: { method: "PUT", path: "customers/:id" }, + delete: { method: "DELETE", path: "customers/:id" }, }, }, declarations: { @@ -175,6 +189,7 @@ export const api = createApi({ create: { method: "POST", path: "declarations" }, update: { method: "PUT", path: "declarations/:id" }, delete: { method: "DELETE", path: "declarations/:id" }, + scan: { method: "POST", path: "declarations/scan" }, }, }, }, diff --git a/lib/icons.tsx b/lib/icons.tsx index 6a72588..38981c8 100644 --- a/lib/icons.tsx +++ b/lib/icons.tsx @@ -65,6 +65,7 @@ export { Triangle as TrianglePlanets, AlertTriangle, Lightbulb, + Flag, Check, MessageSquare, RefreshCw, @@ -82,4 +83,8 @@ export { MapPin, BookOpen, FileCheck, + Inbox, + Truck, + Hourglass, + XCircle, } from "lucide-react-native"; diff --git a/lib/payment-providers.ts b/lib/payment-providers.ts deleted file mode 100644 index ad6b6c6..0000000 --- a/lib/payment-providers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ImageSourcePropType } from "react-native"; - -const PROVIDER_LOGOS: Record = { - telebirr: require("@/assets/telebirr.png"), - cbe: require("@/assets/cbe.png"), - dashen: require("@/assets/dashen.png"), -}; - -export function getProviderLogo(paymentMethod: string): ImageSourcePropType | null { - return PROVIDER_LOGOS[paymentMethod.toLowerCase()] ?? null; -} - -export function isCash(paymentMethod: string): boolean { - return paymentMethod.toLowerCase() === "cash"; -} diff --git a/lib/routes.ts b/lib/routes.ts index ea9b52f..9989f76 100644 --- a/lib/routes.ts +++ b/lib/routes.ts @@ -87,6 +87,17 @@ export const routes = defineRoutes({ guards: ["auth"], meta: { requiresAuth: true }, }, + "proforma-requests/create": { + path: "/proforma-requests/create", + guards: ["auth"], + meta: { requiresAuth: true, title: "Create Proforma Request" }, + }, + "proforma-requests/[id]": { + path: "/proforma-requests/:id", + params: { id: "string" }, + guards: ["auth"], + meta: { requiresAuth: true, title: "Proforma Request" }, + }, "payments/[id]": { path: "/payments/:id", params: { id: "string" }, @@ -104,11 +115,23 @@ export const routes = defineRoutes({ guards: ["auth"], meta: { requiresAuth: true }, }, + "payment-requests/edit": { + path: "/payment-requests/edit", + params: { id: "string" }, + guards: ["auth"], + meta: { requiresAuth: true }, + }, "customers/create": { path: "/customers/create", guards: ["auth"], meta: { requiresAuth: true }, }, + "customers/edit": { + path: "/customers/edit", + params: { id: "string" }, + guards: ["auth"], + meta: { requiresAuth: true }, + }, "customers/[id]": { path: "/customers/:id", params: { id: "string" }, @@ -208,11 +231,6 @@ export const routes = defineRoutes({ guards: ["auth"], meta: { requiresAuth: true, title: "Verification Result" }, }, - "add-receipt": { - path: "/add-receipt", - guards: ["auth"], - meta: { requiresAuth: true, title: "Add Receipt" }, - }, company: { path: "/company", guards: ["auth"], @@ -231,18 +249,18 @@ export const routes = defineRoutes({ "team/index": { path: "/team", guards: ["auth"], - meta: { requiresAuth: true, title: "Workers" }, + meta: { requiresAuth: true, title: "Team" }, }, "team/[id]/details": { path: "/team/:id/details", params: { id: "string" }, guards: ["auth"], - meta: { requiresAuth: true, title: "Worker Details" }, + meta: { requiresAuth: true, title: "Team Member Details" }, }, - "user/create": { - path: "/user/create", + "team/create": { + path: "/team/create", guards: ["auth"], - meta: { requiresAuth: true, title: "Add User" }, + meta: { requiresAuth: true, title: "Add Team Member" }, }, "declarations/index": { path: "/declarations/index", @@ -254,6 +272,11 @@ export const routes = defineRoutes({ guards: ["auth"], meta: { requiresAuth: true, title: "Create Declaration" }, }, + "declarations/scan": { + path: "/declarations/scan", + guards: ["auth"], + meta: { requiresAuth: true, title: "Scan Declaration" }, + }, "declarations/edit": { path: "/declarations/edit", params: { id: "string" }, diff --git a/lib/scan-cache.ts b/lib/scan-cache.ts index af59d1a..216358e 100644 --- a/lib/scan-cache.ts +++ b/lib/scan-cache.ts @@ -1,10 +1,18 @@ -let _scanData: any = null; +export type ScanType = "invoice" | "payment" | "declaration"; -export function setScanData(data: any) { - _scanData = data; +export interface ScanPayload { + type: ScanType; + id?: string; + data: any; } -export function getScanData() { +let _scanData: ScanPayload | null = null; + +export function setScanData(payload: ScanPayload) { + _scanData = payload; +} + +export function getScanData(): ScanPayload | null { const data = _scanData; _scanData = null; return data; diff --git a/package-lock.json b/package-lock.json index 158fee7..2a6ca9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "clsx": "^2.1.1", "expo": "~52.0.35", "expo-camera": "~16.0.18", + "expo-clipboard": "^56.0.4", "expo-constants": "~17.0.7", "expo-document-picker": "~13.0.3", "expo-image-picker": "~16.0.3", @@ -39,6 +40,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.7", + "react-native-bcrypt": "^2.4.0", "react-native-gesture-handler": "~2.20.2", "react-native-get-sms-android": "^2.1.0", "react-native-reanimated": "~3.16.1", @@ -47,6 +49,7 @@ "react-native-svg": "15.8.0", "react-native-timer-picker": "^2.6.3", "react-native-web": "~0.19.13", + "react-native-webview": "^13.12.5", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.11" @@ -97,9 +100,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -159,13 +162,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -175,25 +178,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -212,17 +215,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -242,12 +245,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, @@ -268,9 +271,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -296,49 +299,49 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -348,35 +351,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -386,14 +389,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -403,54 +406,54 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -556,12 +559,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -853,12 +856,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1024,12 +1027,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1039,14 +1042,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1056,14 +1059,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1073,12 +1076,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1088,13 +1091,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1104,17 +1107,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1124,13 +1127,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1140,13 +1143,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1156,12 +1159,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1187,13 +1190,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1203,14 +1206,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1220,12 +1223,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1235,12 +1238,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1250,13 +1253,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1266,13 +1269,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1282,12 +1285,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1297,12 +1300,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1312,16 +1315,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1331,12 +1334,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1346,13 +1349,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1362,12 +1365,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1377,13 +1380,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1393,14 +1396,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1505,12 +1508,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1549,12 +1552,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1564,13 +1567,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1580,12 +1583,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1595,12 +1598,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1629,13 +1632,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1729,26 +1732,26 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -1757,17 +1760,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1808,12 +1811,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -1822,13 +1825,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -7588,6 +7591,17 @@ } } }, + "node_modules/expo-clipboard": { + "version": "56.0.4", + "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-56.0.4.tgz", + "integrity": "sha512-qb4DYlkiowHYHaUYVT2FN9nk/nI1xShXOUYsI7J9dVpQCOHcGFjCBPX1VAvEW4Ye4/Aagd6IuhOVAq/+scBOiA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-constants": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz", @@ -12410,6 +12424,12 @@ } } }, + "node_modules/react-native-bcrypt": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-native-bcrypt/-/react-native-bcrypt-2.4.0.tgz", + "integrity": "sha512-nC8SR/YCCLIluxd1YEkUX2/xjKELLkO0pgIZc1JGUw9o+wnsfpujm0J8NSPmPEqFUe1n2EkysHnAmuKInDusoQ==", + "license": "MIT" + }, "node_modules/react-native-css-interop": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.2.tgz", @@ -12603,6 +12623,20 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/react-native-webview": { + "version": "13.12.5", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.12.5.tgz", + "integrity": "sha512-INOKPom4dFyzkbxbkuQNfeRG9/iYnyRDzrDkJeyvSWgJAW2IDdJkWFJBS2v0RxIL4gqLgHkiIZDOfiLaNnw83Q==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { "version": "0.76.7", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.7.tgz", diff --git a/package.json b/package.json index 7bfb694..6cdebd2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "clsx": "^2.1.1", "expo": "~52.0.35", "expo-camera": "~16.0.18", + "expo-clipboard": "^56.0.4", "expo-constants": "~17.0.7", "expo-document-picker": "~13.0.3", "expo-image-picker": "~16.0.3", @@ -40,6 +41,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.7", + "react-native-bcrypt": "^2.4.0", "react-native-gesture-handler": "~2.20.2", "react-native-get-sms-android": "^2.1.0", "react-native-reanimated": "~3.16.1", @@ -48,6 +50,7 @@ "react-native-svg": "15.8.0", "react-native-timer-picker": "^2.6.3", "react-native-web": "~0.19.13", + "react-native-webview": "^13.12.5", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.11"