2 Commits

Author SHA1 Message Date
Alexis Leboeuf
82fce55403 Merging 2025-12-13 15:17:53 +01:00
Alexis Leboeuf
92b1729728 Role management not working
I can't make it use the correct context
If someone wants to try it, use this branch
2025-12-13 15:17:38 +01:00
49 changed files with 605 additions and 2619 deletions

View File

@@ -11,48 +11,11 @@
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:
npm install firebase
npm install react-native-maps
npx expo install react-native-maps
npm install react-native-maps @react-navigation/native @react-navigation/bottom-tabs react-native-safe-area-context react-native-screens
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-community/datetimepicker
npx expo install expo-image-picker
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.
npx expo start

View File

@@ -1,36 +1,45 @@
import { HapticTab } from '@/components/expoExempleComponents/haptic-tab';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { } from 'expo-router';
import React from 'react';
import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import BonjourScreen from './bonjourFL';
import ListMateriel from './gestionnaire_ressource';
import Home from './home';
import MapScreen from './mapScreen';
import AntDesign from '@expo/vector-icons/AntDesign';
import { Tabs } from 'expo-router';
import React from 'react';
import { useAuthHandler } from '../AuthHandler';
import { useUser } from '../ContextUser';
const Tabs = createBottomTabNavigator();
export default function TabLayout() {
const colorScheme = useColorScheme();
const { role } = useUser();
// Handle auth in tabs layout
useAuthHandler();
return (
<Tabs screenOptions={{tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, headerShown: false, tabBarButton: HapticTab}}>
<Tabs.Screen name="index" options={{ href: null}}/>
<Tabs.Screen name="explore" options={{ href: null }}/>
<Tabs.Screen name="templateSreen" options={{ href: null}}/>
<Tabs.Navigator
initialRouteName='explore'
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}}>
<Tabs.Screen
name="home"
component={Home}
options={{
title: 'Home',
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
),
}}
/>
/>
<Tabs.Screen
name="gestionnaire_ressource"
component={ListMateriel}
options={{
title: 'Ressources',
tabBarIcon: ({ color }) => (
@@ -39,29 +48,33 @@ export default function TabLayout() {
}}
/>
<Tabs.Screen
name="gestion_user"
name="bonjourFL"
component={BonjourScreen}
options={{
title: 'Users',
title: 'Bonjour',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />,
}}
/>
<Tabs.Screen
name="mapScreen"
name="explore"
component={MapScreen}
options={{
title: 'Map',
title: 'MapScreen',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
}}
/>
>
</Tabs.Screen>
<Tabs.Screen
name="addScreen"
name="ajouterChantier"
component={ListMateriel}
options={{
title: 'Ajouter',
href: role === 'resp' ? '/(tabs)/addScreen' : null,
tabBarIcon: ({ color }) => (
<AntDesign name="plus" size={24} color={color} />
<AntDesign name="plus" size={24} color="black" />
),
}}
/>
</Tabs>
/>
</Tabs.Navigator>
);
}
}

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

@@ -0,0 +1,15 @@
import ChantierSummary from '@/components/chantierSummary';
import SelectChantier from '@/components/selectChantier';
import SetStatus from '@/components/setStatus';
import { ThemedView, } from '@/components/themed-view';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useChantier } from '../ContextChantier';
import { useRessources } from '../ContextRessource';
export default function ajouterChantier() {
const { chantier, setChantier } = useChantier();
//const { artisant, setArtisant } = useArtisant();
const { ressources, setRessources } = useRessources();
}

164
app/(tabs)/bonjourFL.tsx Normal file
View File

@@ -0,0 +1,164 @@
import SelectChantier from "@/components/selectChantier";
import { ThemedText } from "@/components/themed-text";
import { ThemedTextInput } from "@/components/themed-textinput";
import { ThemedView } from "@/components/themed-view";
import Constants from "expo-constants"; //pour connaître la taille de la barre menu de l'OS en haut
import { useLocalSearchParams, useRouter } from "expo-router";
import React, { useMemo, useState } from "react";
import { FlatList, Image, StyleSheet, Text } from "react-native";
import rawConcerts from "../../data/concerts.json";
import { useChantier } from "../ContextChantier";
type Concert = {
group: string;
date: string;
nationality: string;
location: string;
price: number;
ticketsLeft: number;
Image: string;
favorite: boolean;
};
export default function BonjourScreen() {
const router = useRouter();
const { nom, prenom } = useLocalSearchParams(); // Recup data ecran precedent
const [search, setSearch] = useState("");
const { chantier, setChantier } = useChantier();
const concertsData: Concert[] = Array.isArray(rawConcerts)
? (rawConcerts as Concert[])
: [];
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) {
return null;
}
return (
<ThemedView lvl={1} shadow={true} style={styles.card}>
<Image source={{ uri: item.Image }} style={styles.image} />
<ThemedView lvl={1} style={styles.info}>
<ThemedText style={styles.group}>{item.group}</ThemedText>
<ThemedText>{item.date}</ThemedText>
<ThemedText>{item.location}</ThemedText>
</ThemedView>
</ThemedView>
);
};
return (
<ThemedView lvl={3} style={styles.container}>
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ paddingBottom: 40 }}
ListHeaderComponent={
<ThemedView opacity="00" style={styles.header}>
<ThemedText style={styles.text}>
Bonjour {prenom} {nom} {chantier && chantier.chef.name}
</ThemedText>
<ThemedView style={styles.inputBack} shadow={true}>
<ThemedTextInput
lvl={0}
style={styles.input}
placeholder="Rechercher un artisant..."
value={search}
onChangeText={setSearch}
/>
</ThemedView>
</ThemedView>
}
ListEmptyComponent={
<Text style={styles.empty}>Aucun résultat n'a été trouvé</Text>
}
/>
<ThemedView
style={{
width: "100%",
position: "absolute",
backgroundColor: "transparent",
}}
>
<SelectChantier></SelectChantier>
</ThemedView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
//backgroundColor: '#00FFFF',
},
header: {
marginTop: 60,
marginBottom: 20,
alignItems: "center",
paddingHorizontal: 20,
},
text: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
},
inputBack: {
width: "100%",
borderRadius: 10,
backgroundColor: "transparent",
},
input: {
width: "100%",
//borderWidth: 1,
//borderColor: '#ccc',
borderRadius: 10,
padding: 10,
fontSize: 16,
},
card: {
flexDirection: "row",
marginHorizontal: 20,
marginBottom: 15,
//borderWidth: 1,
//borderColor: '#ddd',
borderRadius: 10,
padding: 10,
//backgroundColor: '#fafafa',
},
image: {
width: 80,
height: 80,
borderRadius: 8,
marginRight: 10,
},
info: {
flex: 1,
justifyContent: "center",
},
group: {
fontWeight: "bold",
fontSize: 16,
marginBottom: 5,
},
footer: {
padding: 20,
},
empty: {
textAlign: "center",
marginTop: 30,
color: "#888",
},
});

View File

@@ -1,11 +1,11 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { ExternalLink } from '@/components/expoExempleComponents/external-link';
import ParallaxScrollView from '@/components/expoExempleComponents/parallax-scroll-view';
import { ExternalLink } from '@/components/external-link';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import SelectChantier from '@/components/selectChantier';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from '@/components/theme/themed-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Collapsible } from '@/components/ui/collapsible';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';

View File

@@ -1,154 +0,0 @@
import { ThemedText } from "@/components/theme/themed-text";
import { ThemedTextInput } from "@/components/theme/themed-textinput";
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 React, { useEffect, useState } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import { getUsers } from "@/services/ressourcesService";
import SelectChantier from "@/components/selectChantier";
import { User } from "@/class/class";
type Concert = {
group: string;
date: string;
nationality: string;
location: string;
price: number;
ticketsLeft: number;
Image: string;
favorite: boolean;
};
export default function GestionUser() {
const [search, setSearch] = useState("");
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
async function loadData() {
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 }) => {
if (!item) {
return null;
}
return (
<ThemedView lvl={1} shadow={true} style={styles.card}>
<ThemedView lvl={1} style={styles.info}>
<ThemedText style={styles.group}>{item.name} {item.last_name}</ThemedText>
<ThemedText>{item.role}</ThemedText>
</ThemedView>
</ThemedView>
);
};
return (
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
<View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier>
</View>
<FlatList
data={users}
renderItem={renderItem}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ paddingBottom: 40 }}
ListHeaderComponent={
<View style={styles.header}>
<ThemedView style={styles.inputBack} shadow={true}>
<ThemedTextInput
lvl={0}
style={styles.input}
placeholder="Rechercher un artisant..."
value={search}
onChangeText={setSearch}
/>
</ThemedView>
</View>
}
ListEmptyComponent={
<Text style={styles.empty}>Aucun résultat n'a été trouvé</Text>
}
/>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
//backgroundColor: '#00FFFF',
},
header: {
marginTop: 60,
marginBottom: 10,
alignItems: "center",
paddingHorizontal: 20,
},
text: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
},
inputBack: {
width: "100%",
borderRadius: 10,
backgroundColor: "transparent",
},
input: {
width: "100%",
//borderWidth: 1,
//borderColor: '#ccc',
borderRadius: 10,
padding: 10,
fontSize: 16,
},
card: {
flexDirection: "row",
marginHorizontal: 20,
marginBottom: 15,
//borderWidth: 1,
//borderColor: '#ddd',
borderRadius: 10,
padding: 10,
//backgroundColor: '#fafafa',
},
image: {
width: 80,
height: 80,
borderRadius: 8,
marginRight: 10,
},
info: {
flex: 1,
justifyContent: "center",
},
group: {
fontWeight: "bold",
fontSize: 16,
marginBottom: 5,
},
footer: {
padding: 20,
},
empty: {
textAlign: "center",
marginTop: 30,
color: "#888",
},
});

View File

@@ -1,32 +1,23 @@
import { ThemedText } from "@/components/theme/themed-text";
import { ThemedTextInput } from "@/components/theme/themed-textinput";
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 { ThemedButton } from "@/components/theme/themed-button";
import { ThemedText } from "@/components/themed-text";
import { ThemedTextInput } from "@/components/themed-textinput";
import { ThemedView } from "@/components/themed-view";
import { ThemedButton } from "@/components/themed-button";
import { useLocalSearchParams, useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { FlatList, Image, StyleSheet, Text, View } from "react-native";
import { FlatList, Image, StyleSheet, Text } from "react-native";
import { Ressources } from "../../class/class";
import { getReservations, getRessources } from "../../services/ressourcesService";
import SelectChantier from "@/components/selectChantier";
import { useRessources } from "../ContextRessource";
import { useChantier } from "../ContextChantier";
import { getNbUseRessources, getNbUseRessourcesInChantier, isInChantier } from "@/class/utils";
import { useReservations } from "../ContextReservation";
import { getRessources } from "../../services/ressourcesService";
export default function GestionnaireRessource() {
const { nom, prenom } = useLocalSearchParams();
const [search, setSearch] = useState("");
const {ressources, setRessources} = useRessources();
const {reservations, setReservations} = useReservations();
const {chantier, setChantier} = useChantier();
const [filterType, setFilterType] = useState("Tout");
const [ressource, setRessources] = useState<Ressources[]>([]);
const [filterType, setFilterType] = useState("tout");
const [showFilterMenu, setShowFilterMenu] = useState(false);
const [filterChantier, setFilterChantier] = useState(false);
const router = useRouter();
useEffect(() => {
async function loadDataRessources() {
async function loadData() {
try {
const data = await getRessources();
setRessources(data);
@@ -34,23 +25,13 @@ export default function GestionnaireRessource() {
console.error("Erreur lors du chargement :", error);
}
}
async function loadDataReservations() {
try {
const data = await getReservations();
setReservations(data);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadDataRessources();
loadDataReservations();
loadData();
}, []);
const filteredData = ressources.filter((r) => {
const filteredData = ressource.filter((r) => {
const matchName = r.name.toLowerCase().includes(search.toLowerCase());
const matchType = filterType === "Tout" || r.type === filterType;
return matchName && matchType && (!filterChantier || (chantier && isInChantier(r,chantier,reservations)));
const matchType = filterType === "tout" || r.type === filterType;
return matchName && matchType;
});
const renderRessource = ({ item }: { item: Ressources }) => {
@@ -59,121 +40,97 @@ export default function GestionnaireRessource() {
<ThemedView lvl={1} shadow={true} style={styles.card}>
<Image source={{ uri: item.Image }} style={styles.image} />
<ThemedView lvl={1} style={styles.info}>
<ThemedText>Nom : {item.name}</ThemedText>
<ThemedText>Type : {item.type}</ThemedText>
<ThemedText>Quantité totale : {item.quantity}</ThemedText>
<ThemedText>Quantité disponible : {item.quantity-getNbUseRessources(item, reservations)}</ThemedText>
{filterChantier&&chantier &&
<ThemedText>Quantité utilisé dans le chantier : {getNbUseRessourcesInChantier(item,chantier, reservations)}</ThemedText>
}
<ThemedText>{item.id}</ThemedText>
<ThemedText>{item.name}</ThemedText>
<ThemedText>{item.type}</ThemedText>
<ThemedText>{item.quantity}</ThemedText>
<ThemedText>{item.available_quantity}</ThemedText>
</ThemedView>
</ThemedView>
);
};
return (
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
<View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier>
</View>
{/* Overlay menu filtre */}
{showFilterMenu && (
<ThemedView lvl={2} style={styles.filterMenuOverlay}>
<ThemedView lvl={5} style={styles.filterMenu}>
<ThemedText style={styles.filterTitle}>Filtrer par type</ThemedText>
{["Tout", "Outil", "Machine","Ouvrier"].map((t) => (
<ThemedButton
key={t}
lvl={1}
shadow={true}
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
onPress={async () => {
setFilterType(t);
setShowFilterMenu(false);
const updateRessource = await getRessources();
setRessources(updateRessource)
}}
>
<ThemedText style={{ textAlign: "center" }}>{t}</ThemedText>
</ThemedButton>
))}
{/* Bouton "Fermer" remplacé */}
<ThemedButton
lvl={1}
shadow={true}
style={{ padding: 10, borderRadius: 8 }}
onPress={() => setShowFilterMenu(false)}
>
<ThemedText style={{ textAlign: "center" }}>Fermer</ThemedText>
<ThemedView lvl={3} style={styles.container}>
{/* Overlay menu filtre */}
{showFilterMenu && (
<ThemedView lvl={2} style={styles.filterMenuOverlay}>
<ThemedView lvl={1} style={styles.filterMenu}>
<Text style={styles.filterTitle}>Filtrer par type</Text>
{["tout", "Outil", "Machine"].map((t) => (
<ThemedButton
key={t}
lvl={1}
shadow={true}
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
onPress={() => {
setFilterType(t);
setShowFilterMenu(false);
}}
>
<ThemedText style={{ textAlign: "center" }}>{t}</ThemedText>
</ThemedButton>
))}
{/* Bouton "Fermer" remplacé */}
<ThemedButton
lvl={1}
shadow={true}
style={{ padding: 10, borderRadius: 8 }}
onPress={() => setShowFilterMenu(false)}
>
<ThemedText style={{ textAlign: "center" }}>Fermer</ThemedText>
</ThemedButton>
</ThemedView>
</ThemedView>
)}
<FlatList
data={filteredData}
renderItem={renderRessource}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ paddingBottom: 40 }}
ListHeaderComponent={
<ThemedView opacity="00" style={styles.header}>
<ThemedText style={styles.text}>
Bonjour {prenom} {nom}
</ThemedText>
{/* Bouton filtre en haut à droite */}
<ThemedButton
lvl={1}
shadow={true}
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
onPress={() => setShowFilterMenu(true)}
>
<ThemedText>{`Filtre: ${filterType}`}</ThemedText>
</ThemedButton>
<ThemedView lvl={1} shadow={true} style={styles.inputBack}>
<ThemedTextInput
lvl={0}
style={styles.input}
placeholder="Rechercher une ressource..."
value={search}
onChangeText={setSearch}
/>
</ThemedView>
</ThemedView>
)}
<FlatList
data={filteredData}
renderItem={renderRessource}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ paddingBottom: 40 }}
ListHeaderComponent={
<View style={styles.header}>
<ThemedView lvl={1} shadow={true} style={styles.inputBack}>
<ThemedTextInput
lvl={0}
style={styles.input}
placeholder="Rechercher une ressource..."
value={search}
onChangeText={setSearch}
/>
</ThemedView>
{/* Bouton filtre en haut à droite */}
<View style={{flexDirection: "row", gap:5}}>
<ThemedButton
lvl={1}
shadow={true}
style={styles.button}
onPress={() => setShowFilterMenu(true)}
>
<ThemedText>{`Filtre: ${filterType}`}</ThemedText>
</ThemedButton>
<ThemedButton style={styles.button}>
<ThemedText onPress={() => setFilterChantier(!filterChantier)}>
{filterChantier?"chantier courant":"tous"}
</ThemedText>
</ThemedButton>
</View>
</View>
}
ListEmptyComponent={
<ThemedText style={styles.empty}>Aucun résultat trouvé</ThemedText>
}
/>
</View>
}
ListEmptyComponent={
<ThemedText style={styles.empty}>Aucun résultat trouvé</ThemedText>
}
/>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: Constants.statusBarHeight,
marginTop: 60,
},
header: {
marginTop: 60,
marginBottom: 20,
alignItems: "center",
paddingHorizontal: 20,
@@ -234,6 +191,7 @@ const styles = StyleSheet.create({
width: "80%",
borderRadius: 12,
padding: 20,
backgroundColor: "#fff",
},
filterTitle: {
fontSize: 18,
@@ -241,9 +199,4 @@ const styles = StyleSheet.create({
marginBottom: 20,
textAlign: "center",
},
button:{
padding: 10,
borderRadius: 8,
marginTop: 10
},
});

View File

@@ -1,13 +1,11 @@
import ChantierSummary from '@/components/chantierSummary';
import SelectChantier from '@/components/selectChantier';
import SetStatus from '@/components/setStatus';
import { ThemedView, } from '@/components/theme/themed-view';
import { ThemedView, } from '@/components/themed-view';
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
import React from 'react';
import { StyleSheet, View,Text, ScrollView } from 'react-native';
import { StyleSheet, View } from 'react-native';
import { useChantier } from '../ContextChantier';
import Anomaly from '@/components/anomaly';
import { useUser } from '../ContextUser';
@@ -16,25 +14,21 @@ export default function Home() {
const { chantier, setChantier } = useChantier();
const { role } = useUser();
console.log("User", role);
return(
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
<ChantierSummary style={styles.summary} data={{ chantier }} />
<View style={{width:"100%", position: 'absolute'}}>
<SelectChantier></SelectChantier>
</View>
{chantier&&
<View style={{width:"100%", position: 'absolute',marginLeft:"50%"}}>
<SetStatus></SetStatus>
</View>
}
<ScrollView>
<View style={{paddingTop:60}}>
<ChantierSummary style={styles.summary} data={{ chantier }} />
<Anomaly style={styles.anomaly} data={{chantier}}/>
{role === "chef"}
{role === "chef" && (
<View style={{width:"100%", position: 'absolute',marginLeft:"50%"}}>
<SetStatus></SetStatus>
</View>
</ScrollView>
)}
{role === "resp"}
</View>
</ThemedView>
@@ -55,9 +49,7 @@ const styles = StyleSheet.create({
width:"100%"
},
summary:{
padding:10,
},
anomaly:{
marginTop:60,
padding:10,
}
});

View File

@@ -1,10 +1,10 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { HelloWave } from '@/components/expoExempleComponents/hello-wave';
import ParallaxScrollView from '@/components/expoExempleComponents/parallax-scroll-view';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from '@/components/theme/themed-view';
import { HelloWave } from '@/components/hello-wave';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Link } from 'expo-router';
export default function HomeScreen() {

View File

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

View File

@@ -1,31 +0,0 @@
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 React from "react";
import {StyleSheet, View } from "react-native";
import { useChantier } from "../ContextChantier";
export default function TemplateScreen() {
const { chantier, setChantier } = useChantier();
return (
<ThemedView lvl={3} style={styles.back}>
<View style={styles.container}>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
},
});

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

@@ -14,13 +14,11 @@ import { auth, db } from "../firebase_config";
import { Platform, UIManager } from 'react-native';
import { ChantierProvider } from "./ContextChantier";
import { UserProvider } from "./ContextUser";
import { useUser } from "./ContextUser";
import { RessourcesProvider } from "./ContextRessource";
import { ReservationsProvider } from "./ContextReservation";
import LoginScreen from "./login/login";
export const unstable_settings = {
anchor: "(tabs)",
};
@@ -28,56 +26,63 @@ export const unstable_settings = {
export default function RootLayout() {
const colorScheme = useColorScheme();
const router = useRouter();
const [user, setUser] = useState<User | null>(null);
const [userRole, setUserRole] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const { setUser, setRole, user } = useUser();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
if (!currentUser) {
console.log("No user logged in");
setUser(null);
setUserRole(null);
router.replace("/login/login");
setRole(null);
setLoading(false);
return;
}
setUser(currentUser);
const userDocRef = doc(db, "user", currentUser.uid);
const userDoc = await getDoc(userDocRef);
console.log("User doc data:", userDoc.data());
if (!userDoc.exists()) {
router.replace("/login/login");
setUser(null);
setUserRole(null);
setLoading(false);
return;
}
const { role } = userDoc.data();
setRole(userDoc.data().role);
setUser(currentUser);
setUserRole(role);
router.replace("/(tabs)/home");
setLoading(false);
});
return unsubscribe;
return unsubscribe;
}, []);
if (loading) {
return null; // ou un SplashScreen
}
return (
<UserProvider>
<ChantierProvider>
<RessourcesProvider>
<ReservationsProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
{user ? (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="selectChantier" options={{ headerShown: false }}/>
<Stack.Screen name="selectChantier" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: "modal", title: "Modal" }}
/>
<Stack.Screen name="login" options={{ headerShown: false }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
</ReservationsProvider>
) : (
<LoginScreen />
)}
<StatusBar style="auto" />
</ThemeProvider>
</RessourcesProvider>
</ChantierProvider>
</UserProvider>
);
}

10
app/layout.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { Slot } from "expo-router";
import { UserProvider } from "./ContextUser";
export default function Layout() {
return (
<UserProvider>
<Slot />
</UserProvider>
);
}

View File

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

View File

@@ -1,8 +1,8 @@
import { Link } from 'expo-router';
import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from '@/components/theme/themed-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
export default function ModalScreen() {
return (

View File

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

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,162 +0,0 @@
import { Chantier } from '@/class/class';
import { ThemedView } from '@/components/theme/themed-view';
import React, { use, useEffect, useState } from 'react';
import { TouchableOpacity, StyleProp, StyleSheet, View, Image, ViewStyle,Text, TextInput, ScrollView } from 'react-native';
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 = {
data: {
chantier:Chantier|null;
}
style?: StyleProp<ViewStyle>;
};
export default function Anomaly({data,style}: Props) {
const{syncChantier }= useChantier();
const [imageUri, setImageUri] = useState<string | null>(null);
const handleDelete = async (anomaly: string) => {
await deleteAnomalie(data.chantier!.id, anomaly);
data.chantier!.anomalies = data.chantier!.anomalies.filter(a => a !== anomaly);
await syncChantier();
};
const handleAdd = async () => {
if (newAnomaly.trim().length === 0) return;
await addAnomalie(data.chantier!.id, newAnomaly.trim());
data.chantier!.anomalies.push(newAnomaly.trim());
setNewAnomaly("");
await syncChantier();
};
//DEBUG
useEffect(() => {
console.log("imageUri changed", imageUri);
}, [imageUri]);
// Image picker function (not used currently)
const selectImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (!result.canceled) {
console.log(result);
setImageUri(result.assets[0].uri.replace('file://', ''));
} else {
alert('You did not select any image.');
}
};
const [newAnomaly, setNewAnomaly] = useState("");
return(
<View style={style}>
{data.chantier ? (
<ThemedView lvl={4} style={styles.anomaliesContainer}>
<ThemedText style={styles.anomaliesTitle}>Anomalies</ThemedText>
{/* Add Anomaly Section */}
<View style={styles.addContainer}>
<TextInput style={styles.input} placeholder="Nouvelle anomalie..." value={newAnomaly} onChangeText={setNewAnomaly} />
<TouchableOpacity style={styles.addButton} onPress={handleAdd}>
<ThemedText style={styles.addButtonText}>Ajouter</ThemedText>
</TouchableOpacity>
<TouchableOpacity onPress={selectImage} style={styles.addButton}>
<ThemedText style={styles.addButton}>Choisir une image</ThemedText>
</TouchableOpacity>
{imageUri && (
<Image source={{ uri: imageUri }} style={styles.image} />
)}
</View>
{data.chantier.anomalies.length > 0 ? (
data.chantier.anomalies.map((anomaly, index) => (
<ThemedView key={index} lvl={2} style={styles.anomalyItem}>
<ThemedText style={styles.anomalyText}> {anomaly}</ThemedText>
<TouchableOpacity onPress={() => handleDelete(anomaly)} style={styles.deleteButton}>
<Text style={styles.deleteText}></Text>
</TouchableOpacity>
</ThemedView>
))
) : (
<ThemedText style={styles.noAnomaly}>Aucune anomalie</ThemedText>
) }
</ThemedView>
): null}
</View>
)
}
const styles = StyleSheet.create({
//Anomalies styles
anomaliesContainer: {
padding: 5,
borderRadius: 10,
},
anomaliesTitle: {
fontSize: 16,
fontWeight: "bold",
marginBottom: 8,
},
anomalyItem: {
flexDirection: "row",
alignItems: "flex-start",
padding: 8,
marginBottom: 5,
borderRadius: 8,
},
anomalyText: {
flex: 1,
marginLeft: 5,
},
noAnomaly: {
fontStyle: "italic",
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

@@ -1,9 +1,9 @@
import { Chantier } from '@/class/class';
import { ThemedView, } from '@/components/theme/themed-view';
import { ThemedView, } from '@/components/themed-view';
import React from 'react';
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { ThemedText } from './theme/themed-text';
import { getNbItemReservation } from '@/class/utils';
import { ThemedText } from './themed-text';
type Props = {
data: {
@@ -13,27 +13,19 @@ type Props = {
};
export default function ChantierSummary({data,style , ...otherProps }: Props) {
return(
<View style={style}>
<View style={style}>
{data.chantier ? (
<ThemedView lvl={4} style={styles.chantier}>
<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 style={{flex: 1}}>
<ThemedText selectable={true}>Objet: {data.chantier.name}</ThemedText>
<ThemedText selectable={true}>Adresse: {data.chantier.adresse}</ThemedText>
<ThemedText selectable={true}>Chef de chantier: {data.chantier.chef.last_name}{" "}{data.chantier.chef.name}</ThemedText>
<ThemedText selectable={true}>État: {data.chantier.etat}</ThemedText>
<ThemedText selectable={true}>
equipe: {getNbItemReservation(data.chantier.equipe)} ({data.chantier.equipe.length} type{data.chantier.equipe.length>1&&"s"})
</ThemedText>
<ThemedText selectable={true}>
materiel: {getNbItemReservation(data.chantier.materiel)} ({data.chantier.materiel.length} type{data.chantier.materiel.length>1&&"s"})
</ThemedText>
<ThemedText selectable={true}>
vehicules: {getNbItemReservation(data.chantier.vehicules)} ({data.chantier.vehicules.length} type{data.chantier.vehicules.length>1&&"s"})
</ThemedText>
<View>
<ThemedText>Adresse: {data.chantier.adresse}</ThemedText>
<ThemedText>Chef de chantier: {data.chantier.chef.last_name}{" "}{data.chantier.chef.name}</ThemedText>
<ThemedText>État: {data.chantier.etat}</ThemedText>
</View>
</ThemedView>
) :
@@ -53,12 +45,13 @@ const styles = StyleSheet.create({
borderRadius: 10,
//borderWidth: 1,
flexDirection: 'row',
//height: 150,
gap: 10,
height: 150,
},
image:{
margin:0,
width: 70,
height: 140,
borderRadius: 5,
marginRight: 10,
},
});

View File

@@ -7,7 +7,7 @@ import Animated, {
useScrollOffset,
} from 'react-native-reanimated';
import { ThemedView } from '@/components/theme/themed-view';
import { ThemedView } from '@/components/themed-view';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useThemeColor } from '@/hooks/use-theme-color';
@@ -23,7 +23,7 @@ export default function ParallaxScrollView({
headerImage,
headerBackgroundColor,
}: Props) {
const backgroundColor = useThemeColor({}, 'background3');
const backgroundColor = useThemeColor({}, 'background');
const colorScheme = useColorScheme() ?? 'light';
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollOffset(scrollRef);

View File

@@ -1,12 +1,9 @@
import { useChantier } from "@/app/ContextChantier";
import { Chantier } from "@/class/class";
import { getChantiers } from "@/services/ressourcesService";
import { useRouter } from "expo-router";
import { useEffect, useState } from "react";
import {
ActivityIndicator,
Dimensions,
FlatList,
Image,
Pressable,
ScrollView,
@@ -14,16 +11,12 @@ import {
View
} from "react-native";
import Animated, {
interpolate,
LinearTransition,
useAnimatedStyle,
useSharedValue,
withTiming
LinearTransition
} from "react-native-reanimated";
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 { ThemedButton } from "./themed-button";
import { ThemedText } from "./themed-text";
import { ThemedTextInput } from "./themed-textinput";
import { ThemedView } from "./themed-view";
const screenHeight = Dimensions.get("window").height;
const { width, height } = Dimensions.get("window");
@@ -40,11 +33,8 @@ export default function SelectChantier() {
const { chantier, setChantier } = useChantier();
const [search, setSearch] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const [chantiers, setChantiers] = useState<Chantier[]>([]);
const router = useRouter();
const AnimatedThemedView = Animated.createAnimatedComponent(ThemedView);
const AnimatedThemedText = Animated.createAnimatedComponent(ThemedText);
const AnimatedThemedButton = Animated.createAnimatedComponent(ThemedButton);
@@ -52,80 +42,43 @@ export default function SelectChantier() {
Animated.createAnimatedComponent(ThemedTextInput);
async function onPressOpen(){
setIsLoaded(false);
setIsOpen(!isOpen);
if(!isOpen){
const updatedChantiers = await getChantiers();
setIsLoaded(true);
setChantiers(updatedChantiers)
}
}
useEffect(() => {
open.value = withTiming(isOpen ? 1 : 0);
}, [isOpen]);
function onPressAddChantier(){
/*useEffect(() => {
}
useEffect(() => {
async function loadChantiers() {
const list = await getChantiers();
setChantiers(list);
}
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 {
setChantier(chantier);
setIsOpen(false);
}
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 }) => {
const renderChantier = (chantier: Chantier, index: number) => {
return (
<Pressable onPress={() => selectChantier(item)}>
<Pressable key={index} onPress={() => selectChantier(chantier)}>
<ThemedView lvl={4} style={styles.chantier}>
<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 style={{flex: 1}}>
<ThemedText>Objet: {item.name}</ThemedText>
<ThemedText>Adresse: {item.adresse}</ThemedText>
<ThemedText>Chef de chantier: {item.chef.last_name}{" "}{item.chef.name}</ThemedText>
<ThemedText>État: {item.etat}</ThemedText>
<View>
<ThemedText>{chantier.chef != null ? "true" : "false"}</ThemedText>
<ThemedText>Adresse: {chantier.adresse}</ThemedText>
<ThemedText>Chef de chantier: {chantier.chef.last_name}{" "}{chantier.chef.name}</ThemedText>
<ThemedText>État: {chantier.etat}</ThemedText>
</View>
</ThemedView>
</Pressable>
@@ -133,40 +86,39 @@ export default function SelectChantier() {
};
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)}/>)}
<ThemedView lvl={2} shadow={true} style={styles.window}>
<AnimatedThemedButton style={animatedButtonStyle} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}>
<AnimatedThemedView layout={LinearTransition.duration(200)} lvl={2} shadow={true} style={styles.window}>
<AnimatedThemedButton style={isOpen ? styles.buttonOpen : styles.buttonClose} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}>
<ThemedText style={styles.buttonText}>
{isOpen ? "Fermer" : (chantier!=null ? chantier.name : "Chantier")}
{isOpen ? "Fermer" : (chantier!=null ? chantier.adresse : "Chantier")}
</ThemedText>
</AnimatedThemedButton>
{isOpen && (
<View style={styles.menu}>
<ThemedTextInput lvl={1} border={4} style={styles.input} placeholder="Rechercher un chantier" value={search} onChangeText={setSearch}/>
<View style={styles.list}>
{isLoaded?
<FlatList
data={filteredChantiers}
renderItem={renderChantier}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={{ gap: 8 }}
/>
<ThemedButton style={styles.buttonAdd} onPress={() => onPressAddChantier()}>
<ThemedText style={styles.buttonText}>
+
</ThemedText>
</ThemedButton>
: <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />}
</View>
<ThemedView lvl={2} style={styles.list}>
<ScrollView contentContainerStyle={styles.chantiersList}>
{chantiers.map((chantier, index) =>
renderChantier(chantier, index)
)}
</ScrollView>
</ThemedView>
</View>
)}
</ThemedView>
</AnimatedThemedView>
</Animated.View>
);
}
const styles = StyleSheet.create({
windowClose: {
//backgroundColor: '#00FF0040',
@@ -225,7 +177,7 @@ const styles = StyleSheet.create({
borderRadius: 10,
//borderWidth: 1,
flexDirection: 'row',
//height: 130,
height: 100,
},
image:{
margin:0,
@@ -259,4 +211,8 @@ const styles = StyleSheet.create({
buttonText: {
textAlign: "center",
},
buttonAdd:{
borderRadius: 10,
marginBottom: 10,
}
});

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

@@ -3,9 +3,9 @@ import { changeChantierStatus } from "@/services/ressourcesService";
import { useState } from 'react';
import { Dimensions, LayoutAnimation, Modal, Pressable, StyleSheet, View } 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 { ThemedButton } from './themed-button';
import { ThemedText } from './themed-text';
import { ThemedView } from "./themed-view";
const { width, height } = Dimensions.get("window");
@@ -70,7 +70,7 @@ export default function SetStatus() {
<Modal transparent={true} >
<View style={styles.overlay}>
<ThemedView style={styles.overlayView}>
<ThemedText style={{fontSize: 25}}>Changer l'état du chantier en {tempStatus} ?</ThemedText>
<ThemedText style={{fontSize: 25}}>Changer l'était du chantier en {tempStatus} ?</ThemedText>
<View style={styles.overlayView}>
<ThemedButton lvl={2} border={5} style={styles.buttonValid} onPress={() => onConfirm()}>
<ThemedText style={{fontSize: 25}}>Confirmer</ThemedText>
@@ -121,7 +121,7 @@ const styles = StyleSheet.create({
windowBox:{
zIndex: 2,
//backgroundColor: '#00FFFF40',
width:"35%",
width:"30%",
padding: 10,
paddingLeft: 0,
//overflow: 'hidden',
@@ -162,7 +162,6 @@ const styles = StyleSheet.create({
borderRadius: 15,
padding: 10,
height:40,
justifyContent: 'center',
},
centeredText:{
textAlign: 'center',
@@ -189,7 +188,6 @@ const styles = StyleSheet.create({
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
});

View File

@@ -6,32 +6,23 @@ export type ThemedPressableProps = PressableProps & {
lightColor?: string;
darkColor?: string;
lvl?:number;
lvlPressed?:number;
border?:number;
opacity?:string;
shadow?: boolean;
};
//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 lvlStrPressed:string = "background";
var borderColor ="";
var borderWidth = 0;
if(lvl>=0 && lvl<6){
lvlStr+=lvl;
}
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 backgroundPressed = useThemeColor({ light: lightColor, dark: darkColor },lvlStrPressed as 'background0'|'background1'|'background2'|'background3'|'background4'|'background5')+opacity;
if(border!=-1){
var borderStr = "";
@@ -55,9 +46,5 @@ export function ThemedButton({ style, lightColor, darkColor,lvl=1,lvlPressed=1,b
shadowRadius: 6,
}
return <Pressable style={(state) =>[{
backgroundColor: state.pressed ? backgroundPressed: backgroundColor,
borderColor,
borderWidth
}, shadow && shadowStyle, typeof style === 'function' ? style(state) : style,]} {...otherProps}/>;
return <Pressable style={(state) =>[{ backgroundColor, borderColor, borderWidth }, shadow && shadowStyle, typeof style === 'function' ? style(state) : style,]} {...otherProps}/>;
}

View File

@@ -1,8 +1,8 @@
import { PropsWithChildren, useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from '@/components/theme/themed-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';

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,
};
};

140
package-lock.json generated
View File

@@ -9,18 +9,15 @@
"version": "1.0.0",
"dependencies": {
"@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.2",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.25",
"@react-navigation/native": "^7.1.19",
"expo": "~54.0.13",
"expo-constants": "~18.0.9",
"expo-font": "~14.0.9",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.9",
"expo-image-picker": "~17.0.10",
"expo-linking": "~8.0.8",
"expo-location": "~19.0.8",
"expo-router": "~6.0.11",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
@@ -32,8 +29,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.9.0",
"react-native-maps": "1.20.1",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@@ -42,7 +38,6 @@
},
"devDependencies": {
"@types/react": "~19.1.0",
"baseline-browser-mapping": "^2.9.6",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
@@ -2331,7 +2326,6 @@
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz",
"integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==",
"peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
@@ -2397,7 +2391,6 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz",
"integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==",
"peer": true,
"dependencies": {
"@firebase/app": "0.14.6",
"@firebase/component": "0.7.0",
@@ -3583,29 +3576,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": {
"version": "0.81.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz",
@@ -3836,17 +3806,17 @@
"license": "MIT"
},
"node_modules/@react-navigation/bottom-tabs": {
"version": "7.8.12",
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.12.tgz",
"integrity": "sha512-efVt5ydHK+b4ZtjmN81iduaO5dPCmzhLBFwjCR8pV4x4VzUfJmtUJizLqTXpT3WatHdeon2gDPwhhoelsvu/JA==",
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.2.tgz",
"integrity": "sha512-QOcRZj6hA5QZg8PztlEaNOjbQRq75NKM26yTFXdL81OWM2qgry3tascIhBvLUak35NryG1iqQXzSpgn3I/86+g==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^2.9.2",
"@react-navigation/elements": "^2.8.1",
"color": "^4.2.3",
"sf-symbols-typescript": "^2.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^7.1.25",
"@react-navigation/native": "^7.1.19",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0",
@@ -3854,12 +3824,12 @@
}
},
"node_modules/@react-navigation/core": {
"version": "7.13.6",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.6.tgz",
"integrity": "sha512-7QG29HAWOR8wYuPkfTN8L2Po+kE1xn3nsi2sS35sGngq8HYZRHfXvxrhrAZYfFnFq2hUtOhcXnSS6vEWU/5rmA==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.0.tgz",
"integrity": "sha512-Fc/SO23HnlGnkou/z8JQUzwEMvhxuUhr4rdPTIZp/c8q1atq3k632Nfh8fEiGtk+MP1wtIvXdN2a5hBIWpLq3g==",
"license": "MIT",
"dependencies": {
"@react-navigation/routers": "^7.5.2",
"@react-navigation/routers": "^7.5.1",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
@@ -3873,9 +3843,9 @@
}
},
"node_modules/@react-navigation/elements": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.2.tgz",
"integrity": "sha512-J1GltOAGowNLznEphV/kr4zs0U7mUBO1wVA2CqpkN8ePBsoxrAmsd+T5sEYUCXN9KgTDFvc6IfcDqrGSQngd/g==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.1.tgz",
"integrity": "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ==",
"license": "MIT",
"dependencies": {
"color": "^4.2.3",
@@ -3884,7 +3854,7 @@
},
"peerDependencies": {
"@react-native-masked-view/masked-view": ">= 0.2.0",
"@react-navigation/native": "^7.1.25",
"@react-navigation/native": "^7.1.19",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0"
@@ -3896,13 +3866,13 @@
}
},
"node_modules/@react-navigation/native": {
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz",
"integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==",
"version": "7.1.19",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.19.tgz",
"integrity": "sha512-fM7q8di4Q8sp2WUhiUWOe7bEDRyRhbzsKQOd5N2k+lHeCx3UncsRYuw4Q/KN0EovM3wWKqMMmhy/YWuEO04kgw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^7.13.6",
"@react-navigation/core": "^7.13.0",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
@@ -3931,9 +3901,9 @@
}
},
"node_modules/@react-navigation/routers": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.2.tgz",
"integrity": "sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw==",
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz",
"integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==",
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11"
@@ -5316,9 +5286,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
"integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
"version": "2.8.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz",
"integrity": "sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -7130,27 +7100,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": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz",
@@ -7176,15 +7125,6 @@
"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": {
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.15.tgz",
@@ -7969,7 +7909,6 @@
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/firebase/-/firebase-12.6.0.tgz",
"integrity": "sha512-8ZD1Gcv916Qp8/nsFH2+QMIrfX/76ti6cJwxQUENLXXnKlOX/IJZaU2Y3bdYf5r1mbownrQKfnWtrt+MVgdwLA==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/ai": "2.6.0",
"@firebase/analytics": "0.10.19",
@@ -11458,9 +11397,9 @@
}
},
"node_modules/react-is": {
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz",
"integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==",
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
"license": "MIT"
},
"node_modules/react-native": {
@@ -11537,16 +11476,6 @@
"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": {
"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",
@@ -11558,12 +11487,15 @@
}
},
"node_modules/react-native-maps": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.9.0.tgz",
"integrity": "sha512-ZTMjwEP//M4e+3DA9vzL1tcEZpfHGw5FnejuEpauO8HK5542Der2Ux9mZEXZa5I5q3+B0wJKUzvUPRZxeImUHg==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.20.1.tgz",
"integrity": "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ==",
"license": "MIT",
"dependencies": {
"@types/geojson": "^7946.0.10"
"@types/geojson": "^7946.0.13"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": ">= 17.0.1",

View File

@@ -12,18 +12,15 @@
},
"dependencies": {
"@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.2",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.25",
"@react-navigation/native": "^7.1.19",
"expo": "~54.0.13",
"expo-constants": "~18.0.9",
"expo-font": "~14.0.9",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.9",
"expo-image-picker": "~17.0.10",
"expo-linking": "~8.0.8",
"expo-location": "~19.0.8",
"expo-router": "~6.0.11",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
@@ -35,8 +32,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.9.0",
"react-native-maps": "1.20.1",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@@ -45,7 +41,6 @@
},
"devDependencies": {
"@types/react": "~19.1.0",
"baseline-browser-mapping": "^2.9.6",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"

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,8 +1,7 @@
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 { db } from "../firebase_config";
///////////////////////////////////USER/////////////////////////////////////
export async function getUsers(): Promise<User[]> {
try {
const colRef = collection(db, "user");
@@ -10,9 +9,8 @@ export async function getUsers(): Promise<User[]> {
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
...data,
//allocation: data.allocation?.map(convertReservation) || [],
allocation: data.allocation?.map(convertReservation) || [],
} as User;
});
} catch (err) {
@@ -20,7 +18,7 @@ export async function getUsers(): Promise<User[]> {
return [];
}
}
///////////////////////////////////RESSOURCE////////////////////////////////
export async function getRessources(): Promise<Ressources[]> {
try {
const colRef = collection(db, "ressources");
@@ -28,9 +26,8 @@ export async function getRessources(): Promise<Ressources[]> {
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
...data,
//allocation: data.allocation?.map(convertReservation) || [],
allocation: data.allocation?.map(convertReservation) || [],
} as Ressources;
});
} catch (err) {
@@ -39,131 +36,62 @@ export async function getRessources(): Promise<Ressources[]> {
}
}
//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/////////////////////////////////
export async function getChantiers(): Promise<Chantier[]> {
const snap = await getDocs(collection(db, "chantier"));
const chantiers: Chantier[] = [];
try {
const snap = await getDocs(collection(db, "chantier"));
for (const docSnap of snap.docs) {
try {
const data = docSnap.data();
//Faut convertir les Timestamp en Date ( merci à firebase :) )
const dateDep = data.dateDep instanceof Timestamp ? data.dateDep.toDate() : new Date(data.dateDep);
let chef: User | null = null;
if (data.chef) {
const chefSnap = await getDoc(data.chef);
if (chefSnap.exists()) {
chef = chefSnap.data() as User;
}
}
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);
}
for (const docSnap of snap.docs) {
const data = docSnap.data();
//Faut convertir les Timestamp en Date ( merci à firebase :) )
const dateDep = data.dateDep instanceof Timestamp ? data.dateDep.toDate() : new Date(data.dateDep);
let chef: User | null = null;
if (data.chef) {
const chefSnap = await getDoc(data.chef);
if (chefSnap.exists()) {
chef = chefSnap.data() as User;
}
}
let equipe: User[] = [];
if (Array.isArray(data.equipe)) {
equipe = await Promise.all(
data.equipe.map(async (ref: any) => {
const snap = await getDoc(ref);
return snap.exists() ? (snap.data() as User) : null;
})
).then(list => list.filter(x => x !== null)) as User[];
}
chantiers.push({
...data,
id: docSnap.id,
dateDep,
chef,
equipe
} as Chantier);
}
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
);
function convertReservation(res: any): Reservation {
return {
id: res.id,
dateChantier:
res.dateChantier instanceof Timestamp ? res.dateChantier.toDate() : new Date(res.dateChantier),
dateFin:
res.dateFin instanceof Timestamp ? res.dateFin.toDate() : new Date(res.dateFin),
};
}
///////////////////////////////////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
export async function changeChantierStatus(chantierId: string, newStatus: string): Promise<void> {
try {
const chantierRef = doc(db, "chantier", chantierId);
await updateDoc(chantierRef, { etat: newStatus });
console.log("Chantier ${chantierId} status updated to ${newStatus}");
console.log(`Chantier ${chantierId} status updated to ${newStatus}`);
} catch (err) {
console.error("Error", err);
}
}
//ADD CHANTIER
export async function addChantier(chantierData: Omit<Chantier, 'id'>): Promise<string | null> {
try {
const colRef = collection(db, "chantier");
@@ -174,108 +102,4 @@ export async function addChantier(chantierData: Omit<Chantier, 'id'>): Promise<s
console.error("Error adding:", err);
return null;
}
}
//CHANGE CHANTIER ANOMALIE STATUS
export async function addAnomalie(chantierId: string, anomalie_String: 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 {
const chantierRef = doc(db, "chantier", chantierId);
const chantierSnap = await getDoc(chantierRef);
if (chantierSnap.exists()) {
const chantierData = chantierSnap.data();
const anomalies = chantierData.anomalies || [];
//Filtage
const updatedAnomalies = anomalies.filter((anomaly: string) => anomaly !== anomalie_String);
await updateDoc(chantierRef, { anomalies: updatedAnomalies });
console.log("Anomalie deleted");
} else {
console.error("Chantier not found");
}
} catch (err) {
console.error("Error", err);
}
}
type ReservationFirestore = {
chantier: DocumentReference;
ressource: DocumentReference;
quantity: number;
};
async function convertReservation(res: any): Promise<Reservation|null> {
try {
const data = res.data() as ReservationFirestore;
const chantierSnap = await getDoc(data.chantier as DocumentReference);
const ressourceSnap = await getDoc(data.ressource as DocumentReference);
return {
id: res.id,
chantier: {
id: chantierSnap.id,
...(chantierSnap.data() as Omit<Chantier, "id">),
},
ressource: {
id: ressourceSnap.id,
...(ressourceSnap.data() as Omit<Ressources, "id">),
},
quantity: data.quantity,
};
} catch (err) {
console.warn("Reservation ignorée :", res.id , err);
return null;
}
}
//ENVOYER CHANTIER
export async function sendNewChantier(chantier:Chantier): Promise<void> {
const chantierRef = await addDoc(collection(db, "chantier"), {
name:chantier.name,
adresse:chantier.adresse,
etat:chantier.etat,
contact:chantier.contact,
chef: doc(db, "user", chantier.chef.id), //un objet déjà dans la base de donné
date: Timestamp.fromDate(chantier.dateDep),
tempsEst: chantier.tempsEst,
anomalies: chantier.anomalies ?? [], //strings[]
latitude: chantier.latitude,
longitude: chantier.longitude,
})
await Promise.all([
sendNewReservation(chantier.equipe, chantierRef.id),
sendNewReservation(chantier.materiel, chantierRef.id),
sendNewReservation(chantier.vehicules, chantierRef.id),
]);
}
export async function sendNewReservation(list: Reservation[],chantierId:string): Promise<void> {
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),
});
});
}