45 Commits

Author SHA1 Message Date
Rochas
7b4e2c1130 gestion des ressources fonctionnelle, mais pas fini 2025-12-14 15:00:11 +01:00
Rochas
a2e5b1e9cf fix temporaire en cours 2025-12-14 10:58:30 +01:00
Rochas
27d13ace9d test reservation todo corriger getChantier avec la nouvelelle structure 2025-12-14 03:04:12 +01:00
tuanvu
1f698076df Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 23:24:25 +01:00
tuanvu
3d44d635a2 add objet and renovation 2025-12-13 23:22:40 +01:00
Rochas
dfa26df502 correction dépassement text 2025-12-13 23:12:43 +01:00
Rochas
27503103da Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 22:51:15 +01:00
Rochas
9b34feb43e correction anomalies 2025-12-13 22:51:09 +01:00
Alexis Leboeuf
7b5a4c0941 Changed typo in Ressource type in Ouvriers 2025-12-13 22:30:37 +01:00
Alexis Leboeuf
9a3070b6e9 Merging 2025-12-13 22:24:21 +01:00
Alexis Leboeuf
337b8c947e Changed Ouvriers to come from Firebase rather than mocked 2025-12-13 22:24:12 +01:00
Rochas
ee72368b94 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 22:20:42 +01:00
Rochas
a26a2eb572 recherche dans selectChantier 2025-12-13 22:20:27 +01:00
Amaël Kesteman
898f06d577 Feat: Sync entre addRessources et gestionRessources 2025-12-13 21:49:07 +01:00
tuanvu
85fc76ddec mini change in anomalies 2025-12-13 21:19:57 +01:00
tuanvu
208de892d4 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 21:03:50 +01:00
tuanvu
15d7a517f5 resync 2025-12-13 21:03:45 +01:00
Rochas
29abc4289e correction addChantier 2025-12-13 20:51:19 +01:00
Alexis Leboeuf
dc83b1d06d Renamed the tab that displays workers as Ouvrier 2025-12-13 20:47:46 +01:00
Alexis Leboeuf
211dffac98 Removed unnecessary information 2025-12-13 20:26:38 +01:00
Alexis Leboeuf
150c977306 Adding indication for each value in Ressource tab 2025-12-13 20:23:44 +01:00
Amaël Kesteman
abdeaa7d45 Feat: Add localisation nouveau chantier sur la carte 2025-12-13 19:20:22 +01:00
Rochas
a243d791f9 fix temporaire addChantier 2025-12-13 18:58:32 +01:00
Rochas
bcf9907007 add en un seul écran, composant lié aux add dans un dossier /composants/add 2025-12-13 18:48:54 +01:00
Alexis Leboeuf
b5c751cada Fixed typo in README 2025-12-13 17:07:17 +01:00
Alexis Leboeuf
57392aac5c Added extra package installation command on README 2025-12-13 17:06:25 +01:00
Alexis Leboeuf
167ac00299 Merging 2025-12-13 17:05:29 +01:00
Alexis Leboeuf
fcd94fe0c7 Added image URI save 2025-12-13 17:05:15 +01:00
Rochas
82c00cabd4 merge 2025-12-13 17:03:48 +01:00
Rochas
1f055240bb selectRessource au lieu de selectMachine et selectOuvrier 2025-12-13 16:56:33 +01:00
tuanvu
d748c90391 merge erreur 2025-12-13 16:54:40 +01:00
tuanvu
6e4242736f addchantier work 2025-12-13 16:40:43 +01:00
Alexis Leboeuf
038006818d Added image selection on new Anomaly
Caution ! New import, run npm build to download it
Only accesses the Gallery, does not really save the image
2025-12-13 16:34:07 +01:00
Rochas
f213e2dacf Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 15:35:25 +01:00
Rochas
e8e30d541d addChantier / selectMachine / machineSummary : presque fini 2025-12-13 15:35:01 +01:00
tuanvu
406c578cba filter for machine 2025-12-13 15:33:46 +01:00
tuanvu
1407d3b20f filter chefs 2025-12-13 15:32:01 +01:00
Amaël Kesteman
602b571d5c Feat: ajout de la page d'ajout d'un nouvel artisant 2025-12-13 15:30:08 +01:00
Amaël Kesteman
ff010fec4e Feat: ajout de la page d'ajout des ressources. 2025-12-13 15:07:21 +01:00
Rochas
d4b056b197 engin rennomé en machine 2025-12-13 12:37:31 +01:00
Rochas
dc02896fdd selectEngins 2025-12-13 12:31:32 +01:00
tuanvu
91fe9f849b borrow and return Ressource -- need to test 2025-12-13 12:02:20 +01:00
tuanvu
730a9882c7 fini fontionnalité de anomalie, ptet changer styles 2025-12-13 11:26:22 +01:00
Rochas
68af57a394 selection d'un chef de chatier dans addChantier 2025-12-13 00:20:06 +01:00
Rochas
e95bb3fd70 addChanter, selectChefChantier, en cour + Update de themed-button pour prendre en charge le changement de theme pendant la pression 2025-12-12 21:57:09 +01:00
30 changed files with 2058 additions and 450 deletions

View File

@@ -11,9 +11,43 @@
Lien du git : gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet Lien du git : gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet
Différentes commandes a effectuer pour lancer le projet:
#### Différentes commandes a effectuer pour lancer le projet:
npx expo install react-native-maps@1.9.0 npx expo install react-native-maps@1.9.0
npm install react-native-maps @react-navigation/native @react-navigation/bottom-tabs react-native-safe-area-context react-native-screens firebase npm install react-native-maps @react-navigation/native @react-navigation/bottom-tabs react-native-safe-area-context react-native-screens firebase
npm install @react-native-community/datetimepicker
npx expo start npx expo install expo-image-picker
npx expo install expo-location
npx expo start
#### Présentation de l'application :
##### 5 écrans :
Accuil :
- Affiche le chantier sélectionné :
- Résumé du chantier
- état éditable par l'utilisateur
- Liste d'anomalies, possibilité d'en ajouter ou de les supprimer
- Sélectionner un chantier via le bouton en haut à gauche.
Ressources :
Ouvriers :
MapScreen :
Ajouter :
Permet d'ajouter un chantier ou une ressource (ouvrier,véhicule,outil)
##### Fonctionnalité manquante :
Par manque de temps nous n'avons pas peu finnalité certaine fonctionnalité
- possibilité de modifier les ressources d'un chantier (ex: réajustement des besoins)
- modifier la quantité totale d'une ressource (ex: restock de ressources)
- gestion des stocks non finalisée :
Un chantier comptabilise du stock uniquement quand il est "En cours", s'il est dans un autre été, ses réservations ne sont pas comptabilisées, donc les autres chantiers peuvent utiliser le stock libéré. Si on le remet l'état à "En cours" et que le stock n'est pas suffisant, alors la quantité disponible du stock passe en négatif.
Ce problème peut être corrigé en bloquant le changement d'état si la quantité de stock n'est pas suffisante, mais aurait besoin de la possibilité de modifier les ressources du chantier, ou la quantité des ressources.

View File

@@ -1,8 +1,6 @@
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { } from 'expo-router'; import { } from 'expo-router';
import React from 'react'; import React from 'react';
import { HapticTab } from '@/components/expoExempleComponents/haptic-tab'; import { HapticTab } from '@/components/expoExempleComponents/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol'; import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme'; import { Colors } from '@/constants/theme';
@@ -11,11 +9,8 @@ import GestionOuvrier from './gestion_ouvrier';
import ListMateriel from './gestionnaire_ressource'; import ListMateriel from './gestionnaire_ressource';
import Home from './home'; import Home from './home';
import MapScreen from './mapScreen'; import MapScreen from './mapScreen';
import AddChantier from './addChantier';
import AntDesign from '@expo/vector-icons/AntDesign'; import AntDesign from '@expo/vector-icons/AntDesign';
import { UserProvider } from '../ContextUser'; import AddScreen from './addScreen';
import { ChantierProvider } from '../ContextChantier';
import { RessourcesProvider } from '../ContextRessource';
const Tabs = createBottomTabNavigator(); const Tabs = createBottomTabNavigator();
@@ -54,7 +49,7 @@ export default function TabLayout() {
name="GestionOuvrier" name="GestionOuvrier"
component={GestionOuvrier} component={GestionOuvrier}
options={{ options={{
title: 'Bonjour', title: 'Ouvriers',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />, tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />,
}} }}
/> />
@@ -68,8 +63,8 @@ export default function TabLayout() {
> >
</Tabs.Screen> </Tabs.Screen>
<Tabs.Screen <Tabs.Screen
name="addChantier" name="Ajouter"
component={AddChantier} component={AddScreen}
options={{ options={{
title: 'Ajouter', title: 'Ajouter',
tabBarIcon: ({ color }) => ( tabBarIcon: ({ color }) => (
@@ -78,6 +73,7 @@ export default function TabLayout() {
}} }}
/> />
</Tabs.Navigator> </Tabs.Navigator>
); );
} }

View File

@@ -1,178 +0,0 @@
import ChantierSummary from '@/components/chantierSummary';
import SelectChantier from '@/components/selectChantier';
import SetStatus from '@/components/setStatus';
import { ThemedView } from '@/components/theme/themed-view';
import React, { useEffect, useState } from 'react';
import { StyleSheet, ScrollView, Button, TextInput, Text, View } from 'react-native';
import { useChantier } from '../ContextChantier';
import { useRessources } from '../ContextRessource';
import { useUser } from '../ContextUser';
import { getRessources, getUsers, addChantier } from '@/services/ressourcesService';
import { Chantier, Ressources } from '@/class/class';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedButton } from '@/components/theme/themed-button';
import { ThemedTextInput } from '@/components/theme/themed-textinput';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
//Uniquement accessible par le RESPONSSABLE du chantier
//Pour créer ou modifier un chantier
export default function AddChantier() {
const { chantier, setChantier } = useChantier();
const { user, setUser } = useUser();
const { ressources, setRessources } = useRessources();
const [editMode,setEditMode] = useState(false);
const [loading, setLoading] = useState(false);
const [objet, setObjet] = useState('');
const [date, setDate] = useState('');
const [chefChantier, setChefChantier] = useState('');
const [adresse, setAdresse] = useState('');
const [duree, setDuree] = useState('');
const [contact, setContact] = useState('');
const [userSelect, setUserSelect] = useState<string[]>([]);
const [ressourcesSelect, setRessourcesSelect] = useState<string[]>([]);
async function handleAddChantier() {
setLoading(true);
}
const renderInut = (name : string, preFill : string, value : string, setValue : ((text:string) => void)) => {
return (
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>{name}:</ThemedText>
<ThemedTextInput lvl = {1} style = {styles.input} placeholder={preFill} value = {value} onChangeText={setAdresse} />
</View>
);
};
return (
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
{editMode &&
<View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier>
</View>
}
<ScrollView>
<View style = {styles.header}>
<ThemedText style = {styles.text}>
{editMode? "Edition d'un chantier"
:"Ajouter un nouveau chantier"}
</ThemedText>
{renderInut("Objet","Renovation",objet,setObjet)}
{renderInut("Date de départ","TOTO : JOUR + Demi journé",date,setDate)}
{renderInut("Estimation de la durée (1/2 Journée)","14",duree,setDuree)}
{renderInut("Adresse","1 Rue de la Coutellerie, Paris",adresse,setAdresse)}
{renderInut("Contact client","07 01 02 03 04 05",contact,setContact)}
{renderInut("Vehicule","TODO pas un input bien sûre",adresse,setAdresse)}
{renderInut("Chef de chantier","TODO pas un input non plus",chefChantier,setChefChantier)}
<ThemedButton
lvl={1}
shadow={true}
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
onPress={() => handleAddChantier()}
>
<ThemedText>+</ThemedText>
</ThemedButton>
</View>
</ScrollView>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
},
header: {
marginTop:60,
alignItems: "center",
paddingHorizontal: 20,
},
text: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
},
inputBack: {
width: "100%",
borderRadius: 10,
backgroundColor: "transparent",
},
inputLine:{
width: "100%",
//flexDirection: 'row',
paddingVertical: 5,
//alignItems: "center",
},
inputName: {
fontSize: 16,
},
input: {
width: "100%",
borderRadius: 10,
padding: 10,
fontSize: 16,
},
card: {
flexDirection: "row",
marginHorizontal: 20,
marginBottom: 15,
borderRadius: 10,
padding: 10,
},
image: {
width: 80,
height: 80,
borderRadius: 8,
marginRight: 10,
},
info: {
flex: 1,
justifyContent: "center",
},
footer: {
padding: 20,
},
empty: {
textAlign: "center",
marginTop: 30,
color: "#888",
},
filterMenuOverlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "center",
alignItems: "center",
zIndex: 999,
},
filterMenu: {
width: "80%",
borderRadius: 12,
padding: 20,
backgroundColor: "#fff",
},
filterTitle: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 20,
textAlign: "center",
},
});

94
app/(tabs)/addScreen.tsx Normal file
View File

@@ -0,0 +1,94 @@
import { ThemedButton } from "@/components/theme/themed-button";
import { ThemedText } from "@/components/theme/themed-text";
import { useState } from "react";
import { View,StyleSheet } from "react-native";
import AddChantier from "@/components/add/addChantier";
import AddRessource from "@/components/add/addRessource";
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
import { ThemedView } from "@/components/theme/themed-view";
export default function AddScreen() {
const [typeAdd, setTypeAdd] = useState('');
const [editMode, setEditMode] = useState(false);
function onPressSwitchMode(){
setEditMode(!editMode);
}
return(
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
{typeAdd===""? (
<View style={styles.selectTypeAdd} >
<ThemedButton style={styles.button} onPress={() => setTypeAdd("Chantier")}>
<ThemedText>
Ajouter un chantier
</ThemedText>
</ThemedButton>
<ThemedButton style={styles.button} onPress={() => setTypeAdd("Outil")}>
<ThemedText>
Ajouter un équipement
</ThemedText>
</ThemedButton>
<ThemedButton style={styles.button} onPress={() => setTypeAdd("Machine")}>
<ThemedText>
Ajouter un vehicule ou machine
</ThemedText>
</ThemedButton>
<ThemedButton style={styles.button} onPress={() => setTypeAdd("Ouvrier")}>
<ThemedText>
Ajouter un ouvrier
</ThemedText>
</ThemedButton>
</View>
):
<View>
<View style={styles.backButton}>
<ThemedButton style={styles.button} border={4} onPress={() => setTypeAdd("")}>
<ThemedText>
Retour
</ThemedText>
</ThemedButton>
</View>
{typeAdd==="Chantier"? (
<AddChantier/>
):
(
<AddRessource ressourceType={typeAdd as 'Outil' | 'Machine' | 'Ouvrier'}/>
)}
</View>
}
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
//marginTop: Constants.statusBarHeight, //pour la barre menu du haut
},
button:{
padding:10,
borderRadius:10,
},
selectTypeAdd:{
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
gap:30,
padding:20
},
backButton:{
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
position: 'absolute',
padding: 20,
zIndex: 100,
//backgroundColor:"#FF0000",
},
});

View File

@@ -3,12 +3,13 @@ import { ThemedTextInput } from "@/components/theme/themed-textinput";
import { ThemedView } from "@/components/theme/themed-view"; import { ThemedView } from "@/components/theme/themed-view";
import Constants from "expo-constants"; //pour connaître la taille de la barre menu de l'OS en haut import Constants from "expo-constants"; //pour connaître la taille de la barre menu de l'OS en haut
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import React, { useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { FlatList, Image, StyleSheet, Text, View } from "react-native"; import { FlatList, Image, StyleSheet, Text, View } from "react-native";
import rawConcerts from "../../data/concerts.json"; import { getUsers } from "@/services/ressourcesService";
import { useChantier } from "../ContextChantier"; import { useChantier } from "../ContextChantier";
import SelectChantier from "@/components/selectChantier"; import SelectChantier from "@/components/selectChantier";
import { Ressources } from "@/class/class";
import { getRessources } from "@/services/ressourcesService";
type Concert = { type Concert = {
group: string; group: string;
@@ -26,31 +27,32 @@ export default function GestionOuvrier() {
const { nom, prenom } = useLocalSearchParams(); // Recup data ecran precedent const { nom, prenom } = useLocalSearchParams(); // Recup data ecran precedent
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { chantier, setChantier } = useChantier(); const { chantier, setChantier } = useChantier();
const [artisans, setRessources] = useState<Ressources[]>([]);
const concertsData: Concert[] = Array.isArray(rawConcerts) useEffect(() => {
? (rawConcerts as Concert[]) async function loadData() {
: []; try {
//Nous ne gardons que les Ouvriers, qui peuvent être assignés à un chantier
const data = (await getRessources()).filter(u => u.type === "Ouvrier");
setRessources(data);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadData();
}, []);
const filteredData = useMemo(() => { const renderItem = ({ item, index }: { item?: Ressources; index: number }) => {
if (!Array.isArray(concertsData)) return [];
const q = search.trim().toLowerCase();
if (!q) return concertsData;
return concertsData.filter(
(item) => !!item && (item.group ?? "").toLowerCase().includes(q)
);
}, [concertsData, search]);
const renderItem = ({ item, index }: { item?: Concert; index: number }) => {
if (!item) { if (!item) {
return null; return null;
} }
return ( return (
<ThemedView lvl={1} shadow={true} style={styles.card}> <ThemedView lvl={1} shadow={true} style={styles.card}>
<Image source={{ uri: item.Image }} style={styles.image} />
<ThemedView lvl={1} style={styles.info}> <ThemedView lvl={1} style={styles.info}>
<ThemedText style={styles.group}>{item.group}</ThemedText> <Image source={{ uri: item.Image }} style={styles.image} />
<ThemedText>{item.date}</ThemedText> <ThemedText style={styles.group}>{item.name}</ThemedText>
<ThemedText>{item.location}</ThemedText> <ThemedText>{item.quantity}</ThemedText>
<ThemedText>{item.type}</ThemedText>
</ThemedView> </ThemedView>
</ThemedView> </ThemedView>
); );
@@ -65,7 +67,7 @@ export default function GestionOuvrier() {
<FlatList <FlatList
data={filteredData} data={artisans}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(_, index) => index.toString()} keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ paddingBottom: 40 }} contentContainerStyle={{ paddingBottom: 40 }}

View File

@@ -10,12 +10,12 @@ import { FlatList, Image, StyleSheet, Text, View } from "react-native";
import { Ressources } from "../../class/class"; import { Ressources } from "../../class/class";
import { getRessources } from "../../services/ressourcesService"; import { getRessources } from "../../services/ressourcesService";
import SelectChantier from "@/components/selectChantier"; import SelectChantier from "@/components/selectChantier";
import { useRessources } from "../ContextRessource";
export default function GestionnaireRessource() { export default function GestionnaireRessource() {
const { nom, prenom } = useLocalSearchParams();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [ressource, setRessources] = useState<Ressources[]>([]); const {ressources, setRessources} = useRessources();
const [filterType, setFilterType] = useState("tout"); const [filterType, setFilterType] = useState("Tout");
const [showFilterMenu, setShowFilterMenu] = useState(false); const [showFilterMenu, setShowFilterMenu] = useState(false);
const router = useRouter(); const router = useRouter();
@@ -31,9 +31,9 @@ export default function GestionnaireRessource() {
loadData(); loadData();
}, []); }, []);
const filteredData = ressource.filter((r) => { const filteredData = ressources.filter((r) => {
const matchName = r.name.toLowerCase().includes(search.toLowerCase()); const matchName = r.name.toLowerCase().includes(search.toLowerCase());
const matchType = filterType === "tout" || r.type === filterType; const matchType = filterType === "Tout" || r.type === filterType;
return matchName && matchType; return matchName && matchType;
}); });
@@ -43,11 +43,11 @@ export default function GestionnaireRessource() {
<ThemedView lvl={1} shadow={true} style={styles.card}> <ThemedView lvl={1} shadow={true} style={styles.card}>
<Image source={{ uri: item.Image }} style={styles.image} /> <Image source={{ uri: item.Image }} style={styles.image} />
<ThemedView lvl={1} style={styles.info}> <ThemedView lvl={1} style={styles.info}>
<ThemedText>{item.id}</ThemedText> <ThemedText>Id : {item.id}</ThemedText>
<ThemedText>{item.name}</ThemedText> <ThemedText>Nom : {item.name}</ThemedText>
<ThemedText>{item.type}</ThemedText> <ThemedText>Type : {item.type}</ThemedText>
<ThemedText>{item.quantity}</ThemedText> <ThemedText>Quantité totale : {item.quantity}</ThemedText>
<ThemedText>{item.available_quantity}</ThemedText> <ThemedText>Quantité disponible : {item.available_quantity}</ThemedText>
</ThemedView> </ThemedView>
</ThemedView> </ThemedView>
); );
@@ -69,15 +69,17 @@ export default function GestionnaireRessource() {
<ThemedView lvl={2} style={styles.filterMenuOverlay}> <ThemedView lvl={2} style={styles.filterMenuOverlay}>
<ThemedView lvl={5} style={styles.filterMenu}> <ThemedView lvl={5} style={styles.filterMenu}>
<ThemedText style={styles.filterTitle}>Filtrer par type</ThemedText> <ThemedText style={styles.filterTitle}>Filtrer par type</ThemedText>
{["tout", "Outil", "Machine"].map((t) => ( {["Tout", "Outil", "Machine","Ouvrier"].map((t) => (
<ThemedButton <ThemedButton
key={t} key={t}
lvl={1} lvl={1}
shadow={true} shadow={true}
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }} style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
onPress={() => { onPress={async () => {
setFilterType(t); setFilterType(t);
setShowFilterMenu(false); setShowFilterMenu(false);
const updateRessource = await getRessources();
setRessources(updateRessource)
}} }}
> >
<ThemedText style={{ textAlign: "center" }}>{t}</ThemedText> <ThemedText style={{ textAlign: "center" }}>{t}</ThemedText>

View File

@@ -4,7 +4,7 @@ import SetStatus from '@/components/setStatus';
import { ThemedView, } from '@/components/theme/themed-view'; import { ThemedView, } from '@/components/theme/themed-view';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
import React from 'react'; import React from 'react';
import { StyleSheet, View,Text } from 'react-native'; import { StyleSheet, View,Text, ScrollView } from 'react-native';
import { useChantier } from '../ContextChantier'; import { useChantier } from '../ContextChantier';
import Anomaly from '@/components/anomaly'; import Anomaly from '@/components/anomaly';
@@ -23,12 +23,18 @@ export default function Home() {
<View style={{width:"100%", position: 'absolute'}}> <View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier> <SelectChantier></SelectChantier>
</View> </View>
{chantier&&
<View style={{width:"100%", position: 'absolute',marginLeft:"50%"}}> <View style={{width:"100%", position: 'absolute',marginLeft:"50%"}}>
<SetStatus></SetStatus> <SetStatus></SetStatus>
</View> </View>
<ChantierSummary style={styles.summary} data={{ chantier }} /> }
<Anomaly style={styles.anomaly} data={{chantier}}/> <ScrollView>
{role === "chef"} <View style={{paddingTop:60}}>
<ChantierSummary style={styles.summary} data={{ chantier }} />
<Anomaly style={styles.anomaly} data={{chantier}}/>
{role === "chef"}
</View>
</ScrollView>
</View> </View>
</ThemedView> </ThemedView>
@@ -43,7 +49,6 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut marginTop: Constants.statusBarHeight, //pour la barre menu du haut
paddingTop : 60,
}, },
header: { header: {
flex: 1, flex: 1,

View File

@@ -1,11 +1,15 @@
// MapScreen.tsx // MapScreen.tsx
import { ThemedMapView } from '@/components/theme/themed-mapview'; import { ThemedMapView } from '@/components/theme/themed-mapview';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedButton } from '@/components/theme/themed-button';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { StyleSheet, View, Dimensions, Image, Text } from 'react-native'; import { StyleSheet, View, Dimensions, Image, Text } from 'react-native';
import MapView, { Marker, Callout, CalloutSubview, PROVIDER_DEFAULT } from 'react-native-maps'; import MapView, { Marker, Callout, CalloutSubview, PROVIDER_DEFAULT } from 'react-native-maps';
import { db } from '../../firebase_config'; import { db } from '../../firebase_config';
import { Chantier } from '@/class/class'; import { Chantier } from '@/class/class';
import { getChantiers } from '@/services/ressourcesService'; import { getChantiers } from '@/services/ressourcesService';
import { useLocation } from '@/hooks/useLocation';
const MapScreen = () => { const MapScreen = () => {
@@ -20,21 +24,30 @@ const region = {
const [chantiers, setMarkers] = useState<Chantier[]>([]); const [chantiers, setMarkers] = useState<Chantier[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
async function loadData() { async function loadData() {
try { try {
setRefreshing(true)
const data = await getChantiers(); const data = await getChantiers();
console.log("Chantiers chargés:", data.length);
console.log("📍 Premier chantier:", data[-1]);
console.log("🗺️ Coordonnées:", data[-1]?.latitude, data[-1]?.longitude);
setMarkers(data); setMarkers(data);
} catch (error) { } catch (error) {
console.error("Erreur lors du chargement :", error); console.error("Erreur lors du chargement :", error);
} finally { } finally {
setLoading(false); setLoading(false);
setRefreshing(false);
} }
} }
useEffect(() => {
loadData(); loadData();
}, []); }, []);
console.log("🔄 Render - Nombre de chantiers:", chantiers.length);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ThemedMapView <ThemedMapView
@@ -44,14 +57,24 @@ const region = {
> >
{Array.isArray(chantiers) && {Array.isArray(chantiers) &&
chantiers.map(chantier => ( chantiers.map(chantier => (
<Marker <Marker
key = {chantier.id} key = {chantier.id}
coordinate={{ latitude: chantier.latitude, longitude: chantier.longitude }} coordinate={{ latitude: chantier.latitude, longitude: chantier.longitude}}
title={chantier.adresse} title={chantier.adresse}
description={chantier.etat} description={chantier.etat}
/> />
))} ))}
</ThemedMapView> </ThemedMapView>
<ThemedButton
lvl={1}
shadow={true}
style={styles.refreshButton}
onPress={() => loadData()}
disabled={refreshing}
>
<ThemedText>Actualiser</ThemedText>
</ThemedButton>
</View> </View>
); );
}; };
@@ -64,6 +87,13 @@ const styles = StyleSheet.create({
width: Dimensions.get('window').width, width: Dimensions.get('window').width,
height: Dimensions.get('window').height, height: Dimensions.get('window').height,
}, },
refreshButton: {
position: 'absolute',
top: 50,
right: 20,
padding: 15,
borderRadius: 8,
},
}); });
export default MapScreen; export default MapScreen;

View File

@@ -1,9 +1,11 @@
import { Chantier } from "@/class/class"; import { Chantier } from "@/class/class";
import { createContext, ReactNode, useContext, useMemo, useState } from "react"; import { createContext, ReactNode, useContext, useMemo, useState } from "react";
import { getChantiers } from "@/services/ressourcesService";
type ChantierContextType = { type ChantierContextType = {
chantier: Chantier | null; chantier: Chantier | null;
setChantier: (p: Chantier | null) => void; setChantier: (p: Chantier | null) => void;
syncChantier: () => Promise<void>;
}; };
const ChantierContext = createContext<ChantierContextType | null>(null); const ChantierContext = createContext<ChantierContextType | null>(null);
@@ -14,8 +16,19 @@ type ChantierProviderProps = {
export const ChantierProvider = ({ children }: ChantierProviderProps) => { export const ChantierProvider = ({ children }: ChantierProviderProps) => {
const [chantier, setChantier] = useState<Chantier | null>(null); const [chantier, setChantier] = useState<Chantier | null>(null);
const syncChantier = async () => {
if (!chantier) return;
const value = useMemo(() => ({ chantier, setChantier }), [chantier]); const all = await getChantiers();
const updated = all.find(c => c.id === chantier.id);
if (updated) {
setChantier(updated);
}
};
const value = useMemo(() => ({ chantier, setChantier,syncChantier }), [chantier]);
return ( return (
<ChantierContext.Provider value={value}> <ChantierContext.Provider value={value}>

View File

@@ -0,0 +1,33 @@
import { Reservation } from "@/class/class";
import { createContext, ReactNode, useContext, useMemo, useState } from "react";
type ReservationContextType = {
reservations: Reservation[];
setReservations: (list: Reservation[]) => void;
};
const ReservationsContext = createContext<ReservationContextType | null>(null);
type ReservationsProviderProps = {
children: ReactNode;
};
export const ReservationsProvider = ({ children }: ReservationsProviderProps) => {
const [reservations, setReservations] = useState<Reservation[]>([]);
const value = useMemo(() => ({ reservations, setReservations }), [reservations]);
return (
<ReservationsContext.Provider value={value}>
{children}
</ReservationsContext.Provider>
);
};
export const useReservations = () => {
const context = useContext(ReservationsContext);
if (!context) {
throw new Error("useRessources doit être utilisé dans <ReservationsContext>");
}
return context;
};

View File

@@ -16,6 +16,9 @@ import { Platform, UIManager } from 'react-native';
import { ChantierProvider } from "./ContextChantier"; import { ChantierProvider } from "./ContextChantier";
import { UserProvider } from "./ContextUser"; import { UserProvider } from "./ContextUser";
import { RessourcesProvider } from "./ContextRessource"; import { RessourcesProvider } from "./ContextRessource";
import { ReservationsProvider } from "./ContextReservation";
import LoginScreen from "./login/login";
export const unstable_settings = { export const unstable_settings = {
@@ -59,18 +62,20 @@ export default function RootLayout() {
<UserProvider> <UserProvider>
<ChantierProvider> <ChantierProvider>
<RessourcesProvider> <RessourcesProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}> <ReservationsProvider>
<Stack> <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack>
<Stack.Screen name="selectChantier" options={{ headerShown: false }}/> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen <Stack.Screen name="selectChantier" options={{ headerShown: false }}/>
name="modal" <Stack.Screen
options={{ presentation: "modal", title: "Modal" }} name="modal"
/> options={{ presentation: "modal", title: "Modal" }}
<Stack.Screen name="login" options={{ headerShown: false }} /> />
</Stack> <Stack.Screen name="login" options={{ headerShown: false }} />
<StatusBar style="auto" /> </Stack>
</ThemeProvider> <StatusBar style="auto" />
</ThemeProvider>
</ReservationsProvider>
</RessourcesProvider> </RessourcesProvider>
</ChantierProvider> </ChantierProvider>
</UserProvider> </UserProvider>

View File

@@ -1,14 +1,15 @@
export type Chantier = { export type Chantier = {
name: string;
id: string; id: string;
adresse: string; adresse: string;
etat: string; etat: string;
contact: string; contact: string;
chef: User; chef: User;
equipe: User[]; equipe: Reservation[];
materiel: Ressources[]; materiel: Reservation[];
vehicules: Reservation[];
dateDep: Date; dateDep: Date;
tempsEst: number; tempsEst: number;
vehicules: Ressources[];
anomalies: string[]; anomalies: string[];
latitude: number; latitude: number;
longitude: number; longitude: number;
@@ -18,15 +19,15 @@ export type User = {
id: string; id: string;
name: string; name: string;
last_name: string; last_name: string;
allocation: Reservation[]; allocation?: Reservation[];
role: string; role: string;
qualifications: string; qualifications: string;
}; };
export type Ressources = { export type Ressources = {
id: number; id: string;
name: string; name: string;
type: string; type: string; //"Machine","Outil","Ouvrier"
Image: string; Image: string;
quantity: number; quantity: number;
available_quantity: number; available_quantity: number;
@@ -35,6 +36,7 @@ export type Ressources = {
export type Reservation = { export type Reservation = {
id: string; id: string;
dateChantier: Date; chantier: Chantier;
dateFin: Date; ressource: Ressources;
quantity: number;
}; };

31
class/utils.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { Reservation,Chantier,User, Ressources } from "./class";
export function getNbItemReservation(reservations:Reservation[]):number{
var res = 0;
reservations.forEach(reserv => {
res += reserv.quantity;
});
return res;
}
export function getReservationOfRessource(ressource:Ressources, allReservations:Reservation[]):Reservation[]{
const res:Reservation[] = [];
allReservations.forEach(reserv => {
if(reserv.ressource.name===ressource.name){
res.push(reserv);
}
});
return res;
}
export function getNbUseRessources(ressource:Ressources, allReservations:Reservation[]):number{
var res:number = 0;
getReservationOfRessource(ressource,allReservations).forEach(reserv => {
if(reserv.chantier.etat==="En cours"){
res+=reserv.quantity;
}
})
return res;
}

View File

@@ -0,0 +1,396 @@
import ChantierSummary from '@/components/chantierSummary';
import SelectChantier from '@/components/selectChantier';
import { ThemedView } from '@/components/theme/themed-view';
import React, { useEffect, useState } from 'react';
import { StyleSheet, ScrollView, Button, TextInput, Text, View, Modal } from 'react-native';
import { useChantier } from '../../app/ContextChantier';
import { useRessources } from '../../app/ContextRessource';
import { useUser } from '../../app/ContextUser';
import { getRessources, getUsers, addChantier, sendNewChantier } from '@/services/ressourcesService';
import { Chantier, Ressources, User, Reservation } from '@/class/class';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedButton } from '@/components/theme/themed-button';
import { ThemedTextInput } from '@/components/theme/themed-textinput';
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
import SelectChafChantier from '@/components/add/select/selectChefChantier';
import SelectRessource from '@/components/add/select/selectRessource';
import { db } from '@/firebase_config';
import { doc } from 'firebase/firestore';
import { useLocation } from '@/hooks/useLocation';
type RessourcesQte = [Ressources, number];
//Uniquement accessible par le RESPONSSABLE du chantier
//Pour créer ou modifier un chantier
export default function AddChantier() {
const { chantier, setChantier } = useChantier();
const { user, setUser } = useUser();
const { ressources, setRessources } = useRessources();
const { geocodeAddress} = useLocation();
const [editMode,setEditMode] = useState(false);
const [objet, setObjet] = useState('');
const [date, setDate] = useState(new Date());
const [morning, setMorning] = useState(true);
const [chefChantier, setChefChantier] = useState<User>();
const [adresse, setAdresse] = useState('');
const [duree, setDuree] = useState('');
const [contact, setContact] = useState('');
const [machines, setMachines] = useState<RessourcesQte[]>();
const [ouvriers, setOuviers] = useState<RessourcesQte[]>();
const [outils, setOutils] = useState<RessourcesQte[]>();
const [showDateSelect,setSowDateSelect] = useState(false);
const [openConfirmation,setOpenConfirmation] = useState(false);
const [userSelect, setUserSelect] = useState<string[]>([]);
const [ressourcesSelect, setRessourcesSelect] = useState<string[]>([]);
async function handleAddChantier() {
setOpenConfirmation(true);
}
const onSelectDate = (event: DateTimePickerEvent, selectedDate?: Date) => {
setSowDateSelect(false);
if (selectedDate) {
setDate(selectedDate);
}
};
async function onConfirm(): Promise<void> {
if (!isValidChantier() || !chefChantier){
alert("Choisir un chef de Chantier");
return;
}
setOpenConfirmation(false);
var latitude=0;
var longitude=0;
try { //verification de l'adresse
const coords = await geocodeAddress(adresse);
if (coords) {
latitude=coords.latitude;
longitude=coords.longitude;
}
else{
console.error("Impossible de géocoder l'adresse");
alert("Adresse introuvable. Veuillez vérifier l'adresse.");
}
} catch (error) {
console.error("Erreur lors de la création du chantier:", error);
alert("Erreur lors de la création du chantier");
}
var chantier: Chantier = {
id:"0",
name: objet,
adresse: adresse,
etat: 'En cours',
contact: contact,
chef: chefChantier,
dateDep: date,
tempsEst: Number(duree),
anomalies: [],
latitude: latitude,
longitude: longitude,
equipe: [],
materiel: [],
vehicules: []
}
if(machines){
machines.forEach(item => {
chantier.vehicules.push({
id:"0",
chantier: chantier,
ressource: item[0],
quantity: item[1],
})
});
}
if(ouvriers){
ouvriers.forEach(item => {
chantier.equipe.push({
id:"0",
chantier: chantier,
ressource: item[0],
quantity: item[1],
})
});
}
if(outils){
outils.forEach(item => {
chantier.materiel.push({
id:"0",
chantier: chantier,
ressource: item[0],
quantity: item[1],
})
});
}
sendNewChantier(chantier);
/*try {
const coords = await geocodeAddress(adresse);
if (!coords) {
console.error("Impossible de géocoder l'adresse");
alert("Adresse introuvable. Veuillez vérifier l'adresse.");
return;
}
const chantierDate = new Date(date);
chantierDate.setHours(morning ? 0 : 12, 0, 0, 0);
//CREATE NEW TYPE CHANTIER FOR FIRESTORE
const chantierFirestore = {
name: objet,
adresse,
etat: "En cours",
contact,
chef: doc(db, "user", chefChantier.id),
equipe: [],
//materiel: materiels
// ? [doc(db, "ressources", String(materiels.id))]
// : [],
vehicules: machines?.map(e =>
doc(db, "ressources", String(e[0].id))
) || [],
anomalies: [],
dateDep: chantierDate,
tempsEst: parseInt(duree) || 1,
latitude: coords.latitude,
longitude: coords.longitude,
};
const id = await addChantier(chantierFirestore as any);
if (id) {
//console.log("Chantier created with ID:", id);
setChantier({ ...chantierFirestore,
name: objet,
id,
chef: chefChantier,
equipe: [],
materiel: [],//materiels ? [materiels] : [],
vehicules: [], //data.map(([ressource]) => ressource)|| []
} as Chantier);
setOpenConfirmation(false);
}
} catch (error) {
console.error("Erreur lors de la création du chantier:", error);
alert("Erreur lors de la création du chantier");
}*/
}
function onCancel(): void {
setOpenConfirmation(false);
}
function isValidChantier(): boolean {
return objet!=="" && duree!=='' && adresse!=='' && contact!=='' && chefChantier!==undefined; //TODO
}
const renderValidationScreen = () => {
return(
<Modal transparent={true} >
<View style={styles.overlay}>
<ThemedView style={styles.overlayView}>
<ThemedText style={{fontSize: 25}}>Créer le nouveau chantier suivant ? :</ThemedText>
<ThemedView lvl={2} style={styles.summaryNewChantier}>
<ThemedText style={{fontSize: 20}}>Objet: {objet===''?"NONE":objet}</ThemedText>
<ThemedText style={{fontSize: 20}}>Date: {date.toLocaleDateString()} {morning ? "matin" : "après-midi"}</ThemedText>
<ThemedText style={{fontSize: 20}}>Durée: {duree===''?"0":duree} demi-journées</ThemedText>
<ThemedText style={{fontSize: 20}}>Adresse: {adresse===''?"NONE":adresse}</ThemedText>
<ThemedText style={{fontSize: 20}}>Contact client: {contact===''?"NONE":contact}</ThemedText>
<ThemedText style={{fontSize: 20}}>Chef de chantier: {chefChantier===undefined?"NONE":chefChantier.name}</ThemedText>
</ThemedView>
<View style={styles.overlayView}>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => onConfirm()}>
<ThemedText style={{fontSize: 25}}>Confirmer</ThemedText>
</ThemedButton>
</View>
<View style={styles.overlayView}>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => onCancel()}>
<ThemedText style={{fontSize: 25}}>Annuler</ThemedText>
</ThemedButton>
</View>
</ThemedView>
</View>
</Modal>
)
}
const renderInputDate = (name : string) => {
return (
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>{name}:</ThemedText>
<View style={{flexDirection: 'row', gap: "4%",}}>
<ThemedView style = {[styles.input,{width:"48%"}]}>
<ThemedButton>
<ThemedText onPress={() => setSowDateSelect(true)} style = {{borderRadius:10}}>{date.toLocaleDateString()}</ThemedText>
</ThemedButton>
</ThemedView>
<ThemedView style = {[styles.input,{width:"48%"}]}>
<ThemedButton>
<ThemedText onPress={() => setMorning(!morning)} style = {{borderRadius:10}}>{morning ? "matin" : "après-midi"}</ThemedText>
</ThemedButton>
</ThemedView>
{showDateSelect && (
<DateTimePicker
value={date}
mode="date" // "time" ou "datetime" selon les besoins
display="default"
onChange={onSelectDate}
/>
)}
</View>
</View>
);
};
const renderInut = (name : string, preFill : string, value : string, setValue : ((text:string) => void),numeric:boolean) => {
return (
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>{name}:</ThemedText>
<ThemedTextInput lvl = {1} style = {styles.input} placeholder={preFill} value = {value} keyboardType={numeric ? "numeric" : "default"} onChangeText={setValue} />
</View>
);
};
return (
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
{editMode &&
<View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier>
</View>
}
<ScrollView>
<View style = {styles.header}>
<ThemedText style = {styles.text}>
{editMode? "Edition d'un chantier"
:"Ajouter un nouveau chantier"}
</ThemedText>
{renderInut("Objet","Renovation",objet,setObjet,false)}
{//renderInut("Date de départ","TOTO : JOUR + Demi journé",date,setDate)
}
{renderInputDate("Date de départ")}
{renderInut("Estimation de la durée (en demi-journées)","14",duree,setDuree,true)}
{renderInut("Adresse","1 Rue de la Coutellerie, Paris",adresse,setAdresse,false)}
{renderInut("Contact client","07 01 02 03 04 05",contact,setContact,true)}
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>Chef de chantier:</ThemedText>
<SelectChafChantier style = {styles.input} sendChefChantier={setChefChantier}/>
</View>
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>Vehicules et machines:</ThemedText>
<SelectRessource style={styles.input} sendRessources={setMachines} ressourceType="Machine"/>
</View>
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>Ouvriers:</ThemedText>
<SelectRessource style={styles.input} sendRessources={setOuviers} ressourceType="Ouvrier"/>
</View>
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>Outils:</ThemedText>
<SelectRessource style={styles.input} sendRessources={setOutils} ressourceType="Outil"/>
</View>
<ThemedButton
lvl={1}
shadow={true}
style={{ padding: 15, borderRadius: 8, marginTop: 10 }}
onPress={() => handleAddChantier()}
>
<ThemedText>Valider</ThemedText>
</ThemedButton>
{openConfirmation && renderValidationScreen()}
</View>
</ScrollView>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
},
header: {
marginTop:80,
alignItems: "center",
paddingHorizontal: 20,
},
text: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
},
inputBack: {
width: "100%",
borderRadius: 10,
backgroundColor: "transparent",
},
inputLine:{
width: "100%",
//flexDirection: 'row',
paddingVertical: 5,
//alignItems: "center",
},
inputName: {
fontSize: 16,
},
input: {
width: "100%",
borderRadius: 10,
padding: 10,
fontSize: 16,
},
overlay:{
backgroundColor:'#00000080',
padding:"5%",
paddingVertical:"20%",
width:"100%",
height:"100%",
},
overlayView:{
borderRadius: 20,
padding: 20,
alignItems: "center",
width: "100%",
gap:5,
//backgroundColor:'#ff0000',
},
buttonValid:{
//borderWidth: 2,
width:'100%',
margin: 0,
borderRadius: 15,
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
summaryNewChantier:{
width:'100%',
borderRadius: 15,
padding:10,
}
});

View File

@@ -0,0 +1,237 @@
import ChantierSummary from '@/components/chantierSummary';
import SelectChantier from '@/components/selectChantier';
import SetStatus from '@/components/setStatus';
import { ThemedView } from '@/components/theme/themed-view';
import React, { useEffect, useState } from 'react';
import { StyleSheet, ScrollView, Button, TextInput, Text, View, Modal } from 'react-native';
import { useChantier } from '@/app/ContextChantier';
import { useRessources } from '@/app/ContextRessource';
import { useUser } from '@/app/ContextUser';
import { getRessources, getUsers, addChantier , addRessources} from '@/services/ressourcesService';
import { Chantier, Ressources, User } from '@/class/class';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedButton } from '@/components/theme/themed-button';
import { ThemedTextInput } from '@/components/theme/themed-textinput';
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
import SelectChafChantier from '@/components/add/select/selectChefChantier';
import SelectMachine from '@/components/selectMachine';
type Dictionary = {
[key: string]: string;
};
const exempleNom: Dictionary = {
'Outil': "Boîte à outils",
'Machine': "Bulldozer",
'Ouvrier': "Charpentier"
};
const exempleQte: Dictionary = {
'Outil': "12",
'Machine': "1",
'Ouvrier': "12"
};
type Props = {
ressourceType: 'Outil' | 'Machine' | 'Ouvrier';
};
export default function AddRessource({ressourceType, ...otherProps }: Props) {
const { ressources, setRessources } = useRessources();
const [editMode,setEditMode] = useState(false);
const [loading, setLoading] = useState(false);
const [nom, setNom] = useState('');
const [quantite, setQuantite] = useState('');
const [quantiteDisponible,setQuantiteDisponible] = useState('');
const [openConfirmation,setOpenConfirmation] = useState(false);
async function handleAddRessource() {
setLoading(true);
setOpenConfirmation(true);
}
async function onConfirm(): Promise<void> {
if(isValidRessource()){
try{
setLoading(true);
const nouvelleRessource : Ressources = {
id : '',
name: nom,
type : ressourceType,
quantity : parseInt(quantite),
available_quantity : parseInt(quantite),
Image : "",
allocation : [],
};
const id = await addRessources(nouvelleRessource);
if(id){
setRessources([...ressources,{...nouvelleRessource, id}]);
setOpenConfirmation(false);
setNom('');
setQuantite('');
setQuantiteDisponible('');
}
}catch(error){
}finally{
setOpenConfirmation(false);
setLoading(false);
}
}
}
function onCancel(): void {
setOpenConfirmation(false);
}
function isValidRessource():Boolean{
return nom!= "" && quantite != ""
}
const renderValidationScreen = () => {
return(
<Modal transparent={true} >
<View style={styles.overlay}>
<ThemedView style={styles.overlayView}>
<ThemedText style={{fontSize: 25}}>Créer la nouvelle ressource {ressourceType} suivante ? :</ThemedText>
<ThemedView lvl={2} style={styles.summaryNewChantier}>
<ThemedText style={{fontSize: 20}}>Nom: {nom===''?"NONE":nom}</ThemedText>
<ThemedText style={{fontSize: 20}}>Quantité Total: {quantite===''?"0":quantite} </ThemedText>
</ThemedView>
<View style={styles.overlayView}>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => onConfirm()}>
<ThemedText style={{fontSize: 25}}>Confirmer</ThemedText>
</ThemedButton>
</View>
<View style={styles.overlayView}>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => onCancel()}>
<ThemedText style={{fontSize: 25}}>Annuler</ThemedText>
</ThemedButton>
</View>
</ThemedView>
</View>
</Modal>
)
}
const renderInut = (name : string, preFill : string, value : string, setValue : ((text:string) => void),numeric:boolean) => {
return (
<View style = {styles.inputLine}>
<ThemedText style = {styles.inputName}>{name}:</ThemedText>
<ThemedTextInput lvl = {1} style = {styles.input} placeholder={preFill} value = {value} keyboardType={numeric ? "numeric" : "default"} onChangeText={setValue} />
</View>
);
};
return (
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
{editMode &&
<View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier>
</View>
}
<ScrollView>
<View style = {styles.header}>
<ThemedText style = {styles.text}>
{editMode? "Edition d'un chantier"
:"Ajouter une ressource " + ressourceType}
</ThemedText>
{renderInut("Nom",exempleNom[ressourceType],nom,setNom,false)}
{renderInut("Quantité Total",exempleQte[ressourceType],quantite,setQuantite,true)}
<ThemedButton
lvl={1}
shadow={true}
style={{ padding: 15, borderRadius: 8, marginTop: 10 }}
onPress={() => handleAddRessource()}
>
<ThemedText>Valider</ThemedText>
</ThemedButton>
{openConfirmation && renderValidationScreen()}
</View>
</ScrollView>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
},
header: {
marginTop:80,
alignItems: "center",
paddingHorizontal: 20,
},
text: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
},
inputBack: {
width: "100%",
borderRadius: 10,
backgroundColor: "transparent",
},
inputLine:{
width: "100%",
//flexDirection: 'row',
paddingVertical: 5,
//alignItems: "center",
},
inputName: {
fontSize: 16,
},
input: {
width: "100%",
borderRadius: 10,
padding: 10,
fontSize: 16,
},
overlay:{
backgroundColor:'#00000080',
padding:"5%",
paddingVertical:"20%",
width:"100%",
height:"100%",
},
overlayView:{
borderRadius: 20,
padding: 20,
alignItems: "center",
width: "100%",
gap:5,
//backgroundColor:'#ff0000',
},
buttonValid:{
//borderWidth: 2,
width:'100%',
margin: 0,
borderRadius: 15,
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
summaryNewChantier:{
width:'100%',
borderRadius: 15,
padding:10,
}
});

View File

@@ -0,0 +1,79 @@
import { Chantier, Ressources } from '@/class/class';
import { ThemedView, } from '@/components/theme/themed-view';
import React, { useEffect, useState } from 'react';
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedButton } from '@/components/theme/themed-button';
import { getNbUseRessources } from '@/class/utils';
import { useReservations } from '@/app/ContextReservation';
import { getReservations } from '@/services/ressourcesService';
type RessourcesQte = [Ressources, number];
type Props = {
ressource:Ressources;
qte:number;
sendRessource: (ressource: RessourcesQte) => void;
style?: StyleProp<ViewStyle>;
};
export default function RessourceSummary({ressource: ressource,qte,style,sendRessource: sendRessource, ...otherProps }: Props) {
const { reservations, setReservations } = useReservations();
const [count,setCount] = useState(qte);
function onPressAdd(ressource: Ressources): void {
if(count<ressource.quantity-getNbUseRessources(ressource,reservations)){
setCount(count+1);
sendRessource([ressource, count+1]);
}
}
function onPressSub(ressource: Ressources): void {
if(count>0){
setCount(count-1);
sendRessource([ressource, count-1]);
}
}
useEffect(() => {
async function loadReservations() {
const list = await getReservations();
setReservations(list);
}
loadReservations();
}, []);
return(
<View style={style}>
<ThemedView lvl={2} border={3} style={{padding:10,width:"100%",borderRadius:10,flexDirection: 'row',justifyContent: 'space-between',}}>
<View>
<ThemedText>Nom : {ressource.name}</ThemedText>
<ThemedText>Restant : {ressource.quantity-getNbUseRessources(ressource,reservations)}/{ressource.quantity}</ThemedText>
</View>
<View style={{alignItems:"center"}}>
<ThemedButton style={styles.button} lvl={3} onPress={() => onPressAdd(ressource)}>
<ThemedText>+</ThemedText>
</ThemedButton>
<ThemedText>{count}/{ressource.quantity-getNbUseRessources(ressource,reservations)}</ThemedText>
<ThemedButton style={styles.button} lvl={3} onPress={() => onPressSub(ressource)}>
<ThemedText>-</ThemedText>
</ThemedButton>
</View>
</ThemedView>
</View>
)
}
const styles = StyleSheet.create({
button:{
padding:5,
marginHorizontal:10,
alignItems:"center",
borderRadius: 20,
width: 40,
height: 40,
},
});

View File

@@ -0,0 +1,156 @@
import { useChantier } from '@/app/ContextChantier';
import { changeChantierStatus } from "@/services/ressourcesService";
import { useEffect, useState } from 'react';
import { Dimensions, FlatList, LayoutAnimation, Modal, Pressable, ScrollView, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import Animated, { LinearTransition } from 'react-native-reanimated';
import { ThemedButton } from '../../theme/themed-button';
import { ThemedText } from '../../theme/themed-text';
import { ThemedView } from "../../theme/themed-view";
import { User } from '@/class/class';
import { getUsers } from "@/services/ressourcesService";
const { width, height } = Dimensions.get("window");
type Props = {
sendChefChantier: (user: User) => void;
style?: StyleProp<ViewStyle>;
};
export default function SelectChafChantier({style,sendChefChantier , ...otherProps }: Props) {
const [chefDeChantier, setChefDeChantier] = useState<User>();
const [isOpen,setIsOpen] = useState(false);
const [users,setUsers] = useState<User[]>([]);
const AnimatedThemedView = Animated.createAnimatedComponent(ThemedView);
useEffect(() => {
async function loadData() {
try {
const data = await getUsers();//TODO chef de chantier uniquement
const chefs = data.filter(user => user.role === "chef");
setUsers(chefs);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadData();
}, []);
function onPressOpen(): void {
setIsOpen(!isOpen);
}
function onPressUser(user: User): void{
sendChefChantier(user);
setChefDeChantier(user);
setIsOpen(false);
}
const chefChantierSummary = ({ item }: { item: User }) => {
if (!item) return null;
return(
<View style={{padding:10,width:"100%"}}>
<ThemedButton lvl={2} style={{padding:10,width:"100%",borderRadius:10}} onPress={() => {onPressUser(item)}}>
<ThemedText>{item.id}</ThemedText>
<ThemedText>{item.name}</ThemedText>
<ThemedText>{item.last_name}</ThemedText>
<ThemedText>{item.role}</ThemedText>
</ThemedButton>
</View>
)
}
const chefChantierSearch = () => {
return(
<Modal transparent={true}>
<View style={styles.overlay}>
<ThemedView style={styles.overlayView}>
<ThemedText style={{fontSize: 25}}>Rechercher un chef de chantier :</ThemedText>
<FlatList
style={{width:"100%"}}
data={users}
renderItem={chefChantierSummary}
keyExtractor={(_, index) => index.toString()}
/>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => setIsOpen(false)}>
<ThemedText style={{fontSize: 25}}>Annuler</ThemedText>
</ThemedButton>
</ThemedView>
</View>
</Modal>
)
}
return(
<ThemedButton style={style} lvl={1} onPress={() => onPressOpen()}>
<ThemedText style={styles.centeredText}>{chefDeChantier?chefDeChantier.name+" "+chefDeChantier.last_name:"selectionner un chef de chantier"}</ThemedText>
{isOpen && chefChantierSearch()}
</ThemedButton>
)
}
const styles = StyleSheet.create({
windowBox:{
zIndex: 2,
//backgroundColor: '#00FFFF40',
width:"100%",
padding: 10,
paddingLeft: 0,
//overflow: 'hidden',
},
window:{
borderRadius:15,
//backgroundColor: '#00FF00',
overflow: 'hidden',
position: 'relative',
},
autoClose: {
position: 'absolute',
top: -height,
left: -width,
width:width*2,
height:height*2,
//backgroundColor: 'rgba(255, 0, 0, 0.5)',
},
button:{
width:'100%',
margin: 0,
borderRadius: 15,
padding: 10,
height:40,
justifyContent: 'center',
},
centeredText:{
textAlign: 'center',
},
overlay:{
backgroundColor:'#00000080',
padding:"5%",
paddingVertical:"20%",
width:"100%",
height:"100%",
},
overlayView:{
borderRadius: 20,
padding: 20,
alignItems: "center",
width: "100%",
height: "100%",
//backgroundColor:'#ff0000',
},
buttonValid:{
//borderWidth: 2,
width:'100%',
margin: 0,
borderRadius: 15,
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
});

View File

@@ -0,0 +1,154 @@
import { useChantier } from '@/app/ContextChantier';
import { changeChantierStatus } from "@/services/ressourcesService";
import { useEffect, useState } from 'react';
import { Dimensions, FlatList, LayoutAnimation, Modal, Pressable, ScrollView, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import Animated, { LinearTransition } from 'react-native-reanimated';
import { ThemedButton } from '@/components/theme/themed-button';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from "@/components/theme/themed-view";
import { Ressources, User } from '@/class/class';
import { getRessources } from "@/services/ressourcesService";
import RessourceSummary from '@/components/add/select/ressourceSummary';
const { width, height } = Dimensions.get("window");
type RessourcesQte = [Ressources, number];
type Props = {
ressourceType: string;
sendRessources: (ressource: RessourcesQte[]) => void;
style?: StyleProp<ViewStyle>;
};
export default function SelectRessource({style,ressourceType,sendRessources: sendRessources , ...otherProps }: Props) {
const [ressources, setRessources] = useState<RessourcesQte[]>([]);
const [isOpen,setIsOpen] = useState(false);
const [listRessource,setListRessource] = useState<Ressources[]>([]);
useEffect(() => {
async function loadData() {
try {
const data = await getRessources();
const ressources = data.filter(user => user.type === ressourceType);
setListRessource(ressources);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadData();
}, []);
useEffect(() => {
sendRessources(ressources);
}, [ressources])
function onPressOpen(): void {
setIsOpen(!isOpen);
}
function getTotalRessource(): number{
var total = 0;
ressources.forEach(element => {
total += element[1]
});
return total;
}
function addRessource(ressource: RessourcesQte): void{
if(ressource[1]>0){
setRessources(prev =>
prev.some(i => i[0].name === ressource[0].name)
? prev.map(i =>
i[0].name === ressource[0].name
? [i[0], ressource[1]]
: i
)
: [...prev, ressource]
);
}
else{
setRessources(prev => prev.filter(item => item[0].name !== ressource[0].name));
}
}
const RessourceSummaryItem = ({ item }: { item: Ressources }) => {
if (!item) return null;
const ressourceQte = ressources.find(([r]) => r.name === item.name);
const qte = ressourceQte? ressourceQte[1]:0;
return(
<RessourceSummary style={{padding:10,width:"100%"}} ressource={item} qte={qte} sendRessource={addRessource}></RessourceSummary>
)
}
const RessourceSearch = () => {
return(
<Modal transparent={true}>
<View style={styles.overlay}>
<ThemedView style={styles.overlayView}>
<ThemedText style={{fontSize: 20}}>{"Rechercher des "+ressourceType+"s :"}</ThemedText>
<FlatList
style={{width:"100%"}}
data={listRessource}
renderItem={RessourceSummaryItem}
keyExtractor={(_, index) => index.toString()}
/>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => setIsOpen(false)}>
<ThemedText style={{fontSize: 25}}>Valider</ThemedText>
</ThemedButton>
</ThemedView>
</View>
</Modal>
)
}
return(
<ThemedButton style={style} lvl={1} onPress={() => onPressOpen()}>
<ThemedText style={styles.centeredText}>{ressources?getTotalRessource()+" "+ressourceType+(getTotalRessource()>1?"s":"")+", "+ ressources.length+" type"+(ressources.length>1?"s":""):"Selectionner des "+ressourceType+"s"}</ThemedText>
{isOpen && RessourceSearch()}
</ThemedButton>
)
}
const styles = StyleSheet.create({
centeredText:{
textAlign: 'center',
},
overlay:{
backgroundColor:'#00000080',
padding:"5%",
paddingVertical:"20%",
width:"100%",
height:"100%",
},
overlayView:{
borderRadius: 20,
padding: 20,
alignItems: "center",
width: "100%",
height: "100%",
//backgroundColor:'#ff0000',
},
buttonValid:{
//borderWidth: 2,
width:'100%',
margin: 0,
borderRadius: 15,
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
});

View File

@@ -1,9 +1,11 @@
import { Chantier } from '@/class/class'; import { Chantier } from '@/class/class';
import { ThemedView, } from '@/components/theme/themed-view'; import { ThemedView } from '@/components/theme/themed-view';
import React from 'react'; import React, { use, useEffect, useState } from 'react';
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { TouchableOpacity, StyleProp, StyleSheet, View, Image, ViewStyle,Text, TextInput, ScrollView } from 'react-native';
import { ThemedText } from './theme/themed-text'; import { ThemedText } from './theme/themed-text';
import { deleteAnomalie, addAnomalie, getChantiers } from '@/services/ressourcesService';
import { useChantier } from '@/app/ContextChantier';
import * as ImagePicker from 'expo-image-picker';
type Props = { type Props = {
data: { data: {
@@ -12,22 +14,82 @@ type Props = {
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
}; };
export default function Anomaly({data,style , ...otherProps }: Props) { export default function Anomaly({data,style}: Props) {
const{syncChantier }= useChantier();
const [imageUri, setImageUri] = useState<string | null>(null);
const handleDelete = async (anomaly: string) => {
await deleteAnomalie(data.chantier!.id, anomaly);
data.chantier!.anomalies = data.chantier!.anomalies.filter(a => a !== anomaly);
await syncChantier();
};
const handleAdd = async () => {
if (newAnomaly.trim().length === 0) return;
await addAnomalie(data.chantier!.id, newAnomaly.trim());
data.chantier!.anomalies.push(newAnomaly.trim());
setNewAnomaly("");
await syncChantier();
};
//DEBUG
useEffect(() => {
console.log("imageUri changed", imageUri);
}, [imageUri]);
// Image picker function (not used currently)
const selectImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (!result.canceled) {
console.log(result);
setImageUri(result.assets[0].uri.replace('file://', ''));
} else {
alert('You did not select any image.');
}
};
const [newAnomaly, setNewAnomaly] = useState("");
return( return(
<View style={style}> <View style={style}>
{data.chantier ? ( {data.chantier ? (
<ThemedView lvl={4} style={styles.anomaliesContainer}> <ThemedView lvl={4} style={styles.anomaliesContainer}>
<ThemedText style={styles.anomaliesTitle}>Anomalies</ThemedText> <ThemedText style={styles.anomaliesTitle}>Anomalies</ThemedText>
{data.chantier.anomalies.length > 0 ? ( {/* Add Anomaly Section */}
data.chantier.anomalies.map((anomaly, index) => ( <View style={styles.addContainer}>
<ThemedView key={index} lvl={2} style={styles.anomalyItem}> <TextInput style={styles.input} placeholder="Nouvelle anomalie..." value={newAnomaly} onChangeText={setNewAnomaly} />
<ThemedText> {anomaly}</ThemedText> <TouchableOpacity style={styles.addButton} onPress={handleAdd}>
</ThemedView> <ThemedText style={styles.addButtonText}>Ajouter</ThemedText>
)) </TouchableOpacity>
) : ( <TouchableOpacity onPress={selectImage} style={styles.addButton}>
<ThemedText style={styles.noAnomaly}>Aucune anomalie</ThemedText> <ThemedText style={styles.addButton}>Choisir une image</ThemedText>
) } </TouchableOpacity>
{imageUri && (
<Image source={{ uri: imageUri }} style={styles.image} />
)}
</View>
{data.chantier.anomalies.length > 0 ? (
data.chantier.anomalies.map((anomaly, index) => (
<ThemedView key={index} lvl={2} style={styles.anomalyItem}>
<ThemedText style={styles.anomalyText}> {anomaly}</ThemedText>
<TouchableOpacity onPress={() => handleDelete(anomaly)} style={styles.deleteButton}>
<Text style={styles.deleteText}></Text>
</TouchableOpacity>
</ThemedView>
))
) : (
<ThemedText style={styles.noAnomaly}>Aucune anomalie</ThemedText>
) }
</ThemedView> </ThemedView>
): null} ): null}
</View> </View>
@@ -39,7 +101,6 @@ const styles = StyleSheet.create({
anomaliesContainer: { anomaliesContainer: {
padding: 5, padding: 5,
borderRadius: 10, borderRadius: 10,
height: 150,
}, },
anomaliesTitle: { anomaliesTitle: {
fontSize: 16, fontSize: 16,
@@ -47,12 +108,55 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
}, },
anomalyItem: { anomalyItem: {
flexDirection: "row",
alignItems: "flex-start",
padding: 8, padding: 8,
marginBottom: 5, marginBottom: 5,
borderRadius: 8, borderRadius: 8,
}, },
anomalyText: {
flex: 1,
marginLeft: 5,
},
noAnomaly: { noAnomaly: {
fontStyle: "italic", fontStyle: "italic",
opacity: 0.7, opacity: 0.7,
}, },
}); //delete button styles
deleteButton: {
backgroundColor: "red",
width: 24,
height: 24,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
},
deleteText: {
color: "white",
fontWeight: "bold",
fontSize: 14,
lineHeight: 14,
},
//add anomaly styles
addContainer: {
marginLeft: 2,
flexDirection: "row",
alignItems: "center"
},
input: {
flex: 1,
backgroundColor: "white",
padding: 8,
borderRadius: 8,
marginRight: 8,
},
addButton: {
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 8,
},
addButtonText: {
fontWeight: "bold",
},
image: { width: 200, height: 200, borderRadius: 10 }
})

View File

@@ -3,6 +3,7 @@ import { ThemedView, } from '@/components/theme/themed-view';
import React from 'react'; import React from 'react';
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { ThemedText } from './theme/themed-text'; import { ThemedText } from './theme/themed-text';
import { getNbItemReservation } from '@/class/utils';
type Props = { type Props = {
data: { data: {
@@ -17,12 +18,23 @@ export default function ChantierSummary({data,style , ...otherProps }: Props) {
{data.chantier ? ( {data.chantier ? (
<ThemedView lvl={4} style={styles.chantier}> <ThemedView lvl={4} style={styles.chantier}>
<View> <View>
<Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=69392bb2&is=6937da32&hm=dcc09e76d3dca89d2418947b46efbd38673b9dc559027724b2e51d493b173bc9&" /*chantier.urlImg*/ }} style={styles.image} /> <Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=693f1a72&is=693dc8f2&hm=86ffb97145fc8d3aec822b87d99be233c98477d4424c1ef58f80eb81b17c7c80&" /*chantier.urlImg*/ }} style={styles.image} />
</View> </View>
<View> <View style={{flex: 1}}>
<ThemedText>Adresse: {data.chantier.adresse}</ThemedText> <ThemedText selectable={true}>Id: {data.chantier.id}</ThemedText>
<ThemedText>Chef de chantier: {data.chantier.chef.last_name}{" "}{data.chantier.chef.name}</ThemedText> <ThemedText selectable={true}>Objet: {data.chantier.name}</ThemedText>
<ThemedText>État: {data.chantier.etat}</ThemedText> <ThemedText selectable={true}>Adresse: {data.chantier.adresse}</ThemedText>
<ThemedText selectable={true}>Chef de chantier: {data.chantier.chef.last_name}{" "}{data.chantier.chef.name}</ThemedText>
<ThemedText selectable={true}>État: {data.chantier.etat}</ThemedText>
<ThemedText selectable={true}>equipe:
{getNbItemReservation(data.chantier.equipe)} ({data.chantier.equipe.length} type{data.chantier.equipe.length>1&&"s"})
</ThemedText>
<ThemedText selectable={true}>materiel:
{getNbItemReservation(data.chantier.materiel)} ({data.chantier.materiel.length} type{data.chantier.materiel.length>1&&"s"})
</ThemedText>
<ThemedText selectable={true}>vehicules:
{getNbItemReservation(data.chantier.vehicules)} ({data.chantier.vehicules.length} type{data.chantier.vehicules.length>1&&"s"})
</ThemedText>
</View> </View>
</ThemedView> </ThemedView>
) : ) :
@@ -42,13 +54,12 @@ const styles = StyleSheet.create({
borderRadius: 10, borderRadius: 10,
//borderWidth: 1, //borderWidth: 1,
flexDirection: 'row', flexDirection: 'row',
height: 150, //height: 150,
gap: 10,
}, },
image:{ image:{
margin:0,
width: 70, width: 70,
height: 140, height: 140,
borderRadius: 5, borderRadius: 5,
marginRight: 10,
}, },
}); });

View File

@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
FlatList,
Image, Image,
Pressable, Pressable,
ScrollView, ScrollView,
@@ -13,7 +14,11 @@ import {
View View
} from "react-native"; } from "react-native";
import Animated, { import Animated, {
LinearTransition interpolate,
LinearTransition,
useAnimatedStyle,
useSharedValue,
withTiming
} from "react-native-reanimated"; } from "react-native-reanimated";
import { ThemedButton } from "@/components/theme/themed-button"; import { ThemedButton } from "@/components/theme/themed-button";
import { ThemedText } from "@/components/theme/themed-text"; import { ThemedText } from "@/components/theme/themed-text";
@@ -56,36 +61,71 @@ export default function SelectChantier() {
} }
} }
function onPressAddChantier(){ useEffect(() => {
router.push("/(tabs)/addChantier") open.value = withTiming(isOpen ? 1 : 0);
setIsOpen(false) }, [isOpen]);
}
useEffect(() => { /*useEffect(() => {
async function loadChantiers() { async function loadChantiers() {
const list = await getChantiers(); const list = await getChantiers();
setChantiers(list); setChantiers(list);
} }
loadChantiers(); loadChantiers();
}, []); }, []);*/
const filteredChantiers = chantiers.filter((chantier) => {
var keyWords:string[] = search.toLowerCase().split(" ") ;
var containsAllKeyWord:boolean = true;
keyWords.forEach(keyWord => {
containsAllKeyWord = containsAllKeyWord && (chantier.adresse.toLowerCase().includes(keyWord) || chantier.name.toLowerCase().includes(keyWord))
});
return containsAllKeyWord
});
function selectChantier(chantier: Chantier): void { function selectChantier(chantier: Chantier): void {
setChantier(chantier); setChantier(chantier);
setIsOpen(false); setIsOpen(false);
} }
const renderChantier = (chantier: Chantier, index: number) => {
const open = useSharedValue(0);
const animatedWindowStyle = useAnimatedStyle(() => {
return {
width: `${interpolate(open.value, [0, 1], [50, 100])}%`,
height: interpolate(
open.value,
[0, 1],
[60, screenHeight * 0.75]
),
padding: 10,
overflow: "hidden",
zIndex: 1000,
};
});
const animatedButtonStyle = useAnimatedStyle(() => ({
width: `${interpolate(open.value, [0, 1], [100, 50])}%`,
margin: interpolate(open.value, [0, 1], [0, 5]),
borderRadius: interpolate(open.value, [0, 1], [15, 10]),
padding: 10,
height: 40,
}));
const renderChantier = ({ item }: { item:Chantier }) => {
return ( return (
<Pressable key={index} onPress={() => selectChantier(chantier)}> <Pressable onPress={() => selectChantier(item)}>
<ThemedView lvl={4} style={styles.chantier}> <ThemedView lvl={4} style={styles.chantier}>
<View> <View>
<Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=69392bb2&is=6937da32&hm=dcc09e76d3dca89d2418947b46efbd38673b9dc559027724b2e51d493b173bc9&" /*chantier.urlImg*/ }} style={styles.image} /> <Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=693f1a72&is=693dc8f2&hm=86ffb97145fc8d3aec822b87d99be233c98477d4424c1ef58f80eb81b17c7c80&" /*chantier.urlImg*/ }} style={styles.image} />
</View> </View>
<View> <View style={{flex: 1}}>
<ThemedText>Adresse: {chantier.adresse}</ThemedText> <ThemedText>Renovation: {item.name}</ThemedText>
<ThemedText>Chef de chantier: {chantier.chef.last_name}{" "}{chantier.chef.name}</ThemedText> <ThemedText>Adresse: {item.adresse}</ThemedText>
<ThemedText>État: {chantier.etat}</ThemedText> <ThemedText>Chef de chantier: {item.chef.last_name}{" "}{item.chef.name}</ThemedText>
<ThemedText>État: {item.etat}</ThemedText>
</View> </View>
</ThemedView> </ThemedView>
</Pressable> </Pressable>
@@ -93,10 +133,10 @@ export default function SelectChantier() {
}; };
return ( return (
<Animated.View layout={LinearTransition.duration(200)} style={isOpen ? styles.windowOpean : styles.windowClose}> <Animated.View style={animatedWindowStyle}>
{isOpen && (<Pressable style={styles.autoClose} onPress={() => setIsOpen(false)}/>)} {isOpen && (<Pressable style={styles.autoClose} onPress={() => setIsOpen(false)}/>)}
<AnimatedThemedView layout={LinearTransition.duration(200)} lvl={2} shadow={true} style={styles.window}> <ThemedView lvl={2} shadow={true} style={styles.window}>
<AnimatedThemedButton style={isOpen ? styles.buttonOpen : styles.buttonClose} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}> <AnimatedThemedButton style={animatedButtonStyle} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}>
<ThemedText style={styles.buttonText}> <ThemedText style={styles.buttonText}>
{isOpen ? "Fermer" : (chantier!=null ? chantier.adresse : "Chantier")} {isOpen ? "Fermer" : (chantier!=null ? chantier.adresse : "Chantier")}
</ThemedText> </ThemedText>
@@ -105,29 +145,28 @@ export default function SelectChantier() {
<View style={styles.menu}> <View style={styles.menu}>
<ThemedTextInput lvl={1} border={4} style={styles.input} placeholder="Rechercher un chantier" value={search} onChangeText={setSearch}/> <ThemedTextInput lvl={1} border={4} style={styles.input} placeholder="Rechercher un chantier" value={search} onChangeText={setSearch}/>
<ThemedButton style={styles.buttonAdd} onPress={() => onPressAddChantier()}>
<ThemedText style={styles.buttonText}>
+
</ThemedText>
</ThemedButton>
<View style={styles.list}> <View style={styles.list}>
{isLoaded? {isLoaded?
<ScrollView contentContainerStyle={styles.chantiersList}> <FlatList
{chantiers.map((chantier, index) => data={filteredChantiers}
renderChantier(chantier, index) renderItem={renderChantier}
)} keyExtractor={(_, index) => index.toString()}
</ScrollView> contentContainerStyle={{ gap: 8 }}
/>
: <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />} : <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />}
</View> </View>
</View> </View>
)} )}
</AnimatedThemedView> </ThemedView>
</Animated.View> </Animated.View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
windowClose: { windowClose: {
//backgroundColor: '#00FF0040', //backgroundColor: '#00FF0040',
@@ -186,7 +225,7 @@ const styles = StyleSheet.create({
borderRadius: 10, borderRadius: 10,
//borderWidth: 1, //borderWidth: 1,
flexDirection: 'row', flexDirection: 'row',
height: 100, //height: 130,
}, },
image:{ image:{
margin:0, margin:0,
@@ -220,11 +259,4 @@ const styles = StyleSheet.create({
buttonText: { buttonText: {
textAlign: "center", textAlign: "center",
}, },
buttonAdd:{
borderRadius: 10,
marginBottom: 10,
height: 30,
alignItems: 'center',
justifyContent: 'center',
}
}); });

View File

@@ -0,0 +1,151 @@
import { useChantier } from '@/app/ContextChantier';
import { changeChantierStatus } from "@/services/ressourcesService";
import { useEffect, useState } from 'react';
import { Dimensions, FlatList, LayoutAnimation, Modal, Pressable, ScrollView, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import Animated, { LinearTransition } from 'react-native-reanimated';
import { ThemedButton } from './theme/themed-button';
import { ThemedText } from './theme/themed-text';
import { ThemedView } from "./theme/themed-view";
import { Ressources, User } from '@/class/class';
import { getRessources } from "@/services/ressourcesService";
import RessourceSummary from '@/components/add/select/ressourceSummary';
type RessourcesQte = [Ressources, number];
type Props = {
sendMachines: (machine: RessourcesQte[]) => void;
style?: StyleProp<ViewStyle>;
};
export default function SelectMachine({style,sendMachines: sendMachines , ...otherProps }: Props) {
const [machines, setMachines] = useState<RessourcesQte[]>([]);
const [isOpen,setIsOpen] = useState(false);
const [listMachines,setListMachines] = useState<Ressources[]>([]);
useEffect(() => {
async function loadData() {
try {
const data = await getRessources();
const ressources = data.filter(user => user.type === "Machine");
setListMachines(ressources);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadData();
}, []);
useEffect(() => {
sendMachines(machines);
}, [machines])
function onPressOpen(): void {
setIsOpen(!isOpen);
}
function getTotalMachine(): number{
var total = 0;
machines.forEach(element => {
total += element[1]
});
return total;
}
function addMachine(machine: RessourcesQte): void{
if(machine[1]>0){
setMachines(prev =>
prev.some(i => i[0].name === machine[0].name)
? prev.map(i =>
i[0].name === machine[0].name
? [i[0], machine[1]]
: i
)
: [...prev, machine]
);
}
else{
setMachines(prev => prev.filter(item => item[0].name !== machine[0].name));
}
}
const MachineSummaryItem = ({ item }: { item: Ressources }) => {
if (!item) return null;
const machineQte = machines.find(([r]) => r.name === item.name);
const qte = machineQte? machineQte[1]:0;
return(
<RessourceSummary style={{padding:10,width:"100%"}} ressource={item} qte={qte} sendRessource={addMachine}></RessourceSummary>
)
}
const MachineSearch = () => {
return(
<Modal transparent={true}>
<View style={styles.overlay}>
<ThemedView style={styles.overlayView}>
<ThemedText style={{fontSize: 25}}>Rechercher des machines :</ThemedText>
<FlatList
style={{width:"100%"}}
data={listMachines}
renderItem={MachineSummaryItem}
keyExtractor={(_, index) => index.toString()}
/>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => setIsOpen(false)}>
<ThemedText style={{fontSize: 25}}>Valider</ThemedText>
</ThemedButton>
</ThemedView>
</View>
</Modal>
)
}
return(
<ThemedButton style={style} lvl={1} onPress={() => onPressOpen()}>
<ThemedText style={styles.centeredText}>{machines? getTotalMachine()+" machine(s) sélectionné dont "+ machines.length+" différente":"sélectionner des machine(s)"}</ThemedText>
{isOpen && MachineSearch()}
</ThemedButton>
)
}
const styles = StyleSheet.create({
centeredText:{
textAlign: 'center',
},
overlay:{
backgroundColor:'#00000080',
padding:"5%",
paddingVertical:"20%",
width:"100%",
height:"100%",
},
overlayView:{
borderRadius: 20,
padding: 20,
alignItems: "center",
width: "100%",
height: "100%",
//backgroundColor:'#ff0000',
},
buttonValid:{
//borderWidth: 2,
width:'100%',
margin: 0,
borderRadius: 15,
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
});

View File

@@ -6,23 +6,32 @@ export type ThemedPressableProps = PressableProps & {
lightColor?: string; lightColor?: string;
darkColor?: string; darkColor?: string;
lvl?:number; lvl?:number;
lvlPressed?:number;
border?:number; border?:number;
opacity?:string; opacity?:string;
shadow?: boolean; shadow?: boolean;
}; };
//nb : pour border ne pas oublier de mettre en plus "borderWidth" dans le style du composant /!\ //nb : pour border ne pas oublier de mettre en plus "borderWidth" dans le style du composant /!\
export function ThemedButton({ style, lightColor, darkColor,lvl=1,border=-1,opacity="FF",shadow=false, ...otherProps }: ThemedPressableProps) { export function ThemedButton({ style, lightColor, darkColor,lvl=1,lvlPressed=1,border=-1,opacity="FF",shadow=false, ...otherProps }: ThemedPressableProps) {
var lvlStr:string = "background"; var lvlStr:string = "background";
var lvlStrPressed:string = "background";
var borderColor =""; var borderColor ="";
var borderWidth = 0; var borderWidth = 0;
if(lvl>=0 && lvl<6){ if(lvl>=0 && lvl<6){
lvlStr+=lvl; lvlStr+=lvl;
} }
else lvlStr+='5'; else lvlStr+='5';
if((lvl+lvlPressed)>=0 && (lvl+lvlPressed)<6){
lvlStrPressed+=(lvl+lvlPressed);
}
else lvlStrPressed+='0';
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor },lvlStr as 'background0'|'background1'|'background2'|'background3'|'background4'|'background5')+opacity; const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor },lvlStr as 'background0'|'background1'|'background2'|'background3'|'background4'|'background5')+opacity;
const backgroundPressed = useThemeColor({ light: lightColor, dark: darkColor },lvlStrPressed as 'background0'|'background1'|'background2'|'background3'|'background4'|'background5')+opacity;
if(border!=-1){ if(border!=-1){
var borderStr = ""; var borderStr = "";
@@ -46,5 +55,9 @@ export function ThemedButton({ style, lightColor, darkColor,lvl=1,border=-1,opac
shadowRadius: 6, shadowRadius: 6,
} }
return <Pressable style={(state) =>[{ backgroundColor, borderColor, borderWidth }, shadow && shadowStyle, typeof style === 'function' ? style(state) : style,]} {...otherProps}/>; return <Pressable style={(state) =>[{
backgroundColor: state.pressed ? backgroundPressed: backgroundColor,
borderColor,
borderWidth
}, shadow && shadowStyle, typeof style === 'function' ? style(state) : style,]} {...otherProps}/>;
} }

View File

@@ -1,45 +0,0 @@
[
{
"group": "Bernard DupYEEd",
"date":"Rennes",
"nationality": "French",
"location": "PlombYEEr",
"price": 20,
"ticketsLeft": 36,
"Image": "https://media.discordapp.net/attachments/1415267028201246812/1424825038657425518/a06e3304-86ca-4b4f-8016-c4ae9844b0df.png?ex=68e9f879&is=68e8a6f9&hm=b6ff1f540d5c382930b56bd6f90565f517ee179347d6ee6aebd5254b10cf4c88&=&format=webp&quality=lossless&width=579&height=579",
"favorite": false
},
{
"group": "MYEEchel Câble",
"date":"Nantes",
"nationality": "French",
"location": "ElectrYEEcien",
"price": 22,
"ticketsLeft": 400,
"Image": "https://media.discordapp.net/attachments/1415267028201246812/1424826240090509332/7fdbfe06-8300-441e-81ac-87851d004dc3.png?ex=68e9f997&is=68e8a817&hm=cc71621c3e7c3c1aaeda5555e9dd4204d43414cd0332c2b116b10d009b68df3c&=&format=webp&quality=lossless&width=579&height=579",
"favorite": false
},
{
"group": "PYEErre soulever",
"date":"Redon",
"nationality": "French",
"location": "GrutYEEr",
"price": 32,
"ticketsLeft": 0,
"Image": "https://media.discordapp.net/attachments/1425108443571945644/1427207643180826757/raw.png?ex=68ee0632&is=68ecb4b2&hm=1efc51065c6abfb1af75b8382f9924c2eb177c7d7672f7ed9837e96ef3076d16&=&format=webp&quality=lossless&width=233&height=350",
"favorite": false
},
{
"group": "Greg NegatYEEf",
"date":"Pacé",
"nationality": "French",
"location": "ElectrYEEcien",
"price": 20,
"ticketsLeft": 36,
"Image": "https://media.discordapp.net/attachments/1415267028201246812/1424826240090509332/7fdbfe06-8300-441e-81ac-87851d004dc3.png?ex=68e9f997&is=68e8a817&hm=cc71621c3e7c3c1aaeda5555e9dd4204d43414cd0332c2b116b10d009b68df3c&=&format=webp&quality=lossless&width=579&height=579",
"favorite": true
}
]

50
hooks/useLocation.tsx Normal file
View File

@@ -0,0 +1,50 @@
import { useState } from 'react';
import * as Location from 'expo-location';
export const useLocation = () => {
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [latitude, setLatitude] = useState<number | null>(null);
const [longitude, setLongitude] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const geocodeAddress = async (address: string) => {
try {
setLoading(true);
setErrorMsg(null);
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
throw new Error('Permission localisation refusée');
}
const result = await Location.geocodeAsync(address);
if (!result || result.length === 0) {
throw new Error("Adresse introuvable");
}
const { latitude, longitude } = result[0];
setLatitude(latitude);
setLongitude(longitude);
return { latitude, longitude };
} catch (error: any) {
console.error(error);
setErrorMsg("Impossible de localiser cette adresse");
return null;
} finally {
setLoading(false);
}
};
return {
latitude,
longitude,
errorMsg,
loading,
geocodeAddress,
};
};

99
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@react-native-community/datetimepicker": "^8.5.1",
"@react-navigation/bottom-tabs": "^7.8.12", "@react-navigation/bottom-tabs": "^7.8.12",
"@react-navigation/elements": "^2.6.3", "@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.25", "@react-navigation/native": "^7.1.25",
@@ -17,7 +18,9 @@
"expo-font": "~14.0.9", "expo-font": "~14.0.9",
"expo-haptics": "~15.0.7", "expo-haptics": "~15.0.7",
"expo-image": "~3.0.9", "expo-image": "~3.0.9",
"expo-image-picker": "~17.0.10",
"expo-linking": "~8.0.8", "expo-linking": "~8.0.8",
"expo-location": "~19.0.8",
"expo-router": "~6.0.11", "expo-router": "~6.0.11",
"expo-splash-screen": "~31.0.10", "expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8", "expo-status-bar": "~3.0.8",
@@ -29,6 +32,7 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-native": "0.81.4", "react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.9.0", "react-native-maps": "1.9.0",
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
@@ -86,7 +90,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
@@ -1468,7 +1471,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -2327,7 +2329,6 @@
"version": "0.14.6", "version": "0.14.6",
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz",
"integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@@ -2393,7 +2394,6 @@
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz",
"integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/app": "0.14.6", "@firebase/app": "0.14.6",
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
@@ -2409,8 +2409,7 @@
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
"license": "Apache-2.0", "license": "Apache-2.0"
"peer": true
}, },
"node_modules/@firebase/auth": { "node_modules/@firebase/auth": {
"version": "1.11.1", "version": "1.11.1",
@@ -2860,7 +2859,6 @@
"integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
@@ -3579,6 +3577,29 @@
} }
} }
}, },
"node_modules/@react-native-community/datetimepicker": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.5.1.tgz",
"integrity": "sha512-TuwM1ORbxCjOp1GOtONj0QnpDpVfq0F4UlfKZYPxL/vmriaLHt2Kgvw63Bv0Bpep4eOkslVVSS1IRfRI6d392g==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
},
"peerDependencies": {
"expo": ">=52.0.0",
"react": "*",
"react-native": "*",
"react-native-windows": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"react-native-windows": {
"optional": true
}
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.81.4", "version": "0.81.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz",
@@ -3873,7 +3894,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz",
"integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==", "integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@react-navigation/core": "^7.13.6", "@react-navigation/core": "^7.13.6",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
@@ -4076,7 +4096,6 @@
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -4148,7 +4167,6 @@
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/types": "8.46.0", "@typescript-eslint/types": "8.46.0",
@@ -4711,7 +4729,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5397,7 +5414,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.9", "baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746", "caniuse-lite": "^1.0.30001746",
@@ -6537,7 +6553,6 @@
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -6735,7 +6750,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -6974,7 +6988,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.13.tgz", "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.13.tgz",
"integrity": "sha512-F1puKXzw8ESnsbvaKdXtcIiyYLQ2kUHqP8LuhgtJS1wm6w55VhtOPg8yl/0i8kPbTA0YfD+KYdXjSfhPXgUPxw==", "integrity": "sha512-F1puKXzw8ESnsbvaKdXtcIiyYLQ2kUHqP8LuhgtJS1wm6w55VhtOPg8yl/0i8kPbTA0YfD+KYdXjSfhPXgUPxw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.11", "@expo/cli": "54.0.11",
@@ -7042,7 +7055,6 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.9.tgz", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.9.tgz",
"integrity": "sha512-sqoXHAOGDcr+M9NlXzj1tGoZyd3zxYDy215W6E0Z0n8fgBaqce9FAYQE2bu5X4G629AYig5go7U6sQz7Pjcm8A==", "integrity": "sha512-sqoXHAOGDcr+M9NlXzj1tGoZyd3zxYDy215W6E0Z0n8fgBaqce9FAYQE2bu5X4G629AYig5go7U6sQz7Pjcm8A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@expo/config": "~12.0.9", "@expo/config": "~12.0.9",
"@expo/env": "~2.0.7" "@expo/env": "~2.0.7"
@@ -7067,7 +7079,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz",
"integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==", "integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fontfaceobserver": "^2.1.0" "fontfaceobserver": "^2.1.0"
}, },
@@ -7103,6 +7114,27 @@
} }
} }
}, },
"node_modules/expo-image-loader": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
"integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "17.0.10",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
"integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~6.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-keep-awake": { "node_modules/expo-keep-awake": {
"version": "15.0.7", "version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz",
@@ -7118,7 +7150,6 @@
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz",
"integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==", "integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"expo-constants": "~18.0.8", "expo-constants": "~18.0.8",
"invariant": "^2.2.4" "invariant": "^2.2.4"
@@ -7128,6 +7159,15 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-location": {
"version": "19.0.8",
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-19.0.8.tgz",
"integrity": "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": { "node_modules/expo-modules-autolinking": {
"version": "3.0.15", "version": "3.0.15",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.15.tgz", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.15.tgz",
@@ -11354,7 +11394,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -11374,7 +11413,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -11411,7 +11449,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz",
"integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==", "integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/create-cache-key-function": "^29.7.0", "@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.4", "@react-native/assets-registry": "0.81.4",
@@ -11469,7 +11506,6 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@egjs/hammerjs": "^2.0.17", "@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.0",
@@ -11480,6 +11516,16 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-image-picker": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-8.2.1.tgz",
"integrity": "sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-is-edge-to-edge": { "node_modules/react-native-is-edge-to-edge": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
@@ -11514,7 +11560,6 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
"integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==", "integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"react-native-is-edge-to-edge": "^1.2.1", "react-native-is-edge-to-edge": "^1.2.1",
"semver": "7.7.2" "semver": "7.7.2"
@@ -11543,7 +11588,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
"react-native": "*" "react-native": "*"
@@ -11554,7 +11598,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"react-freeze": "^1.0.0", "react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1", "react-native-is-edge-to-edge": "^1.2.1",
@@ -11570,7 +11613,6 @@
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.1.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.1.tgz",
"integrity": "sha512-BeNsgwwe4AXUFPAoFU+DKjJ+CVQa3h54zYX77p7GVZrXiiNo3vl03WYDYVEy5R2J2HOPInXtQZB5gmj3vuzrKg==", "integrity": "sha512-BeNsgwwe4AXUFPAoFU+DKjJ+CVQa3h54zYX77p7GVZrXiiNo3vl03WYDYVEy5R2J2HOPInXtQZB5gmj3vuzrKg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.6", "@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1", "@react-native/normalize-colors": "^0.74.1",
@@ -11603,7 +11645,6 @@
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-arrow-functions": "^7.0.0-0",
"@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0",
@@ -11714,7 +11755,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -13137,7 +13177,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -13344,7 +13383,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -14320,7 +14358,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@react-native-community/datetimepicker": "^8.5.1",
"@react-navigation/bottom-tabs": "^7.8.12", "@react-navigation/bottom-tabs": "^7.8.12",
"@react-navigation/elements": "^2.6.3", "@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.25", "@react-navigation/native": "^7.1.25",
@@ -20,7 +21,9 @@
"expo-font": "~14.0.9", "expo-font": "~14.0.9",
"expo-haptics": "~15.0.7", "expo-haptics": "~15.0.7",
"expo-image": "~3.0.9", "expo-image": "~3.0.9",
"expo-image-picker": "~17.0.10",
"expo-linking": "~8.0.8", "expo-linking": "~8.0.8",
"expo-location": "~19.0.8",
"expo-router": "~6.0.11", "expo-router": "~6.0.11",
"expo-splash-screen": "~31.0.10", "expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8", "expo-status-bar": "~3.0.8",
@@ -32,6 +35,7 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-native": "0.81.4", "react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.9.0", "react-native-maps": "1.9.0",
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",

View File

@@ -0,0 +1,24 @@
import { Ressources } from "@/class/class";
import { db } from "@/firebase_config";
import { doc, runTransaction, increment, arrayUnion } from "firebase/firestore";
export async function borrowRessource(chantierId: string, ressource: Ressources, quantity: number) {
const chantierRef = doc(db, "chantier", chantierId);
const ressourceRef = doc(db, "ressources", ressource.id.toString());
await runTransaction(db, async (transaction) => {
const resSnap = await transaction.get(ressourceRef);
if (!resSnap.exists()) throw new Error("Ressource not found");
const available = resSnap.data().available_quantity;
if (available < quantity) throw new Error("Not enough quantity available");
transaction.update(ressourceRef, {
available_quantity: increment(-quantity),
});
const borrowed: Ressources = { ...ressource, quantity };
transaction.update(chantierRef, {
vehicules: arrayUnion(borrowed),
});
});
}

View File

@@ -1,4 +1,4 @@
import { addDoc, collection, doc, getDoc, getDocs, Timestamp, updateDoc } from "firebase/firestore"; import { addDoc, arrayUnion, collection, doc, Firestore, getDoc, getDocs, Timestamp, updateDoc, DocumentReference, query, where } from "firebase/firestore";
import { Chantier, Reservation, Ressources, User } from "../class/class"; import { Chantier, Reservation, Ressources, User } from "../class/class";
import { db } from "../firebase_config"; import { db } from "../firebase_config";
@@ -10,8 +10,9 @@ export async function getUsers(): Promise<User[]> {
return snapshot.docs.map((doc) => { return snapshot.docs.map((doc) => {
const data = doc.data(); const data = doc.data();
return { return {
id: doc.id,
...data, ...data,
allocation: data.allocation?.map(convertReservation) || [], //allocation: data.allocation?.map(convertReservation) || [],
} as User; } as User;
}); });
} catch (err) { } catch (err) {
@@ -27,8 +28,9 @@ export async function getRessources(): Promise<Ressources[]> {
return snapshot.docs.map((doc) => { return snapshot.docs.map((doc) => {
const data = doc.data(); const data = doc.data();
return { return {
id: doc.id,
...data, ...data,
allocation: data.allocation?.map(convertReservation) || [], //allocation: data.allocation?.map(convertReservation) || [],
} as Ressources; } as Ressources;
}); });
} catch (err) { } catch (err) {
@@ -36,49 +38,121 @@ export async function getRessources(): Promise<Ressources[]> {
return []; return [];
} }
} }
//ADD RESSOURCES
export async function addRessources(ressourceData: Omit<Ressources, 'id'>): Promise<string | null> {
try {
const colRef = collection(db, "ressources");
const ressourcesRef = await addDoc(colRef, ressourceData);
console.log(`Ressource ajoutée avec ID: ${ressourcesRef.id}`);
return ressourcesRef.id;
} catch (err) {
console.error("Error adding:", err);
return null;
}
}
///////////////////////////////////CHANTIER///////////////////////////////// ///////////////////////////////////CHANTIER/////////////////////////////////
export async function getChantiers(): Promise<Chantier[]> { export async function getChantiers(): Promise<Chantier[]> {
const snap = await getDocs(collection(db, "chantier"));
const chantiers: Chantier[] = []; const chantiers: Chantier[] = [];
try {
const snap = await getDocs(collection(db, "chantier"));
for (const docSnap of snap.docs) {
const data = docSnap.data();
//Faut convertir les Timestamp en Date ( merci à firebase :) )
const dateDep = data.dateDep instanceof Timestamp ? data.dateDep.toDate() : new Date(data.dateDep);
let chef: User | null = null;
if (data.chef) {
const chefSnap = await getDoc(data.chef);
if (chefSnap.exists()) {
chef = chefSnap.data() as User;
}
}
let equipe: User[] = [];
if (Array.isArray(data.equipe)) {
equipe = await Promise.all(
data.equipe.map(async (ref: any) => {
const snap = await getDoc(ref);
return snap.exists() ? (snap.data() as User) : null;
})
).then(list => list.filter(x => x !== null)) as User[];
}
chantiers.push({ for (const docSnap of snap.docs) {
...data, try {
id: docSnap.id, const data = docSnap.data();
dateDep, //Faut convertir les Timestamp en Date ( merci à firebase :) )
chef, const dateDep = data.dateDep instanceof Timestamp ? data.dateDep.toDate() : new Date(data.dateDep);
equipe let chef: User | null = null;
} as Chantier); if (data.chef) {
const chefSnap = await getDoc(data.chef);
if (chefSnap.exists()) {
chef = chefSnap.data() as User;
}
}
const equipe:Reservation[] = [];
const vehicules:Reservation[] = [];
const materiel:Reservation[] = [];
const all:Reservation[] = await getReservationsByChantier(docSnap.id);
all.forEach(element => {
if(element.ressource.type==="Ouvrier"){
equipe.push(element)
}
else if(element.ressource.type==="Machine"){
vehicules.push(element)
}
else if(element.ressource.type==="Outil"){
materiel.push(element)
}
});
chantiers.push({
...data,
id: docSnap.id,
dateDep,
chef,
equipe,
vehicules,
materiel,
} as Chantier);
} catch (error) {
console.error("Erreur lors de la lecture d'un chantiers : " + error);
//alert("Erreur lors de la lecture d'un chantiers : " + error);
} }
}
return chantiers; return chantiers;
} catch (error) {
alert("Erreur lors de la lecture des chantiers : " + error);
}
return chantiers
} }
//récupère les reservations d'un chantier
export async function getReservationsByChantier(chantierId: string): Promise<Reservation[]> {
const q = query(
collection(db, "Reservation"),
where("chantier", "==", doc(db, "chantier", chantierId)),
);
const snap = await getDocs(q);
const results = await Promise.all(
snap.docs.map(convertReservation)
);
return results.filter(
(r): r is Reservation => r !== null
);
}
///////////////////////////////////RESERVATION/////////////////////////////////
export async function getReservations(): Promise<Reservation[]> {
try {
const snap = await getDocs(collection(db, "Reservation"));
const results = await Promise.all(
snap.docs.map(convertReservation)
);
return results.filter(
(r): r is Reservation => r !== null
);
} catch (error) {
console.error("Erreur lors de la lecture des Reservations : " + error);
return [];
}
}
//CHANGE CHANTIER STATUS //CHANGE CHANTIER STATUS
export async function changeChantierStatus(chantierId: string, newStatus: string): Promise<void> { export async function changeChantierStatus(chantierId: string, newStatus: string): Promise<void> {
try { try {
const chantierRef = doc(db, "chantier", chantierId); const chantierRef = doc(db, "chantier", chantierId);
await updateDoc(chantierRef, { etat: newStatus }); await updateDoc(chantierRef, { etat: newStatus });
console.log(`Chantier ${chantierId} status updated to ${newStatus}`); console.log("Chantier ${chantierId} status updated to ${newStatus}");
} catch (err) { } catch (err) {
console.error("Error", err); console.error("Error", err);
} }
@@ -98,30 +172,19 @@ export async function addChantier(chantierData: Omit<Chantier, 'id'>): Promise<s
} }
//CHANGE CHANTIER ANOMALIE STATUS //CHANGE CHANTIER ANOMALIE STATUS
export async function changeAnomalieStatus(chantierId: string, anomalie_String: string, newStatus: string): Promise<void> { export async function addAnomalie(chantierId: string, anomalie_String: string): Promise<void> {
try { try {
const chantierRef = doc(db, "chantier", chantierId); const chantierRef = doc(db, "chantier", chantierId);
const chantierSnap = await getDoc(chantierRef); await updateDoc(chantierRef, {
if (chantierSnap.exists()) { anomalies: arrayUnion(anomalie_String)
const chantierData = chantierSnap.data(); });
const anomalies = chantierData.anomalies || []; console.log("Anomalie added");
const updatedAnomalies = anomalies.map((anomalie: any) => {
if (anomalie.description === anomalie_String) {
return { ...anomalie, status: newStatus };
}
return anomalie;
});
await updateDoc(chantierRef, { anomalies: updatedAnomalies });
console.log(`Anomalie status updated to ${newStatus}`);
} else {
console.error("Chantier not found");
}
} catch (err) { } catch (err) {
console.error("Error", err); console.error("Error adding anomalie:", err);
} }
} }
//CHANGE CHANTIER ANOMALIE STATUS //DELETE CHANTIER ANOMALIE STATUS
export async function deleteAnomalie(chantierId: string, anomalie_String: string): Promise<void> { export async function deleteAnomalie(chantierId: string, anomalie_String: string): Promise<void> {
try { try {
const chantierRef = doc(db, "chantier", chantierId); const chantierRef = doc(db, "chantier", chantierId);
@@ -129,9 +192,10 @@ export async function deleteAnomalie(chantierId: string, anomalie_String: string
if (chantierSnap.exists()) { if (chantierSnap.exists()) {
const chantierData = chantierSnap.data(); const chantierData = chantierSnap.data();
const anomalies = chantierData.anomalies || []; const anomalies = chantierData.anomalies || [];
const updatedAnomalies = anomalies.filter((anomalie: any) => anomalie.description !== anomalie_String); //Filtage
const updatedAnomalies = anomalies.filter((anomaly: string) => anomaly !== anomalie_String);
await updateDoc(chantierRef, { anomalies: updatedAnomalies }); await updateDoc(chantierRef, { anomalies: updatedAnomalies });
console.log(`Anomalie deleted`); console.log("Anomalie deleted");
} else { } else {
console.error("Chantier not found"); console.error("Chantier not found");
} }
@@ -140,12 +204,63 @@ export async function deleteAnomalie(chantierId: string, anomalie_String: string
} }
} }
function convertReservation(res: any): Reservation { type ReservationFirestore = {
return { chantier: DocumentReference;
id: res.id, ressource: DocumentReference;
dateChantier: quantity: number;
res.dateChantier instanceof Timestamp ? res.dateChantier.toDate() : new Date(res.dateChantier), };
dateFin:
res.dateFin instanceof Timestamp ? res.dateFin.toDate() : new Date(res.dateFin), async function convertReservation(res: any): Promise<Reservation|null> {
}; try {
const data = res.data() as ReservationFirestore;
const chantierSnap = await getDoc(data.chantier as DocumentReference);
const ressourceSnap = await getDoc(data.ressource as DocumentReference);
return {
id: res.id,
chantier: chantierSnap.data() as Chantier,
ressource: ressourceSnap.data() as Ressources,
quantity: data.quantity,
};
} catch (err) {
console.warn("Reservation ignorée :", res.id , err);
return null;
}
}
//ENVOYER CHANTIER
export async function sendNewChantier(chantier:Chantier): Promise<void> {
const chantierRef = await addDoc(collection(db, "chantier"), {
name:chantier.name,
adresse:chantier.adresse,
etat:chantier.etat,
contact:chantier.contact,
chef: doc(db, "user", chantier.chef.id), //un objet déjà dans la base de donné
date: Timestamp.fromDate(chantier.dateDep),
tempsEst: chantier.tempsEst,
anomalies: chantier.anomalies ?? [], //strings[]
latitude: chantier.latitude,
longitude: chantier.longitude,
})
await Promise.all([
sendNewReservation(chantier.equipe, chantierRef.id),
sendNewReservation(chantier.materiel, chantierRef.id),
sendNewReservation(chantier.vehicules, chantierRef.id),
]);
}
export async function sendNewReservation(list: Reservation[],chantierId:string): Promise<void> {
const promises = list.map((reservation) =>
addDoc(collection(db,"Reservation"),{
chantier: doc(db, "chantier", chantierId),
ressource: doc(db, "ressources", reservation.ressource.id),
quantity: reservation.quantity,
})
);
await Promise.all(promises);
} }

View File

@@ -0,0 +1,21 @@
import { Ressources } from "@/class/class";
import { db } from "@/firebase_config";
import { arrayRemove, doc, increment, runTransaction } from "firebase/firestore";
export async function returnRessource(chantierId: string, ressource: Ressources) {
const chantierRef = doc(db, "chantier", chantierId);
const ressourceRef = doc(db, "ressources", ressource.id.toString());
await runTransaction(db, async (transaction) => {
const resSnap = await transaction.get(ressourceRef);
if (!resSnap.exists()) throw new Error("Ressource not found");
transaction.update(ressourceRef, {
available_quantity: increment(ressource.quantity),
});
transaction.update(chantierRef, {
vehicules: arrayRemove(ressource),
});
});
}