feat: major UI/UX overhaul + user management + ticket detail refactor

- 5-tab navigation (Home/Clients/Collect/Tickets/Profile)
- Inline styles throughout (17px min font, SafeAreaView)
- Dashboard fixed to match real API shape
- Ticket detail: 2 tabs (Details + Comments), always-visible comment input
- Installation confirmation: GPS coordinate capture + client location update
- User management screens (Admin only): list, create, detail + role/active toggle
- Tasks folder replaces tickets folder
- Remittance detail: inline styles
- Record payment: prefill from client, live button text
- Icon component with SVG icons
- Color system: primary #0891B2
This commit is contained in:
Nemo
2026-03-24 10:37:57 +08:00
parent baed6dc8d5
commit 4644a3194d
38 changed files with 4967 additions and 2984 deletions

View File

@@ -1,57 +1,81 @@
import { View, Text, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuthStore } from '../../stores/authStore';
import { router } from 'expo-router';
export default function ProfileScreen() {
const { user, logout, tenantSlug } = useAuthStore();
const initials = `${user?.firstName?.[0] ?? ''}${user?.lastName?.[0] ?? ''}`.toUpperCase() || 'U';
const fullName = `${user?.firstName ?? ''} ${user?.lastName ?? ''}`.trim();
const role = user?.roles?.[0] ?? user?.role ?? 'Staff';
const handleLogout = () => {
Alert.alert('Sign Out', 'Are you sure you want to sign out?', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Sign Out', style: 'destructive',
onPress: async () => {
await logout();
router.replace('/(auth)/company-code');
}
}
{ text: 'Sign Out', style: 'destructive', onPress: async () => { await logout(); router.replace('/(auth)/company-code'); } },
]);
};
return (
<ScrollView className="flex-1 bg-gray-50">
<View className="px-4 pt-14 pb-6 bg-primary">
<View className="w-16 h-16 rounded-full bg-white/20 items-center justify-center mb-3">
<Text className="text-white text-2xl font-bold">
{user?.firstName?.[0]?.toUpperCase() ?? 'U'}
</Text>
</View>
<Text className="text-white text-xl font-bold">{user?.firstName} {user?.lastName}</Text>
<Text className="text-white/70 text-sm">{user?.role} · {tenantSlug}</Text>
</View>
<View className="px-4 py-6">
<View className="bg-white rounded-2xl border border-gray-100 mb-4">
{[
{ label: 'Username', value: user?.username },
{ label: 'Email', value: user?.email },
{ label: 'Role', value: user?.role },
{ label: 'Company', value: tenantSlug },
].map((item, i) => (
<View key={item.label} className={`px-4 py-3 ${i > 0 ? 'border-t border-gray-100' : ''}`}>
<Text className="text-gray-500 text-xs mb-0.5">{item.label}</Text>
<Text className="text-gray-900 font-medium">{item.value ?? '—'}</Text>
</View>
))}
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
<ScrollView style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
{/* Header */}
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 20, paddingTop: 16, paddingBottom: 32, alignItems: 'center' }}>
<View style={{ width: 80, height: 80, borderRadius: 40, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center', marginBottom: 12 }}>
<Text style={{ color: '#FFF', fontSize: 32, fontWeight: '800' }}>{initials}</Text>
</View>
<Text style={{ color: '#FFF', fontSize: 24, fontWeight: '800' }}>{fullName}</Text>
<View style={{ backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 5, marginTop: 8 }}>
<Text style={{ color: '#E0F2FE', fontSize: 14, fontWeight: '600' }}>{role}</Text>
</View>
</View>
<TouchableOpacity
className="bg-red-50 border border-red-200 rounded-2xl py-4 items-center"
onPress={handleLogout}
>
<Text className="text-red-600 font-semibold">Sign Out</Text>
</TouchableOpacity>
</View>
</ScrollView>
<View style={{ padding: 16 }}>
{/* Info */}
<View style={{ backgroundColor: '#FFF', borderRadius: 20, marginBottom: 16, borderWidth: 1, borderColor: '#F1F5F9', overflow: 'hidden' }}>
{[
{ label: 'Email', value: user?.email },
{ label: 'Company', value: tenantSlug },
{ label: 'Role', value: role },
].map((row, i, arr) => (
<View key={row.label} style={{ paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: i < arr.length - 1 ? 1 : 0, borderBottomColor: '#F1F5F9' }}>
<Text style={{ fontSize: 12, fontWeight: '600', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 4 }}>{row.label}</Text>
<Text style={{ fontSize: 17, fontWeight: '600', color: '#0F172A' }}>{row.value ?? '—'}</Text>
</View>
))}
</View>
{/* User Management — admin only */}
{(user?.roles?.includes('ADMIN') || user?.role === 'ADMIN' || role === 'ADMIN') && (
<TouchableOpacity
style={{ backgroundColor: '#FFF', borderRadius: 20, borderWidth: 1, borderColor: '#F1F5F9', paddingHorizontal: 20, paddingVertical: 18, marginBottom: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
onPress={() => router.push('/(app)/users')}
activeOpacity={0.7}
>
<View>
<Text style={{ fontSize: 17, fontWeight: '700', color: '#0F172A' }}>User Management</Text>
<Text style={{ fontSize: 14, color: '#64748B', marginTop: 2 }}>Add & manage team members</Text>
</View>
<Text style={{ fontSize: 20, color: '#94A3B8' }}></Text>
</TouchableOpacity>
)}
{/* App version */}
<View style={{ backgroundColor: '#FFF', borderRadius: 20, marginBottom: 16, borderWidth: 1, borderColor: '#F1F5F9', paddingHorizontal: 20, paddingVertical: 16 }}>
<Text style={{ fontSize: 12, fontWeight: '600', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 4 }}>App</Text>
<Text style={{ fontSize: 17, fontWeight: '600', color: '#0F172A' }}>FiberOps Mobile v1.0.0</Text>
</View>
{/* Sign out */}
<TouchableOpacity
style={{ backgroundColor: '#FFF', borderRadius: 20, paddingVertical: 18, alignItems: 'center', borderWidth: 1.5, borderColor: '#FECACA' }}
onPress={handleLogout}
activeOpacity={0.7}
>
<Text style={{ color: '#DC2626', fontSize: 17, fontWeight: '700' }}>Sign Out</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}