feat: complete all screens - client tabs, payments list, remittance detail, new ticket, installations tab
- Client detail: Subscription/Invoices/Payments tabs now fully functional - Payments: proper list with today's total + live search prefill from client - Record payment: debounced live search, reference required for non-cash - Remittances: detail screen with included payments breakdown - Tickets: status filter chips + create button, new ticket with categories - Installations: tab now visible with list + confirm flow - Fix: remove duplicate @react-navigation/elements causing Metro asset error - Fix: metro.config.js asset resolution from node_modules
This commit is contained in:
110
app/(app)/installations/index.tsx
Normal file
110
app/(app)/installations/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { View, Text, FlatList, TouchableOpacity, ActivityIndicator, RefreshControl } from 'react-native';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { router } from 'expo-router';
|
||||
import { api } from '../../../services/api';
|
||||
|
||||
// Installations are tickets of type INSTALLATION (or filtered by subject prefix)
|
||||
// We query tickets with type=INSTALLATION if the API supports it, fallback to all open tickets
|
||||
async function fetchInstallations() {
|
||||
try {
|
||||
const res = await api.get('/api/v1/tickets?type=INSTALLATION&limit=50');
|
||||
return res.data?.data ?? res.data ?? [];
|
||||
} catch {
|
||||
// Fallback: all OPEN tickets
|
||||
const res = await api.get('/api/v1/tickets?status=OPEN&limit=50');
|
||||
return res.data?.data ?? res.data ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_STYLE: Record<string, { bg: string; text: string }> = {
|
||||
OPEN: { bg: '#EFF6FF', text: '#2563EB' },
|
||||
IN_PROGRESS: { bg: '#FFFBEB', text: '#D97706' },
|
||||
RESOLVED: { bg: '#F0FDF4', text: '#16A34A' },
|
||||
CLOSED: { bg: '#F3F4F6', text: '#6B7280' },
|
||||
};
|
||||
|
||||
export default function InstallationsScreen() {
|
||||
const { data, isLoading, refetch, isRefetching } = useQuery({
|
||||
queryKey: ['installations'],
|
||||
queryFn: fetchInstallations,
|
||||
});
|
||||
|
||||
const installations: any[] = data ?? [];
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
{/* Header */}
|
||||
<View className="px-4 pt-14 pb-4 bg-blue-600">
|
||||
<Text className="text-white text-xl font-bold">Installations</Text>
|
||||
<Text className="text-white/70 text-xs mt-0.5">
|
||||
{installations.length} pending
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator color="#2563EB" />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={installations}
|
||||
keyExtractor={(item) => item.id}
|
||||
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} />}
|
||||
contentContainerStyle={{ padding: 16 }}
|
||||
renderItem={({ item }) => {
|
||||
const statusStyle = STATUS_STYLE[item.status] ?? { bg: '#F3F4F6', text: '#6B7280' };
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="bg-white rounded-xl p-4 mb-3 border border-gray-100"
|
||||
onPress={() => router.push(`/(app)/installations/${item.id}`)}
|
||||
>
|
||||
<View className="flex-row justify-between items-start mb-2">
|
||||
<Text className="font-semibold text-gray-900 flex-1 mr-2" numberOfLines={2}>
|
||||
{item.subject}
|
||||
</Text>
|
||||
<View className="rounded-full px-2.5 py-1" style={{ backgroundColor: statusStyle.bg }}>
|
||||
<Text className="text-xs font-semibold" style={{ color: statusStyle.text }}>
|
||||
{item.status?.replace('_', ' ')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-gray-500 text-sm flex-1">
|
||||
{item.client?.firstName} {item.client?.lastName}
|
||||
</Text>
|
||||
<Text className="text-gray-400 text-xs">
|
||||
{item.createdAt ? new Date(item.createdAt).toLocaleDateString() : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{item.client?.address && (
|
||||
<Text className="text-gray-400 text-xs mt-1 ml-0" numberOfLines={1}>
|
||||
📍 {item.client.address}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Confirm button if not yet resolved */}
|
||||
{item.status !== 'RESOLVED' && item.status !== 'CLOSED' && (
|
||||
<TouchableOpacity
|
||||
className="mt-3 bg-green-600 rounded-xl py-2.5 items-center"
|
||||
onPress={() => router.push(`/(app)/installations/${item.id}`)}
|
||||
>
|
||||
<Text className="text-white font-semibold text-sm">📷 Confirm Installation</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View className="items-center py-16">
|
||||
<Text className="text-5xl mb-4">🔌</Text>
|
||||
<Text className="text-gray-700 font-semibold text-base">No installations pending</Text>
|
||||
<Text className="text-gray-400 text-sm mt-1">All caught up!</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user