2 Commits
main ... temp

Author SHA1 Message Date
Alexis Leboeuf
ad91270c9e Merge 2025-12-11 23:42:08 +01:00
Alexis Leboeuf
ee3f5012f7 Almost finished Roles bugfix 2025-12-11 23:41:23 +01:00
33 changed files with 496 additions and 2206 deletions

View File

@@ -11,48 +11,9 @@
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 install expo-image-picker npx expo start
npx expo install expo-location
npx expo start
#### Présentation de l'application :
##### 5 écrans :
Accueil :
- 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 :
Permet de voir les ressources enregistrées dans la base de données et leurs différentes données (Nom , Type, quantité totale et quantité disponible). On peut rechercher par le nom et un filtre est aussi disponible pour affiner la recherche.
Users :
Permet de voir les différents utilisateurs enregistrés dans la base de données, ainsi que leur rang (chef ou responsable).
MapScreen :
Permet de voir les différents chantiers sur une carte avec leurs adresses et leur état.
Ajouter :
Permet d'ajouter un chantier ou une ressource (ouvrier,véhicule,outil)
##### Fonctionnalité manquante :
Par manque de temps nous n'avons pas pu finaliser certaines fonctionnalités
- 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,27 +1,38 @@
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { } from 'expo-router';
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';
import { useColorScheme } from '@/hooks/use-color-scheme'; import { useColorScheme } from '@/hooks/use-color-scheme';
import GestionOuvrier from './gestion_ouvrier';
import ListMateriel from './gestionnaire_ressource';
import Home from './home';
import MapScreen from './mapScreen';
import AddChantier from './addChantier';
import AntDesign from '@expo/vector-icons/AntDesign'; import AntDesign from '@expo/vector-icons/AntDesign';
import { Tabs } from 'expo-router'; import { UserProvider } from '../ContextUser';
import React from 'react'; import { ChantierProvider } from '../ContextChantier';
import { useAuthHandler } from '../AuthHandler'; import { RessourcesProvider } from '../ContextRessource';
import { useUser } from '../ContextUser';
const Tabs = createBottomTabNavigator();
export default function TabLayout() { export default function TabLayout() {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const { role } = useUser();
// Handle auth in tabs layout
useAuthHandler();
return ( return (
<Tabs screenOptions={{tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, headerShown: false, tabBarButton: HapticTab}}> <Tabs.Navigator
<Tabs.Screen name="index" options={{ href: null}}/> initialRouteName='explore'
<Tabs.Screen name="explore" options={{ href: null }}/> screenOptions={{
<Tabs.Screen name="templateSreen" options={{ href: null}}/> tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}}>
<Tabs.Screen <Tabs.Screen
name="home" name="home"
component={Home}
options={{ options={{
title: 'Home', title: 'Home',
tabBarIcon: ({ color }) => ( tabBarIcon: ({ color }) => (
@@ -31,6 +42,7 @@ export default function TabLayout() {
/> />
<Tabs.Screen <Tabs.Screen
name="gestionnaire_ressource" name="gestionnaire_ressource"
component={ListMateriel}
options={{ options={{
title: 'Ressources', title: 'Ressources',
tabBarIcon: ({ color }) => ( tabBarIcon: ({ color }) => (
@@ -39,29 +51,33 @@ export default function TabLayout() {
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name="gestion_user" name="GestionOuvrier"
component={GestionOuvrier}
options={{ options={{
title: 'Users', title: 'Bonjour',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />, tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />,
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name="mapScreen" name="explore"
component={MapScreen}
options={{ options={{
title: 'Map', title: 'MapScreen',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
}} }}
/> >
</Tabs.Screen>
<Tabs.Screen <Tabs.Screen
name="addScreen" name="addChantier"
component={AddChantier}
options={{ options={{
title: 'Ajouter', title: 'Ajouter',
href: role === 'resp' ? '/(tabs)/addScreen' : null,
tabBarIcon: ({ color }) => ( tabBarIcon: ({ color }) => (
<AntDesign name="plus" size={24} color={color} /> <AntDesign name="plus" size={24} color={color} />
), ),
}} }}
/> />
</Tabs>
</Tabs.Navigator>
); );
} }

178
app/(tabs)/addChantier.tsx Normal file
View File

@@ -0,0 +1,178 @@
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",
},
});

View File

@@ -1,94 +0,0 @@
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

@@ -2,11 +2,13 @@ import { ThemedText } from "@/components/theme/themed-text";
import { ThemedTextInput } from "@/components/theme/themed-textinput"; 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 React, { useEffect, useState } from "react"; import { useLocalSearchParams, useRouter } from "expo-router";
import { FlatList, StyleSheet, Text, View } from "react-native"; import React, { useMemo, useState } from "react";
import { getUsers } from "@/services/ressourcesService"; import { FlatList, Image, StyleSheet, Text, View } from "react-native";
import rawConcerts from "../../data/concerts.json";
import { useChantier } from "../ContextChantier";
import SelectChantier from "@/components/selectChantier"; import SelectChantier from "@/components/selectChantier";
import { User } from "@/class/class";
type Concert = { type Concert = {
group: string; group: string;
@@ -19,31 +21,36 @@ type Concert = {
favorite: boolean; favorite: boolean;
}; };
export default function GestionUser() { export default function GestionOuvrier() {
const router = useRouter();
const { nom, prenom } = useLocalSearchParams(); // Recup data ecran precedent
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [users, setUsers] = useState<User[]>([]); const { chantier, setChantier } = useChantier();
useEffect(() => { const concertsData: Concert[] = Array.isArray(rawConcerts)
async function loadData() { ? (rawConcerts as Concert[])
try { : [];
const data = (await getUsers());
setUsers(data);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadData();
}, []);
const renderItem = ({ item, index }: { item?: User; index: number }) => { const filteredData = useMemo(() => {
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.name} {item.last_name}</ThemedText> <ThemedText style={styles.group}>{item.group}</ThemedText>
<ThemedText>{item.role}</ThemedText> <ThemedText>{item.date}</ThemedText>
<ThemedText>{item.location}</ThemedText>
</ThemedView> </ThemedView>
</ThemedView> </ThemedView>
); );
@@ -58,7 +65,7 @@ export default function GestionUser() {
<FlatList <FlatList
data={users} data={filteredData}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(_, index) => index.toString()} keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ paddingBottom: 40 }} contentContainerStyle={{ paddingBottom: 40 }}

View File

@@ -8,25 +8,19 @@ import { useLocalSearchParams, useRouter } from "expo-router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { FlatList, Image, StyleSheet, Text, View } from "react-native"; import { FlatList, Image, StyleSheet, Text, View } from "react-native";
import { Ressources } from "../../class/class"; import { Ressources } from "../../class/class";
import { getReservations, getRessources } from "../../services/ressourcesService"; import { getRessources } from "../../services/ressourcesService";
import SelectChantier from "@/components/selectChantier"; import SelectChantier from "@/components/selectChantier";
import { useRessources } from "../ContextRessource";
import { useChantier } from "../ContextChantier";
import { getNbUseRessources, getNbUseRessourcesInChantier, isInChantier } from "@/class/utils";
import { useReservations } from "../ContextReservation";
export default function GestionnaireRessource() { export default function GestionnaireRessource() {
const { nom, prenom } = useLocalSearchParams();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const {ressources, setRessources} = useRessources(); const [ressource, setRessources] = useState<Ressources[]>([]);
const {reservations, setReservations} = useReservations(); const [filterType, setFilterType] = useState("tout");
const {chantier, setChantier} = useChantier();
const [filterType, setFilterType] = useState("Tout");
const [showFilterMenu, setShowFilterMenu] = useState(false); const [showFilterMenu, setShowFilterMenu] = useState(false);
const [filterChantier, setFilterChantier] = useState(false);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
async function loadDataRessources() { async function loadData() {
try { try {
const data = await getRessources(); const data = await getRessources();
setRessources(data); setRessources(data);
@@ -34,23 +28,13 @@ export default function GestionnaireRessource() {
console.error("Erreur lors du chargement :", error); console.error("Erreur lors du chargement :", error);
} }
} }
async function loadDataReservations() { loadData();
try {
const data = await getReservations();
setReservations(data);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadDataRessources();
loadDataReservations();
}, []); }, []);
const filteredData = ressources.filter((r) => { const filteredData = ressource.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 && (!filterChantier || (chantier && isInChantier(r,chantier,reservations)));
}); });
const renderRessource = ({ item }: { item: Ressources }) => { const renderRessource = ({ item }: { item: Ressources }) => {
@@ -59,13 +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>Nom : {item.name}</ThemedText> <ThemedText>{item.id}</ThemedText>
<ThemedText>Type : {item.type}</ThemedText> <ThemedText>{item.name}</ThemedText>
<ThemedText>Quantité totale : {item.quantity}</ThemedText> <ThemedText>{item.type}</ThemedText>
<ThemedText>Quantité disponible : {item.quantity-getNbUseRessources(item, reservations)}</ThemedText> <ThemedText>{item.quantity}</ThemedText>
{filterChantier&&chantier && <ThemedText>{item.available_quantity}</ThemedText>
<ThemedText>Quantité utilisé dans le chantier : {getNbUseRessourcesInChantier(item,chantier, reservations)}</ThemedText>
}
</ThemedView> </ThemedView>
</ThemedView> </ThemedView>
); );
@@ -87,31 +69,28 @@ 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","Ouvrier"].map((t) => ( {["tout", "Outil", "Machine"].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={async () => { onPress={() => {
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>
</ThemedButton> </ThemedButton>
))} ))}
{/* Bouton "Fermer" remplacé */} {/* Bouton "Fermer" remplacé */}
<ThemedButton <ThemedButton
lvl={1} lvl={1}
shadow={true} shadow={true}
style={{ padding: 10, borderRadius: 8 }} style={{ padding: 10, borderRadius: 8 }}
onPress={() => setShowFilterMenu(false)} onPress={() => setShowFilterMenu(false)}
> >
<ThemedText style={{ textAlign: "center" }}>Fermer</ThemedText> <ThemedText style={{ textAlign: "center" }}>Fermer</ThemedText>
</ThemedButton> </ThemedButton>
</ThemedView> </ThemedView>
@@ -136,22 +115,14 @@ export default function GestionnaireRessource() {
</ThemedView> </ThemedView>
{/* Bouton filtre en haut à droite */} {/* Bouton filtre en haut à droite */}
<View style={{flexDirection: "row", gap:5}}> <ThemedButton
<ThemedButton lvl={1}
lvl={1} shadow={true}
shadow={true} style={{ padding: 10, borderRadius: 8, marginTop: 10 }}
style={styles.button} onPress={() => setShowFilterMenu(true)}
onPress={() => setShowFilterMenu(true)} >
> <ThemedText>{`Filtre: ${filterType}`}</ThemedText>
<ThemedText>{`Filtre: ${filterType}`}</ThemedText> </ThemedButton>
</ThemedButton>
<ThemedButton style={styles.button}>
<ThemedText onPress={() => setFilterChantier(!filterChantier)}>
{filterChantier?"chantier courant":"tous"}
</ThemedText>
</ThemedButton>
</View>
</View> </View>
} }
ListEmptyComponent={ ListEmptyComponent={
@@ -241,9 +212,4 @@ const styles = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
textAlign: "center", textAlign: "center",
}, },
button:{
padding: 10,
borderRadius: 8,
marginTop: 10
},
}); });

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, ScrollView } from 'react-native'; import { StyleSheet, View,Text } from 'react-native';
import { useChantier } from '../ContextChantier'; import { useChantier } from '../ContextChantier';
import Anomaly from '@/components/anomaly'; import Anomaly from '@/components/anomaly';
@@ -16,6 +16,7 @@ export default function Home() {
const { chantier, setChantier } = useChantier(); const { chantier, setChantier } = useChantier();
const { role } = useUser(); const { role } = useUser();
console.log("ROLE USER", role)
return( return(
<ThemedView lvl={3} style={styles.back}> <ThemedView lvl={3} style={styles.back}>
@@ -23,18 +24,12 @@ 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 }} />
<ScrollView> <Anomaly style={styles.anomaly} data={{chantier}}/>
<View style={{paddingTop:60}}> {role === "chef"}
<ChantierSummary style={styles.summary} data={{ chantier }} />
<Anomaly style={styles.anomaly} data={{chantier}}/>
{role === "chef"}
</View>
</ScrollView>
</View> </View>
</ThemedView> </ThemedView>
@@ -49,6 +44,7 @@ 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,15 +1,11 @@
// 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 = () => {
@@ -24,30 +20,21 @@ 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
@@ -57,24 +44,14 @@ 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.name} 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>
); );
}; };
@@ -87,13 +64,6 @@ 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,45 +0,0 @@
import { useRouter, useSegments } from "expo-router";
import { onAuthStateChanged } from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
import { useEffect } from "react";
import { auth, db } from "../firebase_config";
import { useUser } from "./ContextUser";
export function useAuthHandler() {
const router = useRouter();
const segments = useSegments();
const { setUser, setRole } = useUser();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
if (!currentUser) {
setUser(null);
setRole(null);
router.replace("/login/login");
return;
}
const userDocRef = doc(db, "user", currentUser.uid);
const userDoc = await getDoc(userDocRef);
if (!userDoc.exists()) {
setUser(null);
setRole(null);
router.replace("/login/login");
return;
}
const { role } = userDoc.data();
setUser(currentUser);
setRole(role);
// Only redirect if we're on login page
const inAuthGroup = segments[0] === 'login';
if (inAuthGroup) {
router.replace("/(tabs)/home");
}
});
return unsubscribe;
}, []);
}

View File

@@ -1,11 +1,9 @@
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);
@@ -16,19 +14,8 @@ 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 all = await getChantiers(); const value = useMemo(() => ({ chantier, setChantier }), [chantier]);
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

@@ -1,33 +0,0 @@
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,10 +16,7 @@ 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 { useUser } from "./ContextUser";
import LoginScreen from "./login/login";
export const unstable_settings = { export const unstable_settings = {
anchor: "(tabs)", anchor: "(tabs)",
@@ -28,14 +25,12 @@ export const unstable_settings = {
export default function RootLayout() { export default function RootLayout() {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User | null>(null); const { setUser, setRole } = useUser();
const [userRole, setUserRole] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
if (!currentUser) { if (!currentUser) {
setUser(null); setUser(null);
setUserRole(null); setRole(null);
router.replace("/login/login"); router.replace("/login/login");
return; return;
} }
@@ -46,13 +41,14 @@ export default function RootLayout() {
if (!userDoc.exists()) { if (!userDoc.exists()) {
router.replace("/login/login"); router.replace("/login/login");
setUser(null); setUser(null);
setUserRole(null); setRole(null);
return; return;
} }
const { role } = userDoc.data(); const { role } = userDoc.data();
console.log("ROLE APP",role)
setUser(currentUser); setUser(currentUser);
setUserRole(role); setRole(role);
router.replace("/(tabs)/home"); router.replace("/(tabs)/home");
}); });
return unsubscribe; return unsubscribe;
@@ -62,7 +58,7 @@ export default function RootLayout() {
<UserProvider> <UserProvider>
<ChantierProvider> <ChantierProvider>
<RessourcesProvider> <RessourcesProvider>
<ReservationsProvider> <RootLayout></RootLayout>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}> <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack> <Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
@@ -75,7 +71,6 @@ export default function RootLayout() {
</Stack> </Stack>
<StatusBar style="auto" /> <StatusBar style="auto" />
</ThemeProvider> </ThemeProvider>
</ReservationsProvider>
</RessourcesProvider> </RessourcesProvider>
</ChantierProvider> </ChantierProvider>
</UserProvider> </UserProvider>

View File

@@ -1,11 +1,16 @@
import { ThemedText } from "@/components/theme/themed-text"; import { ThemedText } from "@/components/theme/themed-text";
import { ThemedTextInput } from "@/components/theme/themed-textinput"; import { ThemedTextInput } from "@/components/theme/themed-textinput";
import { ThemedView } from "@/components/theme/themed-view"; import { ThemedView } from "@/components/theme/themed-view";
import { signInWithEmailAndPassword } from "firebase/auth"; import { router } from "expo-router";
import {
signInWithEmailAndPassword
} from "firebase/auth";
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, StyleSheet, View } from "react-native"; import { Button, StyleSheet, View } from "react-native";
import { auth } from "../../firebase_config"; import { auth } from "../../firebase_config";
const DEFAULT_ROLE = "resp";
const LoginScreen: React.FC = () => { const LoginScreen: React.FC = () => {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
@@ -13,11 +18,21 @@ const LoginScreen: React.FC = () => {
const handleLogin = async () => { const handleLogin = async () => {
try { try {
await signInWithEmailAndPassword(auth, email, password); await signInWithEmailAndPassword(auth, email, password);
router.replace("/(tabs)");
} catch (error: any) { } catch (error: any) {
alert(error.message); alert(error.message);
} }
}; };
/*const handleRegister = async () => {
try {
await createUserWithEmailAndPassword(auth, email, password);
router.replace("/(tabs)");
} catch (error: any) {
alert(error.message);
}
}; */
return ( return (
<ThemedView lvl={1} style={styles.container}> <ThemedView lvl={1} style={styles.container}>
<ThemedText style={styles.title}>Se connecter</ThemedText> <ThemedText style={styles.title}>Se connecter</ThemedText>
@@ -30,6 +45,7 @@ const LoginScreen: React.FC = () => {
value={email} value={email}
onChangeText={setEmail} onChangeText={setEmail}
autoCapitalize="none" autoCapitalize="none"
/> />
<ThemedTextInput <ThemedTextInput
lvl = {2} lvl = {2}
@@ -43,6 +59,7 @@ const LoginScreen: React.FC = () => {
/> />
<Button title="Se connecter" onPress={handleLogin} /> <Button title="Se connecter" onPress={handleLogin} />
<View style={{ height: 10 }} /> <View style={{ height: 10 }} />
{/* <Button title="S'inscrire" onPress={handleRegister} /> */}
</ThemedView> </ThemedView>
); );
}; };
@@ -59,6 +76,7 @@ const styles = StyleSheet.create({
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
//borderColor: "#ccc",
borderRadius: 8, borderRadius: 8,
padding: 10, padding: 10,
marginBottom: 10, marginBottom: 10,

View File

@@ -1,15 +1,14 @@
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: Reservation[]; equipe: User[];
materiel: Reservation[]; materiel: Ressources[];
vehicules: Reservation[];
dateDep: Date; dateDep: Date;
tempsEst: number; tempsEst: number;
vehicules: Ressources[];
anomalies: string[]; anomalies: string[];
latitude: number; latitude: number;
longitude: number; longitude: number;
@@ -19,24 +18,23 @@ 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: string; id: number;
name: string; name: string;
type: string; //"Machine","Outil","Ouvrier" type: string;
Image: string; Image: string;
quantity: number; quantity: number;
//available_quantity: number; available_quantity: number;
//allocation: Reservation[]; allocation: Reservation[];
}; };
export type Reservation = { export type Reservation = {
id: string; id: string;
chantier: Chantier; dateChantier: Date;
ressource: Ressources; dateFin: Date;
quantity: number;
}; };

View File

@@ -1,54 +0,0 @@
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;
}
export function getNbUseRessourcesInChantier(ressource:Ressources,chantier: Chantier, allReservations:Reservation[]):number{
var res:number = 0;
getReservationOfRessource(ressource,allReservations).forEach(reserv => {
if(reserv.chantier.id === chantier.id && reserv.ressource.id===ressource.id){
res+=reserv.quantity;
}
})
return res;
}
export function isInChantier(ressource:Ressources, chantier: Chantier, allReservations:Reservation[]):boolean{
console.log(allReservations.length+ " --------------------------------");
const reservations:Reservation[] = getReservationOfRessource(ressource,allReservations);
var res=false;
reservations.forEach(reserv => {
console.log(reserv.chantier.id + " " + chantier.id)
if(reserv.chantier.id === chantier.id){
res=true;
}
});
return res;
}

View File

@@ -1,396 +0,0 @@
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

@@ -1,230 +0,0 @@
import SelectChantier from '@/components/selectChantier';
import { useRessources } from '@/app/ContextRessource';
import { Ressources } from '@/class/class';
import { ThemedButton } from '@/components/theme/themed-button';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedTextInput } from '@/components/theme/themed-textinput';
import { ThemedView } from '@/components/theme/themed-view';
import { addRessources } from '@/services/ressourcesService';
import React, { useState } from 'react';
import { Modal, ScrollView, StyleSheet, View } from 'react-native';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
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

@@ -1,79 +0,0 @@
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

@@ -1,156 +0,0 @@
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

@@ -1,154 +0,0 @@
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,11 +1,9 @@
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, { use, useEffect, useState } from 'react'; import React from 'react';
import { TouchableOpacity, StyleProp, StyleSheet, View, Image, ViewStyle,Text, TextInput, ScrollView } 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 { deleteAnomalie, addAnomalie, getChantiers } from '@/services/ressourcesService';
import { useChantier } from '@/app/ContextChantier';
import * as ImagePicker from 'expo-image-picker';
type Props = { type Props = {
data: { data: {
@@ -14,82 +12,22 @@ type Props = {
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
}; };
export default function Anomaly({data,style}: Props) { export default function Anomaly({data,style , ...otherProps }: 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>
{/* Add Anomaly Section */} {data.chantier.anomalies.length > 0 ? (
<View style={styles.addContainer}> data.chantier.anomalies.map((anomaly, index) => (
<TextInput style={styles.input} placeholder="Nouvelle anomalie..." value={newAnomaly} onChangeText={setNewAnomaly} /> <ThemedView key={index} lvl={2} style={styles.anomalyItem}>
<TouchableOpacity style={styles.addButton} onPress={handleAdd}> <ThemedText> {anomaly}</ThemedText>
<ThemedText style={styles.addButtonText}>Ajouter</ThemedText> </ThemedView>
</TouchableOpacity> ))
<TouchableOpacity onPress={selectImage} style={styles.addButton}> ) : (
<ThemedText style={styles.addButton}>Choisir une image</ThemedText> <ThemedText style={styles.noAnomaly}>Aucune anomalie</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>
@@ -101,6 +39,7 @@ const styles = StyleSheet.create({
anomaliesContainer: { anomaliesContainer: {
padding: 5, padding: 5,
borderRadius: 10, borderRadius: 10,
height: 150,
}, },
anomaliesTitle: { anomaliesTitle: {
fontSize: 16, fontSize: 16,
@@ -108,55 +47,12 @@ 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,7 +3,6 @@ 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: {
@@ -18,22 +17,12 @@ 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:"" /*chantier.urlImg*/ }} style={styles.image} /> <Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=69392bb2&is=6937da32&hm=dcc09e76d3dca89d2418947b46efbd38673b9dc559027724b2e51d493b173bc9&" /*chantier.urlImg*/ }} style={styles.image} />
</View> </View>
<View style={{flex: 1}}> <View>
<ThemedText selectable={true}>Objet: {data.chantier.name}</ThemedText> <ThemedText>Adresse: {data.chantier.adresse}</ThemedText>
<ThemedText selectable={true}>Adresse: {data.chantier.adresse}</ThemedText> <ThemedText>Chef de chantier: {data.chantier.chef.last_name}{" "}{data.chantier.chef.name}</ThemedText>
<ThemedText selectable={true}>Chef de chantier: {data.chantier.chef.last_name}{" "}{data.chantier.chef.name}</ThemedText> <ThemedText>État: {data.chantier.etat}</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>
) : ) :
@@ -53,12 +42,13 @@ 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,7 +6,6 @@ import { useEffect, useState } from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
FlatList,
Image, Image,
Pressable, Pressable,
ScrollView, ScrollView,
@@ -14,11 +13,7 @@ import {
View View
} from "react-native"; } from "react-native";
import Animated, { import Animated, {
interpolate, LinearTransition
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";
@@ -61,71 +56,36 @@ export default function SelectChantier() {
} }
} }
useEffect(() => { function onPressAddChantier(){
open.value = withTiming(isOpen ? 1 : 0); router.push("/(tabs)/addChantier")
}, [isOpen]); setIsOpen(false)
}
/*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 onPress={() => selectChantier(item)}> <Pressable key={index} onPress={() => selectChantier(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=693f1a72&is=693dc8f2&hm=86ffb97145fc8d3aec822b87d99be233c98477d4424c1ef58f80eb81b17c7c80&" /*chantier.urlImg*/ }} style={styles.image} /> <Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=69392bb2&is=6937da32&hm=dcc09e76d3dca89d2418947b46efbd38673b9dc559027724b2e51d493b173bc9&" /*chantier.urlImg*/ }} style={styles.image} />
</View> </View>
<View style={{flex: 1}}> <View>
<ThemedText>Objet: {item.name}</ThemedText> <ThemedText>Adresse: {chantier.adresse}</ThemedText>
<ThemedText>Adresse: {item.adresse}</ThemedText> <ThemedText>Chef de chantier: {chantier.chef.last_name}{" "}{chantier.chef.name}</ThemedText>
<ThemedText>Chef de chantier: {item.chef.last_name}{" "}{item.chef.name}</ThemedText> <ThemedText>État: {chantier.etat}</ThemedText>
<ThemedText>État: {item.etat}</ThemedText>
</View> </View>
</ThemedView> </ThemedView>
</Pressable> </Pressable>
@@ -133,40 +93,41 @@ export default function SelectChantier() {
}; };
return ( return (
<Animated.View style={animatedWindowStyle}> <Animated.View layout={LinearTransition.duration(200)} style={isOpen ? styles.windowOpean : styles.windowClose}>
{isOpen && (<Pressable style={styles.autoClose} onPress={() => setIsOpen(false)}/>)} {isOpen && (<Pressable style={styles.autoClose} onPress={() => setIsOpen(false)}/>)}
<ThemedView lvl={2} shadow={true} style={styles.window}> <AnimatedThemedView layout={LinearTransition.duration(200)} lvl={2} shadow={true} style={styles.window}>
<AnimatedThemedButton style={animatedButtonStyle} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}> <AnimatedThemedButton style={isOpen ? styles.buttonOpen : styles.buttonClose} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}>
<ThemedText style={styles.buttonText}> <ThemedText style={styles.buttonText}>
{isOpen ? "Fermer" : (chantier!=null ? chantier.name : "Chantier")} {isOpen ? "Fermer" : (chantier!=null ? chantier.adresse : "Chantier")}
</ThemedText> </ThemedText>
</AnimatedThemedButton> </AnimatedThemedButton>
{isOpen && ( {isOpen && (
<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?
<FlatList <ScrollView contentContainerStyle={styles.chantiersList}>
data={filteredChantiers} {chantiers.map((chantier, index) =>
renderItem={renderChantier} renderChantier(chantier, index)
keyExtractor={(_, index) => index.toString()} )}
contentContainerStyle={{ gap: 8 }} </ScrollView>
/>
: <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />} : <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />}
</View> </View>
</View> </View>
)} )}
</ThemedView> </AnimatedThemedView>
</Animated.View> </Animated.View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
windowClose: { windowClose: {
//backgroundColor: '#00FF0040', //backgroundColor: '#00FF0040',
@@ -225,7 +186,7 @@ const styles = StyleSheet.create({
borderRadius: 10, borderRadius: 10,
//borderWidth: 1, //borderWidth: 1,
flexDirection: 'row', flexDirection: 'row',
//height: 130, height: 100,
}, },
image:{ image:{
margin:0, margin:0,
@@ -259,4 +220,11 @@ const styles = StyleSheet.create({
buttonText: { buttonText: {
textAlign: "center", textAlign: "center",
}, },
buttonAdd:{
borderRadius: 10,
marginBottom: 10,
height: 30,
alignItems: 'center',
justifyContent: 'center',
}
}); });

View File

@@ -1,151 +0,0 @@
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

@@ -121,7 +121,7 @@ const styles = StyleSheet.create({
windowBox:{ windowBox:{
zIndex: 2, zIndex: 2,
//backgroundColor: '#00FFFF40', //backgroundColor: '#00FFFF40',
width:"35%", width:"30%",
padding: 10, padding: 10,
paddingLeft: 0, paddingLeft: 0,
//overflow: 'hidden', //overflow: 'hidden',

View File

@@ -6,32 +6,23 @@ 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,lvlPressed=1,border=-1,opacity="FF",shadow=false, ...otherProps }: ThemedPressableProps) { export function ThemedButton({ style, lightColor, darkColor,lvl=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 = "";
@@ -55,9 +46,5 @@ export function ThemedButton({ style, lightColor, darkColor,lvl=1,lvlPressed=1,b
shadowRadius: 6, shadowRadius: 6,
} }
return <Pressable style={(state) =>[{ return <Pressable style={(state) =>[{ backgroundColor, borderColor, borderWidth }, shadow && shadowStyle, typeof style === 'function' ? style(state) : style,]} {...otherProps}/>;
backgroundColor: state.pressed ? backgroundPressed: backgroundColor,
borderColor,
borderWidth
}, shadow && shadowStyle, typeof style === 'function' ? style(state) : style,]} {...otherProps}/>;
} }

45
data/concerts.json Normal file
View File

@@ -0,0 +1,45 @@
[
{
"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
}
]

View File

@@ -1,50 +0,0 @@
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,
};
};

67
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"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",
@@ -18,9 +17,7 @@
"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,7 +29,6 @@
"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",
@@ -3583,29 +3579,6 @@
} }
} }
}, },
"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",
@@ -7130,27 +7103,6 @@
} }
} }
}, },
"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",
@@ -7176,15 +7128,6 @@
"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",
@@ -11537,16 +11480,6 @@
"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",

View File

@@ -12,7 +12,6 @@
}, },
"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",
@@ -21,9 +20,7 @@
"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",
@@ -35,7 +32,6 @@
"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

@@ -1,24 +0,0 @@
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, arrayUnion, collection, doc, DocumentReference, getDoc, getDocs, query, Timestamp, updateDoc, where } from "firebase/firestore"; import { addDoc, collection, doc, getDoc, getDocs, Timestamp, updateDoc } 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,9 +10,8 @@ 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) {
@@ -28,9 +27,8 @@ 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) {
@@ -38,126 +36,49 @@ export async function getRessources(): Promise<Ressources[]> {
return []; return [];
} }
} }
//ADD RESSOURCES
export async function addRessources(ressourceData: Ressources): Promise<string | null> {
try {
const ressourcesRef = await addDoc(collection(db, "ressources"), {
name:ressourceData.name,
type:ressourceData.type,
Image: ressourceData.Image,
quantity: ressourceData.quantity,
});
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();
for (const docSnap of snap.docs) { //Faut convertir les Timestamp en Date ( merci à firebase :) )
try { const dateDep = data.dateDep instanceof Timestamp ? data.dateDep.toDate() : new Date(data.dateDep);
const data = docSnap.data(); let chef: User | null = null;
//Faut convertir les Timestamp en Date ( merci à firebase :) ) if (data.chef) {
const dateDep = data.dateDep instanceof Timestamp ? data.dateDep.toDate() : new Date(data.dateDep); const chefSnap = await getDoc(data.chef);
let chef: User | null = null; if (chefSnap.exists()) {
if (data.chef) { chef = chefSnap.data() as User;
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);
}
} }
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({
...data,
id: docSnap.id,
dateDep,
chef,
equipe
} as Chantier);
}
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);
} }
@@ -177,30 +98,21 @@ export async function addChantier(chantierData: Omit<Chantier, 'id'>): Promise<s
} }
//CHANGE CHANTIER ANOMALIE STATUS //CHANGE CHANTIER ANOMALIE STATUS
export async function addAnomalie(chantierId: string, anomalie_String: string): Promise<void> { export async function changeAnomalieStatus(chantierId: string, anomalie_String: string, newStatus: string): Promise<void> {
try {
const chantierRef = doc(db, "chantier", chantierId);
await updateDoc(chantierRef, {
anomalies: arrayUnion(anomalie_String)
});
console.log("Anomalie added");
} catch (err) {
console.error("Error adding anomalie:", err);
}
}
//DELETE CHANTIER ANOMALIE STATUS
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);
const chantierSnap = await getDoc(chantierRef); const chantierSnap = await getDoc(chantierRef);
if (chantierSnap.exists()) { if (chantierSnap.exists()) {
const chantierData = chantierSnap.data(); const chantierData = chantierSnap.data();
const anomalies = chantierData.anomalies || []; const anomalies = chantierData.anomalies || [];
//Filtage const updatedAnomalies = anomalies.map((anomalie: any) => {
const updatedAnomalies = anomalies.filter((anomaly: string) => anomaly !== anomalie_String); if (anomalie.description === anomalie_String) {
return { ...anomalie, status: newStatus };
}
return anomalie;
});
await updateDoc(chantierRef, { anomalies: updatedAnomalies }); await updateDoc(chantierRef, { anomalies: updatedAnomalies });
console.log("Anomalie deleted"); console.log(`Anomalie status updated to ${newStatus}`);
} else { } else {
console.error("Chantier not found"); console.error("Chantier not found");
} }
@@ -209,73 +121,31 @@ export async function deleteAnomalie(chantierId: string, anomalie_String: string
} }
} }
type ReservationFirestore = { //CHANGE CHANTIER ANOMALIE STATUS
chantier: DocumentReference; export async function deleteAnomalie(chantierId: string, anomalie_String: string): Promise<void> {
ressource: DocumentReference;
quantity: number;
};
async function convertReservation(res: any): Promise<Reservation|null> {
try { try {
const data = res.data() as ReservationFirestore; const chantierRef = doc(db, "chantier", chantierId);
const chantierSnap = await getDoc(chantierRef);
const chantierSnap = await getDoc(data.chantier as DocumentReference); if (chantierSnap.exists()) {
const ressourceSnap = await getDoc(data.ressource as DocumentReference); const chantierData = chantierSnap.data();
const anomalies = chantierData.anomalies || [];
return { const updatedAnomalies = anomalies.filter((anomalie: any) => anomalie.description !== anomalie_String);
id: res.id, await updateDoc(chantierRef, { anomalies: updatedAnomalies });
chantier: { console.log(`Anomalie deleted`);
id: chantierSnap.id, } else {
...(chantierSnap.data() as Omit<Chantier, "id">), console.error("Chantier not found");
}, }
ressource: {
id: ressourceSnap.id,
...(ressourceSnap.data() as Omit<Ressources, "id">),
},
quantity: data.quantity,
};
} catch (err) { } catch (err) {
console.warn("Reservation ignorée :", res.id , err); console.error("Error", err);
return null;
} }
} }
function convertReservation(res: any): Reservation {
return {
//ENVOYER CHANTIER id: res.id,
export async function sendNewChantier(chantier:Chantier): Promise<void> { dateChantier:
res.dateChantier instanceof Timestamp ? res.dateChantier.toDate() : new Date(res.dateChantier),
const chantierRef = await addDoc(collection(db, "chantier"), { dateFin:
name:chantier.name, res.dateFin instanceof Timestamp ? res.dateFin.toDate() : new Date(res.dateFin),
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> {
list.forEach(reservation => {
console.log("log: " + reservation.ressource.id);
});
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

@@ -1,21 +0,0 @@
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),
});
});
}