Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
677972626f | ||
|
|
b7eb0be157 | ||
|
|
41b2b58102 | ||
|
|
5271fe1f54 | ||
|
|
3296d301ee | ||
|
|
707ae1dc30 | ||
|
|
7b4e2c1130 | ||
|
|
a2e5b1e9cf | ||
|
|
27d13ace9d | ||
|
|
0de6462c31 | ||
|
|
1f698076df | ||
|
|
3d44d635a2 | ||
|
|
dfa26df502 | ||
|
|
27503103da | ||
|
|
9b34feb43e | ||
|
|
7b5a4c0941 | ||
|
|
9a3070b6e9 | ||
|
|
337b8c947e | ||
|
|
ee72368b94 | ||
|
|
a26a2eb572 | ||
|
|
898f06d577 | ||
|
|
85fc76ddec | ||
|
|
208de892d4 | ||
|
|
15d7a517f5 | ||
|
|
29abc4289e | ||
|
|
dc83b1d06d | ||
|
|
211dffac98 | ||
|
|
150c977306 | ||
|
|
abdeaa7d45 | ||
|
|
a243d791f9 | ||
|
|
bcf9907007 | ||
|
|
b5c751cada | ||
|
|
57392aac5c | ||
|
|
167ac00299 | ||
|
|
fcd94fe0c7 | ||
|
|
82c00cabd4 | ||
|
|
1f055240bb | ||
|
|
d748c90391 | ||
|
|
6e4242736f | ||
|
|
038006818d | ||
|
|
f213e2dacf | ||
|
|
e8e30d541d | ||
|
|
406c578cba | ||
|
|
1407d3b20f | ||
|
|
602b571d5c | ||
|
|
ff010fec4e | ||
|
|
d4b056b197 | ||
|
|
dc02896fdd | ||
|
|
91fe9f849b | ||
|
|
730a9882c7 | ||
|
|
68af57a394 | ||
|
|
e95bb3fd70 |
43
README.md
43
README.md
@@ -11,9 +11,48 @@
|
||||
|
||||
Lien du git : gitlab2.istic.univ-rennes1.fr/trochas/mmm-projet
|
||||
|
||||
Différentes commandes a effectuer pour lancer le projet:
|
||||
|
||||
#### Différentes commandes a effectuer pour lancer le projet:
|
||||
|
||||
npx expo install react-native-maps@1.9.0
|
||||
npm install react-native-maps @react-navigation/native @react-navigation/bottom-tabs react-native-safe-area-context react-native-screens firebase
|
||||
npm install @react-native-community/datetimepicker
|
||||
|
||||
npx expo start
|
||||
npx expo install expo-image-picker
|
||||
npx expo install expo-location
|
||||
|
||||
npx expo start
|
||||
|
||||
|
||||
#### Présentation de l'application :
|
||||
|
||||
##### 5 écrans :
|
||||
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.
|
||||
@@ -1,38 +1,27 @@
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { } from 'expo-router';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
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 GestionOuvrier from './gestion_ouvrier';
|
||||
import ListMateriel from './gestionnaire_ressource';
|
||||
import Home from './home';
|
||||
import MapScreen from './mapScreen';
|
||||
import AddChantier from './addChantier';
|
||||
import AntDesign from '@expo/vector-icons/AntDesign';
|
||||
import { UserProvider } from '../ContextUser';
|
||||
import { ChantierProvider } from '../ContextChantier';
|
||||
import { RessourcesProvider } from '../ContextRessource';
|
||||
|
||||
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 }) => (
|
||||
@@ -42,7 +31,6 @@ export default function TabLayout() {
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="gestionnaire_ressource"
|
||||
component={ListMateriel}
|
||||
options={{
|
||||
title: 'Ressources',
|
||||
tabBarIcon: ({ color }) => (
|
||||
@@ -51,33 +39,29 @@ export default function TabLayout() {
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="GestionOuvrier"
|
||||
component={GestionOuvrier}
|
||||
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="addChantier"
|
||||
component={AddChantier}
|
||||
name="addScreen"
|
||||
options={{
|
||||
title: 'Ajouter',
|
||||
href: role === 'resp' ? '/(tabs)/addScreen' : null,
|
||||
tabBarIcon: ({ color }) => (
|
||||
<AntDesign name="plus" size={24} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
</Tabs.Navigator>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import ChantierSummary from '@/components/chantierSummary';
|
||||
import SelectChantier from '@/components/selectChantier';
|
||||
import SetStatus from '@/components/setStatus';
|
||||
|
||||
import { ThemedView } from '@/components/theme/themed-view';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StyleSheet, ScrollView, Button, TextInput, Text, View } from 'react-native';
|
||||
import { useChantier } from '../ContextChantier';
|
||||
import { useRessources } from '../ContextRessource';
|
||||
import { useUser } from '../ContextUser';
|
||||
import { getRessources, getUsers, addChantier } from '@/services/ressourcesService';
|
||||
import { Chantier, Ressources } from '@/class/class';
|
||||
import { ThemedText } from '@/components/theme/themed-text';
|
||||
import { ThemedButton } from '@/components/theme/themed-button';
|
||||
import { ThemedTextInput } from '@/components/theme/themed-textinput';
|
||||
import Constants from 'expo-constants'; //pour connaître la taille de la barre menu de l'OS en haut
|
||||
|
||||
//Uniquement accessible par le RESPONSSABLE du chantier
|
||||
//Pour créer ou modifier un chantier
|
||||
export default function AddChantier() {
|
||||
const { chantier, setChantier } = useChantier();
|
||||
const { user, setUser } = useUser();
|
||||
const { ressources, setRessources } = useRessources();
|
||||
|
||||
const [editMode,setEditMode] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [objet, setObjet] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const [chefChantier, setChefChantier] = useState('');
|
||||
const [adresse, setAdresse] = useState('');
|
||||
const [duree, setDuree] = useState('');
|
||||
const [contact, setContact] = useState('');
|
||||
const [userSelect, setUserSelect] = useState<string[]>([]);
|
||||
const [ressourcesSelect, setRessourcesSelect] = useState<string[]>([]);
|
||||
|
||||
async function handleAddChantier() {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
const renderInut = (name : string, preFill : string, value : string, setValue : ((text:string) => void)) => {
|
||||
return (
|
||||
<View style = {styles.inputLine}>
|
||||
<ThemedText style = {styles.inputName}>{name}:</ThemedText>
|
||||
<ThemedTextInput lvl = {1} style = {styles.input} placeholder={preFill} value = {value} onChangeText={setAdresse} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<ThemedView lvl={3} style={styles.back}>
|
||||
<View style={styles.container}>
|
||||
{editMode &&
|
||||
<View style={{width:"100%", position: 'absolute'}}>
|
||||
<SelectChantier></SelectChantier>
|
||||
</View>
|
||||
}
|
||||
<ScrollView>
|
||||
<View style = {styles.header}>
|
||||
|
||||
<ThemedText style = {styles.text}>
|
||||
{editMode? "Edition d'un chantier"
|
||||
:"Ajouter un nouveau chantier"}
|
||||
</ThemedText>
|
||||
{renderInut("Objet","Renovation",objet,setObjet)}
|
||||
{renderInut("Date de départ","TOTO : JOUR + Demi journé",date,setDate)}
|
||||
{renderInut("Estimation de la durée (1/2 Journée)","14",duree,setDuree)}
|
||||
{renderInut("Adresse","1 Rue de la Coutellerie, Paris",adresse,setAdresse)}
|
||||
{renderInut("Contact client","07 01 02 03 04 05",contact,setContact)}
|
||||
{renderInut("Vehicule","TODO pas un input bien sûre",adresse,setAdresse)}
|
||||
{renderInut("Chef de chantier","TODO pas un input non plus",chefChantier,setChefChantier)}
|
||||
|
||||
|
||||
<ThemedButton
|
||||
lvl={1}
|
||||
shadow={true}
|
||||
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
|
||||
onPress={() => handleAddChantier()}
|
||||
>
|
||||
<ThemedText>+</ThemedText>
|
||||
</ThemedButton>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
back:{
|
||||
height:"100%",
|
||||
width:"100%",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
|
||||
},
|
||||
header: {
|
||||
marginTop:60,
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
text: {
|
||||
fontSize: 22,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 10,
|
||||
},
|
||||
inputBack: {
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
inputLine:{
|
||||
width: "100%",
|
||||
//flexDirection: 'row',
|
||||
paddingVertical: 5,
|
||||
//alignItems: "center",
|
||||
},
|
||||
inputName: {
|
||||
fontSize: 16,
|
||||
},
|
||||
input: {
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
card: {
|
||||
flexDirection: "row",
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 15,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
},
|
||||
image: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 8,
|
||||
marginRight: 10,
|
||||
},
|
||||
info: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
},
|
||||
footer: {
|
||||
padding: 20,
|
||||
},
|
||||
empty: {
|
||||
textAlign: "center",
|
||||
marginTop: 30,
|
||||
color: "#888",
|
||||
},
|
||||
filterMenuOverlay: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 999,
|
||||
},
|
||||
filterMenu: {
|
||||
width: "80%",
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
filterTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 20,
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
94
app/(tabs)/addScreen.tsx
Normal file
94
app/(tabs)/addScreen.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
@@ -2,13 +2,11 @@ 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 { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { FlatList, Image, StyleSheet, Text, View } from "react-native";
|
||||
import rawConcerts from "../../data/concerts.json";
|
||||
|
||||
import { useChantier } from "../ContextChantier";
|
||||
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;
|
||||
@@ -21,36 +19,31 @@ type Concert = {
|
||||
favorite: boolean;
|
||||
};
|
||||
|
||||
export default function GestionOuvrier() {
|
||||
const router = useRouter();
|
||||
const { nom, prenom } = useLocalSearchParams(); // Recup data ecran precedent
|
||||
export default function GestionUser() {
|
||||
const [search, setSearch] = useState("");
|
||||
const { chantier, setChantier } = useChantier();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
const concertsData: Concert[] = Array.isArray(rawConcerts)
|
||||
? (rawConcerts as Concert[])
|
||||
: [];
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
const data = (await getUsers());
|
||||
setUsers(data);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement :", error);
|
||||
}
|
||||
}
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
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 }) => {
|
||||
const renderItem = ({ item, index }: { item?: User; 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>
|
||||
<ThemedText style={styles.group}>{item.name} {item.last_name}</ThemedText>
|
||||
<ThemedText>{item.role}</ThemedText>
|
||||
</ThemedView>
|
||||
</ThemedView>
|
||||
);
|
||||
@@ -65,7 +58,7 @@ export default function GestionOuvrier() {
|
||||
|
||||
|
||||
<FlatList
|
||||
data={filteredData}
|
||||
data={users}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
@@ -8,19 +8,25 @@ import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
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);
|
||||
@@ -28,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 }) => {
|
||||
@@ -43,11 +59,13 @@ 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>
|
||||
);
|
||||
@@ -69,28 +87,31 @@ export default function GestionnaireRessource() {
|
||||
<ThemedView lvl={2} style={styles.filterMenuOverlay}>
|
||||
<ThemedView lvl={5} style={styles.filterMenu}>
|
||||
<ThemedText style={styles.filterTitle}>Filtrer par type</ThemedText>
|
||||
{["tout", "Outil", "Machine"].map((t) => (
|
||||
{["Tout", "Outil", "Machine","Ouvrier"].map((t) => (
|
||||
<ThemedButton
|
||||
key={t}
|
||||
lvl={1}
|
||||
shadow={true}
|
||||
style={{ padding: 10, borderRadius: 8, marginBottom: 10 }}
|
||||
onPress={() => {
|
||||
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)}
|
||||
>
|
||||
{/* Bouton "Fermer" remplacé */}
|
||||
<ThemedButton
|
||||
lvl={1}
|
||||
shadow={true}
|
||||
style={{ padding: 10, borderRadius: 8 }}
|
||||
onPress={() => setShowFilterMenu(false)}
|
||||
>
|
||||
<ThemedText style={{ textAlign: "center" }}>Fermer</ThemedText>
|
||||
</ThemedButton>
|
||||
</ThemedView>
|
||||
@@ -115,14 +136,22 @@ export default function GestionnaireRessource() {
|
||||
</ThemedView>
|
||||
|
||||
{/* Bouton filtre en haut à droite */}
|
||||
<ThemedButton
|
||||
lvl={1}
|
||||
shadow={true}
|
||||
style={{ padding: 10, borderRadius: 8, marginTop: 10 }}
|
||||
onPress={() => setShowFilterMenu(true)}
|
||||
>
|
||||
<ThemedText>{`Filtre: ${filterType}`}</ThemedText>
|
||||
</ThemedButton>
|
||||
<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={
|
||||
@@ -212,4 +241,9 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 20,
|
||||
textAlign: "center",
|
||||
},
|
||||
button:{
|
||||
padding: 10,
|
||||
borderRadius: 8,
|
||||
marginTop: 10
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import SetStatus from '@/components/setStatus';
|
||||
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,Text } from 'react-native';
|
||||
import { StyleSheet, View,Text, ScrollView } from 'react-native';
|
||||
import { useChantier } from '../ContextChantier';
|
||||
import Anomaly from '@/components/anomaly';
|
||||
|
||||
@@ -16,7 +16,6 @@ export default function Home() {
|
||||
const { chantier, setChantier } = useChantier();
|
||||
const { role } = useUser();
|
||||
|
||||
console.log("ROLE USER", role)
|
||||
|
||||
return(
|
||||
<ThemedView lvl={3} style={styles.back}>
|
||||
@@ -24,12 +23,18 @@ export default function Home() {
|
||||
<View style={{width:"100%", position: 'absolute'}}>
|
||||
<SelectChantier></SelectChantier>
|
||||
</View>
|
||||
{chantier&&
|
||||
<View style={{width:"100%", position: 'absolute',marginLeft:"50%"}}>
|
||||
<SetStatus></SetStatus>
|
||||
</View>
|
||||
<ChantierSummary style={styles.summary} data={{ chantier }} />
|
||||
<Anomaly style={styles.anomaly} data={{chantier}}/>
|
||||
{role === "chef"}
|
||||
}
|
||||
<ScrollView>
|
||||
<View style={{paddingTop:60}}>
|
||||
<ChantierSummary style={styles.summary} data={{ chantier }} />
|
||||
<Anomaly style={styles.anomaly} data={{chantier}}/>
|
||||
{role === "chef"}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
</View>
|
||||
</ThemedView>
|
||||
@@ -44,7 +49,6 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginTop: Constants.statusBarHeight, //pour la barre menu du haut
|
||||
paddingTop : 60,
|
||||
},
|
||||
header: {
|
||||
flex: 1,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// MapScreen.tsx
|
||||
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;
|
||||
|
||||
45
app/AuthHandler.tsx
Normal file
45
app/AuthHandler.tsx
Normal 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;
|
||||
}, []);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
33
app/ContextReservation.tsx
Normal file
33
app/ContextReservation.tsx
Normal 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;
|
||||
};
|
||||
@@ -16,7 +16,10 @@ import { Platform, UIManager } from 'react-native';
|
||||
import { ChantierProvider } from "./ContextChantier";
|
||||
import { UserProvider } from "./ContextUser";
|
||||
import { RessourcesProvider } from "./ContextRessource";
|
||||
import { useUser } from "./ContextUser";
|
||||
import { ReservationsProvider } from "./ContextReservation";
|
||||
import LoginScreen from "./login/login";
|
||||
|
||||
|
||||
|
||||
export const unstable_settings = {
|
||||
anchor: "(tabs)",
|
||||
@@ -25,12 +28,14 @@ export const unstable_settings = {
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const router = useRouter();
|
||||
const { setUser, setRole } = useUser();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [userRole, setUserRole] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
|
||||
if (!currentUser) {
|
||||
setUser(null);
|
||||
setRole(null);
|
||||
setUserRole(null);
|
||||
router.replace("/login/login");
|
||||
return;
|
||||
}
|
||||
@@ -41,14 +46,13 @@ export default function RootLayout() {
|
||||
if (!userDoc.exists()) {
|
||||
router.replace("/login/login");
|
||||
setUser(null);
|
||||
setRole(null);
|
||||
setUserRole(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const { role } = userDoc.data();
|
||||
console.log("ROLE APP",role)
|
||||
setUser(currentUser);
|
||||
setRole(role);
|
||||
setUserRole(role);
|
||||
router.replace("/(tabs)/home");
|
||||
});
|
||||
return unsubscribe;
|
||||
@@ -58,7 +62,7 @@ export default function RootLayout() {
|
||||
<UserProvider>
|
||||
<ChantierProvider>
|
||||
<RessourcesProvider>
|
||||
<RootLayout></RootLayout>
|
||||
<ReservationsProvider>
|
||||
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
@@ -71,6 +75,7 @@ export default function RootLayout() {
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</ReservationsProvider>
|
||||
</RessourcesProvider>
|
||||
</ChantierProvider>
|
||||
</UserProvider>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { ThemedText } from "@/components/theme/themed-text";
|
||||
import { ThemedTextInput } from "@/components/theme/themed-textinput";
|
||||
import { ThemedView } from "@/components/theme/themed-view";
|
||||
import { router } from "expo-router";
|
||||
import {
|
||||
signInWithEmailAndPassword
|
||||
} from "firebase/auth";
|
||||
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,21 +13,11 @@ 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</ThemedText>
|
||||
@@ -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,7 +59,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
//borderColor: "#ccc",
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
marginBottom: 10,
|
||||
|
||||
@@ -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
54
class/utils.tsx
Normal 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;
|
||||
}
|
||||
396
components/add/addChantier.tsx
Normal file
396
components/add/addChantier.tsx
Normal 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,
|
||||
}
|
||||
});
|
||||
230
components/add/addRessource.tsx
Normal file
230
components/add/addRessource.tsx
Normal 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,
|
||||
}
|
||||
});
|
||||
79
components/add/select/ressourceSummary.tsx
Normal file
79
components/add/select/ressourceSummary.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
156
components/add/select/selectChefChantier.tsx
Normal file
156
components/add/select/selectChefChantier.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
154
components/add/select/selectRessource.tsx
Normal file
154
components/add/select/selectRessource.tsx
Normal 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',
|
||||
},
|
||||
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Chantier } from '@/class/class';
|
||||
import { ThemedView, } from '@/components/theme/themed-view';
|
||||
import React from 'react';
|
||||
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
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: {
|
||||
@@ -12,22 +14,82 @@ type Props = {
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function Anomaly({data,style , ...otherProps }: Props) {
|
||||
export default function Anomaly({data,style}: Props) {
|
||||
|
||||
const{syncChantier }= useChantier();
|
||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
||||
|
||||
const handleDelete = async (anomaly: string) => {
|
||||
await deleteAnomalie(data.chantier!.id, anomaly);
|
||||
data.chantier!.anomalies = data.chantier!.anomalies.filter(a => a !== anomaly);
|
||||
await syncChantier();
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (newAnomaly.trim().length === 0) return;
|
||||
await addAnomalie(data.chantier!.id, newAnomaly.trim());
|
||||
data.chantier!.anomalies.push(newAnomaly.trim());
|
||||
setNewAnomaly("");
|
||||
await syncChantier();
|
||||
};
|
||||
|
||||
//DEBUG
|
||||
useEffect(() => {
|
||||
console.log("imageUri changed", imageUri);
|
||||
}, [imageUri]);
|
||||
|
||||
|
||||
// Image picker function (not used currently)
|
||||
const selectImage = async () => {
|
||||
let result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ['images'],
|
||||
allowsEditing: true,
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
console.log(result);
|
||||
setImageUri(result.assets[0].uri.replace('file://', ''));
|
||||
} else {
|
||||
alert('You did not select any image.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const [newAnomaly, setNewAnomaly] = useState("");
|
||||
|
||||
return(
|
||||
<View style={style}>
|
||||
{data.chantier ? (
|
||||
<ThemedView lvl={4} style={styles.anomaliesContainer}>
|
||||
<ThemedText style={styles.anomaliesTitle}>Anomalies</ThemedText>
|
||||
{data.chantier.anomalies.length > 0 ? (
|
||||
data.chantier.anomalies.map((anomaly, index) => (
|
||||
<ThemedView key={index} lvl={2} style={styles.anomalyItem}>
|
||||
<ThemedText>• {anomaly}</ThemedText>
|
||||
</ThemedView>
|
||||
))
|
||||
) : (
|
||||
<ThemedText style={styles.noAnomaly}>Aucune anomalie</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>
|
||||
@@ -39,7 +101,6 @@ const styles = StyleSheet.create({
|
||||
anomaliesContainer: {
|
||||
padding: 5,
|
||||
borderRadius: 10,
|
||||
height: 150,
|
||||
},
|
||||
anomaliesTitle: {
|
||||
fontSize: 16,
|
||||
@@ -47,12 +108,55 @@ const styles = StyleSheet.create({
|
||||
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 }
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { ThemedView, } from '@/components/theme/themed-view';
|
||||
import React from 'react';
|
||||
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import { ThemedText } from './theme/themed-text';
|
||||
import { getNbItemReservation } from '@/class/utils';
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
@@ -17,12 +18,22 @@ export default function ChantierSummary({data,style , ...otherProps }: Props) {
|
||||
{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>
|
||||
) :
|
||||
@@ -42,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,
|
||||
},
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
@@ -13,7 +14,11 @@ import {
|
||||
View
|
||||
} from "react-native";
|
||||
import Animated, {
|
||||
LinearTransition
|
||||
interpolate,
|
||||
LinearTransition,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming
|
||||
} from "react-native-reanimated";
|
||||
import { ThemedButton } from "@/components/theme/themed-button";
|
||||
import { ThemedText } from "@/components/theme/themed-text";
|
||||
@@ -56,36 +61,71 @@ export default function SelectChantier() {
|
||||
}
|
||||
}
|
||||
|
||||
function onPressAddChantier(){
|
||||
router.push("/(tabs)/addChantier")
|
||||
setIsOpen(false)
|
||||
}
|
||||
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>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>
|
||||
@@ -93,41 +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}/>
|
||||
|
||||
<ThemedButton style={styles.buttonAdd} onPress={() => onPressAddChantier()}>
|
||||
<ThemedText style={styles.buttonText}>
|
||||
+
|
||||
</ThemedText>
|
||||
</ThemedButton>
|
||||
<View style={styles.list}>
|
||||
{isLoaded?
|
||||
<ScrollView contentContainerStyle={styles.chantiersList}>
|
||||
{chantiers.map((chantier, index) =>
|
||||
renderChantier(chantier, index)
|
||||
)}
|
||||
</ScrollView>
|
||||
<FlatList
|
||||
data={filteredChantiers}
|
||||
renderItem={renderChantier}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
contentContainerStyle={{ gap: 8 }}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
: <ActivityIndicator style={{height:"100%"}} color="#808080" size="large" />}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
)}
|
||||
</AnimatedThemedView>
|
||||
</ThemedView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
windowClose: {
|
||||
//backgroundColor: '#00FF0040',
|
||||
@@ -186,7 +225,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 10,
|
||||
//borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
height: 100,
|
||||
//height: 130,
|
||||
},
|
||||
image:{
|
||||
margin:0,
|
||||
@@ -220,11 +259,4 @@ const styles = StyleSheet.create({
|
||||
buttonText: {
|
||||
textAlign: "center",
|
||||
},
|
||||
buttonAdd:{
|
||||
borderRadius: 10,
|
||||
marginBottom: 10,
|
||||
height: 30,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
});
|
||||
|
||||
151
components/selectMachine.tsx
Normal file
151
components/selectMachine.tsx
Normal 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',
|
||||
},
|
||||
|
||||
});
|
||||
@@ -121,7 +121,7 @@ const styles = StyleSheet.create({
|
||||
windowBox:{
|
||||
zIndex: 2,
|
||||
//backgroundColor: '#00FFFF40',
|
||||
width:"30%",
|
||||
width:"35%",
|
||||
padding: 10,
|
||||
paddingLeft: 0,
|
||||
//overflow: 'hidden',
|
||||
|
||||
@@ -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}/>;
|
||||
}
|
||||
|
||||
@@ -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
50
hooks/useLocation.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
67
package-lock.json
generated
67
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-native-community/datetimepicker": "^8.5.1",
|
||||
"@react-navigation/bottom-tabs": "^7.8.12",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.25",
|
||||
@@ -17,7 +18,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,6 +32,7 @@
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-image-picker": "^8.2.1",
|
||||
"react-native-maps": "1.9.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
@@ -3579,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",
|
||||
@@ -7103,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",
|
||||
@@ -7128,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",
|
||||
@@ -11480,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",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.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.25",
|
||||
@@ -20,7 +21,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,6 +35,7 @@
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-image-picker": "^8.2.1",
|
||||
"react-native-maps": "1.9.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
|
||||
24
services/borrowRessource.ts
Normal file
24
services/borrowRessource.ts
Normal 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),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
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";
|
||||
|
||||
@@ -10,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) {
|
||||
@@ -27,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,49 +38,126 @@ export async function getRessources(): Promise<Ressources[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
//ADD RESSOURCES
|
||||
export async function addRessources(ressourceData: Ressources): Promise<string | null> {
|
||||
try {
|
||||
const ressourcesRef = await addDoc(collection(db, "ressources"), {
|
||||
name:ressourceData.name,
|
||||
type:ressourceData.type,
|
||||
Image: ressourceData.Image,
|
||||
quantity: ressourceData.quantity,
|
||||
});
|
||||
|
||||
console.log(`Ressource ajoutée avec ID: ${ressourcesRef.id}`);
|
||||
return ressourcesRef.id;
|
||||
} catch (err) {
|
||||
console.error("Error adding:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
///////////////////////////////////CHANTIER/////////////////////////////////
|
||||
export async function getChantiers(): Promise<Chantier[]> {
|
||||
const snap = await getDocs(collection(db, "chantier"));
|
||||
const chantiers: Chantier[] = [];
|
||||
try {
|
||||
const snap = await getDocs(collection(db, "chantier"));
|
||||
|
||||
for (const docSnap of snap.docs) {
|
||||
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);
|
||||
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
|
||||
}
|
||||
|
||||
//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);
|
||||
}
|
||||
@@ -98,30 +177,19 @@ export async function addChantier(chantierData: Omit<Chantier, 'id'>): Promise<s
|
||||
}
|
||||
|
||||
//CHANGE CHANTIER ANOMALIE STATUS
|
||||
export async function changeAnomalieStatus(chantierId: string, anomalie_String: string, newStatus: string): Promise<void> {
|
||||
export async function addAnomalie(chantierId: string, anomalie_String: string): Promise<void> {
|
||||
try {
|
||||
const chantierRef = doc(db, "chantier", chantierId);
|
||||
const chantierSnap = await getDoc(chantierRef);
|
||||
if (chantierSnap.exists()) {
|
||||
const chantierData = chantierSnap.data();
|
||||
const anomalies = chantierData.anomalies || [];
|
||||
const updatedAnomalies = anomalies.map((anomalie: any) => {
|
||||
if (anomalie.description === anomalie_String) {
|
||||
return { ...anomalie, status: newStatus };
|
||||
}
|
||||
return anomalie;
|
||||
});
|
||||
await updateDoc(chantierRef, { anomalies: updatedAnomalies });
|
||||
console.log(`Anomalie status updated to ${newStatus}`);
|
||||
} else {
|
||||
console.error("Chantier not found");
|
||||
}
|
||||
await updateDoc(chantierRef, {
|
||||
anomalies: arrayUnion(anomalie_String)
|
||||
});
|
||||
console.log("Anomalie added");
|
||||
} catch (err) {
|
||||
console.error("Error", err);
|
||||
console.error("Error adding anomalie:", err);
|
||||
}
|
||||
}
|
||||
|
||||
//CHANGE CHANTIER ANOMALIE STATUS
|
||||
//DELETE CHANTIER ANOMALIE STATUS
|
||||
export async function deleteAnomalie(chantierId: string, anomalie_String: string): Promise<void> {
|
||||
try {
|
||||
const chantierRef = doc(db, "chantier", chantierId);
|
||||
@@ -129,9 +197,10 @@ export async function deleteAnomalie(chantierId: string, anomalie_String: string
|
||||
if (chantierSnap.exists()) {
|
||||
const chantierData = chantierSnap.data();
|
||||
const anomalies = chantierData.anomalies || [];
|
||||
const updatedAnomalies = anomalies.filter((anomalie: any) => anomalie.description !== anomalie_String);
|
||||
//Filtage
|
||||
const updatedAnomalies = anomalies.filter((anomaly: string) => anomaly !== anomalie_String);
|
||||
await updateDoc(chantierRef, { anomalies: updatedAnomalies });
|
||||
console.log(`Anomalie deleted`);
|
||||
console.log("Anomalie deleted");
|
||||
} else {
|
||||
console.error("Chantier not found");
|
||||
}
|
||||
@@ -140,12 +209,73 @@ export async function deleteAnomalie(chantierId: string, anomalie_String: string
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
21
services/returnRessource.ts
Normal file
21
services/returnRessource.ts
Normal 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),
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user