69 Commits

Author SHA1 Message Date
trochas
677972626f Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-15 15:42:23 +01:00
trochas
b7eb0be157 fix bug addRessource 2025-12-15 15:42:18 +01:00
Amaël Kesteman
41b2b58102 Feat: correction Map pour afficher le nom au lieu de l'adresse + Update README 2025-12-15 13:51:14 +01:00
Rochas
5271fe1f54 correction état trop petit pour certain écran 2025-12-14 16:15:33 +01:00
Rochas
3296d301ee gestion_user au lieu de gestion_ouvrier (qui est une ressource, donc en double avec gestionnaire_ressource) + filtre dans gestion_ressource pour avoir seulement les ressources du chantier courrant 2025-12-14 16:08:27 +01:00
Rochas
707ae1dc30 Merge branch 'reservation-test' 2025-12-14 15:02:24 +01:00
Rochas
7b4e2c1130 gestion des ressources fonctionnelle, mais pas fini 2025-12-14 15:00:11 +01:00
Rochas
a2e5b1e9cf fix temporaire en cours 2025-12-14 10:58:30 +01:00
Rochas
27d13ace9d test reservation todo corriger getChantier avec la nouvelelle structure 2025-12-14 03:04:12 +01:00
tuanvu
0de6462c31 auth with handler 2025-12-14 01:58:02 +01:00
tuanvu
1f698076df Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 23:24:25 +01:00
tuanvu
3d44d635a2 add objet and renovation 2025-12-13 23:22:40 +01:00
Rochas
dfa26df502 correction dépassement text 2025-12-13 23:12:43 +01:00
Rochas
27503103da Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 22:51:15 +01:00
Rochas
9b34feb43e correction anomalies 2025-12-13 22:51:09 +01:00
Alexis Leboeuf
7b5a4c0941 Changed typo in Ressource type in Ouvriers 2025-12-13 22:30:37 +01:00
Alexis Leboeuf
9a3070b6e9 Merging 2025-12-13 22:24:21 +01:00
Alexis Leboeuf
337b8c947e Changed Ouvriers to come from Firebase rather than mocked 2025-12-13 22:24:12 +01:00
Rochas
ee72368b94 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 22:20:42 +01:00
Rochas
a26a2eb572 recherche dans selectChantier 2025-12-13 22:20:27 +01:00
Amaël Kesteman
898f06d577 Feat: Sync entre addRessources et gestionRessources 2025-12-13 21:49:07 +01:00
tuanvu
85fc76ddec mini change in anomalies 2025-12-13 21:19:57 +01:00
tuanvu
208de892d4 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 21:03:50 +01:00
tuanvu
15d7a517f5 resync 2025-12-13 21:03:45 +01:00
Rochas
29abc4289e correction addChantier 2025-12-13 20:51:19 +01:00
Alexis Leboeuf
dc83b1d06d Renamed the tab that displays workers as Ouvrier 2025-12-13 20:47:46 +01:00
Alexis Leboeuf
211dffac98 Removed unnecessary information 2025-12-13 20:26:38 +01:00
Alexis Leboeuf
150c977306 Adding indication for each value in Ressource tab 2025-12-13 20:23:44 +01:00
Amaël Kesteman
abdeaa7d45 Feat: Add localisation nouveau chantier sur la carte 2025-12-13 19:20:22 +01:00
Rochas
a243d791f9 fix temporaire addChantier 2025-12-13 18:58:32 +01:00
Rochas
bcf9907007 add en un seul écran, composant lié aux add dans un dossier /composants/add 2025-12-13 18:48:54 +01:00
Alexis Leboeuf
b5c751cada Fixed typo in README 2025-12-13 17:07:17 +01:00
Alexis Leboeuf
57392aac5c Added extra package installation command on README 2025-12-13 17:06:25 +01:00
Alexis Leboeuf
167ac00299 Merging 2025-12-13 17:05:29 +01:00
Alexis Leboeuf
fcd94fe0c7 Added image URI save 2025-12-13 17:05:15 +01:00
Rochas
82c00cabd4 merge 2025-12-13 17:03:48 +01:00
Rochas
1f055240bb selectRessource au lieu de selectMachine et selectOuvrier 2025-12-13 16:56:33 +01:00
tuanvu
d748c90391 merge erreur 2025-12-13 16:54:40 +01:00
tuanvu
6e4242736f addchantier work 2025-12-13 16:40:43 +01:00
Alexis Leboeuf
038006818d Added image selection on new Anomaly
Caution ! New import, run npm build to download it
Only accesses the Gallery, does not really save the image
2025-12-13 16:34:07 +01:00
Rochas
f213e2dacf Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-13 15:35:25 +01:00
Rochas
e8e30d541d addChantier / selectMachine / machineSummary : presque fini 2025-12-13 15:35:01 +01:00
tuanvu
406c578cba filter for machine 2025-12-13 15:33:46 +01:00
tuanvu
1407d3b20f filter chefs 2025-12-13 15:32:01 +01:00
Amaël Kesteman
602b571d5c Feat: ajout de la page d'ajout d'un nouvel artisant 2025-12-13 15:30:08 +01:00
Amaël Kesteman
ff010fec4e Feat: ajout de la page d'ajout des ressources. 2025-12-13 15:07:21 +01:00
Rochas
d4b056b197 engin rennomé en machine 2025-12-13 12:37:31 +01:00
Rochas
dc02896fdd selectEngins 2025-12-13 12:31:32 +01:00
tuanvu
91fe9f849b borrow and return Ressource -- need to test 2025-12-13 12:02:20 +01:00
tuanvu
730a9882c7 fini fontionnalité de anomalie, ptet changer styles 2025-12-13 11:26:22 +01:00
Rochas
68af57a394 selection d'un chef de chatier dans addChantier 2025-12-13 00:20:06 +01:00
Rochas
e95bb3fd70 addChanter, selectChefChantier, en cour + Update de themed-button pour prendre en charge le changement de theme pendant la pression 2025-12-12 21:57:09 +01:00
Rochas
8ba664c2ad addChantier 2025-12-11 23:22:45 +01:00
Rochas
46c6707d14 correction home 2025-12-11 22:36:56 +01:00
Alexis Leboeuf
b874fdfabf Merging 2025-12-11 22:07:29 +01:00
Alexis Leboeuf
ff6aafb24c Fixed double Chantier display
+ temporarily removed role condition to change chantier status
2025-12-11 22:07:18 +01:00
Rochas
d87d92adfe correction merge selectChantier 2025-12-11 22:05:21 +01:00
Rochas
78152b84ae correction merge selectChantier 2025-12-11 21:56:35 +01:00
Alexis Leboeuf
4eef83e83c Merging branch 'userrole' to implement role management 2025-12-11 21:51:06 +01:00
Rochas
ef916b2f9b correction addChantier 2025-12-11 21:38:20 +01:00
Rochas
b0c6829585 clean des composants, dossiers 2025-12-11 21:25:58 +01:00
Rochas
7092d09ba8 clean des composants, dossiers 2025-12-11 21:24:28 +01:00
tuanvu
7c71fece6d Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet 2025-12-11 21:19:17 +01:00
tuanvu
bc566c4f7f change and delete anomaly 2025-12-11 21:19:11 +01:00
Amaël Kesteman
02547dca84 Feat: fix add chantier (continuer la page) 2025-12-11 21:16:49 +01:00
Amaël Kesteman
6446c3f975 Feat: Ajout de la page pour nouveau chantier ( A CORRIGER) 2025-12-11 21:07:59 +01:00
tuanvu
72dafa13c9 add anomaly 2025-12-11 20:42:16 +01:00
Rochas
7c06871e45 clean gestionnaire_ressource et ouvrier, ajout de select Chantier + templateScreen 2025-12-11 19:57:15 +01:00
trochas
2be434e2cf bonjour -> ouvriers 2025-12-11 16:32:53 +01:00
48 changed files with 2616 additions and 576 deletions

View File

@@ -11,11 +11,48 @@
Lien du git : gitlab2.istic.univ-rennes1.fr/trochas/mmm-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
#### Différentes commandes a effectuer pour lancer le projet:
npx expo start
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.

View File

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

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

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

View File

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

View File

@@ -1,164 +0,0 @@
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/external-link';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ExternalLink } from '@/components/expoExempleComponents/external-link';
import ParallaxScrollView from '@/components/expoExempleComponents/parallax-scroll-view';
import SelectChantier from '@/components/selectChantier';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from '@/components/theme/themed-view';
import { Collapsible } from '@/components/ui/collapsible';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';

154
app/(tabs)/gestion_user.tsx Normal file
View File

@@ -0,0 +1,154 @@
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,23 +1,32 @@
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 { 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 { useLocalSearchParams, useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { FlatList, Image, StyleSheet, Text } from "react-native";
import { FlatList, Image, StyleSheet, Text, View } from "react-native";
import { Ressources } from "../../class/class";
import { getRessources } from "../../services/ressourcesService";
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";
export default function GestionnaireRessource() {
const { nom, prenom } = useLocalSearchParams();
const [search, setSearch] = useState("");
const [ressource, setRessources] = useState<Ressources[]>([]);
const [filterType, setFilterType] = useState("tout");
const {ressources, setRessources} = useRessources();
const {reservations, setReservations} = useReservations();
const {chantier, setChantier} = useChantier();
const [filterType, setFilterType] = useState("Tout");
const [showFilterMenu, setShowFilterMenu] = useState(false);
const [filterChantier, setFilterChantier] = useState(false);
const router = useRouter();
useEffect(() => {
async function loadData() {
async function loadDataRessources() {
try {
const data = await getRessources();
setRessources(data);
@@ -25,13 +34,23 @@ export default function GestionnaireRessource() {
console.error("Erreur lors du chargement :", error);
}
}
loadData();
async function loadDataReservations() {
try {
const data = await getReservations();
setReservations(data);
} catch (error) {
console.error("Erreur lors du chargement :", error);
}
}
loadDataRessources();
loadDataReservations();
}, []);
const filteredData = ressource.filter((r) => {
const filteredData = ressources.filter((r) => {
const matchName = r.name.toLowerCase().includes(search.toLowerCase());
const matchType = filterType === "tout" || r.type === filterType;
return matchName && matchType;
const matchType = filterType === "Tout" || r.type === filterType;
return matchName && matchType && (!filterChantier || (chantier && isInChantier(r,chantier,reservations)));
});
const renderRessource = ({ item }: { item: Ressources }) => {
@@ -40,97 +59,121 @@ 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>{item.id}</ThemedText>
<ThemedText>{item.name}</ThemedText>
<ThemedText>{item.type}</ThemedText>
<ThemedText>{item.quantity}</ThemedText>
<ThemedText>{item.available_quantity}</ThemedText>
<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>
}
</ThemedView>
</ThemedView>
);
};
return (
<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>
<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>
</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>
}
ListEmptyComponent={
<ThemedText style={styles.empty}>Aucun résultat trouvé</ThemedText>
}
/>
)}
<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>
</ThemedView>
);
}
const styles = StyleSheet.create({
back:{
height:"100%",
width:"100%",
},
container: {
flex: 1,
marginTop: 60,
marginTop: Constants.statusBarHeight,
},
header: {
marginTop: 60,
marginBottom: 20,
alignItems: "center",
paddingHorizontal: 20,
@@ -191,7 +234,6 @@ const styles = StyleSheet.create({
width: "80%",
borderRadius: 12,
padding: 20,
backgroundColor: "#fff",
},
filterTitle: {
fontSize: 18,
@@ -199,4 +241,9 @@ const styles = StyleSheet.create({
marginBottom: 20,
textAlign: "center",
},
button:{
padding: 10,
borderRadius: 8,
marginTop: 10
},
});

View File

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

View File

@@ -1,10 +1,10 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
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 { 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 { Link } from 'expo-router';
export default function HomeScreen() {

View File

@@ -1,11 +1,15 @@
// MapScreen.tsx
import { ThemedMapView } from '@/components/themed-mapview';
import { ThemedMapView } from '@/components/theme/themed-mapview';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedButton } from '@/components/theme/themed-button';
import React, { useEffect, useState } from 'react';
import { 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 = () => {
@@ -20,21 +24,30 @@ const region = {
const [chantiers, setMarkers] = useState<Chantier[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
async function loadData() {
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
@@ -44,14 +57,24 @@ const region = {
>
{Array.isArray(chantiers) &&
chantiers.map(chantier => (
<Marker
key = {chantier.id}
coordinate={{ latitude: chantier.latitude, longitude: chantier.longitude }}
title={chantier.adresse}
coordinate={{ latitude: chantier.latitude, longitude: chantier.longitude}}
title={chantier.name}
description={chantier.etat}
/>
))}
</ThemedMapView>
<ThemedButton
lvl={1}
shadow={true}
style={styles.refreshButton}
onPress={() => loadData()}
disabled={refreshing}
>
<ThemedText>Actualiser</ThemedText>
</ThemedButton>
</View>
);
};
@@ -64,6 +87,13 @@ 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

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

45
app/AuthHandler.tsx Normal file
View File

@@ -0,0 +1,45 @@
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,9 +1,11 @@
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);
@@ -14,8 +16,19 @@ type ChantierProviderProps = {
export const ChantierProvider = ({ children }: ChantierProviderProps) => {
const [chantier, setChantier] = useState<Chantier | null>(null);
const syncChantier = async () => {
if (!chantier) return;
const value = useMemo(() => ({ chantier, setChantier }), [chantier]);
const all = await getChantiers();
const updated = all.find(c => c.id === chantier.id);
if (updated) {
setChantier(updated);
}
};
const value = useMemo(() => ({ chantier, setChantier,syncChantier }), [chantier]);
return (
<ChantierContext.Provider value={value}>

View File

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

View File

@@ -15,6 +15,10 @@ import { auth, db } from "../firebase_config";
import { Platform, UIManager } from 'react-native';
import { ChantierProvider } from "./ContextChantier";
import { UserProvider } from "./ContextUser";
import { RessourcesProvider } from "./ContextRessource";
import { ReservationsProvider } from "./ContextReservation";
import LoginScreen from "./login/login";
export const unstable_settings = {
@@ -57,18 +61,22 @@ export default function RootLayout() {
return (
<UserProvider>
<ChantierProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" 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>
<RessourcesProvider>
<ReservationsProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" 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>
</RessourcesProvider>
</ChantierProvider>
</UserProvider>
);

View File

@@ -1,16 +1,11 @@
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 { 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 React, { useState } from "react";
import { Button, StyleSheet, View } from "react-native";
import { auth } from "../../firebase_config";
const DEFAULT_ROLE = "resp";
const LoginScreen: React.FC = () => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
@@ -18,24 +13,14 @@ const LoginScreen: React.FC = () => {
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 / S'incrire</ThemedText>
<ThemedText style={styles.title}>Se connecter</ThemedText>
<ThemedTextInput
lvl = {2}
border = {5}
@@ -45,7 +30,6 @@ const LoginScreen: React.FC = () => {
value={email}
onChangeText={setEmail}
autoCapitalize="none"
/>
<ThemedTextInput
lvl = {2}
@@ -59,7 +43,6 @@ const LoginScreen: React.FC = () => {
/>
<Button title="Se connecter" onPress={handleLogin} />
<View style={{ height: 10 }} />
{/* <Button title="S'inscrire" onPress={handleRegister} /> */}
</ThemedView>
);
};
@@ -76,9 +59,8 @@ 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/themed-text';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/theme/themed-text';
import { ThemedView } from '@/components/theme/themed-view';
export default function ModalScreen() {
return (

View File

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

54
class/utils.tsx Normal file
View File

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

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

View File

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

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

View File

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

View File

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

162
components/anomaly.tsx Normal file
View File

@@ -0,0 +1,162 @@
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/themed-view';
import { ThemedView, } from '@/components/theme/themed-view';
import React from 'react';
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { ThemedText } from './themed-text';
import { ThemedText } from './theme/themed-text';
import { getNbItemReservation } from '@/class/utils';
type Props = {
data: {
@@ -13,19 +13,27 @@ 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:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=69392bb2&is=6937da32&hm=dcc09e76d3dca89d2418947b46efbd38673b9dc559027724b2e51d493b173bc9&" /*chantier.urlImg*/ }} style={styles.image} />
<Image source={{ uri:"" /*chantier.urlImg*/ }} style={styles.image} />
</View>
<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 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>
</ThemedView>
) :
@@ -45,13 +53,12 @@ const styles = StyleSheet.create({
borderRadius: 10,
//borderWidth: 1,
flexDirection: 'row',
height: 150,
//height: 150,
gap: 10,
},
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/themed-view';
import { ThemedView } from '@/components/theme/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({}, 'background');
const backgroundColor = useThemeColor({}, 'background3');
const colorScheme = useColorScheme() ?? 'light';
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollOffset(scrollRef);

View File

@@ -1,9 +1,12 @@
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,
@@ -11,12 +14,16 @@ import {
View
} from "react-native";
import Animated, {
LinearTransition
interpolate,
LinearTransition,
useAnimatedStyle,
useSharedValue,
withTiming
} from "react-native-reanimated";
import { ThemedButton } from "./themed-button";
import { ThemedText } from "./themed-text";
import { ThemedTextInput } from "./themed-textinput";
import { ThemedView } from "./themed-view";
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";
const screenHeight = Dimensions.get("window").height;
const { width, height } = Dimensions.get("window");
@@ -33,8 +40,11 @@ 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);
@@ -42,43 +52,80 @@ export default function SelectChantier() {
Animated.createAnimatedComponent(ThemedTextInput);
async function onPressOpen(){
setIsLoaded(false);
setIsOpen(!isOpen);
if(!isOpen){
const updatedChantiers = await getChantiers();
setIsLoaded(true);
setChantiers(updatedChantiers)
}
}
function onPressAddChantier(){
useEffect(() => {
open.value = withTiming(isOpen ? 1 : 0);
}, [isOpen]);
}
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 renderChantier = (chantier: Chantier, index: number) => {
const open = useSharedValue(0);
const animatedWindowStyle = useAnimatedStyle(() => {
return {
width: `${interpolate(open.value, [0, 1], [50, 100])}%`,
height: interpolate(
open.value,
[0, 1],
[60, screenHeight * 0.75]
),
padding: 10,
overflow: "hidden",
zIndex: 1000,
};
});
const animatedButtonStyle = useAnimatedStyle(() => ({
width: `${interpolate(open.value, [0, 1], [100, 50])}%`,
margin: interpolate(open.value, [0, 1], [0, 5]),
borderRadius: interpolate(open.value, [0, 1], [15, 10]),
padding: 10,
height: 40,
}));
const renderChantier = ({ item }: { item:Chantier }) => {
return (
<Pressable key={index} onPress={() => selectChantier(chantier)}>
<Pressable onPress={() => selectChantier(item)}>
<ThemedView lvl={4} style={styles.chantier}>
<View>
<Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=69392bb2&is=6937da32&hm=dcc09e76d3dca89d2418947b46efbd38673b9dc559027724b2e51d493b173bc9&" /*chantier.urlImg*/ }} style={styles.image} />
<Image source={{ uri:"https://cdn.discordapp.com/attachments/1425108443571945644/1427207643180826757/raw.png?ex=693f1a72&is=693dc8f2&hm=86ffb97145fc8d3aec822b87d99be233c98477d4424c1ef58f80eb81b17c7c80&" /*chantier.urlImg*/ }} style={styles.image} />
</View>
<View>
<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 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>
</ThemedView>
</Pressable>
@@ -86,39 +133,40 @@ export default function SelectChantier() {
};
return (
<Animated.View layout={LinearTransition.duration(200)} style={isOpen ? styles.windowOpean : styles.windowClose}>
<Animated.View style={animatedWindowStyle}>
{isOpen && (<Pressable style={styles.autoClose} onPress={() => setIsOpen(false)}/>)}
<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()}>
<ThemedView lvl={2} shadow={true} style={styles.window}>
<AnimatedThemedButton style={animatedButtonStyle} lvl={isOpen ? 1 : 1} onPress={() => onPressOpen()}>
<ThemedText style={styles.buttonText}>
{isOpen ? "Fermer" : (chantier!=null ? chantier.adresse : "Chantier")}
{isOpen ? "Fermer" : (chantier!=null ? chantier.name : "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>
<ThemedView lvl={2} style={styles.list}>
<ScrollView contentContainerStyle={styles.chantiersList}>
{chantiers.map((chantier, index) =>
renderChantier(chantier, index)
)}
</ScrollView>
</ThemedView>
: <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />}
</View>
</View>
)}
</AnimatedThemedView>
</ThemedView>
</Animated.View>
);
}
const styles = StyleSheet.create({
windowClose: {
//backgroundColor: '#00FF0040',
@@ -177,7 +225,7 @@ const styles = StyleSheet.create({
borderRadius: 10,
//borderWidth: 1,
flexDirection: 'row',
height: 100,
//height: 130,
},
image:{
margin:0,
@@ -211,8 +259,4 @@ const styles = StyleSheet.create({
buttonText: {
textAlign: "center",
},
buttonAdd:{
borderRadius: 10,
marginBottom: 10,
}
});

View File

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

View File

@@ -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 './themed-button';
import { ThemedText } from './themed-text';
import { ThemedView } from "./themed-view";
import { ThemedButton } from './theme/themed-button';
import { ThemedText } from './theme/themed-text';
import { ThemedView } from "./theme/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'était du chantier en {tempStatus} ?</ThemedText>
<ThemedText style={{fontSize: 25}}>Changer l'état 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:"30%",
width:"35%",
padding: 10,
paddingLeft: 0,
//overflow: 'hidden',
@@ -162,6 +162,7 @@ const styles = StyleSheet.create({
borderRadius: 15,
padding: 10,
height:40,
justifyContent: 'center',
},
centeredText:{
textAlign: 'center',
@@ -188,6 +189,7 @@ const styles = StyleSheet.create({
padding: 10,
height:60,
alignItems: "center",
justifyContent: 'center',
},
});

View File

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

View File

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

View File

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

50
hooks/useLocation.tsx Normal file
View File

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

140
package-lock.json generated
View File

@@ -9,15 +9,18 @@
"version": "1.0.0",
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@react-navigation/bottom-tabs": "^7.8.2",
"@react-native-community/datetimepicker": "^8.5.1",
"@react-navigation/bottom-tabs": "^7.8.12",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.19",
"@react-navigation/native": "^7.1.25",
"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",
@@ -29,7 +32,8 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
"react-native-maps": "1.20.1",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.9.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@@ -38,6 +42,7 @@
},
"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"
@@ -2326,6 +2331,7 @@
"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",
@@ -2391,6 +2397,7 @@
"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",
@@ -3576,6 +3583,29 @@
}
}
},
"node_modules/@react-native-community/datetimepicker": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.5.1.tgz",
"integrity": "sha512-TuwM1ORbxCjOp1GOtONj0QnpDpVfq0F4UlfKZYPxL/vmriaLHt2Kgvw63Bv0Bpep4eOkslVVSS1IRfRI6d392g==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
},
"peerDependencies": {
"expo": ">=52.0.0",
"react": "*",
"react-native": "*",
"react-native-windows": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"react-native-windows": {
"optional": true
}
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.81.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz",
@@ -3806,17 +3836,17 @@
"license": "MIT"
},
"node_modules/@react-navigation/bottom-tabs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.2.tgz",
"integrity": "sha512-QOcRZj6hA5QZg8PztlEaNOjbQRq75NKM26yTFXdL81OWM2qgry3tascIhBvLUak35NryG1iqQXzSpgn3I/86+g==",
"version": "7.8.12",
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.12.tgz",
"integrity": "sha512-efVt5ydHK+b4ZtjmN81iduaO5dPCmzhLBFwjCR8pV4x4VzUfJmtUJizLqTXpT3WatHdeon2gDPwhhoelsvu/JA==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^2.8.1",
"@react-navigation/elements": "^2.9.2",
"color": "^4.2.3",
"sf-symbols-typescript": "^2.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^7.1.19",
"@react-navigation/native": "^7.1.25",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0",
@@ -3824,12 +3854,12 @@
}
},
"node_modules/@react-navigation/core": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.0.tgz",
"integrity": "sha512-Fc/SO23HnlGnkou/z8JQUzwEMvhxuUhr4rdPTIZp/c8q1atq3k632Nfh8fEiGtk+MP1wtIvXdN2a5hBIWpLq3g==",
"version": "7.13.6",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.6.tgz",
"integrity": "sha512-7QG29HAWOR8wYuPkfTN8L2Po+kE1xn3nsi2sS35sGngq8HYZRHfXvxrhrAZYfFnFq2hUtOhcXnSS6vEWU/5rmA==",
"license": "MIT",
"dependencies": {
"@react-navigation/routers": "^7.5.1",
"@react-navigation/routers": "^7.5.2",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
@@ -3843,9 +3873,9 @@
}
},
"node_modules/@react-navigation/elements": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.1.tgz",
"integrity": "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ==",
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.2.tgz",
"integrity": "sha512-J1GltOAGowNLznEphV/kr4zs0U7mUBO1wVA2CqpkN8ePBsoxrAmsd+T5sEYUCXN9KgTDFvc6IfcDqrGSQngd/g==",
"license": "MIT",
"dependencies": {
"color": "^4.2.3",
@@ -3854,7 +3884,7 @@
},
"peerDependencies": {
"@react-native-masked-view/masked-view": ">= 0.2.0",
"@react-navigation/native": "^7.1.19",
"@react-navigation/native": "^7.1.25",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0"
@@ -3866,13 +3896,13 @@
}
},
"node_modules/@react-navigation/native": {
"version": "7.1.19",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.19.tgz",
"integrity": "sha512-fM7q8di4Q8sp2WUhiUWOe7bEDRyRhbzsKQOd5N2k+lHeCx3UncsRYuw4Q/KN0EovM3wWKqMMmhy/YWuEO04kgw==",
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz",
"integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^7.13.0",
"@react-navigation/core": "^7.13.6",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
@@ -3901,9 +3931,9 @@
}
},
"node_modules/@react-navigation/routers": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz",
"integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.2.tgz",
"integrity": "sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw==",
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11"
@@ -5286,9 +5316,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"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==",
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
"integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -7100,6 +7130,27 @@
}
}
},
"node_modules/expo-image-loader": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
"integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "17.0.10",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
"integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~6.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-keep-awake": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz",
@@ -7125,6 +7176,15 @@
"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",
@@ -7909,6 +7969,7 @@
"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",
@@ -11397,9 +11458,9 @@
}
},
"node_modules/react-is": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz",
"integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==",
"license": "MIT"
},
"node_modules/react-native": {
@@ -11476,6 +11537,16 @@
"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",
@@ -11487,15 +11558,12 @@
}
},
"node_modules/react-native-maps": {
"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==",
"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==",
"license": "MIT",
"dependencies": {
"@types/geojson": "^7946.0.13"
},
"engines": {
"node": ">=18"
"@types/geojson": "^7946.0.10"
},
"peerDependencies": {
"react": ">= 17.0.1",

View File

@@ -12,15 +12,18 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@react-navigation/bottom-tabs": "^7.8.2",
"@react-native-community/datetimepicker": "^8.5.1",
"@react-navigation/bottom-tabs": "^7.8.12",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.19",
"@react-navigation/native": "^7.1.25",
"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,7 +35,8 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
"react-native-maps": "1.20.1",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.9.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@@ -41,6 +45,7 @@
},
"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

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

View File

@@ -1,7 +1,8 @@
import { addDoc, collection, doc, getDoc, getDocs, Timestamp, updateDoc } from "firebase/firestore";
import { addDoc, arrayUnion, collection, doc, DocumentReference, getDoc, getDocs, query, Timestamp, updateDoc, where } 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");
@@ -9,8 +10,9 @@ 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) {
@@ -18,7 +20,7 @@ export async function getUsers(): Promise<User[]> {
return [];
}
}
///////////////////////////////////RESSOURCE////////////////////////////////
export async function getRessources(): Promise<Ressources[]> {
try {
const colRef = collection(db, "ressources");
@@ -26,8 +28,9 @@ 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) {
@@ -36,62 +39,131 @@ export async function getRessources(): Promise<Ressources[]> {
}
}
export async function getChantiers(): Promise<Chantier[]> {
const snap = await getDocs(collection(db, "chantier"));
const chantiers: Chantier[] = [];
//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,
});
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);
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 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);
}
}
return chantiers;
} catch (error) {
alert("Erreur lors de la lecture des chantiers : " + error);
}
return chantiers
}
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),
};
//récupère les reservations d'un chantier
export async function getReservationsByChantier(chantierId: string): Promise<Reservation[]> {
const q = query(
collection(db, "Reservation"),
where("chantier", "==", doc(db, "chantier", chantierId)),
);
const snap = await getDocs(q);
const results = await Promise.all(
snap.docs.map(convertReservation)
);
return results.filter(
(r): r is Reservation => r !== null
);
}
///////////////////////////////////RESERVATION/////////////////////////////////
export async function getReservations(): Promise<Reservation[]> {
try {
const snap = await getDocs(collection(db, "Reservation"));
const results = await Promise.all(
snap.docs.map(convertReservation)
);
return results.filter(
(r): r is Reservation => r !== null
);
} catch (error) {
console.error("Erreur lors de la lecture des Reservations : " + error);
return [];
}
}
//CHANGE CHANTIER STATUS
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");
@@ -102,4 +174,108 @@ 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

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