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
This commit is contained in:
@@ -1,64 +1,175 @@
|
|||||||
import { useState } from 'react';
|
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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useLocalSearchParams, router } from 'expo-router';
|
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';
|
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<string, string> = { ACTIVE: '#166534', SUSPENDED: '#92400E', CANCELLED: '#991B1B', PENDING: '#475569' };
|
||||||
|
const STATUS_BG: Record<string, string> = { ACTIVE: '#DCFCE7', SUSPENDED: '#FEF3C7', CANCELLED: '#FEE2E2', PENDING: '#F1F5F9' };
|
||||||
|
const TICKET_STATUS_COLOR: Record<string, string> = { OPEN: '#0891B2', IN_PROGRESS: '#D97706', RESOLVED: '#16A34A', CLOSED: '#6B7280' };
|
||||||
|
const TYPE_COLOR: Record<string, string> = { INSTALLATION: '#0891B2', SUPPORT: '#7C3AED', BILLING: '#D97706' };
|
||||||
|
|
||||||
const STATUS_COLOR: Record<string, string> = {
|
|
||||||
ACTIVE: '#166534', SUSPENDED: '#92400E', CANCELLED: '#991B1B', PENDING: '#475569',
|
|
||||||
};
|
|
||||||
const STATUS_BG: Record<string, string> = {
|
|
||||||
ACTIVE: '#DCFCE7', SUSPENDED: '#FEF3C7', CANCELLED: '#FEE2E2', PENDING: '#F1F5F9',
|
|
||||||
};
|
|
||||||
const INV_STATUS: Record<string, { label: string; color: string; bg: string }> = {
|
const INV_STATUS: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
PAID: { label: 'Paid', color: '#166534', bg: '#DCFCE7' },
|
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' },
|
OVERDUE: { label: 'Overdue', color: '#991B1B', bg: '#FEE2E2' },
|
||||||
PARTIAL: { label: 'Partial', color: '#0E7490', bg: '#CFFAFE' },
|
PARTIAL: { label: 'Partial', color: '#0E7490', bg: '#CFFAFE' },
|
||||||
|
DRAFT: { label: 'Draft', color: '#6B7280', bg: '#F1F5F9' },
|
||||||
VOID: { label: 'Void', 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 (
|
return (
|
||||||
<TouchableOpacity
|
<View style={{ paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: isLast ? 0 : 1, borderBottomColor: '#F1F5F9' }}>
|
||||||
disabled={!onPress}
|
|
||||||
onPress={onPress}
|
|
||||||
style={{ paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: isLast ? 0 : 1, borderBottomColor: '#F1F5F9' }}
|
|
||||||
activeOpacity={onPress ? 0.7 : 1}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: 12, fontWeight: '600', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>{label}</Text>
|
<Text style={{ fontSize: 12, fontWeight: '600', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>{label}</Text>
|
||||||
<Text style={{ fontSize: 17, fontWeight: '500', color: onPress ? '#0891B2' : '#0F172A' }}>{value ?? '—'}</Text>
|
<Text style={{ fontSize: 17, fontWeight: '500', color: value ? '#0F172A' : '#CBD5E1' }}>{value ?? '—'}</Text>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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 (
|
||||||
|
<Modal visible animationType="slide" transparent onRequestClose={onClose}>
|
||||||
|
<TouchableOpacity style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }} activeOpacity={1} onPress={onClose}>
|
||||||
|
<TouchableOpacity activeOpacity={1} style={{ backgroundColor: '#FFF', borderTopLeftRadius: 28, borderTopRightRadius: 28, padding: 24 }}>
|
||||||
|
<Text style={{ fontSize: 20, fontWeight: '800', color: '#0F172A', marginBottom: 4 }}>Record Payment</Text>
|
||||||
|
<Text style={{ fontSize: 15, color: '#64748B', marginBottom: 20 }}>
|
||||||
|
{invoice.invoiceNumber} · Balance: <Text style={{ fontWeight: '700', color: '#991B1B' }}>₱{Number(invoice.balance).toLocaleString()}</Text>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A', marginBottom: 8 }}>Amount (₱)</Text>
|
||||||
|
<TextInput
|
||||||
|
style={{ backgroundColor: '#F8FAFC', borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 14, paddingHorizontal: 16, paddingVertical: 14, fontSize: 24, fontWeight: '800', color: '#0F172A', textAlign: 'center', marginBottom: 16 }}
|
||||||
|
placeholder="0.00"
|
||||||
|
placeholderTextColor="#CBD5E1"
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
value={amount}
|
||||||
|
onChangeText={setAmount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>Method</Text>
|
||||||
|
<View style={{ flexDirection: 'row', marginBottom: 16 }}>
|
||||||
|
{METHODS.map(m => (
|
||||||
|
<TouchableOpacity key={m} onPress={() => setMethod(m)}
|
||||||
|
style={{ flex: 1, borderRadius: 12, paddingVertical: 12, alignItems: 'center', marginHorizontal: 3, backgroundColor: method === m ? '#0891B2' : '#F1F5F9' }}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: '700', color: method === m ? '#FFF' : '#64748B' }}>{m}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A', marginBottom: 8 }}>Reference # <Text style={{ fontWeight: '400', color: '#94A3B8' }}>(optional)</Text></Text>
|
||||||
|
<TextInput
|
||||||
|
style={{ backgroundColor: '#F8FAFC', borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 14, paddingHorizontal: 16, paddingVertical: 12, fontSize: 16, color: '#0F172A', marginBottom: 20 }}
|
||||||
|
placeholder="GCash ref, OR number..."
|
||||||
|
placeholderTextColor="#94A3B8"
|
||||||
|
value={ref}
|
||||||
|
onChangeText={setRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ backgroundColor: amount && Number(amount) > 0 ? '#059669' : '#CBD5E1', borderRadius: 14, paddingVertical: 18, alignItems: 'center' }}
|
||||||
|
onPress={submit}
|
||||||
|
disabled={loading}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{loading ? <ActivityIndicator color="#FFF" /> : <Text style={{ color: '#FFF', fontSize: 17, fontWeight: '700' }}>Confirm Payment</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main Screen ───────────────────────────────────────────────────────────────
|
||||||
export default function ClientDetailScreen() {
|
export default function ClientDetailScreen() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const params = useLocalSearchParams<{ id: string; tab?: string }>();
|
||||||
const [tab, setTab] = useState('Profile');
|
const id = params.id;
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
const initialTab = (params.tab === 'invoices' ? 'Invoices' : 'Profile') as Tab;
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>(initialTab);
|
||||||
|
const [payingInvoice, setPayingInvoice] = useState<any>(null);
|
||||||
|
|
||||||
const { data: client, isLoading } = useQuery({
|
const { data: client, isLoading } = useQuery({
|
||||||
queryKey: ['client', id],
|
queryKey: ['client', id],
|
||||||
queryFn: () => api.get(`/api/v1/clients/${id}`).then(r => r.data),
|
queryFn: () => api.get(`/api/v1/clients/${id}`).then(r => r.data),
|
||||||
});
|
});
|
||||||
const { data: subData, isLoading: subLoading } = useQuery({
|
|
||||||
queryKey: ['client-subscription', id],
|
const { data: invoices } = useQuery({
|
||||||
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({
|
|
||||||
queryKey: ['client-invoices', id],
|
queryKey: ['client-invoices', id],
|
||||||
queryFn: () => api.get(`/api/v1/invoices?clientId=${id}&limit=20`).then(r => r.data?.data ?? r.data ?? []),
|
queryFn: async () => {
|
||||||
enabled: tab === 'Invoices',
|
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],
|
const { data: tickets } = useQuery({
|
||||||
queryFn: () => api.get(`/api/v1/payments?clientId=${id}&limit=20`).then(r => r.data?.data ?? r.data ?? []),
|
queryKey: ['client-tickets', id],
|
||||||
enabled: tab === 'Payments',
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
||||||
@@ -69,154 +180,212 @@ export default function ClientDetailScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColor = STATUS_COLOR[client?.status] ?? '#475569';
|
const statusStyle = {
|
||||||
const statusBg = STATUS_BG[client?.status] ?? '#F1F5F9';
|
color: STATUS_COLOR[client?.status] ?? '#475569',
|
||||||
|
bg: STATUS_BG[client?.status] ?? '#F1F5F9',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
||||||
<View style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
|
<View style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 16, paddingTop: 12, paddingBottom: 20 }}>
|
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 16, paddingTop: 12, paddingBottom: 18 }}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }} activeOpacity={0.7} hitSlop={{ top: 10, bottom: 10, left: 0, right: 20 }}>
|
<TouchableOpacity onPress={() => router.back()} style={{ marginBottom: 12 }} activeOpacity={0.7}>
|
||||||
<Text style={{ color: '#A5F3FC', fontSize: 17, fontWeight: '600' }}>← Back</Text>
|
<Text style={{ color: '#A5F3FC', fontSize: 17, fontWeight: '600' }}>← Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
<Text style={{ color: '#FFF', fontSize: 24, fontWeight: '800' }}>{client?.firstName} {client?.lastName}</Text>
|
||||||
<View style={{ flex: 1, marginRight: 12 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 8, gap: 8 }}>
|
||||||
<Text style={{ color: '#FFF', fontSize: 22, fontWeight: '800' }}>{client?.firstName} {client?.lastName}</Text>
|
<Text style={{ color: '#A5F3FC', fontSize: 14 }}>{client?.accountNumber}</Text>
|
||||||
<Text style={{ color: '#A5F3FC', fontSize: 15, marginTop: 3 }}>{client?.accountNumber}</Text>
|
<View style={{ borderRadius: 20, paddingHorizontal: 12, paddingVertical: 5, backgroundColor: statusStyle.bg }}>
|
||||||
</View>
|
<Text style={{ fontSize: 13, fontWeight: '700', color: statusStyle.color }}>{client?.status}</Text>
|
||||||
<View style={{ borderRadius: 20, paddingHorizontal: 12, paddingVertical: 6, backgroundColor: statusBg }}>
|
|
||||||
<Text style={{ fontSize: 13, fontWeight: '700', color: statusColor }}>{client?.status}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<View style={{ flexDirection: 'row', backgroundColor: '#FFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9' }}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ backgroundColor: '#FFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9' }} contentContainerStyle={{ paddingHorizontal: 8 }}>
|
||||||
{TABS.map(t => (
|
{TABS.map(tab => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity key={tab} onPress={() => setActiveTab(tab)} style={{ paddingHorizontal: 16, paddingVertical: 16, borderBottomWidth: 2.5, borderBottomColor: activeTab === tab ? '#0891B2' : 'transparent' }} activeOpacity={0.7}>
|
||||||
key={t}
|
<Text style={{ fontSize: 15, fontWeight: '700', color: activeTab === tab ? '#0891B2' : '#94A3B8' }}>{tab}</Text>
|
||||||
onPress={() => setTab(t)}
|
|
||||||
style={{ flex: 1, paddingVertical: 14, alignItems: 'center', borderBottomWidth: 2.5, borderBottomColor: tab === t ? '#0891B2' : 'transparent' }}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: 14, fontWeight: '700', color: tab === t ? '#0891B2' : '#94A3B8' }}>{t}</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</ScrollView>
|
||||||
|
|
||||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, paddingBottom: 40 }}>
|
{/* ── PROFILE TAB ── */}
|
||||||
{/* PROFILE */}
|
{activeTab === 'Profile' && (
|
||||||
{tab === 'Profile' && (
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 40 }}>
|
||||||
<View style={{ backgroundColor: '#FFF', borderRadius: 20, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
<View style={{ backgroundColor: '#FFF', borderRadius: 20, margin: 16, borderWidth: 1, borderColor: '#F1F5F9', overflow: 'hidden' }}>
|
||||||
<InfoRow label="Account Number" value={client?.accountNumber} />
|
<InfoRow label="Full Name" value={`${client?.firstName} ${client?.lastName}`} />
|
||||||
|
<InfoRow label="Account #" value={client?.accountNumber} />
|
||||||
<InfoRow label="Email" value={client?.email} />
|
<InfoRow label="Email" value={client?.email} />
|
||||||
<InfoRow label="Phone" value={client?.phone} onPress={() => client?.phone && Linking.openURL(`tel:${client.phone}`)} />
|
<InfoRow label="Phone" value={client?.phone} />
|
||||||
<InfoRow label="Address" value={client?.address} />
|
<InfoRow label="Address" value={client?.address} />
|
||||||
<InfoRow label="Area" value={client?.area?.name} />
|
<InfoRow label="Area" value={client?.area?.name} />
|
||||||
<InfoRow label="Date Joined" value={client?.createdAt ? new Date(client.createdAt).toLocaleDateString('en-PH', { year: 'numeric', month: 'long', day: 'numeric' }) : undefined} isLast />
|
<InfoRow label="Zone" value={client?.zone?.name} isLast />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Map + Navigate */}
|
||||||
|
<View style={{ marginHorizontal: 16, marginBottom: 16 }}>
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>Location</Text>
|
||||||
|
{client?.lat && client?.lng ? (
|
||||||
|
<>
|
||||||
|
<View style={{ borderRadius: 16, overflow: 'hidden', height: 200, marginBottom: 10 }}>
|
||||||
|
<MapView
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
initialRegion={{ latitude: client.lat, longitude: client.lng, latitudeDelta: 0.005, longitudeDelta: 0.005 }}
|
||||||
|
scrollEnabled={false}
|
||||||
|
zoomEnabled={false}
|
||||||
|
>
|
||||||
|
<Marker coordinate={{ latitude: client.lat, longitude: client.lng }} title={`${client.firstName} ${client.lastName}`} description={client.address ?? ''} />
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ backgroundColor: '#0891B2', borderRadius: 14, paddingVertical: 16, alignItems: 'center' }}
|
||||||
|
onPress={navigateToClient}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#FFF', fontSize: 16, fontWeight: '700' }}>📍 Navigate to Client</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<View style={{ backgroundColor: '#F1F5F9', borderRadius: 16, padding: 24, alignItems: 'center' }}>
|
||||||
|
<Text style={{ fontSize: 15, color: '#94A3B8', marginBottom: 6 }}>No location recorded</Text>
|
||||||
|
<Text style={{ fontSize: 13, color: '#CBD5E1', textAlign: 'center' }}>Location is recorded when a technician confirms an installation</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* SUBSCRIPTION */}
|
{/* ── SUBSCRIPTION TAB ── */}
|
||||||
{tab === 'Subscription' && (
|
{activeTab === 'Subscription' && (
|
||||||
subLoading ? (
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, paddingBottom: 40 }}>
|
||||||
<View style={{ paddingVertical: 60, alignItems: 'center' }}><ActivityIndicator color="#0891B2" size="large" /></View>
|
{!sub ? (
|
||||||
) : !subData ? (
|
<View style={{ alignItems: 'center', paddingVertical: 48 }}>
|
||||||
<View style={{ alignItems: 'center', paddingVertical: 60 }}>
|
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No active subscription</Text>
|
||||||
<Text style={{ fontSize: 17, color: '#94A3B8' }}>No active subscription</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View>
|
<View style={{ backgroundColor: '#FFF', borderRadius: 20, borderWidth: 1, borderColor: '#F1F5F9', overflow: 'hidden' }}>
|
||||||
<View style={{ backgroundColor: '#FFF', borderRadius: 20, borderWidth: 1, borderColor: '#F1F5F9', marginBottom: 12 }}>
|
<InfoRow label="Plan" value={sub.plan?.name} />
|
||||||
<InfoRow label="Plan" value={subData.plan?.name} />
|
<InfoRow label="Speed" value={sub.plan?.speed ? `${sub.plan.speed} Mbps` : null} />
|
||||||
<InfoRow label="Speed" value={subData.plan?.speedMbps ? `${subData.plan.speedMbps} Mbps` : undefined} />
|
<InfoRow label="Monthly" value={sub.plan?.price ? `₱${Number(sub.plan.price).toLocaleString()}` : null} />
|
||||||
<InfoRow label="Monthly Rate" value={subData.plan?.price ? `₱${Number(subData.plan.price).toLocaleString()}` : undefined} />
|
<InfoRow label="Status" value={sub.status} />
|
||||||
<InfoRow label="Status" value={subData.status} />
|
<InfoRow label="Start Date" value={sub.startDate ? new Date(sub.startDate).toLocaleDateString('en-PH', { year: 'numeric', month: 'long', day: 'numeric' }) : null} />
|
||||||
<InfoRow label="Start Date" value={subData.startDate ? new Date(subData.startDate).toLocaleDateString('en-PH', { year: 'numeric', month: 'long', day: 'numeric' }) : undefined} />
|
<InfoRow label="Billing Day" value={sub.billingDay ? `Day ${sub.billingDay}` : null} isLast />
|
||||||
<InfoRow label="Billing Day" value={subData.billingCycleDay ? `Day ${subData.billingCycleDay} of every month` : undefined} isLast />
|
|
||||||
</View>
|
|
||||||
{subData.nextBillingDate && (
|
|
||||||
<View style={{ backgroundColor: '#ECFEFF', borderRadius: 14, padding: 16, borderWidth: 1, borderColor: '#A5F3FC' }}>
|
|
||||||
<Text style={{ fontSize: 13, fontWeight: '600', color: '#0E7490' }}>Next billing date</Text>
|
|
||||||
<Text style={{ fontSize: 17, fontWeight: '700', color: '#0E7490', marginTop: 2 }}>{new Date(subData.nextBillingDate).toLocaleDateString('en-PH', { year: 'numeric', month: 'long', day: 'numeric' })}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</ScrollView>
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* INVOICES */}
|
{/* ── INVOICES TAB ── */}
|
||||||
{tab === 'Invoices' && (
|
{activeTab === 'Invoices' && (
|
||||||
invLoading ? (
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, paddingBottom: 40 }}>
|
||||||
<View style={{ paddingVertical: 60, alignItems: 'center' }}><ActivityIndicator color="#0891B2" size="large" /></View>
|
{!invoices ? (
|
||||||
) : !invoices?.length ? (
|
<ActivityIndicator color="#0891B2" style={{ marginTop: 40 }} />
|
||||||
<View style={{ alignItems: 'center', paddingVertical: 60 }}>
|
) : invoices.length === 0 ? (
|
||||||
<Text style={{ fontSize: 17, color: '#94A3B8' }}>No invoices yet</Text>
|
<View style={{ alignItems: 'center', paddingVertical: 48 }}>
|
||||||
|
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No invoices found</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
invoices.map((inv: any) => {
|
invoices.map((inv: any) => {
|
||||||
const st = INV_STATUS[inv.status] ?? { label: inv.status, color: '#6B7280', bg: '#F1F5F9' };
|
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 (
|
return (
|
||||||
<View key={inv.id} style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 18, marginBottom: 10, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
<View key={inv.id} style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 10 }}>
|
||||||
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A' }}>{inv.invoiceNumber}</Text>
|
<View>
|
||||||
<View style={{ borderRadius: 20, paddingHorizontal: 10, paddingVertical: 4, backgroundColor: st.bg }}>
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A' }}>{inv.invoiceNumber}</Text>
|
||||||
<Text style={{ fontSize: 12, fontWeight: '700', color: st.color }}>{st.label}</Text>
|
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 2 }}>Issued: {issued}</Text>
|
||||||
|
{inv.dueDate && (
|
||||||
|
<Text style={{ fontSize: 13, color: '#64748B' }}>Due: {new Date(inv.dueDate).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={{ alignItems: 'flex-end' }}>
|
||||||
|
<View style={{ borderRadius: 20, paddingHorizontal: 12, paddingVertical: 5, backgroundColor: st.bg }}>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: '700', color: st.color }}>{st.label}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: '800', color: '#0F172A', marginTop: 6 }}>₱{Number(inv.total).toLocaleString()}</Text>
|
||||||
|
{isUnpaid && (
|
||||||
|
<Text style={{ fontSize: 13, color: '#991B1B', fontWeight: '600' }}>Balance: ₱{Number(inv.balance).toLocaleString()}</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
{isUnpaid && (
|
||||||
<Text style={{ fontSize: 15, color: '#64748B' }}>Due: {inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : '—'}</Text>
|
<TouchableOpacity
|
||||||
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A' }}>₱{Number(inv.amount ?? inv.totalAmount ?? 0).toLocaleString()}</Text>
|
style={{ backgroundColor: '#059669', borderRadius: 12, paddingVertical: 13, alignItems: 'center' }}
|
||||||
</View>
|
onPress={() => setPayingInvoice(inv)}
|
||||||
{inv.balance > 0 && (
|
activeOpacity={0.8}
|
||||||
<Text style={{ fontSize: 14, color: '#DC2626', marginTop: 6, fontWeight: '600' }}>Balance: ₱{Number(inv.balance).toLocaleString()}</Text>
|
>
|
||||||
|
<Text style={{ color: '#FFF', fontSize: 15, fontWeight: '700' }}>+ Record Payment</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* PAYMENTS */}
|
|
||||||
{tab === 'Payments' && (
|
|
||||||
payLoading ? (
|
|
||||||
<View style={{ paddingVertical: 60, alignItems: 'center' }}><ActivityIndicator color="#0891B2" size="large" /></View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{ backgroundColor: '#059669', borderRadius: 14, paddingVertical: 16, alignItems: 'center', marginBottom: 16 }}
|
|
||||||
onPress={() => router.push({ pathname: '/(app)/payments/record', params: { prefillClientId: id, prefillName: `${client?.firstName} ${client?.lastName}`, prefillAccountNumber: client?.accountNumber } })}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<Text style={{ color: '#FFF', fontSize: 17, fontWeight: '700' }}>+ Record Payment</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{!payments?.length ? (
|
|
||||||
<View style={{ alignItems: 'center', paddingVertical: 40 }}>
|
|
||||||
<Text style={{ fontSize: 17, color: '#94A3B8' }}>No payments recorded</Text>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
payments.map((p: any) => (
|
|
||||||
<View key={p.id} style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 18, marginBottom: 10, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A' }}>{p.paymentMethod}</Text>
|
|
||||||
<Text style={{ fontSize: 14, color: '#64748B', marginTop: 3 }}>
|
|
||||||
{p.paymentDate ? new Date(p.paymentDate).toLocaleDateString('en-PH') : new Date(p.createdAt).toLocaleDateString('en-PH')}
|
|
||||||
</Text>
|
|
||||||
{p.referenceNumber && <Text style={{ fontSize: 13, color: '#94A3B8', marginTop: 2 }}>Ref: {p.referenceNumber}</Text>}
|
|
||||||
</View>
|
|
||||||
<Text style={{ fontSize: 18, fontWeight: '800', color: '#166534' }}>₱{Number(p.amount).toLocaleString()}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── TICKETS TAB ── */}
|
||||||
|
{activeTab === 'Tickets' && (
|
||||||
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, paddingBottom: 40 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ backgroundColor: '#0891B2', borderRadius: 14, paddingVertical: 14, alignItems: 'center', marginBottom: 16 }}
|
||||||
|
onPress={() => router.push({ pathname: '/(app)/tasks/new', params: { prefillClientId: id, prefillName: `${client?.firstName} ${client?.lastName}` } })}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#FFF', fontSize: 16, fontWeight: '700' }}>+ New Ticket</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{!tickets ? (
|
||||||
|
<ActivityIndicator color="#0891B2" style={{ marginTop: 40 }} />
|
||||||
|
) : tickets.length === 0 ? (
|
||||||
|
<View style={{ alignItems: 'center', paddingVertical: 48 }}>
|
||||||
|
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No tickets for this client</Text>
|
||||||
</View>
|
</View>
|
||||||
|
) : (
|
||||||
|
tickets.map((t: any) => {
|
||||||
|
const typeColor = TYPE_COLOR[t.type] ?? '#6B7280';
|
||||||
|
const statusColor = TICKET_STATUS_COLOR[t.status] ?? '#6B7280';
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={t.id}
|
||||||
|
onPress={() => 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 }}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||||
|
<View style={{ borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3, backgroundColor: `${typeColor}20` }}>
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: '700', color: typeColor }}>{t.type}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: '600', color: statusColor }}>{t.status?.replace('_', ' ')}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A' }} numberOfLines={1}>{t.subject}</Text>
|
||||||
|
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 3 }}>
|
||||||
|
{t.assignedTo ? `Assigned: ${t.assignedTo.firstName} ${t.assignedTo.lastName}` : 'Unassigned'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Pay Invoice Modal */}
|
||||||
|
{payingInvoice && (
|
||||||
|
<PayInvoiceModal
|
||||||
|
invoice={payingInvoice}
|
||||||
|
onClose={() => setPayingInvoice(null)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setPayingInvoice(null);
|
||||||
|
qc.invalidateQueries({ queryKey: ['client-invoices', id] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['unpaid-invoices'] });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useQueries } from '@tanstack/react-query';
|
import { useQueries } from '@tanstack/react-query';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { api } from '../../services/api';
|
import { api } from '../../services/api';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
|
||||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
||||||
const PRIORITY_COLOR: Record<string, string> = { HIGH: '#DC2626', NORMAL: '#0891B2' };
|
const PRIORITY_COLOR: Record<string, string> = { HIGH: '#DC2626', NORMAL: '#0891B2' };
|
||||||
const PRIORITY_BG: Record<string, string> = { HIGH: '#FEE2E2', NORMAL: '#ECFEFF' };
|
const PRIORITY_BG: Record<string, string> = { HIGH: '#FEE2E2', NORMAL: '#ECFEFF' };
|
||||||
const STATUS_COLOR: Record<string, string> = { OPEN: '#0891B2', IN_PROGRESS: '#D97706', RESOLVED: '#16A34A', CLOSED: '#6B7280' };
|
|
||||||
const TYPE_COLOR: Record<string, string> = { INSTALLATION: '#0891B2', SUPPORT: '#7C3AED', BILLING: '#D97706' };
|
const TYPE_COLOR: Record<string, string> = { INSTALLATION: '#0891B2', SUPPORT: '#7C3AED', BILLING: '#D97706' };
|
||||||
|
|
||||||
// ─── KPI Card ────────────────────────────────────────────────────────────────
|
|
||||||
function KpiCard({ label, value, color, bg }: { label: string; value: string | number; color: string; bg: string }) {
|
function KpiCard({ label, value, color, bg }: { label: string; value: string | number; color: string; bg: string }) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, marginHorizontal: 5, borderRadius: 16, padding: 16, backgroundColor: bg }}>
|
<View style={{ flex: 1, marginHorizontal: 5, borderRadius: 16, padding: 16, backgroundColor: bg }}>
|
||||||
@@ -21,11 +18,8 @@ function KpiCard({ label, value, color, bg }: { label: string; value: string | n
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Ticket Row ───────────────────────────────────────────────────────────────
|
function TicketRow({ task, onPress }: { task: any; onPress: () => void }) {
|
||||||
function TaskRow({ task, onPress }: { task: any; onPress: () => void }) {
|
|
||||||
const typeColor = TYPE_COLOR[task.type] ?? '#6B7280';
|
const typeColor = TYPE_COLOR[task.type] ?? '#6B7280';
|
||||||
const isHigh = task.priority === 'HIGH';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
@@ -34,74 +28,126 @@ function TaskRow({ task, onPress }: { task: any; onPress: () => void }) {
|
|||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||||
<View style={{ flexDirection: 'row', gap: 6, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 6, alignItems: 'center' }}>
|
||||||
<View style={{ borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3, backgroundColor: `${typeColor}15` }}>
|
<View style={{ borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3, backgroundColor: `${typeColor}20` }}>
|
||||||
<Text style={{ fontSize: 11, fontWeight: '700', color: typeColor }}>{task.type}</Text>
|
<Text style={{ fontSize: 11, fontWeight: '700', color: typeColor }}>{task.type}</Text>
|
||||||
</View>
|
</View>
|
||||||
{isHigh && (
|
{task.priority === 'HIGH' && (
|
||||||
<View style={{ borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3, backgroundColor: PRIORITY_BG.HIGH }}>
|
<View style={{ borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3, backgroundColor: PRIORITY_BG.HIGH }}>
|
||||||
<Text style={{ fontSize: 11, fontWeight: '700', color: PRIORITY_COLOR.HIGH }}>HIGH</Text>
|
<Text style={{ fontSize: 11, fontWeight: '700', color: PRIORITY_COLOR.HIGH }}>HIGH</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text style={{ fontSize: 12, fontWeight: '600', color: STATUS_COLOR[task.status] ?? '#6B7280' }}>
|
<Text style={{ fontSize: 12, fontWeight: '600', color: '#64748B' }}>
|
||||||
{task.status?.replace('_', ' ')}
|
{task.status?.replace('_', ' ')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A' }} numberOfLines={1}>{task.subject}</Text>
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A' }} numberOfLines={1}>{task.subject}</Text>
|
||||||
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 3 }}>
|
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 3 }}>
|
||||||
{task.client?.firstName} {task.client?.lastName}
|
{task.client?.firstName} {task.client?.lastName}
|
||||||
{task.assignedTo
|
{task.assignedTo ? ` · ${task.assignedTo.firstName}` : ' · Unassigned'}
|
||||||
? ` · ${task.assignedTo.firstName} ${task.assignedTo.lastName}`
|
|
||||||
: ' · Unassigned'}
|
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 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 (
|
||||||
|
<View style={{ backgroundColor: '#FFF', borderRadius: 14, padding: 16, marginBottom: 10, borderWidth: 1, borderColor: isOverdue ? '#FCA5A5' : '#F1F5F9', borderLeftWidth: 4, borderLeftColor: isOverdue ? '#DC2626' : '#F59E0B' }}>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A' }}>
|
||||||
|
{inv.client?.firstName} {inv.client?.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 2 }}>{inv.invoiceNumber}</Text>
|
||||||
|
<Text style={{ fontSize: 13, color: isOverdue ? '#DC2626' : '#D97706', fontWeight: '600', marginTop: 3 }}>
|
||||||
|
{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'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ alignItems: 'flex-end' }}>
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: '800', color: '#991B1B' }}>₱{Number(inv.balance).toLocaleString()}</Text>
|
||||||
|
<TouchableOpacity onPress={navigate} style={{ marginTop: 8, backgroundColor: '#ECFEFF', borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6 }} activeOpacity={0.7}>
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: '700', color: '#0891B2' }}>📍 Navigate</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DashboardScreen() {
|
export default function DashboardScreen() {
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
|
const role = user?.roles?.[0] ?? user?.role ?? '';
|
||||||
|
const isAdminOrStaff = role === 'ADMIN' || role === 'STAFF';
|
||||||
|
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
||||||
|
|
||||||
const [summaryQ, tasksQ] = useQueries({
|
const [summaryQ, ticketsQ, invoicesQ] = useQueries({
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
queryKey: ['dashboard'],
|
queryKey: ['dashboard'],
|
||||||
queryFn: () => api.get('/api/v1/dashboard/summary').then(r => r.data),
|
queryFn: () => api.get('/api/v1/dashboard/summary').then(r => r.data),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: ['dashboard-tasks'],
|
queryKey: ['dashboard-tickets'],
|
||||||
queryFn: () =>
|
queryFn: async () => {
|
||||||
api.get('/api/v1/tickets?status=OPEN&status=IN_PROGRESS&limit=20')
|
const res = await api.get('/api/v1/tickets?limit=50');
|
||||||
.then(r => r.data?.data ?? r.data ?? []),
|
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 isLoading = summaryQ.isLoading || ticketsQ.isLoading || invoicesQ.isLoading;
|
||||||
const isRefetching = summaryQ.isRefetching || tasksQ.isRefetching;
|
const isRefetching = summaryQ.isRefetching || ticketsQ.isRefetching || invoicesQ.isRefetching;
|
||||||
const summary = summaryQ.data;
|
|
||||||
|
|
||||||
// Real dashboard API shape:
|
const summary = summaryQ.data ?? {};
|
||||||
// { subscribers: { total, active, pending, suspended },
|
const allActiveTickets: any[] = ticketsQ.data ?? [];
|
||||||
// billing: { unpaidInvoices, overdueInvoices },
|
const unpaidInvoices: any[] = invoicesQ.data ?? [];
|
||||||
// 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 allTasks: any[] = tasksQ.data ?? [];
|
const unassigned = allActiveTickets.filter((t: any) => !t.assignedToId).slice(0, 10);
|
||||||
const unassigned = allTasks.filter((t: any) => !t.assignedToId);
|
const assignedToMe = allActiveTickets.filter((t: any) => t.assignedToId === user?.id).slice(0, 10);
|
||||||
const assigned = allTasks.filter((t: any) => !!t.assignedToId);
|
const byPrio = (a: any, b: any) => (a.priority === 'HIGH' ? -1 : 1) - (b.priority === 'HIGH' ? -1 : 1);
|
||||||
const prioOrder: Record<string, number> = { HIGH: 0, NORMAL: 1 };
|
|
||||||
const byPrio = (a: any, b: any) => (prioOrder[a.priority] ?? 2) - (prioOrder[b.priority] ?? 2);
|
|
||||||
|
|
||||||
const refetchAll = () => { summaryQ.refetch(); tasksQ.refetch(); };
|
const refetchAll = () => { summaryQ.refetch(); ticketsQ.refetch(); invoicesQ.refetch(); };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
||||||
@@ -121,26 +167,27 @@ export default function DashboardScreen() {
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={{ padding: 16 }}>
|
<View style={{ padding: 16 }}>
|
||||||
|
|
||||||
{/* KPI Row 1 */}
|
{/* KPI Row 1 */}
|
||||||
<View style={{ flexDirection: 'row', marginBottom: 10 }}>
|
<View style={{ flexDirection: 'row', marginBottom: 10 }}>
|
||||||
<KpiCard label="Subscribers" value={totalClients} color="#0E7490" bg="#ECFEFF" />
|
<KpiCard label="Subscribers" value={summary?.subscribers?.total ?? '—'} color="#0E7490" bg="#ECFEFF" />
|
||||||
<KpiCard label="Active" value={activeSubscribers} color="#166534" bg="#F0FDF4" />
|
<KpiCard label="Active" value={summary?.subscribers?.active ?? '—'} color="#166534" bg="#F0FDF4" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* KPI Row 2 */}
|
{/* KPI Row 2 */}
|
||||||
<View style={{ flexDirection: 'row', marginBottom: 10 }}>
|
<View style={{ flexDirection: 'row', marginBottom: 16 }}>
|
||||||
<KpiCard label="Unpaid Invoices" value={unpaidInvoices} color="#991B1B" bg="#FEF2F2" />
|
<KpiCard label="Unpaid Invoices" value={summary?.billing?.unpaidInvoices ?? '—'} color="#991B1B" bg="#FEF2F2" />
|
||||||
<KpiCard label="Open Tasks" value={openTickets} color="#92400E" bg="#FFFBEB" />
|
<KpiCard label="Open Tickets" value={summary?.support?.openTickets ?? '—'} color="#92400E" bg="#FFFBEB" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Revenue card */}
|
{/* Revenue — ADMIN/STAFF only */}
|
||||||
{thisMonthRevenue !== null && (
|
{isAdminOrStaff && summary?.revenue?.thisMonth != null && (
|
||||||
<View style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 18, marginBottom: 16, borderWidth: 1, borderColor: '#F1F5F9', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
<View style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 18, marginBottom: 16, borderWidth: 1, borderColor: '#F1F5F9', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontSize: 12, fontWeight: '700', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>This Month's Revenue</Text>
|
<Text style={{ fontSize: 12, fontWeight: '700', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>This Month's Revenue</Text>
|
||||||
<Text style={{ fontSize: 24, fontWeight: '800', color: '#166534' }}>₱{Number(thisMonthRevenue).toLocaleString()}</Text>
|
<Text style={{ fontSize: 24, fontWeight: '800', color: '#166534' }}>₱{Number(summary.revenue.thisMonth).toLocaleString()}</Text>
|
||||||
</View>
|
</View>
|
||||||
{summary?.revenue?.growth !== undefined && (
|
{summary.revenue.growth !== undefined && (
|
||||||
<View style={{ backgroundColor: '#F0FDF4', borderRadius: 12, paddingHorizontal: 12, paddingVertical: 6 }}>
|
<View style={{ backgroundColor: '#F0FDF4', borderRadius: 12, paddingHorizontal: 12, paddingVertical: 6 }}>
|
||||||
<Text style={{ fontSize: 15, fontWeight: '800', color: '#16A34A' }}>+{summary.revenue.growth}%</Text>
|
<Text style={{ fontSize: 15, fontWeight: '800', color: '#16A34A' }}>+{summary.revenue.growth}%</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -148,10 +195,10 @@ export default function DashboardScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Unassigned Tasks */}
|
{/* ── Unassigned Tickets ── */}
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A' }}>Unassigned</Text>
|
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A' }}>Unassigned Tickets</Text>
|
||||||
{unassigned.length > 0 && (
|
{unassigned.length > 0 && (
|
||||||
<View style={{ backgroundColor: '#FEE2E2', borderRadius: 20, paddingHorizontal: 8, paddingVertical: 2, marginLeft: 8 }}>
|
<View style={{ backgroundColor: '#FEE2E2', borderRadius: 20, paddingHorizontal: 8, paddingVertical: 2, marginLeft: 8 }}>
|
||||||
<Text style={{ fontSize: 12, fontWeight: '700', color: '#DC2626' }}>{unassigned.length}</Text>
|
<Text style={{ fontSize: 12, fontWeight: '700', color: '#DC2626' }}>{unassigned.length}</Text>
|
||||||
@@ -165,26 +212,48 @@ export default function DashboardScreen() {
|
|||||||
|
|
||||||
{unassigned.length === 0 ? (
|
{unassigned.length === 0 ? (
|
||||||
<View style={{ backgroundColor: '#FFF', borderRadius: 14, padding: 20, alignItems: 'center', marginBottom: 20, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
<View style={{ backgroundColor: '#FFF', borderRadius: 14, padding: 20, alignItems: 'center', marginBottom: 20, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
||||||
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No unassigned tasks</Text>
|
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No unassigned tickets 🎉</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={{ marginBottom: 20 }}>
|
<View style={{ marginBottom: 20 }}>
|
||||||
{[...unassigned].sort(byPrio).slice(0, 5).map((t: any) => (
|
{[...unassigned].sort(byPrio).map((t: any) => (
|
||||||
<TaskRow key={t.id} task={t} onPress={() => router.push(`/(app)/tasks/${t.id}`)} />
|
<TicketRow key={t.id} task={t} onPress={() => router.push(`/(app)/tasks/${t.id}`)} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Assigned Tasks */}
|
{/* ── Assigned to Me ── */}
|
||||||
{assigned.length > 0 && (
|
{assignedToMe.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A', marginBottom: 10 }}>Assigned Tasks</Text>
|
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A', marginBottom: 10 }}>Assigned to Me</Text>
|
||||||
{[...assigned].sort(byPrio).slice(0, 5).map((t: any) => (
|
<View style={{ marginBottom: 20 }}>
|
||||||
<TaskRow key={t.id} task={t} onPress={() => router.push(`/(app)/tasks/${t.id}`)} />
|
{[...assignedToMe].sort(byPrio).map((t: any) => (
|
||||||
|
<TicketRow key={t.id} task={t} onPress={() => router.push(`/(app)/tasks/${t.id}`)} />
|
||||||
))}
|
))}
|
||||||
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Unpaid Invoices ── */}
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||||
|
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A' }}>Unpaid Invoices</Text>
|
||||||
|
<TouchableOpacity onPress={() => router.push('/(app)/payments')} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: '600', color: '#0891B2' }}>View all</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{unpaidInvoices.length === 0 ? (
|
||||||
|
<View style={{ backgroundColor: '#FFF', borderRadius: 14, padding: 20, alignItems: 'center', marginBottom: 20, borderWidth: 1, borderColor: '#F1F5F9' }}>
|
||||||
|
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No unpaid invoices 🎉</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={{ marginBottom: 20 }}>
|
||||||
|
{unpaidInvoices.map((inv: any) => (
|
||||||
|
<InvoiceRow key={inv.id} inv={inv} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={{ height: 24 }} />
|
<View style={{ height: 24 }} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { api } from '../../../services/api';
|
||||||
|
|
||||||
export default function CollectScreen() {
|
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 (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
||||||
<View style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
|
<View style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
|
||||||
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 20, paddingTop: 16, paddingBottom: 24 }}>
|
|
||||||
|
{/* Header */}
|
||||||
|
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 16, paddingTop: 16, paddingBottom: 20 }}>
|
||||||
<Text style={{ color: '#FFF', fontSize: 28, fontWeight: '800' }}>Collect</Text>
|
<Text style={{ color: '#FFF', fontSize: 28, fontWeight: '800' }}>Collect</Text>
|
||||||
<Text style={{ color: '#A5F3FC', fontSize: 15, marginTop: 2 }}>Payments & remittances</Text>
|
<Text style={{ color: '#A5F3FC', fontSize: 14, marginTop: 2 }}>Unpaid invoices · sorted by due date</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16 }}>
|
{/* Action Buttons */}
|
||||||
|
<View style={{ flexDirection: 'row', padding: 16, gap: 10 }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{ backgroundColor: '#ECFEFF', borderRadius: 20, padding: 20, marginBottom: 14, borderWidth: 1.5, borderColor: '#67E8F9', flexDirection: 'row', alignItems: 'center' }}
|
style={{ flex: 1, backgroundColor: '#059669', borderRadius: 14, paddingVertical: 16, alignItems: 'center' }}
|
||||||
onPress={() => router.push('/(app)/payments/record')}
|
onPress={() => router.push('/(app)/payments/record')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<View style={{ width: 56, height: 56, borderRadius: 16, backgroundColor: '#0891B2', alignItems: 'center', justifyContent: 'center', marginRight: 16 }}>
|
<Text style={{ color: '#FFF', fontSize: 16, fontWeight: '700' }}>+ Record Payment</Text>
|
||||||
<Text style={{ fontSize: 26 }}>💳</Text>
|
|
||||||
</View>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={{ fontSize: 18, fontWeight: '800', color: '#0F172A' }}>Record Payment</Text>
|
|
||||||
<Text style={{ fontSize: 15, color: '#64748B', marginTop: 3 }}>Cash, GCash, Maya, or bank</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={{ fontSize: 22, color: '#94A3B8' }}>›</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ flex: 1, backgroundColor: '#0891B2', borderRadius: 14, paddingVertical: 16, alignItems: 'center' }}
|
||||||
|
onPress={() => router.push('/(app)/remittances')}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#FFF', fontSize: 16, fontWeight: '700' }}>📋 Remittances</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<View style={{ paddingHorizontal: 16, marginBottom: 8 }}>
|
||||||
|
<View style={{ backgroundColor: '#FFF', borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 14, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14 }}>
|
||||||
|
<TextInput
|
||||||
|
style={{ flex: 1, paddingVertical: 12, fontSize: 16, color: '#0F172A' }}
|
||||||
|
placeholder="Search by name, account, invoice #"
|
||||||
|
placeholderTextColor="#94A3B8"
|
||||||
|
value={search}
|
||||||
|
onChangeText={setSearch}
|
||||||
|
/>
|
||||||
|
{search.length > 0 && (
|
||||||
|
<TouchableOpacity onPress={() => setSearch('')} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<View style={{ width: 20, height: 20, borderRadius: 10, backgroundColor: '#CBD5E1', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Text style={{ color: '#FFF', fontSize: 12, fontWeight: '800' }}>×</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<ActivityIndicator color="#0891B2" size="large" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40 }}
|
||||||
|
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} tintColor="#0891B2" />}
|
||||||
|
>
|
||||||
|
{/* Summary banner */}
|
||||||
|
{filtered.length > 0 && (
|
||||||
|
<View style={{ backgroundColor: '#FEF2F2', borderRadius: 14, padding: 14, marginBottom: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1, borderColor: '#FCA5A5' }}>
|
||||||
|
<View>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: '600', color: '#991B1B' }}>{filtered.length} unpaid invoice{filtered.length !== 1 ? 's' : ''}</Text>
|
||||||
|
<Text style={{ fontSize: 11, color: '#DC2626', marginTop: 2 }}>Total outstanding</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 20, fontWeight: '800', color: '#991B1B' }}>₱{totalUnremitted.toLocaleString()}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<View style={{ alignItems: 'center', paddingVertical: 60 }}>
|
||||||
|
<Text style={{ fontSize: 16, color: '#94A3B8' }}>
|
||||||
|
{search ? 'No results found' : 'All invoices paid! 🎉'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
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 (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={inv.id}
|
||||||
|
onPress={() => 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 */}
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A' }}>
|
||||||
|
{inv.client?.firstName} {inv.client?.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 2 }}>
|
||||||
|
{inv.client?.accountNumber} · {inv.invoiceNumber}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ alignItems: 'flex-end' }}>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: '800', color: '#991B1B' }}>
|
||||||
|
₱{Number(inv.balance).toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
{inv.status === 'PARTIAL' && (
|
||||||
|
<Text style={{ fontSize: 11, color: '#D97706', fontWeight: '600', marginTop: 2 }}>PARTIAL</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Due date + navigate */}
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}>
|
||||||
|
<View style={{ backgroundColor: isOverdue ? '#FEE2E2' : '#FFFBEB', borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4 }}>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: '600', color: isOverdue ? '#DC2626' : '#D97706' }}>
|
||||||
|
{isOverdue
|
||||||
|
? `⚠️ Overdue ${Math.abs(daysLeft ?? 0)}d`
|
||||||
|
: daysLeft !== null
|
||||||
|
? `Due in ${daysLeft}d`
|
||||||
|
: 'No due date'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{ backgroundColor: '#F0FDF4', borderRadius: 20, padding: 20, borderWidth: 1.5, borderColor: '#86EFAC', flexDirection: 'row', alignItems: 'center' }}
|
onPress={(e) => { e.stopPropagation?.(); navigate(inv); }}
|
||||||
onPress={() => router.push('/(app)/remittances')}
|
style={{
|
||||||
|
flexDirection: 'row', alignItems: 'center',
|
||||||
|
backgroundColor: hasLocation ? '#ECFEFF' : '#F1F5F9',
|
||||||
|
borderRadius: 10, paddingHorizontal: 12, paddingVertical: 7,
|
||||||
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={{ width: 56, height: 56, borderRadius: 16, backgroundColor: '#059669', alignItems: 'center', justifyContent: 'center', marginRight: 16 }}>
|
<Text style={{ fontSize: 13, fontWeight: '700', color: hasLocation ? '#0891B2' : '#94A3B8' }}>
|
||||||
<Text style={{ fontSize: 26 }}>📋</Text>
|
{hasLocation ? '📍 Navigate' : '📍 No location'}
|
||||||
</View>
|
</Text>
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={{ fontSize: 18, fontWeight: '800', color: '#0F172A' }}>Remittances</Text>
|
|
||||||
<Text style={{ fontSize: 15, color: '#64748B', marginTop: 3 }}>Submit & track daily collections</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={{ fontSize: 22, color: '#94A3B8' }}>›</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
276
package-lock.json
generated
276
package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"nativewind": "^4.1.23",
|
"nativewind": "^4.1.23",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-maps": "1.20.1",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
@@ -58,7 +59,6 @@
|
|||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -2644,7 +2644,6 @@
|
|||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
@@ -2658,7 +2657,6 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
@@ -2668,7 +2666,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
@@ -3403,6 +3400,145 @@
|
|||||||
"node": ">= 20.19.4"
|
"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": {
|
"node_modules/@react-native/normalize-colors": {
|
||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz",
|
||||||
@@ -3667,6 +3803,12 @@
|
|||||||
"@types/responselike": "^1.0.0"
|
"@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": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
@@ -3730,7 +3872,7 @@
|
|||||||
"version": "19.1.17",
|
"version": "19.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||||
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -4336,7 +4478,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -4567,7 +4708,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -4613,7 +4753,6 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anymatch": "~3.1.2",
|
"anymatch": "~3.1.2",
|
||||||
@@ -4638,7 +4777,6 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
@@ -5053,7 +5191,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"cssesc": "bin/cssesc"
|
"cssesc": "bin/cssesc"
|
||||||
@@ -5066,7 +5203,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -5254,14 +5391,12 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/dlv": {
|
"node_modules/dlv": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
@@ -6185,7 +6320,6 @@
|
|||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
@@ -6202,7 +6336,6 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
@@ -6221,7 +6354,6 @@
|
|||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
@@ -6541,7 +6673,6 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
@@ -6690,6 +6821,21 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/hosted-git-info": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
|
||||||
@@ -6882,7 +7028,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
@@ -6937,7 +7082,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -6975,7 +7119,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
@@ -7271,7 +7414,6 @@
|
|||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
@@ -7648,7 +7790,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -7859,7 +8000,6 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
@@ -7934,21 +8074,6 @@
|
|||||||
"node": ">=20.19.4"
|
"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": {
|
"node_modules/metro-cache": {
|
||||||
"version": "0.83.3",
|
"version": "0.83.3",
|
||||||
"resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz",
|
"resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz",
|
||||||
@@ -8155,21 +8280,6 @@
|
|||||||
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
|
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/metro/node_modules/ws": {
|
||||||
"version": "7.5.10",
|
"version": "7.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
@@ -8469,7 +8579,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -8822,7 +8931,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -8873,7 +8981,6 @@
|
|||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -8902,7 +9009,6 @@
|
|||||||
"version": "15.1.0",
|
"version": "15.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.0.0",
|
"postcss-value-parser": "^4.0.0",
|
||||||
@@ -8920,7 +9026,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
||||||
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -8946,7 +9051,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -8989,7 +9093,6 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -9015,7 +9118,6 @@
|
|||||||
"version": "6.1.2",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
@@ -9029,7 +9131,6 @@
|
|||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
@@ -9181,7 +9282,6 @@
|
|||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"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": {
|
"node_modules/react-fast-compare": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||||
@@ -9397,6 +9517,28 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/react-native-reanimated": {
|
||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz",
|
||||||
@@ -9598,7 +9740,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
@@ -9608,7 +9749,6 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
@@ -9818,7 +9958,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"iojs": ">=1.0.0",
|
"iojs": ">=1.0.0",
|
||||||
@@ -9866,7 +10005,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -10396,7 +10534,6 @@
|
|||||||
"version": "3.4.19",
|
"version": "3.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
@@ -10663,7 +10800,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -10845,7 +10982,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"nativewind": "^4.1.23",
|
"nativewind": "^4.1.23",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-maps": "1.20.1",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user