From 346e41445b601def76c2e4c219790646c779ca02 Mon Sep 17 00:00:00 2001 From: Nemo Date: Tue, 24 Mar 2026 11:27:18 +0800 Subject: [PATCH] feat: dashboard tickets+invoices, collect screen overhaul, client detail tickets tab + pay invoice + map - Dashboard: active tickets (unassigned + assigned to me, top 10), top 10 unpaid invoices by due date, revenue hidden for TECHNICIAN/COLLECTOR - Collect screen: unpaid invoices sorted overdue-first, remittance button at top, navigate button (Google Maps/Waze) - Client Detail: added Tickets tab, removed Payments tab, Invoices tab has pay button per invoice + status tags + ordered by issuedDate - Client Profile tab: map view + navigate button using client lat/lng - Installed react-native-maps --- app/(app)/clients/[id].tsx | 457 ++++++++++++++++++++++++----------- app/(app)/dashboard.tsx | 189 ++++++++++----- app/(app)/payments/index.tsx | 223 +++++++++++++++-- package-lock.json | 276 +++++++++++++++------ package.json | 1 + 5 files changed, 846 insertions(+), 300 deletions(-) diff --git a/app/(app)/clients/[id].tsx b/app/(app)/clients/[id].tsx index 75eb29e..2a9ae49 100644 --- a/app/(app)/clients/[id].tsx +++ b/app/(app)/clients/[id].tsx @@ -1,64 +1,175 @@ import { useState } from 'react'; -import { View, Text, ScrollView, TouchableOpacity, ActivityIndicator, Linking } from 'react-native'; +import { View, Text, ScrollView, TouchableOpacity, ActivityIndicator, Linking, Alert, TextInput, Modal } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useLocalSearchParams, router } from 'expo-router'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import MapView, { Marker } from 'react-native-maps'; import { api } from '../../../services/api'; -const TABS = ['Profile', 'Subscription', 'Invoices', 'Payments']; +const TABS = ['Profile', 'Subscription', 'Invoices', 'Tickets'] as const; +type Tab = typeof TABS[number]; + +const STATUS_COLOR: Record = { ACTIVE: '#166534', SUSPENDED: '#92400E', CANCELLED: '#991B1B', PENDING: '#475569' }; +const STATUS_BG: Record = { ACTIVE: '#DCFCE7', SUSPENDED: '#FEF3C7', CANCELLED: '#FEE2E2', PENDING: '#F1F5F9' }; +const TICKET_STATUS_COLOR: Record = { OPEN: '#0891B2', IN_PROGRESS: '#D97706', RESOLVED: '#16A34A', CLOSED: '#6B7280' }; +const TYPE_COLOR: Record = { INSTALLATION: '#0891B2', SUPPORT: '#7C3AED', BILLING: '#D97706' }; -const STATUS_COLOR: Record = { - ACTIVE: '#166534', SUSPENDED: '#92400E', CANCELLED: '#991B1B', PENDING: '#475569', -}; -const STATUS_BG: Record = { - ACTIVE: '#DCFCE7', SUSPENDED: '#FEF3C7', CANCELLED: '#FEE2E2', PENDING: '#F1F5F9', -}; const INV_STATUS: Record = { PAID: { label: 'Paid', color: '#166534', bg: '#DCFCE7' }, - UNPAID: { label: 'Unpaid', color: '#92400E', bg: '#FEF3C7' }, + SENT: { label: 'Unpaid', color: '#92400E', bg: '#FEF3C7' }, OVERDUE: { label: 'Overdue', color: '#991B1B', bg: '#FEE2E2' }, PARTIAL: { label: 'Partial', color: '#0E7490', bg: '#CFFAFE' }, + DRAFT: { label: 'Draft', color: '#6B7280', bg: '#F1F5F9' }, VOID: { label: 'Void', color: '#6B7280', bg: '#F1F5F9' }, }; -function InfoRow({ label, value, onPress, isLast }: { label: string; value?: string | null; onPress?: () => void; isLast?: boolean }) { +function InfoRow({ label, value, isLast }: { label: string; value?: string | null; isLast?: boolean }) { return ( - + {label} - {value ?? '—'} - + {value ?? '—'} + ); } +// ── Pay Invoice Modal ───────────────────────────────────────────────────────── +function PayInvoiceModal({ invoice, onClose, onSuccess }: { invoice: any; onClose: () => void; onSuccess: () => void }) { + const [amount, setAmount] = useState(''); + const [method, setMethod] = useState('CASH'); + const [ref, setRef] = useState(''); + const [loading, setLoading] = useState(false); + const METHODS = ['CASH', 'GCASH', 'MAYA', 'BANK']; + + const submit = async () => { + const amt = Number(amount); + if (!amt || amt <= 0) return Alert.alert('Required', 'Enter a valid amount.'); + if (amt > Number(invoice.balance)) { + Alert.alert('Over Payment', `Amount exceeds balance of ₱${Number(invoice.balance).toLocaleString()}`); + return; + } + setLoading(true); + try { + await api.post('/api/v1/payments', { + clientId: invoice.clientId, + invoiceId: invoice.id, + amount: amt, + channel: method, + referenceNumber: ref.trim() || undefined, + paymentDate: new Date().toISOString(), + }); + Alert.alert('Payment Recorded!', `₱${amt.toLocaleString()} applied to ${invoice.invoiceNumber}`); + onSuccess(); + } catch (e: any) { + const msg = e?.response?.data?.message; + Alert.alert('Error', Array.isArray(msg) ? msg.join('\n') : msg ?? 'Payment failed.'); + } finally { setLoading(false); } + }; + + return ( + + + + Record Payment + + {invoice.invoiceNumber} · Balance: ₱{Number(invoice.balance).toLocaleString()} + + + Amount (₱) + + + Method + + {METHODS.map(m => ( + setMethod(m)} + style={{ flex: 1, borderRadius: 12, paddingVertical: 12, alignItems: 'center', marginHorizontal: 3, backgroundColor: method === m ? '#0891B2' : '#F1F5F9' }} + activeOpacity={0.7} + > + {m} + + ))} + + + Reference # (optional) + + + 0 ? '#059669' : '#CBD5E1', borderRadius: 14, paddingVertical: 18, alignItems: 'center' }} + onPress={submit} + disabled={loading} + activeOpacity={0.8} + > + {loading ? : Confirm Payment} + + + + + ); +} + +// ── Main Screen ─────────────────────────────────────────────────────────────── export default function ClientDetailScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); - const [tab, setTab] = useState('Profile'); + const params = useLocalSearchParams<{ id: string; tab?: string }>(); + const id = params.id; + const qc = useQueryClient(); + + const initialTab = (params.tab === 'invoices' ? 'Invoices' : 'Profile') as Tab; + const [activeTab, setActiveTab] = useState(initialTab); + const [payingInvoice, setPayingInvoice] = useState(null); const { data: client, isLoading } = useQuery({ queryKey: ['client', id], queryFn: () => api.get(`/api/v1/clients/${id}`).then(r => r.data), }); - const { data: subData, isLoading: subLoading } = useQuery({ - queryKey: ['client-subscription', id], - queryFn: () => api.get(`/api/v1/subscriptions?clientId=${id}&limit=1`).then(r => r.data?.data?.[0] ?? r.data?.[0] ?? null), - enabled: tab === 'Subscription', - }); - const { data: invoices, isLoading: invLoading } = useQuery({ + + const { data: invoices } = useQuery({ queryKey: ['client-invoices', id], - queryFn: () => api.get(`/api/v1/invoices?clientId=${id}&limit=20`).then(r => r.data?.data ?? r.data ?? []), - enabled: tab === 'Invoices', + queryFn: async () => { + const res = await api.get(`/api/v1/invoices?clientId=${id}&limit=50`); + const items: any[] = res.data?.data ?? res.data ?? []; + items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + return items; + }, + enabled: activeTab === 'Invoices', }); - const { data: payments, isLoading: payLoading } = useQuery({ - queryKey: ['client-payments', id], - queryFn: () => api.get(`/api/v1/payments?clientId=${id}&limit=20`).then(r => r.data?.data ?? r.data ?? []), - enabled: tab === 'Payments', + + const { data: tickets } = useQuery({ + queryKey: ['client-tickets', id], + queryFn: async () => { + const res = await api.get(`/api/v1/tickets?clientId=${id}&limit=50`); + return res.data?.data ?? res.data ?? []; + }, + enabled: activeTab === 'Tickets', }); + const sub = client?.subscriptions?.[0]; + + const navigateToClient = () => { + const lat = client?.lat; + const lng = client?.lng; + if (!lat || !lng) { + Alert.alert('No Location', 'This client has no recorded location yet. Location is recorded during installation confirmation.'); + return; + } + Alert.alert('Navigate to Client', `${client?.firstName} ${client?.lastName}`, [ + { text: 'Google Maps', onPress: () => Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`) }, + { text: 'Waze', onPress: () => Linking.openURL(`waze://?ll=${lat},${lng}&navigate=yes`) }, + { text: 'Cancel', style: 'cancel' }, + ]); + }; + if (isLoading) { return ( @@ -69,154 +180,212 @@ export default function ClientDetailScreen() { ); } - const statusColor = STATUS_COLOR[client?.status] ?? '#475569'; - const statusBg = STATUS_BG[client?.status] ?? '#F1F5F9'; + const statusStyle = { + color: STATUS_COLOR[client?.status] ?? '#475569', + bg: STATUS_BG[client?.status] ?? '#F1F5F9', + }; return ( + {/* Header */} - - router.back()} style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }} activeOpacity={0.7} hitSlop={{ top: 10, bottom: 10, left: 0, right: 20 }}> + + router.back()} style={{ marginBottom: 12 }} activeOpacity={0.7}> ← Back - - - {client?.firstName} {client?.lastName} - {client?.accountNumber} - - - {client?.status} + {client?.firstName} {client?.lastName} + + {client?.accountNumber} + + {client?.status} {/* Tabs */} - - {TABS.map(t => ( - setTab(t)} - style={{ flex: 1, paddingVertical: 14, alignItems: 'center', borderBottomWidth: 2.5, borderBottomColor: tab === t ? '#0891B2' : 'transparent' }} - activeOpacity={0.7} - > - {t} + + {TABS.map(tab => ( + setActiveTab(tab)} style={{ paddingHorizontal: 16, paddingVertical: 16, borderBottomWidth: 2.5, borderBottomColor: activeTab === tab ? '#0891B2' : 'transparent' }} activeOpacity={0.7}> + {tab} ))} - + - - {/* PROFILE */} - {tab === 'Profile' && ( - - - - client?.phone && Linking.openURL(`tel:${client.phone}`)} /> - - - + {/* ── PROFILE TAB ── */} + {activeTab === 'Profile' && ( + + + + + + + + + - )} - {/* SUBSCRIPTION */} - {tab === 'Subscription' && ( - subLoading ? ( - - ) : !subData ? ( - - No active subscription + {/* Map + Navigate */} + + Location + {client?.lat && client?.lng ? ( + <> + + + + + + + 📍 Navigate to Client + + + ) : ( + + No location recorded + Location is recorded when a technician confirms an installation + + )} + + + )} + + {/* ── SUBSCRIPTION TAB ── */} + {activeTab === 'Subscription' && ( + + {!sub ? ( + + No active subscription ) : ( - - - - - - - - - - {subData.nextBillingDate && ( - - Next billing date - {new Date(subData.nextBillingDate).toLocaleDateString('en-PH', { year: 'numeric', month: 'long', day: 'numeric' })} - - )} + + + + + + + - ) - )} + )} + + )} - {/* INVOICES */} - {tab === 'Invoices' && ( - invLoading ? ( - - ) : !invoices?.length ? ( - - No invoices yet + {/* ── INVOICES TAB ── */} + {activeTab === 'Invoices' && ( + + {!invoices ? ( + + ) : invoices.length === 0 ? ( + + No invoices found ) : ( invoices.map((inv: any) => { const st = INV_STATUS[inv.status] ?? { label: inv.status, color: '#6B7280', bg: '#F1F5F9' }; + const isUnpaid = ['SENT', 'PARTIAL', 'OVERDUE'].includes(inv.status) && Number(inv.balance) > 0; + const issued = inv.createdAt ? new Date(inv.createdAt).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'; return ( - - - {inv.invoiceNumber} - - {st.label} + + + + {inv.invoiceNumber} + Issued: {issued} + {inv.dueDate && ( + Due: {new Date(inv.dueDate).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })} + )} + + + + {st.label} + + ₱{Number(inv.total).toLocaleString()} + {isUnpaid && ( + Balance: ₱{Number(inv.balance).toLocaleString()} + )} - - Due: {inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : '—'} - ₱{Number(inv.amount ?? inv.totalAmount ?? 0).toLocaleString()} - - {inv.balance > 0 && ( - Balance: ₱{Number(inv.balance).toLocaleString()} + {isUnpaid && ( + setPayingInvoice(inv)} + activeOpacity={0.8} + > + + Record Payment + )} ); }) - ) - )} + )} + + )} - {/* PAYMENTS */} - {tab === 'Payments' && ( - payLoading ? ( - + {/* ── TICKETS TAB ── */} + {activeTab === 'Tickets' && ( + + router.push({ pathname: '/(app)/tasks/new', params: { prefillClientId: id, prefillName: `${client?.firstName} ${client?.lastName}` } })} + activeOpacity={0.8} + > + + New Ticket + + + {!tickets ? ( + + ) : tickets.length === 0 ? ( + + No tickets for this client + ) : ( - <> - router.push({ pathname: '/(app)/payments/record', params: { prefillClientId: id, prefillName: `${client?.firstName} ${client?.lastName}`, prefillAccountNumber: client?.accountNumber } })} - activeOpacity={0.8} - > - + Record Payment - - - {!payments?.length ? ( - - No payments recorded - - ) : ( - payments.map((p: any) => ( - - - - {p.paymentMethod} - - {p.paymentDate ? new Date(p.paymentDate).toLocaleDateString('en-PH') : new Date(p.createdAt).toLocaleDateString('en-PH')} - - {p.referenceNumber && Ref: {p.referenceNumber}} - - ₱{Number(p.amount).toLocaleString()} + tickets.map((t: any) => { + const typeColor = TYPE_COLOR[t.type] ?? '#6B7280'; + const statusColor = TICKET_STATUS_COLOR[t.status] ?? '#6B7280'; + return ( + router.push(`/(app)/tasks/${t.id}`)} + activeOpacity={0.8} + style={{ backgroundColor: '#FFF', borderRadius: 14, padding: 16, marginBottom: 10, borderWidth: 1, borderColor: '#F1F5F9', borderLeftWidth: 4, borderLeftColor: typeColor }} + > + + + {t.type} + {t.status?.replace('_', ' ')} - )) - )} - - ) - )} - + {t.subject} + + {t.assignedTo ? `Assigned: ${t.assignedTo.firstName} ${t.assignedTo.lastName}` : 'Unassigned'} + + + ); + }) + )} + + )} + + {/* Pay Invoice Modal */} + {payingInvoice && ( + setPayingInvoice(null)} + onSuccess={() => { + setPayingInvoice(null); + qc.invalidateQueries({ queryKey: ['client-invoices', id] }); + qc.invalidateQueries({ queryKey: ['unpaid-invoices'] }); + }} + /> + )} ); } diff --git a/app/(app)/dashboard.tsx b/app/(app)/dashboard.tsx index 7ed0fae..f19bb51 100644 --- a/app/(app)/dashboard.tsx +++ b/app/(app)/dashboard.tsx @@ -1,17 +1,14 @@ -import { View, Text, ScrollView, RefreshControl, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { View, Text, ScrollView, RefreshControl, ActivityIndicator, TouchableOpacity, Linking, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useQueries } from '@tanstack/react-query'; import { router } from 'expo-router'; import { api } from '../../services/api'; import { useAuthStore } from '../../stores/authStore'; -// ─── Constants ──────────────────────────────────────────────────────────────── const PRIORITY_COLOR: Record = { HIGH: '#DC2626', NORMAL: '#0891B2' }; const PRIORITY_BG: Record = { HIGH: '#FEE2E2', NORMAL: '#ECFEFF' }; -const STATUS_COLOR: Record = { OPEN: '#0891B2', IN_PROGRESS: '#D97706', RESOLVED: '#16A34A', CLOSED: '#6B7280' }; const TYPE_COLOR: Record = { INSTALLATION: '#0891B2', SUPPORT: '#7C3AED', BILLING: '#D97706' }; -// ─── KPI Card ──────────────────────────────────────────────────────────────── function KpiCard({ label, value, color, bg }: { label: string; value: string | number; color: string; bg: string }) { return ( @@ -21,11 +18,8 @@ function KpiCard({ label, value, color, bg }: { label: string; value: string | n ); } -// ─── Ticket Row ─────────────────────────────────────────────────────────────── -function TaskRow({ task, onPress }: { task: any; onPress: () => void }) { +function TicketRow({ task, onPress }: { task: any; onPress: () => void }) { const typeColor = TYPE_COLOR[task.type] ?? '#6B7280'; - const isHigh = task.priority === 'HIGH'; - return ( void }) { > - + {task.type} - {isHigh && ( + {task.priority === 'HIGH' && ( HIGH )} - + {task.status?.replace('_', ' ')} {task.subject} {task.client?.firstName} {task.client?.lastName} - {task.assignedTo - ? ` · ${task.assignedTo.firstName} ${task.assignedTo.lastName}` - : ' · Unassigned'} + {task.assignedTo ? ` · ${task.assignedTo.firstName}` : ' · Unassigned'} ); } -// ─── Main Screen ────────────────────────────────────────────────────────────── +function InvoiceRow({ inv }: { inv: any }) { + const dueDate = inv.dueDate ? new Date(inv.dueDate) : null; + const today = new Date(); + const isOverdue = dueDate && dueDate < today; + const daysLeft = dueDate ? Math.ceil((dueDate.getTime() - today.getTime()) / 86400000) : null; + + const navigate = () => { + const lat = inv.client?.lat; + const lng = inv.client?.lng; + if (!lat || !lng) { + Alert.alert('No Location', 'This client does not have a recorded location yet.'); + return; + } + Alert.alert('Navigate', `Open navigation to ${inv.client?.firstName} ${inv.client?.lastName}?`, [ + { text: 'Google Maps', onPress: () => Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`) }, + { text: 'Waze', onPress: () => Linking.openURL(`waze://?ll=${lat},${lng}&navigate=yes`) }, + { text: 'Cancel', style: 'cancel' }, + ]); + }; + + return ( + + + + + {inv.client?.firstName} {inv.client?.lastName} + + {inv.invoiceNumber} + + {isOverdue + ? `Overdue by ${Math.abs(daysLeft ?? 0)} day${Math.abs(daysLeft ?? 0) !== 1 ? 's' : ''}` + : daysLeft !== null + ? `Due in ${daysLeft} day${daysLeft !== 1 ? 's' : ''}` + : 'No due date'} + + + + ₱{Number(inv.balance).toLocaleString()} + + 📍 Navigate + + + + + ); +} + export default function DashboardScreen() { const { user } = useAuthStore(); + const role = user?.roles?.[0] ?? user?.role ?? ''; + const isAdminOrStaff = role === 'ADMIN' || role === 'STAFF'; + const hour = new Date().getHours(); const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening'; - const [summaryQ, tasksQ] = useQueries({ + const [summaryQ, ticketsQ, invoicesQ] = useQueries({ queries: [ { queryKey: ['dashboard'], queryFn: () => api.get('/api/v1/dashboard/summary').then(r => r.data), }, { - queryKey: ['dashboard-tasks'], - queryFn: () => - api.get('/api/v1/tickets?status=OPEN&status=IN_PROGRESS&limit=20') - .then(r => r.data?.data ?? r.data ?? []), + queryKey: ['dashboard-tickets'], + queryFn: async () => { + const res = await api.get('/api/v1/tickets?limit=50'); + const all: any[] = res.data?.data ?? res.data ?? []; + return all.filter((t: any) => t.status === 'OPEN' || t.status === 'IN_PROGRESS'); + }, + }, + { + queryKey: ['dashboard-invoices'], + queryFn: async () => { + const res = await api.get('/api/v1/invoices?limit=50'); + const all: any[] = res.data?.data ?? res.data ?? []; + const unpaid = all.filter((inv: any) => ['SENT', 'PARTIAL', 'OVERDUE'].includes(inv.status) && Number(inv.balance) > 0); + unpaid.sort((a: any, b: any) => { + const da = a.dueDate ? new Date(a.dueDate).getTime() : Infinity; + const db = b.dueDate ? new Date(b.dueDate).getTime() : Infinity; + return da - db; + }); + return unpaid.slice(0, 10); + }, }, ], }); - const isLoading = summaryQ.isLoading || tasksQ.isLoading; - const isRefetching = summaryQ.isRefetching || tasksQ.isRefetching; - const summary = summaryQ.data; + const isLoading = summaryQ.isLoading || ticketsQ.isLoading || invoicesQ.isLoading; + const isRefetching = summaryQ.isRefetching || ticketsQ.isRefetching || invoicesQ.isRefetching; - // Real dashboard API shape: - // { subscribers: { total, active, pending, suspended }, - // billing: { unpaidInvoices, overdueInvoices }, - // support: { openTickets, inProgressTickets }, - // tasks: { pending }, - // revenue: { thisMonth, lastMonth, growth } } - const totalClients = summary?.subscribers?.total ?? '—'; - const activeSubscribers = summary?.subscribers?.active ?? '—'; - const unpaidInvoices = summary?.billing?.unpaidInvoices ?? '—'; - const openTickets = summary?.support?.openTickets ?? '—'; - const thisMonthRevenue = summary?.revenue?.thisMonth ?? null; + const summary = summaryQ.data ?? {}; + const allActiveTickets: any[] = ticketsQ.data ?? []; + const unpaidInvoices: any[] = invoicesQ.data ?? []; - const allTasks: any[] = tasksQ.data ?? []; - const unassigned = allTasks.filter((t: any) => !t.assignedToId); - const assigned = allTasks.filter((t: any) => !!t.assignedToId); - const prioOrder: Record = { HIGH: 0, NORMAL: 1 }; - const byPrio = (a: any, b: any) => (prioOrder[a.priority] ?? 2) - (prioOrder[b.priority] ?? 2); + const unassigned = allActiveTickets.filter((t: any) => !t.assignedToId).slice(0, 10); + const assignedToMe = allActiveTickets.filter((t: any) => t.assignedToId === user?.id).slice(0, 10); + const byPrio = (a: any, b: any) => (a.priority === 'HIGH' ? -1 : 1) - (b.priority === 'HIGH' ? -1 : 1); - const refetchAll = () => { summaryQ.refetch(); tasksQ.refetch(); }; + const refetchAll = () => { summaryQ.refetch(); ticketsQ.refetch(); invoicesQ.refetch(); }; return ( @@ -121,26 +167,27 @@ export default function DashboardScreen() { ) : ( + {/* KPI Row 1 */} - - + + {/* KPI Row 2 */} - - - + + + - {/* Revenue card */} - {thisMonthRevenue !== null && ( + {/* Revenue — ADMIN/STAFF only */} + {isAdminOrStaff && summary?.revenue?.thisMonth != null && ( This Month's Revenue - ₱{Number(thisMonthRevenue).toLocaleString()} + ₱{Number(summary.revenue.thisMonth).toLocaleString()} - {summary?.revenue?.growth !== undefined && ( + {summary.revenue.growth !== undefined && ( +{summary.revenue.growth}% @@ -148,10 +195,10 @@ export default function DashboardScreen() { )} - {/* Unassigned Tasks */} + {/* ── Unassigned Tickets ── */} - Unassigned + Unassigned Tickets {unassigned.length > 0 && ( {unassigned.length} @@ -165,26 +212,48 @@ export default function DashboardScreen() { {unassigned.length === 0 ? ( - No unassigned tasks + No unassigned tickets 🎉 ) : ( - {[...unassigned].sort(byPrio).slice(0, 5).map((t: any) => ( - router.push(`/(app)/tasks/${t.id}`)} /> + {[...unassigned].sort(byPrio).map((t: any) => ( + router.push(`/(app)/tasks/${t.id}`)} /> ))} )} - {/* Assigned Tasks */} - {assigned.length > 0 && ( + {/* ── Assigned to Me ── */} + {assignedToMe.length > 0 && ( <> - Assigned Tasks - {[...assigned].sort(byPrio).slice(0, 5).map((t: any) => ( - router.push(`/(app)/tasks/${t.id}`)} /> - ))} + Assigned to Me + + {[...assignedToMe].sort(byPrio).map((t: any) => ( + router.push(`/(app)/tasks/${t.id}`)} /> + ))} + )} + {/* ── Unpaid Invoices ── */} + + Unpaid Invoices + router.push('/(app)/payments')} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> + View all + + + + {unpaidInvoices.length === 0 ? ( + + No unpaid invoices 🎉 + + ) : ( + + {unpaidInvoices.map((inv: any) => ( + + ))} + + )} + )} diff --git a/app/(app)/payments/index.tsx b/app/(app)/payments/index.tsx index e1f5f71..b01d5f2 100644 --- a/app/(app)/payments/index.tsx +++ b/app/(app)/payments/index.tsx @@ -1,47 +1,218 @@ -import { View, Text, TouchableOpacity, ScrollView } from 'react-native'; +import { View, Text, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator, Linking, Alert, TextInput } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { api } from '../../../services/api'; export default function CollectScreen() { + const [search, setSearch] = useState(''); + + const { data: invoices, isLoading, isRefetching, refetch } = useQuery({ + queryKey: ['unpaid-invoices'], + queryFn: async () => { + const res = await api.get('/api/v1/invoices?limit=100'); + const all: any[] = res.data?.data ?? res.data ?? []; + const unpaid = all.filter((inv: any) => + ['SENT', 'PARTIAL', 'OVERDUE'].includes(inv.status) && Number(inv.balance) > 0 + ); + // Sort: overdue first, then by due date ascending + unpaid.sort((a: any, b: any) => { + const today = new Date().getTime(); + const da = a.dueDate ? new Date(a.dueDate).getTime() : Infinity; + const db = b.dueDate ? new Date(b.dueDate).getTime() : Infinity; + const aOverdue = da < today; + const bOverdue = db < today; + if (aOverdue && !bOverdue) return -1; + if (!aOverdue && bOverdue) return 1; + return da - db; + }); + return unpaid; + }, + }); + + const filtered = (invoices ?? []).filter((inv: any) => { + if (!search.trim()) return true; + const q = search.toLowerCase(); + const name = `${inv.client?.firstName ?? ''} ${inv.client?.lastName ?? ''}`.toLowerCase(); + const acct = inv.client?.accountNumber?.toLowerCase() ?? ''; + const num = inv.invoiceNumber?.toLowerCase() ?? ''; + return name.includes(q) || acct.includes(q) || num.includes(q); + }); + + const today = new Date(); + + const navigate = (inv: any) => { + const lat = inv.client?.lat; + const lng = inv.client?.lng; + if (!lat || !lng) { + Alert.alert('No Location', `${inv.client?.firstName} ${inv.client?.lastName} has no recorded location yet.\n\nLocation is set during installation confirmation.`); + return; + } + const name = encodeURIComponent(`${inv.client?.firstName} ${inv.client?.lastName}`); + Alert.alert( + '📍 Navigate to Client', + `${inv.client?.firstName} ${inv.client?.lastName}\n${inv.client?.address ?? ''}`, + [ + { text: 'Google Maps', onPress: () => Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&destination_place_id=${name}`) }, + { text: 'Waze', onPress: () => Linking.openURL(`waze://?ll=${lat},${lng}&navigate=yes`) }, + { text: 'Cancel', style: 'cancel' }, + ] + ); + }; + + const totalUnremitted = filtered.reduce((s: number, inv: any) => s + Number(inv.balance ?? 0), 0); + return ( - + + {/* Header */} + Collect - Payments & remittances + Unpaid invoices · sorted by due date - + {/* Action Buttons */} + router.push('/(app)/payments/record')} - activeOpacity={0.7} + activeOpacity={0.8} > - - 💳 - - - Record Payment - Cash, GCash, Maya, or bank - - + + Record Payment - router.push('/(app)/remittances')} - activeOpacity={0.7} + activeOpacity={0.8} > - - 📋 - - - Remittances - Submit & track daily collections - - + 📋 Remittances - + + + {/* Search */} + + + + {search.length > 0 && ( + setSearch('')} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> + + × + + + )} + + + + {isLoading ? ( + + + + ) : ( + } + > + {/* Summary banner */} + {filtered.length > 0 && ( + + + {filtered.length} unpaid invoice{filtered.length !== 1 ? 's' : ''} + Total outstanding + + ₱{totalUnremitted.toLocaleString()} + + )} + + {filtered.length === 0 ? ( + + + {search ? 'No results found' : 'All invoices paid! 🎉'} + + + ) : ( + filtered.map((inv: any) => { + const dueDate = inv.dueDate ? new Date(inv.dueDate) : null; + const isOverdue = dueDate && dueDate < today; + const daysLeft = dueDate ? Math.ceil((dueDate.getTime() - today.getTime()) / 86400000) : null; + const hasLocation = !!(inv.client?.lat && inv.client?.lng); + + return ( + router.push({ pathname: '/(app)/clients/[id]', params: { id: inv.clientId, tab: 'invoices' } })} + activeOpacity={0.8} + style={{ + backgroundColor: '#FFF', + borderRadius: 16, + padding: 16, + marginBottom: 12, + borderWidth: 1, + borderColor: isOverdue ? '#FCA5A5' : '#F1F5F9', + borderLeftWidth: 4, + borderLeftColor: isOverdue ? '#DC2626' : '#F59E0B', + }} + > + {/* Client + amount */} + + + + {inv.client?.firstName} {inv.client?.lastName} + + + {inv.client?.accountNumber} · {inv.invoiceNumber} + + + + + ₱{Number(inv.balance).toLocaleString()} + + {inv.status === 'PARTIAL' && ( + PARTIAL + )} + + + + {/* Due date + navigate */} + + + + {isOverdue + ? `⚠️ Overdue ${Math.abs(daysLeft ?? 0)}d` + : daysLeft !== null + ? `Due in ${daysLeft}d` + : 'No due date'} + + + + { e.stopPropagation?.(); navigate(inv); }} + style={{ + flexDirection: 'row', alignItems: 'center', + backgroundColor: hasLocation ? '#ECFEFF' : '#F1F5F9', + borderRadius: 10, paddingHorizontal: 12, paddingVertical: 7, + }} + activeOpacity={0.7} + > + + {hasLocation ? '📍 Navigate' : '📍 No location'} + + + + + ); + }) + )} + + )} ); diff --git a/package-lock.json b/package-lock.json index b4198af..6fc643e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "nativewind": "^4.1.23", "react": "19.1.0", "react-native": "0.81.5", + "react-native-maps": "1.20.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", @@ -58,7 +59,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2644,7 +2644,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2658,7 +2657,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2668,7 +2666,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3403,6 +3400,145 @@ "node": ">= 20.19.4" } }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.84.1", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.84.1.tgz", + "integrity": "sha512-NswINguTz0eg1Dc0oGO/1dejXSr6iQaz8/NnCRn5HJdA3dGfqadS7zlYv0YjiWpgKgcW6uENaIEgJOQww0KSpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.84.1", + "hermes-parser": "0.32.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": { + "version": "0.84.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.84.1.tgz", + "integrity": "sha512-vorvcvptGxtK0qTDCFQb+W3CU6oIhzcX5dduetWRBoAhXdthEQM0MQnF+GTXoXL8/luffKgy7PlZRG/WeI/oRQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.84.1" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": { + "version": "0.84.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.84.1.tgz", + "integrity": "sha512-3GpmCKk21f4oe32bKIdmkdn+WydvhhZL+1nsoFBGi30Qrq9vL16giKu31OcnWshYz139x+mVAvCyoyzgn8RXSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@react-native/babel-plugin-codegen": "0.84.1", + "babel-plugin-syntax-hermes-parser": "0.32.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/codegen": { + "version": "0.84.1", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.84.1.tgz", + "integrity": "sha512-n1RIU0QAavgCg1uC5+s53arL7/mpM+16IBhJ3nCFSd/iK5tUmCwxQDcIDC703fuXfpub/ZygeSjVN8bcOWn0gA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "hermes-parser": "0.32.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "tinyglobby": "^0.2.15", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", + "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-parser": "0.32.0" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.84.1", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.84.1.tgz", + "integrity": "sha512-KlRawK4aXxRLlR3HYVfZKhfQp7sejQefQ/LttUWUkErhKO0AFt+yznoSLq7xwIrH9K3A3YwImHuFVtUtuDmurA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/js-polyfills": "0.84.1", + "@react-native/metro-babel-transformer": "0.84.1", + "metro-config": "^0.83.3", + "metro-runtime": "^0.83.3" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/metro-config/node_modules/@react-native/js-polyfills": { + "version": "0.84.1", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.84.1.tgz", + "integrity": "sha512-UsTe2AbUugsfyI7XIHMQq4E7xeC8a6GrYwuK+NohMMMJMxmyM3JkzIk+GB9e2il6ScEQNMJNaj+q+i5za8itxQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.4" + } + }, "node_modules/@react-native/normalize-colors": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", @@ -3667,6 +3803,12 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3730,7 +3872,7 @@ "version": "19.1.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -4336,7 +4478,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4567,7 +4708,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -4613,7 +4753,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -4638,7 +4777,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -5053,7 +5191,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -5066,7 +5203,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -5254,14 +5391,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/dom-serializer": { @@ -6185,7 +6320,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6202,7 +6336,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6221,7 +6354,6 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -6541,7 +6673,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -6690,6 +6821,21 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -6882,7 +7028,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -6937,7 +7082,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6975,7 +7119,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7271,7 +7414,6 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7648,7 +7790,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -7859,7 +8000,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -7934,21 +8074,6 @@ "node": ">=20.19.4" } }, - "node_modules/metro-babel-transformer/node_modules/hermes-estree": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "license": "MIT" - }, - "node_modules/metro-babel-transformer/node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.32.0" - } - }, "node_modules/metro-cache": { "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", @@ -8155,21 +8280,6 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT" }, - "node_modules/metro/node_modules/hermes-estree": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "license": "MIT" - }, - "node_modules/metro/node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.32.0" - } - }, "node_modules/metro/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -8469,7 +8579,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -8822,7 +8931,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8873,7 +8981,6 @@ "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -8902,7 +9009,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -8920,7 +9026,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -8946,7 +9051,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, "funding": [ { "type": "opencollective", @@ -8989,7 +9093,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9015,7 +9118,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -9029,7 +9131,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/pretty-bytes": { @@ -9181,7 +9282,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -9275,6 +9375,26 @@ } } }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -9397,6 +9517,28 @@ "react-native": "*" } }, + "node_modules/react-native-maps": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.20.1.tgz", + "integrity": "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.13" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">= 17.0.1", + "react-native": ">= 0.64.3", + "react-native-web": ">= 0.11" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz", @@ -9598,7 +9740,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -9608,7 +9749,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -9818,7 +9958,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -9866,7 +10005,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -10396,7 +10534,6 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -10663,7 +10800,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10845,7 +10982,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/utils-merge": { diff --git a/package.json b/package.json index 8622a7b..c4b422f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "nativewind": "^4.1.23", "react": "19.1.0", "react-native": "0.81.5", + "react-native-maps": "1.20.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0",