Initial commit

This commit is contained in:
2026-08-31 16:50:53 +02:00
commit a68c8864b0
80 changed files with 22222 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import {Stack} from 'expo-router';
export default function QualityLayout() {
return (
<Stack screenOptions={{headerShown: false}}>
<Stack.Screen name="index" />
<Stack.Screen name="add" />
</Stack>
);
}
+373
View File
@@ -0,0 +1,373 @@
import { useAlert } from '@/components/AlertComponent';
import { AppDatePicker } from '@/components/AppDatePicker';
import GenericDropdown from '@/components/GenericDropdown';
import FileAttachmentCard from '@/components/FileAttachmentCard';
import api from '@/utils/api';
import { formatDate, formatPickerDate } from '@/utils/dateTime';
import { DateType } from 'react-native-ui-datepicker';
import { useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { Calendar, CheckSquare, ChevronLeft, Square } from 'lucide-react-native';
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, KeyboardAvoidingView, Modal, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import * as DocumentPicker from 'expo-document-picker';
export default function AddQualityControlScreen() {
const router = useRouter();
const alert = useAlert();
const [isSubmitting, setIsSubmitting] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
// Form fields
const [date, setDate] = useState<DateType>(new Date());
const [subactivityId, setSubactivityId] = useState<number | null>(null);
const [workType, setWorkType] = useState<number | null>(null);
const [refDoc, setRefDoc] = useState<number | null>(null);
const [controlType, setControlType] = useState('');
const [instrument, setInstrument] = useState<number | null>(null);
const [result, setResult] = useState<number | null>(null);
// Attachments
const [attachments, setAttachments] = useState<DocumentPicker.DocumentPickerAsset[]>([]);
// Checkboxes
const [checkSegnaletica, setCheckSegnaletica] = useState(false);
const [checkSoggettiTerzi, setCheckSoggettiTerzi] = useState(false);
const [checkUtilizzoDPI, setCheckUtilizzoDPI] = useState(false);
const [checkControlloVisivo, setCheckControlloVisivo] = useState(false);
const [checkControlloDim, setCheckControlloDim] = useState(false);
const [checkConformita, setCheckConformita] = useState(false);
const [checkControlloFunz, setCheckControlloFunz] = useState(false);
// Lists
const [subactivities, setSubactivities] = useState<{id: number | string, label: string}[]>([]);
const [workTypeList, setWorkTypeList] = useState<{id: number, label: string}[]>([]);
const [refDocList, setRefDocList] = useState<{id: number, label: string}[]>([]);
const [instrumentList, setInstrumentList] = useState<{id: number, label: string}[]>([]);
const results = [
{ id: 1, label: 'Positivo' },
{ id: 0, label: 'Negativo' },
];
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
// Load Subactivities
const subRes = await api.get('/subactivity/get-subactivities');
if (subRes.data?.success) {
const mappedSubs = subRes.data.subactivities.map((s: any) => ({
id: s.id,
uuid: s.uuid,
label: s.label
}));
setSubactivities(mappedSubs);
}
// Load Related Tables
const relRes = await api.get('/quality-control/get-related-tables');
if (relRes.data?.success) {
setWorkTypeList(relRes.data.data.workType || []);
setRefDocList(relRes.data.data.refDocument || []);
setInstrumentList(relRes.data.data.instrument || []);
}
} catch (error) {
console.error('Errore nel caricamento dei dati iniziali:', error);
alert.showAlert('error', 'Errore', 'Impossibile caricare i dati per il form.');
}
};
const pickDocument = async () => {
try {
const result = await DocumentPicker.getDocumentAsync({
multiple: true,
copyToCacheDirectory: true,
});
if (!result.canceled && result.assets) {
setAttachments(prev => [...prev, ...result.assets]);
}
} catch (error) {
console.error('Errore durante la selezione del documento:', error);
alert.showAlert('error', 'Errore', 'Impossibile selezionare il documento.');
}
};
const removeAttachment = (index: number) => {
setAttachments(prev => prev.filter((_, i) => i !== index));
};
const handleSave = async () => {
if (!subactivityId || !date || result === null) {
alert.showAlert('warning', 'Dati Mancanti', 'Compila tutti i campi obbligatori (Cantiere, Data, Esito).');
return;
}
setIsSubmitting(true);
try {
const formattedDate = formatPickerDate(date);
const selectedSub = subactivities.find(s => s.id === subactivityId) as any;
const payload = {
subactivity_uuid: selectedSub?.uuid,
id_subactivity: subactivityId,
subactivity_id: subactivityId, // just in case
id_work_type: workType,
date: formattedDate,
id_ref_document: refDoc,
control_type: controlType,
id_instrument: instrument,
result: result,
check_segnaletica: checkSegnaletica ? 1 : 0,
check_soggetti_terzi: checkSoggettiTerzi ? 1 : 0,
check_utilizzo_dpi: checkUtilizzoDPI ? 1 : 0,
check_controllo_visivo: checkControlloVisivo ? 1 : 0,
check_controllo_dim: checkControlloDim ? 1 : 0,
check_conformita: checkConformita ? 1 : 0,
check_controllo_funz: checkControlloFunz ? 1 : 0,
};
const response = await api.post('/quality-control/add', { post: JSON.stringify(payload) });
if (response.data?.success) {
const qcId = response.data.id;
// Upload attachments if present
if (attachments.length > 0) {
for (let i = 0; i < attachments.length; i++) {
const file = attachments[i];
const formData = new FormData();
const fileName = file.name || `document_${i}`;
const fileType = file.mimeType || 'application/octet-stream';
const fileUri = Platform.OS === 'android' ? file.uri : file.uri.replace('file://', '');
formData.append("files", {
name: fileName,
type: fileType,
uri: fileUri
} as any);
formData.append('model_classname', 'QualityControl');
formData.append('model_id', qcId);
formData.append('method', 'put');
formData.append('name', fileName);
formData.append('type', fileType);
await api.post('/quality-control/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
}
alert.showAlert('success', 'Salvato', 'Controllo di Qualità salvato con successo.');
router.back();
} else {
alert.showAlert('error', 'Errore', response.data?.message || 'Salvataggio non riuscito.');
}
} catch (error) {
console.error('Errore durante il salvataggio:', error);
alert.showAlert('error', 'Errore', 'Impossibile completare il salvataggio.');
} finally {
setIsSubmitting(false);
}
};
const CheckboxRow = ({ label, value, onChange }: { label: string, value: boolean, onChange: (v: boolean) => void }) => (
<TouchableOpacity
onPress={() => onChange(!value)}
className="flex-row items-center bg-white border border-gray-100 rounded-xl px-4 py-4 mb-3 active:bg-gray-50 shadow-sm"
>
{value ? <CheckSquare size={24} color="#1071C2" /> : <Square size={24} color="#9ca3af" />}
<Text className="ml-3 text-base text-gray-800 flex-1">{label}</Text>
</TouchableOpacity>
);
return (
<View className="flex-1 bg-gray-50">
<StatusBar style="dark" />
{/* Header */}
<View className="bg-white px-4 pb-6 shadow-sm border-b border-gray-100">
<SafeAreaView edges={['top']} className='pt-5'>
<View className='flex-row items-center gap-4 px-2'>
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
<ChevronLeft size={28} color="#4b5563" pointerEvents="none" />
</TouchableOpacity>
<Text className="text-xl font-bold text-gray-800 leading-tight uppercase flex-1 pr-4">
Nuovo Controllo
</Text>
</View>
</SafeAreaView>
</View>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'padding'}
className="flex-1"
>
<ScrollView
contentContainerStyle={{ padding: 24 }}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Date */}
<Text className="text-lg font-bold text-primary-dark mb-3">Data <Text className="text-red-500">*</Text></Text>
<TouchableOpacity
onPress={() => setShowDatePicker(true)}
className="flex-row items-center bg-white border border-gray-200 rounded-2xl px-5 py-4 mb-6 active:bg-gray-50 shadow-sm"
>
<Text className="flex-1 text-base text-gray-800 font-medium">
{date ? formatDate(formatPickerDate(date)) : 'Seleziona data...'}
</Text>
<Calendar size={20} color="#6b7280" />
</TouchableOpacity>
{/* Subactivity / Cantiere */}
<Text className="text-lg font-bold text-primary-dark mb-3">Cantiere <Text className="text-red-500">*</Text></Text>
<View className="mb-6 shadow-sm">
<GenericDropdown
options={subactivities}
selectedId={subactivityId}
onSelect={(id) => setSubactivityId(id as number)}
placeholder="Seleziona cantiere..."
searchPlaceholder="Cerca cantiere..."
/>
</View>
{/* Work Type */}
<Text className="text-lg font-bold text-primary-dark mb-3">Tipologia Lavorazione</Text>
<View className="mb-6 shadow-sm">
<GenericDropdown
options={workTypeList}
selectedId={workType}
onSelect={setWorkType}
placeholder="Seleziona tipologia..."
showSearch={false}
/>
</View>
{/* Reference Document */}
<Text className="text-lg font-bold text-primary-dark mb-3">Documento di Riferimento</Text>
<View className="mb-6 shadow-sm">
<GenericDropdown
options={refDocList}
selectedId={refDoc}
onSelect={setRefDoc}
placeholder="Seleziona documento..."
showSearch={false}
/>
</View>
{/* Control Type */}
<Text className="text-lg font-bold text-primary-dark mb-3">Tipo di Controllo</Text>
<View className="bg-white rounded-2xl border border-gray-200 mb-6 shadow-sm">
<TextInput
className="px-5 py-4 text-base text-gray-800 font-medium"
placeholder="Inserisci tipo di controllo"
placeholderTextColor="#6a7282"
value={controlType}
onChangeText={setControlType}
/>
</View>
{/* Instrument */}
<Text className="text-lg font-bold text-primary-dark mb-3">Strumento Utilizzato</Text>
<View className="mb-8 shadow-sm">
<GenericDropdown
options={instrumentList}
selectedId={instrument}
onSelect={setInstrument}
placeholder="Seleziona strumento..."
showSearch={false}
/>
</View>
{/* Checkboxes */}
<Text className="text-lg font-bold text-primary-dark mb-4 mt-2 border-t border-gray-200 pt-6">Checklist Controlli</Text>
<CheckboxRow label="Presenza e visibilità segnaletica" value={checkSegnaletica} onChange={setCheckSegnaletica} />
<CheckboxRow label="Presenza soggetti terzi" value={checkSoggettiTerzi} onChange={setCheckSoggettiTerzi} />
<CheckboxRow label="Corretto utilizzo DPI" value={checkUtilizzoDPI} onChange={setCheckUtilizzoDPI} />
<CheckboxRow label="Controllo visivo" value={checkControlloVisivo} onChange={setCheckControlloVisivo} />
<CheckboxRow label="Controllo Dimensionale/Elaborati" value={checkControlloDim} onChange={setCheckControlloDim} />
<CheckboxRow label="Controllo conformità mat. posato" value={checkConformita} onChange={setCheckConformita} />
<CheckboxRow label="Controllo funzionale" value={checkControlloFunz} onChange={setCheckControlloFunz} />
{/* Result */}
<Text className="text-lg font-bold text-primary-dark mb-3 mt-6 border-t border-gray-200 pt-6">Esito <Text className="text-red-500">*</Text></Text>
<View className="mb-10 shadow-sm">
<GenericDropdown
options={results}
selectedId={result}
onSelect={setResult}
placeholder="Seleziona esito..."
showSearch={false}
/>
</View>
{/* Attachments Section */}
<View className="mb-8">
<View className="flex-row items-center justify-between mb-4 border-t border-gray-200 pt-6">
<Text className="text-lg font-bold text-primary-dark">
Allegati <Text className="text-sm font-normal text-gray-500">({attachments.length})</Text>
</Text>
<TouchableOpacity activeOpacity={0.7} onPress={pickDocument}>
<Text className="text-[#1071C2] font-bold text-base uppercase">Aggiungi</Text>
</TouchableOpacity>
</View>
<View className="mt-2">
{attachments.map((file, index) => (
<FileAttachmentCard
key={index}
file={file}
onRemove={() => removeAttachment(index)}
/>
))}
{attachments.length === 0 && (
<Text className="text-gray-400 font-medium text-center py-4 bg-white border border-gray-200 border-dashed rounded-2xl">
Nessun file allegato
</Text>
)}
</View>
</View>
{/* Save Button */}
<TouchableOpacity
onPress={handleSave}
disabled={isSubmitting || !subactivityId || !date || result === null}
className={`w-full py-4 rounded-2xl shadow-lg flex-row items-center justify-center ${(!subactivityId || !date || result === null || isSubmitting) ? 'bg-gray-300' : 'bg-[#1071C2] active:scale-[0.98]'}`}
>
{isSubmitting ? (
<ActivityIndicator color="white" />
) : (
<Text className="text-white text-lg font-bold uppercase">Salva</Text>
)}
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
{/* Date Picker Modal */}
<Modal visible={showDatePicker} transparent animationType="fade">
<View className="flex-1 justify-center items-center bg-black/50">
<View className="bg-white rounded-3xl p-6 w-[90%] shadow-2xl">
<Text className="text-lg font-bold text-gray-800 mb-4">Seleziona Data</Text>
<AppDatePicker
mode="single"
date={date}
onChange={(d) => setDate(d.date || new Date())}
/>
<TouchableOpacity
onPress={() => setShowDatePicker(false)}
className="mt-6 w-full py-4 bg-[#1071C2] rounded-xl active:scale-[0.98]"
>
<Text className="text-white text-center font-bold text-lg">Conferma</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</View>
);
}
+169
View File
@@ -0,0 +1,169 @@
import { useAlert } from '@/components/AlertComponent';
import FilterModal from '@/components/FilterModal';
import LoadingScreen from '@/components/LoadingScreen';
import QualityControlCard from '@/components/QualityControlCard';
import { StatusBar } from 'expo-status-bar';
import api from '@/utils/api';
import { ClipboardCheck, Filter, Plus } from 'lucide-react-native';
import React, { useCallback, useEffect, useState } from 'react';
import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Place, QualityControlItem } from '@/types/types';
import { useRouter, useFocusEffect } from 'expo-router';
export default function QualityControlScreen() {
const router = useRouter();
const alert = useAlert();
const [qualityControls, setQualityControls] = useState<QualityControlItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// Filters state
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [places, setPlaces] = useState<Place[]>([]);
const [filterRange, setFilterRange] = useState<{ startDate: string | null; endDate: string | null }>({ startDate: null, endDate: null });
const [filterPlace, setFilterPlace] = useState<any>(null);
const activeFiltersCount = (filterRange.startDate ? 1 : 0) + (filterPlace ? 1 : 0);
const fetchPlaces = async () => {
try {
const response = await api.get('/construction-site/get-construction-sites');
if (response.data?.success) {
setPlaces(response.data.constructionSites || []);
}
} catch (error) {
console.error('Errore nel recupero dei cantieri:', error);
}
};
const fetchQualityControls = async (currentRange = filterRange, currentPlace = filterPlace, isSilent = false) => {
try {
if (!refreshing && !isSilent) setIsLoading(true);
const rangeParam = currentRange.startDate ? currentRange : null;
// The API expects 'constructionSite' which is likely the ID. The filterPlace stores the ID or the Place object?
// According to PlaceFilter and InvoiceScreen, currentPlace is the selectedPlaceId.
const params = { range: rangeParam, constructionSite: currentPlace };
const response = await api.post('/quality-control/list', { params });
if (response.data?.success) {
setQualityControls(response.data.result || []);
}
} catch (error) {
console.error('Errore nel recupero dei controlli qualità:', error);
alert.showAlert('error', 'Errore', 'Impossibile recuperare i dati. Riprova più tardi.');
} finally {
setIsLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchPlaces();
}, []);
useFocusEffect(
useCallback(() => {
fetchQualityControls(filterRange, filterPlace, true);
}, [filterRange, filterPlace])
);
const onRefresh = () => {
setRefreshing(true);
fetchQualityControls();
};
if (isLoading && !refreshing) {
return <LoadingScreen />;
}
return (
<View className="flex-1 bg-primary-dark">
<StatusBar style="light" />
{/* Header */}
<SafeAreaView edges={['top']} className='pt-5'>
<View className="pb-6 px-6 z-10 flex-row justify-between items-center">
<View>
<Text className="text-white text-md font-semibold uppercase tracking-wider mb-1">Lista dei controlli di qualità</Text>
<Text className="text-yellow-400 text-3xl font-bold leading-tight">Controlli di Qualità</Text>
</View>
<View className="bg-white/10 p-4 rounded-full">
<ClipboardCheck size={32} color="white" pointerEvents="none" />
</View>
</View>
</SafeAreaView>
<View className="flex-1 bg-gray-50 rounded-t-[2.5rem] overflow-hidden">
<ScrollView
contentContainerStyle={{ paddingBottom: 160 }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1071C2']} />
}
>
<View className="flex-1 p-5 items-center">
<View className="w-full mt-2">
{qualityControls.length === 0 ? (
<View className="bg-white p-6 rounded-3xl border border-gray-100 items-center justify-center border-dashed">
<Text className="text-gray-400 font-medium">Nessun controllo registrato</Text>
</View>
) : (
<View>
{qualityControls.map((item, index) => (
<QualityControlCard key={item.id || index} item={item} />
))}
</View>
)}
</View>
</View>
</ScrollView>
{/* FAB Add */}
<TouchableOpacity
onPress={() => router.push('/quality/add')}
className="absolute bottom-[6.5rem] right-6 w-16 h-16 bg-white border border-[#1071C2] rounded-full shadow-lg items-center justify-center active:scale-90"
>
<Plus size={32} color="#1071C2" pointerEvents="none" />
</TouchableOpacity>
{/* FAB Filter */}
<TouchableOpacity
onPress={() => setIsFilterVisible(true)}
className="absolute bottom-8 right-6 w-16 h-16 bg-[#1071C2] rounded-full shadow-lg items-center justify-center active:scale-90"
>
<Filter size={28} color="white" pointerEvents="none" />
{activeFiltersCount > 0 && (
<View className="absolute top-0 right-0 bg-red-500 w-6 h-6 rounded-full items-center justify-center border-2 border-white">
<Text className="text-white text-xs font-bold">{activeFiltersCount}</Text>
</View>
)}
</TouchableOpacity>
</View>
{/* Filter Modal */}
<FilterModal
visible={isFilterVisible}
showPlace={true}
showDate={true}
places={places}
currentRange={filterRange}
currentPlace={filterPlace}
onApply={(range, place) => {
setFilterRange(range);
setFilterPlace(place);
setIsFilterVisible(false);
fetchQualityControls(range, place);
}}
onReset={() => {
setFilterRange({ startDate: null, endDate: null });
setFilterPlace(null);
setIsFilterVisible(false);
fetchQualityControls({ startDate: null, endDate: null }, null);
}}
onClose={() => setIsFilterVisible(false)}
/>
</View>
);
}