@movalib/movalib-commons 1.0.66 → 1.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +6 -0
- package/.env.production +4 -0
- package/devIndex.tsx +127 -12
- package/dist/devIndex.js +104 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/src/MovaLogin.js +11 -2
- package/dist/src/MovaSignUp.d.ts +1 -1
- package/dist/src/MovaSignUp.js +42 -10
- package/dist/src/QRCode.d.ts +1 -0
- package/dist/src/QRCode.js +13 -12
- package/dist/src/ScheduleFields.d.ts +24 -0
- package/dist/src/ScheduleFields.js +202 -0
- package/dist/src/helpers/ApiHelper.d.ts +20 -0
- package/dist/src/helpers/ApiHelper.js +74 -0
- package/dist/src/helpers/CookieUtils.d.ts +6 -0
- package/dist/src/helpers/CookieUtils.js +28 -0
- package/dist/src/helpers/Enums.d.ts +8 -1
- package/dist/src/helpers/Enums.js +10 -2
- package/dist/src/helpers/Tools.d.ts +6 -2
- package/dist/src/helpers/Tools.js +20 -7
- package/dist/src/services/AuthenticationService.d.ts +6 -0
- package/dist/src/services/AuthenticationService.js +106 -0
- package/dist/src/services/GarageService.d.ts +5 -0
- package/dist/src/services/GarageService.js +16 -0
- package/dist/src/services/UserService.d.ts +10 -0
- package/dist/src/services/UserService.js +76 -0
- package/index.ts +3 -1
- package/package.json +9 -7
- package/public/index.html +1 -1
- package/src/MovaLogin.tsx +24 -5
- package/src/MovaSignUp.tsx +71 -15
- package/src/QRCode.css +4 -0
- package/src/QRCode.tsx +12 -13
- package/src/ScheduleFields.tsx +301 -0
- package/src/helpers/ApiHelper.ts +95 -0
- package/src/helpers/CookieUtils.ts +23 -0
- package/src/helpers/Enums.ts +8 -1
- package/src/helpers/Tools.ts +20 -2
- package/src/services/AuthenticationService.ts +68 -0
- package/src/services/GarageService.ts +14 -0
- package/src/services/UserService.ts +25 -0
- package/tsconfig.json +1 -1
- package/webpack.config.js +9 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { FunctionComponent, useEffect, useState } from 'react';
|
|
3
|
+
import { Checkbox, FormControlLabel, Grid, IconButton, FormHelperText, FormControl, RadioGroup, Radio } from '@mui/material';
|
|
4
|
+
import AddIcon from '@mui/icons-material/AddRounded';
|
|
5
|
+
import DeleteIcon from '@mui/icons-material/DeleteRounded';
|
|
6
|
+
import { TimePicker } from '@mui/x-date-pickers';
|
|
7
|
+
import theme from '../theme'; // Import du thème personnalisé
|
|
8
|
+
import Schedule from './models/Schedule';
|
|
9
|
+
import { DayOfWeek } from './helpers/Enums';
|
|
10
|
+
import Logger from './helpers/Logger';
|
|
11
|
+
import { flexCenter } from './helpers/Tools';
|
|
12
|
+
|
|
13
|
+
export const FR_WEEK_DAYS: string[] = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];
|
|
14
|
+
|
|
15
|
+
type DayInterval = {
|
|
16
|
+
startTime: Date | '' | null,
|
|
17
|
+
endTime: Date | '' | null,
|
|
18
|
+
countryCode: string,
|
|
19
|
+
error: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DaySchedule = {
|
|
23
|
+
day: DayOfWeek,
|
|
24
|
+
checked: boolean,
|
|
25
|
+
allDay: boolean,
|
|
26
|
+
intervals: DayInterval[],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const initialSchedules: DaySchedule[] = [
|
|
30
|
+
{day: DayOfWeek.MONDAY, checked: false, allDay: false, intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
31
|
+
{day: DayOfWeek.TUESDAY, checked: false, allDay: false,intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
32
|
+
{day: DayOfWeek.WEDNESDAY, checked: false, allDay: false,intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
33
|
+
{day: DayOfWeek.THURSDAY, checked: false, allDay: false,intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
34
|
+
{day: DayOfWeek.FRIDAY, checked: false, allDay: false,intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
35
|
+
{day: DayOfWeek.SATURDAY, checked: false, allDay: false,intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
36
|
+
{day: DayOfWeek.SUNDAY, checked: false, allDay: false,intervals: [{startTime: null, endTime: null, countryCode: 'fr', error: null}]},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
interface ScheduleFieldsProps {
|
|
40
|
+
timePickerStep: number,
|
|
41
|
+
size: 'small' | 'medium',
|
|
42
|
+
onChange: (schedules: DaySchedule[]) => void,
|
|
43
|
+
schedules?: Schedule[] | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ScheduleFields: FunctionComponent<ScheduleFieldsProps> = ({ timePickerStep, schedules, size, onChange }) => {
|
|
47
|
+
|
|
48
|
+
const mergeSchedules = (apiSchedules: Schedule[]): DaySchedule[] => {
|
|
49
|
+
// Créez une copie profonde du tableau initialSchedules pour éviter toute mutation accidentelle
|
|
50
|
+
const mergedSchedules = JSON.parse(JSON.stringify(initialSchedules));
|
|
51
|
+
|
|
52
|
+
// Parcourez chaque élément de apiSchedules et mettez à jour updatedSchedules
|
|
53
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
54
|
+
|
|
55
|
+
for (const apiSchedule of apiSchedules) {
|
|
56
|
+
const dayIndex = mergedSchedules.findIndex((schedule: { day: DayOfWeek; }) => schedule.day === apiSchedule.dayOfWeek);
|
|
57
|
+
if (dayIndex !== -1) {
|
|
58
|
+
mergedSchedules[dayIndex].checked = apiSchedule.intervals.length > 0;
|
|
59
|
+
mergedSchedules[dayIndex].intervals = apiSchedule.intervals.map(interval => ({
|
|
60
|
+
...interval,
|
|
61
|
+
// On injecte une date fictive même si nous n'utilisons que l'heure, le Timepicker attend une Date JS Valide
|
|
62
|
+
startTime: new Date(`${today}T${interval.startTime!}`),
|
|
63
|
+
endTime: new Date(`${today}T${interval.endTime!}`),
|
|
64
|
+
error: null, // Réinitialisez l'erreur car les données sont maintenant mises à jour
|
|
65
|
+
}));
|
|
66
|
+
mergedSchedules[dayIndex].allDay = apiSchedule.isAllWorkingDay;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Logger.info(mergedSchedules);
|
|
71
|
+
|
|
72
|
+
return mergedSchedules;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Cette fonction renvoie une liste de DaySchedule depuis une liste de Schedule
|
|
77
|
+
* @returns
|
|
78
|
+
*/
|
|
79
|
+
const initializeFromSchedules = () => {
|
|
80
|
+
|
|
81
|
+
if (schedules && schedules.length > 0 ){
|
|
82
|
+
|
|
83
|
+
return schedules.map(s => {
|
|
84
|
+
const iList: DayInterval[] = s.intervals.map(i => ({
|
|
85
|
+
startTime: new Date(i.startTime!),
|
|
86
|
+
endTime: new Date(i.endTime!),
|
|
87
|
+
countryCode: i.countryCode,
|
|
88
|
+
error: null,
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
day: s.dayOfWeek,
|
|
93
|
+
checked: iList.length > 0, // le checked est à true si il y a au moins un interval de saisi
|
|
94
|
+
intervals: iList,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Si des schedules sont transmises on initialise un nouveau tableau de DaySchedule[]
|
|
102
|
+
const [schedule, setSchedule] = useState<DaySchedule[]>(initialSchedules);
|
|
103
|
+
|
|
104
|
+
// Chargement initial des données du garage
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
|
|
107
|
+
if(schedules && schedules.length > 0){
|
|
108
|
+
setSchedule(mergeSchedules(schedules));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
}, [schedules]); // Exécute le hook à chaque fois que les schedules transmis en props changent
|
|
112
|
+
|
|
113
|
+
const handleDayChecked = (dayIndex: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
const newSchedule = [...schedule];
|
|
115
|
+
newSchedule[dayIndex].checked = event.target.checked;
|
|
116
|
+
// On réinitialise aussi les intervals
|
|
117
|
+
newSchedule[dayIndex].intervals = [{startTime: null, endTime: null, countryCode: 'fr', error: null}];
|
|
118
|
+
onChange(newSchedule);
|
|
119
|
+
setSchedule(newSchedule);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleAllDayChecked = (e: React.ChangeEvent<HTMLInputElement>, value: string, dayIndex: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
|
|
125
|
+
const newSchedule = [...schedule];
|
|
126
|
+
newSchedule[dayIndex].allDay = event.target.checked;
|
|
127
|
+
onChange(newSchedule);
|
|
128
|
+
setSchedule(newSchedule);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const validateInterval = (start: Date | '', end: Date | ''): string | null => {
|
|
132
|
+
if (start instanceof Date && end instanceof Date && start.getTime() > end.getTime()) {
|
|
133
|
+
return "L'heure de début doit être antérieure à l'heure de fin";
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const validateIntervals = (intervals: DayInterval[]): string | null => {
|
|
139
|
+
|
|
140
|
+
// On tri les intervals par date de début
|
|
141
|
+
const sortedIntervals = [...intervals].sort((a, b) =>
|
|
142
|
+
a.startTime && b.startTime ? a.startTime.toISOString().localeCompare(b.startTime.toISOString()) : 0);
|
|
143
|
+
|
|
144
|
+
console.log(sortedIntervals)
|
|
145
|
+
for (let i = 0; i < sortedIntervals.length; i++) {
|
|
146
|
+
if (
|
|
147
|
+
sortedIntervals[i].startTime?.toString().localeCompare(sortedIntervals[i].endTime?.toString()!)! >= 0 ||
|
|
148
|
+
(i < sortedIntervals.length - 1 &&
|
|
149
|
+
sortedIntervals[i].endTime?.toString().localeCompare(sortedIntervals[i + 1].startTime?.toString()!)! > 0)
|
|
150
|
+
) {
|
|
151
|
+
return 'Attention vos intervales se chevauchent'; // start time is after or equal to end time, or end time of current interval is after start time of next interval
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null; // all intervals are valid and there are no overlapping intervals
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
const handleIntervalChange = (dayIndex: number, intervalIndex: number, type: 'startTime' | 'endTime') =>
|
|
159
|
+
(newValue: "" | Date | null) => {
|
|
160
|
+
|
|
161
|
+
const newSchedule = [...schedule];
|
|
162
|
+
|
|
163
|
+
newSchedule[dayIndex].intervals[intervalIndex][type] = newValue!;
|
|
164
|
+
|
|
165
|
+
newSchedule[dayIndex].intervals[intervalIndex].error = validateInterval(
|
|
166
|
+
newSchedule[dayIndex].intervals[intervalIndex].startTime!,
|
|
167
|
+
newSchedule[dayIndex].intervals[intervalIndex].endTime!
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
//Contrôle le chevauchement d'intervales
|
|
171
|
+
if(type === 'endTime' && intervalIndex > 0 && !newSchedule[dayIndex].intervals[intervalIndex].error){
|
|
172
|
+
console.log(dayIndex);
|
|
173
|
+
console.log(newSchedule[dayIndex].intervals);
|
|
174
|
+
newSchedule[dayIndex].intervals[intervalIndex].error = validateIntervals(
|
|
175
|
+
newSchedule[dayIndex].intervals);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setSchedule(newSchedule);
|
|
179
|
+
onChange(newSchedule);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const addInterval = (dayIndex: number) => () => {
|
|
183
|
+
Logger.info(dayIndex);
|
|
184
|
+
|
|
185
|
+
const newSchedule = [...schedule];
|
|
186
|
+
newSchedule[dayIndex].intervals.push({startTime: null, endTime: null, countryCode: 'fr', error: null});
|
|
187
|
+
Logger.info(newSchedule);
|
|
188
|
+
setSchedule(newSchedule);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const deleteInterval = (dayIndex: number, intervalIndex: number) => () => {
|
|
192
|
+
const newSchedule = [...schedule];
|
|
193
|
+
newSchedule[dayIndex].intervals.splice(intervalIndex, 1);
|
|
194
|
+
onChange(newSchedule);
|
|
195
|
+
setSchedule(newSchedule);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const getDayOfWeekIndex = (day: DayOfWeek) => {
|
|
199
|
+
if(day){
|
|
200
|
+
// Convertir les valeurs de l'énumération en un tableau
|
|
201
|
+
const daysArray = Object.values(DayOfWeek);
|
|
202
|
+
return daysArray.indexOf(day);
|
|
203
|
+
}
|
|
204
|
+
return -1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Grid container spacing={1} sx={{ minWidth: '650px', maxWidth: '800px'}} >
|
|
209
|
+
{schedule.map((daySchedule, dayIndex) => (
|
|
210
|
+
<>
|
|
211
|
+
<Grid item xs={1} />
|
|
212
|
+
|
|
213
|
+
<Grid item xs={2} key={dayIndex+1}>
|
|
214
|
+
<FormControlLabel
|
|
215
|
+
control={
|
|
216
|
+
<Checkbox
|
|
217
|
+
checked={daySchedule.checked}
|
|
218
|
+
onChange={handleDayChecked(dayIndex)}
|
|
219
|
+
/>
|
|
220
|
+
}
|
|
221
|
+
label={FR_WEEK_DAYS[getDayOfWeekIndex(daySchedule.day)]}
|
|
222
|
+
/>
|
|
223
|
+
</Grid>
|
|
224
|
+
|
|
225
|
+
<Grid item xs={7} key={(dayIndex+1)*Math.random()}>
|
|
226
|
+
{daySchedule.intervals.map((interval, intervalIndex) => (
|
|
227
|
+
<Grid container key={intervalIndex+1}
|
|
228
|
+
style={{ paddingTop: intervalIndex > 0 ? theme.spacing(1) : 0 }}>
|
|
229
|
+
<Grid item xs={6} key={(intervalIndex+1)*Math.random()} sx={{ textAlign: 'center' }}>
|
|
230
|
+
<TimePicker
|
|
231
|
+
views={['hours', 'minutes']}
|
|
232
|
+
minutesStep={timePickerStep ?? 30} // Valeur par défaut de 30 minutes au cas ou
|
|
233
|
+
disabled={!daySchedule.checked}
|
|
234
|
+
value={interval.startTime}
|
|
235
|
+
formatDensity='dense'
|
|
236
|
+
onChange={handleIntervalChange(dayIndex, intervalIndex, 'startTime')}
|
|
237
|
+
slotProps={{
|
|
238
|
+
textField: {
|
|
239
|
+
size: 'small',
|
|
240
|
+
sx: {
|
|
241
|
+
width:'150px',
|
|
242
|
+
minWidth:'100px',
|
|
243
|
+
padding: 0,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
actionBar: {
|
|
247
|
+
sx: { display: 'none' },
|
|
248
|
+
},
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</Grid>
|
|
252
|
+
<Grid item xs={6} key={(intervalIndex+1)*Math.random()} sx={{ display: 'contents'}}>
|
|
253
|
+
<TimePicker
|
|
254
|
+
minutesStep={timePickerStep ?? 30} // Valeur par défaut de 30 minutes au cas ou
|
|
255
|
+
disabled={!(interval.startTime instanceof Date)}
|
|
256
|
+
value={interval.endTime}
|
|
257
|
+
formatDensity='dense'
|
|
258
|
+
onChange={handleIntervalChange(dayIndex, intervalIndex, 'endTime')}
|
|
259
|
+
//onClose={handleIntervalClose}
|
|
260
|
+
closeOnSelect={false}
|
|
261
|
+
slotProps={{
|
|
262
|
+
textField: {
|
|
263
|
+
size: 'small',
|
|
264
|
+
sx: {
|
|
265
|
+
width:'150px',
|
|
266
|
+
minWidth:'100px',
|
|
267
|
+
padding: 0,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
actionBar: {
|
|
271
|
+
sx: { display: 'none' },
|
|
272
|
+
},
|
|
273
|
+
}}
|
|
274
|
+
/>
|
|
275
|
+
{intervalIndex > 0 &&
|
|
276
|
+
<IconButton onClick={deleteInterval(dayIndex, intervalIndex)} disabled={!daySchedule.checked} sx={{ ml: 2 }}>
|
|
277
|
+
<DeleteIcon />
|
|
278
|
+
</IconButton>
|
|
279
|
+
}
|
|
280
|
+
</Grid>
|
|
281
|
+
<Grid item xs={12} key={(intervalIndex+1)*Math.random()} >
|
|
282
|
+
{interval.error && <FormHelperText error sx={{mb:1}}>{interval.error}</FormHelperText>}
|
|
283
|
+
</Grid>
|
|
284
|
+
</Grid>
|
|
285
|
+
))}
|
|
286
|
+
</Grid>
|
|
287
|
+
|
|
288
|
+
<Grid item xs={1} key={(dayIndex+1)*Math.random()} sx={{ pl: 0 }}>
|
|
289
|
+
<IconButton onClick={addInterval(dayIndex)} disabled={!daySchedule.checked}>
|
|
290
|
+
<AddIcon />
|
|
291
|
+
</IconButton>
|
|
292
|
+
</Grid>
|
|
293
|
+
|
|
294
|
+
<Grid item xs={1} />
|
|
295
|
+
</>
|
|
296
|
+
))}
|
|
297
|
+
</Grid>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default ScheduleFields;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { APIMethod } from "./Enums";
|
|
2
|
+
import { COOKIE_GARAGE_TOKEN, readCookie } from "./CookieUtils";
|
|
3
|
+
import Logger from "./Logger";
|
|
4
|
+
|
|
5
|
+
export const API_BASE_URL = process.env.REACT_APP_API_URL ?? 'https://localhost:8443/api';
|
|
6
|
+
|
|
7
|
+
export type APIResponse<T> = {
|
|
8
|
+
success: boolean;
|
|
9
|
+
data?: T;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type APIRequest = {
|
|
14
|
+
url: string,
|
|
15
|
+
method: APIMethod,
|
|
16
|
+
body?: string | FormData
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function handleError(error: Error): APIResponse<null> {
|
|
20
|
+
let msg = error.message.includes('fetch') ? "Connexion impossible" : error.message;
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: msg
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function handleResponse(response: Response): Promise<APIResponse<any>> {
|
|
28
|
+
|
|
29
|
+
const contentType = response.headers.get("content-type");
|
|
30
|
+
const isJson = contentType && contentType.includes("application/json");
|
|
31
|
+
|
|
32
|
+
const dataPromise = isJson ? response.json() : response.text();
|
|
33
|
+
|
|
34
|
+
return dataPromise.then(data => {
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
|
|
38
|
+
Logger.error(data);
|
|
39
|
+
|
|
40
|
+
let errorMsg: string;
|
|
41
|
+
|
|
42
|
+
switch(response.status){
|
|
43
|
+
case 403:
|
|
44
|
+
errorMsg = 'Accès non autorisé (403)'; break;
|
|
45
|
+
case 404:
|
|
46
|
+
errorMsg = 'La ressource demandée est introuvable (404)'; break;
|
|
47
|
+
case 500:
|
|
48
|
+
errorMsg = 'Une erreur interne du serveur est survenue (500)'; break;
|
|
49
|
+
default:
|
|
50
|
+
errorMsg = (typeof data === 'string') ? data : `Une erreur est survenue (${response.status})`; break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { success: false, error: errorMsg };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { success: true, data };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ATTENTION : cela signifie que toutes les requêtes du Front seront de type "preflight"
|
|
63
|
+
* Il faut penser à configurer l'API pour autoriser les headers
|
|
64
|
+
* @param options
|
|
65
|
+
* @param dispatch
|
|
66
|
+
* @returns
|
|
67
|
+
*/
|
|
68
|
+
export const request = (options:APIRequest): Promise<APIResponse<any>> => {
|
|
69
|
+
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
|
|
72
|
+
// Si le corps est de type FormData, il ne faut pas définir le content-type manuellement (le navigateur prend le relai est injecte un boundary (délimiteur)
|
|
73
|
+
if (!(options.body instanceof FormData)) {
|
|
74
|
+
headers.append('Content-Type', 'application/json');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Récupération du TOKEN depuis les cookies et intégration au header
|
|
78
|
+
if(readCookie(COOKIE_GARAGE_TOKEN)) {
|
|
79
|
+
headers.append('Authorization', 'Bearer ' + readCookie(COOKIE_GARAGE_TOKEN))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// On intègre les headers aux options
|
|
83
|
+
const defaults = {headers: headers};
|
|
84
|
+
options = Object.assign({}, defaults, options);
|
|
85
|
+
|
|
86
|
+
Logger.info(options);
|
|
87
|
+
|
|
88
|
+
// Appel API
|
|
89
|
+
return fetch(options.url, options)
|
|
90
|
+
.then(handleResponse)
|
|
91
|
+
.catch(error => {
|
|
92
|
+
Logger.error('There has been a problem with your fetch operation : ', error.message);
|
|
93
|
+
return handleError(error);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Cookies from "js-cookie";
|
|
2
|
+
|
|
3
|
+
export const COOKIE_GARAGE_TOKEN: string = 'movalibGarageToken';
|
|
4
|
+
export const COOKIE_INDIVIDUAL_TOKEN: string = 'movalibIndividualToken';
|
|
5
|
+
export const COOKIE_DEFAULT_EXPIRES: number = 7;
|
|
6
|
+
|
|
7
|
+
export const createCookie = (name:string, value:string) => {
|
|
8
|
+
if(name && value){
|
|
9
|
+
Cookies.set(name, value, { expires: COOKIE_DEFAULT_EXPIRES, sameSite: 'None', secure: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const deleteCookie = (name:string) => {
|
|
14
|
+
if(name){
|
|
15
|
+
Cookies.remove(name);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const readCookie = (name:string) => {
|
|
20
|
+
if(name){
|
|
21
|
+
return Cookies.get(name);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/helpers/Enums.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export enum APIMethod {
|
|
2
|
+
GET = 'GET',
|
|
3
|
+
PUT = 'PUT',
|
|
4
|
+
PATCH = 'PATCH',
|
|
5
|
+
POST = 'POST',
|
|
6
|
+
DELETE = 'DELETE'
|
|
7
|
+
}
|
|
1
8
|
|
|
2
9
|
export enum DateFormatTypes {
|
|
3
10
|
SHORT_FORMAT_DATE = 'dd/MM/yyyy',
|
|
@@ -81,7 +88,7 @@ export enum MovaAppType {
|
|
|
81
88
|
/**
|
|
82
89
|
* Application Utilisateur (automobiliste)
|
|
83
90
|
*/
|
|
84
|
-
|
|
91
|
+
INDIVIDUAL = "INDIVIDUAL",
|
|
85
92
|
/**
|
|
86
93
|
* Application Administrateur (MovaTeam)
|
|
87
94
|
*/
|
package/src/helpers/Tools.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
import Cookies from "js-cookie";
|
|
1
2
|
import VehicleTire from "../models/VehicleTire";
|
|
2
3
|
import { MovaFormField } from "./Types";
|
|
4
|
+
import { CSSProperties } from "react";
|
|
5
|
+
|
|
6
|
+
export const flexEnd:CSSProperties = {
|
|
7
|
+
display: 'flex',
|
|
8
|
+
justifyContent: 'end',
|
|
9
|
+
alignItems: 'center'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const flexCenter:CSSProperties = {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
alignItems: 'center'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const isEmpty = (data: Object): boolean => {
|
|
19
|
+
return Object.keys(data).length === 0;
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
export const formatFrenchVehiclePlate = (input: string | undefined): string => {
|
|
5
23
|
if(input){
|
|
@@ -20,7 +38,7 @@ export const formatFrenchVehiclePlate = (input: string | undefined): string => {
|
|
|
20
38
|
return "";
|
|
21
39
|
};
|
|
22
40
|
|
|
23
|
-
export
|
|
41
|
+
export const formatVehicleTire = (vehicleTire: VehicleTire) => {
|
|
24
42
|
if(vehicleTire){
|
|
25
43
|
let concatened = `${vehicleTire.width}${vehicleTire.height}${vehicleTire.diameter}${vehicleTire.speedIndex}`;
|
|
26
44
|
return formatVehicleTireStr(concatened);
|
|
@@ -28,7 +46,7 @@ export function formatVehicleTire(vehicleTire: VehicleTire){
|
|
|
28
46
|
return '';
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
export
|
|
49
|
+
export const formatVehicleTireStr = (input: string) => {
|
|
32
50
|
|
|
33
51
|
let formatted = input.toUpperCase().replace(/[^0-9A-QS-Za-qs-z]/g, '').slice(0, 10);
|
|
34
52
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { APIResponse, API_BASE_URL, request } from "../helpers/ApiHelper";
|
|
2
|
+
import { COOKIE_GARAGE_TOKEN, COOKIE_INDIVIDUAL_TOKEN, createCookie } from "../helpers/CookieUtils";
|
|
3
|
+
import { APIMethod, MovaAppType } from "../helpers/Enums";
|
|
4
|
+
import Logger from "../helpers/Logger";
|
|
5
|
+
import User from "../models/User";
|
|
6
|
+
import UserService from "./UserService";
|
|
7
|
+
|
|
8
|
+
export default class AuthenticationService {
|
|
9
|
+
|
|
10
|
+
static async login(appType: MovaAppType, email: string, password: string): Promise<APIResponse<User>> {
|
|
11
|
+
try {
|
|
12
|
+
|
|
13
|
+
// Contextualisation selon l'application demandeuse
|
|
14
|
+
let url = '';
|
|
15
|
+
let tokenCookie = '';
|
|
16
|
+
|
|
17
|
+
Logger.info(API_BASE_URL);
|
|
18
|
+
|
|
19
|
+
switch(appType){
|
|
20
|
+
case MovaAppType.GARAGE:
|
|
21
|
+
url = `${API_BASE_URL}/garage/login`;
|
|
22
|
+
tokenCookie = COOKIE_GARAGE_TOKEN;
|
|
23
|
+
break;
|
|
24
|
+
case MovaAppType.INDIVIDUAL:
|
|
25
|
+
url = `${API_BASE_URL}/login`;
|
|
26
|
+
tokenCookie = COOKIE_INDIVIDUAL_TOKEN;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let tokenResponse = await request({
|
|
31
|
+
url: url,
|
|
32
|
+
method: APIMethod.POST,
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
email : email,
|
|
35
|
+
password: password
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if(tokenResponse.success){
|
|
40
|
+
|
|
41
|
+
Logger.info(tokenResponse);
|
|
42
|
+
|
|
43
|
+
createCookie(tokenCookie, tokenResponse.data.accessToken);
|
|
44
|
+
|
|
45
|
+
// Si le login est un succès, on renvoie les infos de l'utilisateur connecté
|
|
46
|
+
let userResponse = await UserService.getCurrentUser();
|
|
47
|
+
|
|
48
|
+
if(!userResponse || !userResponse.success) {
|
|
49
|
+
const errorMsg = 'Erreur au chargement de votre profil';
|
|
50
|
+
Logger.error(errorMsg);
|
|
51
|
+
return { success: false, error: errorMsg };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { success: true, data: userResponse.data };
|
|
55
|
+
|
|
56
|
+
} else {
|
|
57
|
+
|
|
58
|
+
return tokenResponse;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
Logger.error('Error occurred during login:', error);
|
|
63
|
+
let errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
|
64
|
+
|
|
65
|
+
return { success: false, error: errorMessage };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { APIResponse, API_BASE_URL, request } from "../helpers/ApiHelper";
|
|
2
|
+
import { APIMethod } from "../helpers/Enums";
|
|
3
|
+
import Garage from "../models/Garage";
|
|
4
|
+
|
|
5
|
+
export default class GarageService {
|
|
6
|
+
|
|
7
|
+
static getAdministratedGarages(): Promise<APIResponse<Garage[]>> {
|
|
8
|
+
return request({
|
|
9
|
+
url: `${API_BASE_URL}/user/garages`,
|
|
10
|
+
method: APIMethod.GET,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { APIResponse, API_BASE_URL, request } from "../helpers/ApiHelper";
|
|
2
|
+
import { APIMethod } from "../helpers/Enums";
|
|
3
|
+
import Logger from "../helpers/Logger";
|
|
4
|
+
import User from "../models/User";
|
|
5
|
+
|
|
6
|
+
export default class UserService {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param email
|
|
10
|
+
* @param password
|
|
11
|
+
* @returns
|
|
12
|
+
*/
|
|
13
|
+
static async getCurrentUser(): Promise<APIResponse<User>> {
|
|
14
|
+
try {
|
|
15
|
+
return await request({
|
|
16
|
+
url: `${API_BASE_URL}/user/me`,
|
|
17
|
+
method: APIMethod.GET
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
Logger.error('Error occurred during getting user:', error);
|
|
21
|
+
return Promise.reject(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
|
37
37
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
|
38
38
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
|
39
|
-
//
|
|
39
|
+
//"types": [], /* Specify type package names to be included without being referenced in a source file. */
|
|
40
40
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
41
41
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
|
42
42
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
package/webpack.config.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const webpack = require('webpack');
|
|
3
4
|
|
|
4
5
|
module.exports = {
|
|
5
6
|
mode: 'development', // ou 'production' ou 'none'
|
|
6
7
|
module: {
|
|
7
8
|
rules: [
|
|
9
|
+
{
|
|
10
|
+
test: /\.css$/i,
|
|
11
|
+
use: ['style-loader', 'css-loader'],
|
|
12
|
+
},
|
|
8
13
|
{
|
|
9
14
|
test: /\.tsx?$/,
|
|
10
15
|
use: 'ts-loader',
|
|
@@ -31,5 +36,9 @@ module.exports = {
|
|
|
31
36
|
new HtmlWebpackPlugin({
|
|
32
37
|
template: path.resolve(__dirname, 'public', 'index.html'), // Chemin vers votre fichier HTML de base
|
|
33
38
|
}),
|
|
39
|
+
new webpack.DefinePlugin({
|
|
40
|
+
'REACT_APP_API_URL': JSON.stringify(process.env.REACT_APP_API_URL),
|
|
41
|
+
'REACT_APP_MOVALIB_APP_URL': JSON.stringify(process.env.REACT_APP_MOVALIB_APP_URL)
|
|
42
|
+
}),
|
|
34
43
|
],
|
|
35
44
|
};
|