Files
fiberops-mobile/app/(app)/installations/index.tsx
Nemo baed6dc8d5 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
2026-03-23 21:22:57 +08:00

111 lines
4.4 KiB
TypeScript

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>
);
}