Files
fiberops-mobile/app/(app)/manual-tasks/new.tsx

231 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, ScrollView, Alert, ActivityIndicator } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { api } from '../../../services/api';
import { useAuthStore } from '../../../stores/authStore';
const TASK_TYPES = [
{ value: 'SUSPEND_CLIENT', label: 'Suspend Client', color: '#DC2626', bg: '#FEE2E2' },
{ value: 'REACTIVATE_CLIENT', label: 'Reactivate Client', color: '#059669', bg: '#D1FAE5' },
{ value: 'ACTIVATE_CLIENT', label: 'Activate Client', color: '#0891B2', bg: '#ECFEFF' },
];
export default function NewManualTaskScreen() {
const qc = useQueryClient();
const { user } = useAuthStore();
const [taskType, setTaskType] = useState('SUSPEND_CLIENT');
const [client, setClient] = useState<any>(null);
const [clientSearch, setClientSearch] = useState('');
const [debouncedClientSearch, setDebouncedClientSearch] = useState('');
const [assignTo, setAssignTo] = useState<any>(null);
const [showUserPicker, setShowUserPicker] = useState(false);
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(false);
// Client search
const { data: clientResults, isFetching: searchingClients } = useQuery({
queryKey: ['manual-task-clients', debouncedClientSearch],
queryFn: () => api.get(`/api/v1/clients?search=${debouncedClientSearch}&limit=8`).then(r => r.data?.data ?? r.data ?? []),
enabled: debouncedClientSearch.trim().length >= 2,
});
// Users list
const { data: usersData } = useQuery({
queryKey: ['manual-task-users'],
queryFn: () => api.get('/api/v1/users').then(r => r.data?.data ?? r.data ?? []),
});
const handleClientSearchChange = (v: string) => {
setClientSearch(v);
setTimeout(() => setDebouncedClientSearch(v), 400);
};
const submit = async () => {
if (!client) return Alert.alert('Required', 'Please select a client.');
setLoading(true);
try {
await api.post('/api/v1/manual-tasks', {
type: taskType,
clientId: client.id,
assignedToId: assignTo?.id ?? undefined,
notes: notes.trim() || undefined,
});
qc.invalidateQueries({ queryKey: ['manual-tasks'] });
Alert.alert('Task created', '', [{ text: 'OK', onPress: () => router.back() }]);
} catch (e: any) {
Alert.alert('Error', e?.response?.data?.message ?? 'Could not create task.');
} finally {
setLoading(false);
}
};
const users: any[] = usersData ?? [];
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
<View style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
{/* Header */}
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 20, paddingTop: 12, paddingBottom: 20 }}>
<TouchableOpacity onPress={() => router.back()} style={{ marginBottom: 12 }} activeOpacity={0.7}>
<Text style={{ color: '#A5F3FC', fontSize: 17, fontWeight: '600' }}> Back</Text>
</TouchableOpacity>
<Text style={{ color: '#FFF', fontSize: 28, fontWeight: '800' }}>New Manual Task</Text>
</View>
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, paddingBottom: 40 }} keyboardShouldPersistTaps="handled">
{/* Task Type */}
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>
Task Type <Text style={{ color: '#DC2626' }}>*</Text>
</Text>
<View style={{ marginBottom: 20 }}>
{TASK_TYPES.map(t => (
<TouchableOpacity
key={t.value}
onPress={() => setTaskType(t.value)}
style={{
flexDirection: 'row', alignItems: 'center',
borderRadius: 14, padding: 16, marginBottom: 8,
backgroundColor: taskType === t.value ? t.bg : '#FFF',
borderWidth: 1.5,
borderColor: taskType === t.value ? t.color : '#E2E8F0',
}}
activeOpacity={0.7}
>
<View style={{
width: 20, height: 20, borderRadius: 10, borderWidth: 2,
borderColor: t.color, alignItems: 'center', justifyContent: 'center', marginRight: 12,
backgroundColor: taskType === t.value ? t.color : 'transparent',
}}>
{taskType === t.value && <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: '#FFF' }} />}
</View>
<View style={{ borderRadius: 20, paddingHorizontal: 12, paddingVertical: 5, backgroundColor: t.bg, marginRight: 10 }}>
<Text style={{ fontSize: 13, fontWeight: '700', color: t.color }}>{t.label}</Text>
</View>
</TouchableOpacity>
))}
</View>
{/* Client */}
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>
Client <Text style={{ color: '#DC2626' }}>*</Text>
</Text>
{client ? (
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: '#ECFEFF', borderRadius: 14, padding: 18, marginBottom: 20, borderWidth: 1, borderColor: '#A5F3FC' }}>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 17, fontWeight: '700', color: '#0E7490' }}>{client.firstName} {client.lastName}</Text>
<Text style={{ fontSize: 15, color: '#0891B2', marginTop: 2 }}>{client.accountNumber}</Text>
</View>
<TouchableOpacity onPress={() => { setClient(null); setClientSearch(''); setDebouncedClientSearch(''); }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0891B2' }}>Change</Text>
</TouchableOpacity>
</View>
) : (
<View style={{ marginBottom: 20 }}>
<View style={{ backgroundColor: '#FFF', borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 14, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: 8 }}>
<TextInput
style={{ flex: 1, paddingVertical: 14, fontSize: 16, color: '#0F172A' }}
placeholder="Search by name or account #"
placeholderTextColor="#94A3B8"
value={clientSearch}
onChangeText={handleClientSearchChange}
autoCapitalize="none"
/>
{clientSearch.length > 0 && (
<TouchableOpacity onPress={() => { setClientSearch(''); setDebouncedClientSearch(''); }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<View style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: '#CBD5E1', alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: '#FFF', fontSize: 13, fontWeight: '800', lineHeight: 16 }}>×</Text>
</View>
</TouchableOpacity>
)}
{searchingClients && <ActivityIndicator size="small" color="#0891B2" style={{ marginLeft: 8 }} />}
</View>
{debouncedClientSearch.trim().length >= 2 && (
<View style={{ backgroundColor: '#FFF', borderRadius: 14, borderWidth: 1, borderColor: '#E2E8F0', overflow: 'hidden' }}>
{(clientResults ?? []).length === 0 && !searchingClients ? (
<Text style={{ paddingHorizontal: 16, paddingVertical: 14, fontSize: 15, color: '#94A3B8' }}>No clients found</Text>
) : (
(clientResults ?? []).map((c: any, i: number, arr: any[]) => (
<TouchableOpacity
key={c.id}
style={{ paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: i < arr.length - 1 ? 1 : 0, borderBottomColor: '#F1F5F9' }}
onPress={() => { setClient(c); setClientSearch(''); setDebouncedClientSearch(''); }}
activeOpacity={0.7}
>
<Text style={{ fontSize: 16, fontWeight: '600', color: '#0F172A' }}>{c.firstName} {c.lastName}</Text>
<Text style={{ fontSize: 14, color: '#64748B', marginTop: 2 }}>{c.accountNumber}</Text>
</TouchableOpacity>
))
)}
</View>
)}
</View>
)}
{/* Assign To */}
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>
Assign To <Text style={{ fontWeight: '400', color: '#94A3B8' }}>(optional)</Text>
</Text>
{assignTo ? (
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: '#F0FDF4', borderRadius: 14, padding: 16, marginBottom: 20, borderWidth: 1, borderColor: '#86EFAC' }}>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 17, fontWeight: '700', color: '#166534' }}>{assignTo.firstName} {assignTo.lastName}</Text>
<Text style={{ fontSize: 14, color: '#16A34A', marginTop: 2 }}>{assignTo.roles?.[0] ?? assignTo.role ?? 'Staff'}</Text>
</View>
<TouchableOpacity onPress={() => setAssignTo(null)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Text style={{ fontSize: 15, fontWeight: '700', color: '#059669' }}>Clear</Text>
</TouchableOpacity>
</View>
) : (
<View style={{ backgroundColor: '#FFF', borderRadius: 14, borderWidth: 1, borderColor: '#E2E8F0', overflow: 'hidden', marginBottom: 20 }}>
{users.length === 0 ? (
<View style={{ padding: 16, alignItems: 'center' }}>
<ActivityIndicator size="small" color="#0891B2" />
</View>
) : (
users.map((u: any, i: number) => (
<TouchableOpacity
key={u.id}
style={{ paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: i < users.length - 1 ? 1 : 0, borderBottomColor: '#F1F5F9', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
onPress={() => setAssignTo(u)}
activeOpacity={0.7}
>
<Text style={{ fontSize: 16, fontWeight: '600', color: '#0F172A' }}>{u.firstName} {u.lastName}</Text>
<Text style={{ fontSize: 13, color: '#94A3B8' }}>{u.roles?.[0] ?? u.role ?? 'Staff'}</Text>
</TouchableOpacity>
))
)}
</View>
)}
{/* Notes */}
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>
Notes <Text style={{ fontWeight: '400', color: '#94A3B8' }}>(optional)</Text>
</Text>
<TextInput
style={{ backgroundColor: '#FFF', borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 14, paddingHorizontal: 18, paddingVertical: 14, fontSize: 16, color: '#0F172A', marginBottom: 24, minHeight: 100, textAlignVertical: 'top' }}
placeholder="Additional notes or instructions..."
placeholderTextColor="#94A3B8"
value={notes}
onChangeText={setNotes}
multiline
/>
{/* Submit */}
<TouchableOpacity
style={{ backgroundColor: client ? '#059669' : '#CBD5E1', borderRadius: 14, paddingVertical: 18, alignItems: 'center' }}
onPress={submit}
disabled={loading || !client}
activeOpacity={0.8}
>
{loading ? <ActivityIndicator color="#FFF" /> : <Text style={{ color: '#FFF', fontSize: 17, fontWeight: '700' }}>Create Task</Text>}
</TouchableOpacity>
</ScrollView>
</View>
</SafeAreaView>
);
}