@movalib/movalib-commons 1.0.60 → 1.0.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@movalib/movalib-commons",
3
- "version": "1.0.60",
3
+ "version": "1.0.62",
4
4
  "description": "Bibliothèque d'objets communs à l'ensemble des projets React de Movalib",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,7 +31,9 @@
31
31
  "react-router-dom": "^5.1.2",
32
32
  "react-scripts": "5.0.1",
33
33
  "qr-code-styling": "^1.4.4",
34
- "typescript": "^4.9.5"
34
+ "typescript": "^4.9.5",
35
+ "date-fns": "^2.29.3",
36
+ "date-fns-tz": "^2.0.0"
35
37
  },
36
38
  "eslintConfig": {
37
39
  "extends": [
@@ -0,0 +1,47 @@
1
+ import { DigitalPassportIndex } from './helpers/Enums';
2
+ import type { FC } from 'react';
3
+ import DigitalPassportGreen from "./assets/images/speedometer_green.png";
4
+ import DigitalPassportOrange from "./assets/images/speedometer_orange.png";
5
+ import DigitalPassportRed from "./assets/images/speedometer_red.png";
6
+
7
+ interface MovaDigitalPassportProps {
8
+ digitalPassportIndex: DigitalPassportIndex
9
+ }
10
+
11
+ const MovaDigitalPassport: FC<MovaDigitalPassportProps> = ({ digitalPassportIndex }) => {
12
+
13
+ const getDigitalPassportImage = () => {
14
+ switch(digitalPassportIndex){
15
+ case DigitalPassportIndex.A:
16
+ return DigitalPassportGreen;
17
+ case DigitalPassportIndex.B:
18
+ return DigitalPassportOrange;
19
+ case DigitalPassportIndex.C:
20
+ return DigitalPassportRed;
21
+ }
22
+ }
23
+
24
+ const handleOnClick = (e: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
25
+ e.preventDefault()
26
+
27
+ // On affiche la popup explicative sur la passport digital
28
+
29
+
30
+ }
31
+
32
+ return (
33
+ <>
34
+ {digitalPassportIndex &&
35
+ <img src={getDigitalPassportImage()} style={{
36
+ position: 'relative',
37
+ width: '25%',
38
+ top: '-25px',
39
+ right: '-15px',
40
+ zIndex: 200}} alt='Passport Digital'
41
+ onClick={(e) => handleOnClick(e)}/>
42
+ }
43
+ </>
44
+ );
45
+ }
46
+
47
+ export default MovaDigitalPassport;
@@ -0,0 +1,499 @@
1
+ import { Button, Card, CardActions, CardContent, FormControl, FormHelperText, Grid, IconButton, InputLabel,
2
+ Link, MenuItem, Select, SelectChangeEvent, TextField, Typography, darken, useTheme } from '@mui/material';
3
+ import { useState, type FC, useEffect, useRef } from 'react';
4
+ import CarFigure from "../assets/images/car_figure.png";
5
+ import { MovaFormField, MovaVehicleForm } from './helpers/Types';
6
+ import Vehicle from './models/Vehicle';
7
+ import Document from './models/Document';
8
+ import VehicleTire from './models/VehicleTire';
9
+ import MovaVehicleTireField from './MovaVehicleTireField';
10
+ import ConfirmationDialog from './ConfirmationDialog';
11
+ import { DocumentType, DateFormatTypes } from './helpers/Enums';
12
+ import { validateField, formatVehicleTire, formatFrenchVehiclePlate} from './helpers/Tools';
13
+ import MovaDigitalPassport from './MovaDigitalPassport';
14
+ import Loader from './Loader';
15
+ import { formatDateByCountryCode } from './helpers/DateUtils';
16
+ import CancelIcon from '@mui/icons-material/CloseRounded';
17
+ import EditIcon from '@mui/icons-material/EditRounded';
18
+ import CloseIcon from '@mui/icons-material/CloseRounded';
19
+ import Logger from './helpers/Logger';
20
+
21
+ interface VehicleFullCardProps {
22
+ vehicle: Vehicle,
23
+ onError: (message: string) => void,
24
+ onUploadDocument: (data: FormData) => void,
25
+ onDeleteDocument: (documentId: string) => void,
26
+ editMode: boolean,
27
+ focused?: boolean,
28
+ onUpdate?: (form: MovaVehicleForm) => void,
29
+ onDelete?: () => void;
30
+ }
31
+
32
+ const initialUserFormState = {
33
+ currentMileage: { value: null, isValid: true },
34
+ averageMileagePerYear: { value: null, isValid: true },
35
+ tireSize: { value: null, isValid: true },
36
+ tireWidth: { value: '', isValid: true },
37
+ tireHeight: { value: '', isValid: true },
38
+ tireDiameter: { value: '', isValid: true },
39
+ tireSpeedIndex: { value: '', isValid: true }
40
+ }
41
+
42
+ const VehicleFullCard: FC<VehicleFullCardProps> = ({ vehicle, onError, onUploadDocument, onDeleteDocument, editMode = false, focused = false, onUpdate, onDelete }) => {
43
+
44
+ const theme = useTheme();
45
+ const [loading, setLoading] = useState(false);
46
+ const [localEditMode, setLocalEditMode] = useState(editMode);
47
+ const [openConfirmDocumentDelete, setOpenConfirmDocumentDelete] = useState(false);
48
+ const [openConfirmVehicleDelete, setOpenConfirmVehicleDelete] = useState(false);
49
+ const [documentToDelete, setDocumentToDelete] = useState('');
50
+ // Formulaire utilisé pour les modifications d'informations sur le véhicule
51
+ const [form, setForm] = useState<MovaVehicleForm>(initialUserFormState);
52
+ const [vehicleDocuments, setVehicleDocuments] = useState<Document[]>([]);
53
+ // Références aux éventuels documents uploadés depuis la fiche
54
+ const invoiceInputRef = useRef(null);
55
+ const tirePictureInputRef = useRef(null);
56
+
57
+ useEffect(() => {
58
+ initForm();
59
+ }, [vehicle]);
60
+
61
+ const initForm = () => {
62
+ if(vehicle){
63
+
64
+ setForm(prevForm => (
65
+ { ...prevForm, ['currentMileage'] : { ...prevForm['currentMileage'], value: vehicle.currentMileage }}));
66
+ setForm(prevForm => (
67
+ { ...prevForm, ['averageMileagePerYear'] : { ...prevForm['averageMileagePerYear'], value: vehicle.averageMileagePerYear }}));
68
+ setForm(prevForm => (
69
+ { ...prevForm, ['tireWidth'] : { ...prevForm['tireWidth'], value: vehicle.tireWidth }}));
70
+ setForm(prevForm => (
71
+ { ...prevForm, ['tireHeight'] : { ...prevForm['tireHeight'], value: vehicle.tireHeight }}));
72
+ setForm(prevForm => (
73
+ { ...prevForm, ['tireDiameter'] : { ...prevForm['tireDiameter'], value: vehicle.tireDiameter }}));
74
+ setForm(prevForm => (
75
+ { ...prevForm, ['tireSpeedIndex'] : { ...prevForm['tireSpeedIndex'], value: vehicle.tireSpeedIndex }}));
76
+
77
+ if(isVehicleTireSizeDefined(vehicle)){
78
+ setForm(prevForm => (
79
+ { ...prevForm, ['tireSize'] : { ...prevForm['tireSize'], value: vehicle.tireSize }}));
80
+ }
81
+
82
+ Logger.info(form);
83
+ }
84
+ }
85
+
86
+ const isVehicleTireSizeDefined = (vehicle:Vehicle) => {
87
+ return vehicle.tireSize && vehicle.tireSize.diameter && vehicle.tireSize.height && vehicle.tireSize.speedIndex && vehicle.tireSize.width;
88
+ }
89
+
90
+ const validateForm = () => {
91
+ let newForm: MovaVehicleForm = { ...form };
92
+
93
+ // Validator pour les champs obligatoires
94
+ newForm.currentMileage = validateField(form.currentMileage, value => !!value, 'Champ obligatoire');
95
+ newForm.averageMileagePerYear = validateField(form.averageMileagePerYear, value => !!value, 'Champ obligatoire');
96
+
97
+ // La validation de la saisie des pneumatiques se fait dans le composant "MovaVehicleTireField", traitée dans le callback "handleOnChangeVehicleTire"
98
+
99
+ setForm(newForm);
100
+
101
+ return newForm.currentMileage.isValid && newForm.averageMileagePerYear.isValid && newForm.tireSize.isValid;
102
+ }
103
+
104
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
105
+ handleChange(e.target.name, e.target.value);
106
+ }
107
+
108
+ const handleSelectChange = (e: SelectChangeEvent<string>): void => {
109
+ handleChange(e.target.name, e.target.value);
110
+ }
111
+
112
+ const handleChange = (fieldName:string, fieldValue:string): void => {
113
+ const newField: MovaFormField = { [fieldName]: { value: fieldValue, isValid: true } };
114
+
115
+ setForm({ ...form, ...newField});
116
+ }
117
+
118
+ const uploadVehicleDocument = (document: File, documentType:DocumentType) => {
119
+
120
+ if(vehicle && document && documentType){
121
+
122
+ // Utilisation d'un formData pour permettre le trasnfert de fichier vers l'API
123
+ let formData = new FormData();
124
+ formData.append("documentType", documentType);
125
+ // Ajouter la facture à FormData
126
+ formData.append('file', document);
127
+
128
+ // Appel du callback correspondant
129
+ if(onUploadDocument)
130
+ onUploadDocument(formData);
131
+ }
132
+ }
133
+
134
+ /**
135
+ *
136
+ * @param event L'upload des documents se fait directement lors du téléchargement
137
+ * @param docType
138
+ */
139
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, docType:DocumentType) => {
140
+ event.preventDefault();
141
+
142
+ if(event && event.target.files && event.target.files.length > 0 && docType){
143
+ uploadVehicleDocument(event.target.files[0], docType);
144
+ }
145
+
146
+ };
147
+
148
+ const handleOnClickEdit = () => {
149
+
150
+ // On passe la fiche véhicule en mode édition
151
+ setLocalEditMode(true);
152
+ }
153
+
154
+ const handleOnChangeVehicleTire = (vehicleTire: VehicleTire, isValid:boolean) => {
155
+ setForm(prevForm => (
156
+ { ...prevForm, ['tireSize'] : { ...prevForm['tireSize'], value: vehicleTire, isValid: isValid }}));
157
+ }
158
+
159
+ const handleOnClickDeleteVehicle = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
160
+ e.preventDefault();
161
+ setOpenConfirmVehicleDelete(true);
162
+ }
163
+
164
+ const handleDeleteDocument = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, documentId: string) => {
165
+ e.preventDefault();
166
+ setDocumentToDelete(documentId);
167
+ setOpenConfirmDocumentDelete(true);
168
+ }
169
+
170
+ const handleOnClickValidate = () => {
171
+
172
+ if(validateForm()) {
173
+ setLoading (true);
174
+
175
+ Logger.info(form.tireSize.value);
176
+
177
+ let query = {
178
+ currentMileage : form.currentMileage.value,
179
+ averageMileagePerYear : form.averageMileagePerYear.value,
180
+ tireWidth: form.tireSize.isValid && form.tireSize.value ? (form.tireSize.value as VehicleTire).width : undefined,
181
+ tireHeight : form.tireSize.isValid && form.tireSize.value ? (form.tireSize.value as VehicleTire).height : undefined,
182
+ tireDiameter: form.tireSize.isValid && form.tireSize.value ? (form.tireSize.value as VehicleTire).diameter : undefined,
183
+ tireSpeedIndex: form.tireSize.isValid && form.tireSize.value ? (form.tireSize.value as VehicleTire).speedIndex : undefined
184
+ }
185
+ Logger.info(query)
186
+
187
+ // Appel du callback correspondant
188
+ if(onUpdate)
189
+ onUpdate(form);
190
+ }
191
+ }
192
+
193
+ const handleOnClickCancel = () => {
194
+ initForm();
195
+ setLocalEditMode(false);
196
+ }
197
+
198
+ const handleCloseConfirmDocumentDelete = () => {
199
+ setOpenConfirmDocumentDelete(false);
200
+ setDocumentToDelete('');
201
+ }
202
+
203
+ const handleCloseConfirmVehicleDelete = () => {
204
+ setOpenConfirmVehicleDelete(false);
205
+ }
206
+
207
+ /**
208
+ *
209
+ */
210
+ const handleConfirmDocumentDelete = () => {
211
+ setOpenConfirmDocumentDelete(false);
212
+
213
+ if(vehicle && documentToDelete){
214
+
215
+ // Appel du callback correspondant
216
+ if(onDeleteDocument)
217
+ onDeleteDocument(documentToDelete);
218
+ }
219
+ }
220
+
221
+ const handleConfirmVehicleDelete = () => {
222
+ setOpenConfirmVehicleDelete(false);
223
+
224
+ if(vehicle && onDelete){
225
+
226
+ // Appel du callback correspondant
227
+ onDelete();
228
+ }
229
+ }
230
+
231
+ return (
232
+ <>
233
+ { vehicle &&
234
+ <Card variant='outlined' sx={{ maxWidth: 345,
235
+ backgroundColor: focused ? theme.palette.primary.light : 'white',
236
+ overflow: 'visible', mt: 4, pb: 1 }}
237
+ >
238
+ <img src={CarFigure} style={{
239
+ position: 'relative',
240
+ width: '40%',
241
+ top: '-25px',
242
+ left: '-15px',
243
+ zIndex: 200}} alt='Icone Voiture'>
244
+ </img>
245
+
246
+ <MovaDigitalPassport digitalPassportIndex={vehicle.digitalPassportIndex} />
247
+
248
+ <CardContent sx={{ pt: 0, pb: 0}}>
249
+ <Typography variant="h6" component="div" align="center" sx={{ mb:1 }} color={darken(theme.palette.primary.main, 0.2)}>
250
+ {vehicle.brand && `${vehicle.brand} `}
251
+ {vehicle.model && `${vehicle.model} `}
252
+ {vehicle.version && `${vehicle.version}`}
253
+ </Typography>
254
+ <Grid container justifyContent="space-between">
255
+
256
+ <Grid item xs={12}>
257
+ <Typography variant="body1" color="text.primary">
258
+ <b>{formatFrenchVehiclePlate(vehicle.plate)}</b>
259
+ </Typography>
260
+ </Grid>
261
+
262
+ {!localEditMode && <Grid container textAlign='justify' sx={{ pt: 2 }}>
263
+ <Grid item xs={8} >
264
+ <Typography variant="body1" color="text.secondary">
265
+ Km actuel :
266
+ </Typography>
267
+ </Grid>
268
+ <Grid item xs={4} sx={{ textAlign: 'right' }}>
269
+ <Typography variant="body1" color="text.secondary">
270
+ <b>{vehicle.currentMileage} km</b>
271
+ </Typography>
272
+ </Grid>
273
+ </Grid> }
274
+
275
+ {localEditMode && <Grid item xs={12}>
276
+ <TextField
277
+ label="Kilométrage actuel"
278
+ name="currentMileage"
279
+ variant="outlined"
280
+ type="number"
281
+ required
282
+ value={form.currentMileage.value}
283
+ onChange={(e) => handleInputChange(e)}
284
+ error={(Boolean(form.currentMileage.error))}
285
+ sx={{
286
+ width: '100%',
287
+ mt: 2,
288
+ '& input': { textTransform: 'uppercase' } // CSS pour forcer les majuscules dans l'input
289
+ }}
290
+ helperText={Boolean(form.currentMileage.error && form.currentMileage.value > 0)
291
+ ? form.currentMileage.error : "Sur ton tableau de bord 😉"}
292
+ />
293
+ </Grid> }
294
+
295
+ {!localEditMode && <Grid container textAlign='justify' sx={{ pt: 2 }}>
296
+ <Grid item xs={8} >
297
+ <Typography variant="body1" color="text.secondary">
298
+ Km moyen annuel :
299
+ </Typography>
300
+ </Grid>
301
+ <Grid item xs={4} sx={{ textAlign: 'right' }}>
302
+ <Typography variant="body1" color="text.secondary">
303
+ <b>{vehicle.averageMileagePerYear} km</b>
304
+ </Typography>
305
+ </Grid>
306
+ </Grid> }
307
+
308
+ {localEditMode && <Grid item xs={12}>
309
+ <FormControl fullWidth margin="normal" error={Boolean(form.averageMileagePerYear.error)}>
310
+ <InputLabel id="averageMileagePerYear-label">Kilométrage moyen annuel</InputLabel>
311
+ <Select
312
+ labelId="averageMileagePerYear-label"
313
+ id="averageMileagePerYear"
314
+ name="averageMileagePerYear"
315
+ value={form.averageMileagePerYear.value ?
316
+ String(form.averageMileagePerYear.value) : ''}
317
+ onChange={e => handleSelectChange(e)}
318
+ label="Kilométrage moyen annuel"
319
+ >
320
+ <MenuItem value={5000}>5 000</MenuItem>
321
+ <MenuItem value={10000}>10 000</MenuItem>
322
+ <MenuItem value={15000}>15 000</MenuItem>
323
+ <MenuItem value={20000}>20 000</MenuItem>
324
+ <MenuItem value={25000}>25 000</MenuItem>
325
+ <MenuItem value={30000}>30 000</MenuItem>
326
+ <MenuItem value={50000}>50 000</MenuItem>
327
+ <MenuItem value={75000}>75 000</MenuItem>
328
+ <MenuItem value={100000}>100 000</MenuItem>
329
+ <MenuItem value={999999}>+100 000</MenuItem>
330
+ </Select>
331
+ <FormHelperText>{form.averageMileagePerYear.error}</FormHelperText>
332
+ </FormControl>
333
+ </Grid>}
334
+
335
+ {!localEditMode && <Grid container textAlign='justify' sx={{ pt: 2 }}>
336
+ <Grid item xs={6}>
337
+ <Typography variant="body1" color="text.secondary">
338
+ Pneumatiques :
339
+ </Typography>
340
+ </Grid>
341
+ <Grid item xs={6} sx={{ textAlign: 'right' }}>
342
+ <Typography variant="body1" color="text.secondary">
343
+ {isVehicleTireSizeDefined(vehicle) ? <b>{formatVehicleTire(vehicle.tireSize)}</b> : '-' }
344
+ </Typography>
345
+ </Grid>
346
+ </Grid> }
347
+
348
+ {localEditMode && <Grid item xs={12} sx={{ mt: 1 }}>
349
+ <MovaVehicleTireField
350
+ vehicleTire={form.tireSize.value}
351
+ onChangeVehicleTire={handleOnChangeVehicleTire}
352
+ />
353
+ </Grid> }
354
+
355
+ </Grid>
356
+
357
+ {!localEditMode && <>
358
+
359
+ <Grid item xs={12}>
360
+ <Typography variant="h6" component="div" align="center" sx={{ mt: 3, mb:1 }} color={darken(theme.palette.primary.main, 0.2)}>
361
+ CARNET DU VÉHICULE
362
+ </Typography>
363
+ </Grid>
364
+
365
+ {/** Les FACTURES du véhicule */}
366
+ {vehicleDocuments && vehicleDocuments?.filter(doc => doc.type === DocumentType.VEHICLE_MAINTENANCE_INVOICE)
367
+ .map((invoice, index) => (
368
+ <Grid container sx={{ justifyContent: 'center', alignItems: 'center' }} key={index+1}>
369
+ <Grid item xs={11} sm={6} md={4} key={(index+1)*50} sx={{ textAlign: 'left' }} >
370
+ <Link color={darken('#F29ABA', 0.2)} href={invoice.fileSignedUrl} target="_blank" rel="noopener">
371
+ Facture du <>{formatDateByCountryCode(invoice.creationDate, 'fr', DateFormatTypes.SHORT_FORMAT_DATE)}</>
372
+ </Link>
373
+ </Grid>
374
+ <Grid item xs={1} sm={1} md={1} key={(index+1)*100} sx={{ textAlign: 'right' }}>
375
+ <IconButton onClick={(e) => handleDeleteDocument(e, invoice.id)}>
376
+ <CloseIcon />
377
+ </IconButton>
378
+ </Grid>
379
+ </Grid>
380
+ ))}
381
+
382
+ {/** Les PHOTOS du véhicule */}
383
+ {vehicleDocuments && vehicleDocuments?.filter(doc => doc.type === DocumentType.VEHICLE_TIRE_PHOTO)
384
+ .map((tirePhoto, index) => (
385
+ <Grid container sx={{ justifyContent: 'center', alignItems: 'center' }} key={index+1}>
386
+ <Grid item xs={11} sm={6} md={4} key={(index+1)*50} sx={{ textAlign: 'left' }} >
387
+ <Link color={darken('#F29ABA', 0.2)} href={tirePhoto.fileSignedUrl} target="_blank" rel="noopener">
388
+ Photo pneu du <>{formatDateByCountryCode(tirePhoto.creationDate, 'fr', DateFormatTypes.SHORT_FORMAT_DATE)}</>
389
+ </Link>
390
+ </Grid>
391
+ <Grid item xs={1} sm={1} md={1} key={(index+1)*100} sx={{ textAlign: 'right' }}>
392
+ <IconButton onClick={(e) => handleDeleteDocument(e, tirePhoto.id)}>
393
+ <CloseIcon />
394
+ </IconButton>
395
+ </Grid>
396
+ </Grid>
397
+ ))}
398
+
399
+ <Grid container >
400
+ <Grid item xs={6} sm={6} md={4} sx={{ mt: 2 }} >
401
+ {/* Input caché de type "file" */}
402
+ <div>
403
+ <input
404
+ accept="image/*, application/pdf"
405
+ type="file"
406
+ style={{ display: 'none' }}
407
+ ref={invoiceInputRef}
408
+ id="raised-button-invoice"
409
+ onChange={(e) => handleFileChange(e, DocumentType.VEHICLE_MAINTENANCE_INVOICE)}
410
+ />
411
+ <label htmlFor="raised-button-invoice">
412
+ <Button size='large' component="span" variant="outlined"
413
+ sx={{ alignItems: 'normal', width:'90%', mt: 2, mb: 1, height: '70px', p:1,
414
+ color:darken(theme.palette.primary.main, 0.2) }}>
415
+ Ajouter Facture
416
+ </Button>
417
+ </label>
418
+ </div>
419
+ </Grid>
420
+
421
+ <Grid item xs={6} sm={6} md={4} sx={{ mt: 2 }} >
422
+ {/* Input caché de type "file" */}
423
+ <div>
424
+ <input
425
+ accept="image/*"
426
+ type="file"
427
+ style={{ display: 'none' }}
428
+ ref={tirePictureInputRef}
429
+ id="raised-button-tire"
430
+ onChange={(e) => handleFileChange(e, DocumentType.VEHICLE_TIRE_PHOTO)}
431
+ />
432
+ <label htmlFor="raised-button-tire">
433
+ <Button component="span" size='large' variant="outlined"
434
+ sx={{ alignItems: 'normal', width:'90%', mt: 2, mb: 1, height: '70px', p:1,
435
+ color:darken(theme.palette.primary.main, 0.2) }}>
436
+ Ajouter Photo Pneu
437
+ </Button>
438
+ </label>
439
+ </div>
440
+ </Grid>
441
+ </Grid>
442
+
443
+ </>
444
+ }
445
+
446
+ </CardContent>
447
+
448
+ <Loader loading={loading} />
449
+
450
+ <CardActions sx={{ mt: 3, justifyContent: localEditMode ? 'center' : 'end' }}>
451
+ {!localEditMode &&
452
+ <>
453
+ <Button onClick={handleOnClickEdit} color="inherit" sx={{ width: '45%' }} variant='text'>
454
+ <EditIcon sx={{ mr: 1 }} />MODIFIER
455
+ </Button>
456
+ </>
457
+ }
458
+
459
+ {localEditMode &&
460
+ <>
461
+ <Button onClick={handleOnClickCancel} sx={{ width: '45%', color:theme.palette.text.secondary }} variant='text'>
462
+ <CancelIcon sx={{ mr: 1 }} />ANNULER
463
+ </Button>
464
+
465
+ <Button onClick={handleOnClickValidate} sx={{ width: '45%', color: darken(theme.palette.primary.main, 0.2) }} variant='text'>
466
+ <EditIcon sx={{ mr: 1 }} />VALIDER
467
+ </Button>
468
+ </>
469
+ }
470
+ </CardActions>
471
+
472
+ </Card>
473
+ }
474
+
475
+ {localEditMode && onDelete &&
476
+ <Button onClick={(e) => handleOnClickDeleteVehicle(e)} sx={{ width: '90', mt: 4, color:theme.palette.error.light,
477
+ borderColor: theme.palette.error.light }} variant='outlined'>
478
+ Supprimer le véhicule
479
+ </Button>
480
+ }
481
+
482
+ <ConfirmationDialog
483
+ open={openConfirmDocumentDelete}
484
+ onClose={handleCloseConfirmDocumentDelete}
485
+ onConfirm={handleConfirmDocumentDelete}
486
+ message="Êtes-vous sûr de vouloir supprimer ce document ?"
487
+ />
488
+
489
+ <ConfirmationDialog
490
+ open={openConfirmVehicleDelete}
491
+ onClose={handleCloseConfirmVehicleDelete}
492
+ onConfirm={handleConfirmVehicleDelete}
493
+ message="Êtes-vous sûr de vouloir supprimer ce véhicule ?"
494
+ />
495
+ </>
496
+ );
497
+ }
498
+
499
+ export default VehicleFullCard;
@@ -0,0 +1,63 @@
1
+ import { format, utcToZonedTime } from 'date-fns-tz';
2
+ import { fr } from 'date-fns/locale';
3
+ import { DateFormatTypes } from './Enums';
4
+
5
+
6
+ export const formatDateByCountryCode = (date: Date | undefined, countryCode: string, formatType: DateFormatTypes): string => {
7
+
8
+ if(date){
9
+ // Tableau de correspondance entre les codes de pays et les fuseaux horaires
10
+ const countryTimeZones: { [key: string]: string } = {
11
+ 'FR': 'Europe/Paris',
12
+ // Ajoutez d'autres correspondances de codes de pays et de fuseaux horaires ici
13
+ };
14
+
15
+ const timeZone = countryTimeZones[countryCode.toUpperCase()];
16
+
17
+ if (!timeZone) {
18
+ throw new Error('Code de pays non pris en charge');
19
+ }
20
+
21
+ // Convertir la date UTC en date du fuseau horaire local
22
+ const zonedDate = utcToZonedTime(date, timeZone);
23
+
24
+ // Formater la date
25
+ switch(formatType){
26
+
27
+ case DateFormatTypes.SHORT_FORMAT_DATE:
28
+ case DateFormatTypes.LONG_FORMAT_DATETIME:
29
+ return format(zonedDate, formatType, { timeZone, locale: fr });
30
+
31
+ case DateFormatTypes.LONG_FORMAT_DATETIME_LITERAL:
32
+ return getLongFormattedDateTime(zonedDate, timeZone, fr);
33
+ }
34
+
35
+ return format(zonedDate, formatType, { timeZone, locale: fr });
36
+ }
37
+
38
+ return '';
39
+ };
40
+
41
+ export function getLongFormattedDateTime(date: Date | undefined, timeZone: string, locale: Locale){
42
+ if(date){
43
+ const day = capitalizeFirstLetter(format(date, 'eeee', { timeZone, locale: locale }));
44
+ const month = capitalizeFirstLetter(format(date, 'MMMM', { timeZone, locale: locale }));
45
+ const hours = format(date, 'HH', { timeZone, locale: locale });
46
+ const minutes = format(date, 'mm', { timeZone, locale: locale });
47
+ const dayOfMonth = format(date, 'dd', { timeZone, locale });
48
+
49
+ return `${day} ${dayOfMonth} ${month} à ${hours}:${minutes}`;
50
+ }
51
+ return '';
52
+ }
53
+
54
+ export function capitalizeFirstLetter(str: string): string {
55
+ if (str.length === 0) {
56
+ return str;
57
+ }
58
+
59
+ const firstChar = str.charAt(0).toUpperCase();
60
+ const restOfString = str.slice(1);
61
+
62
+ return firstChar + restOfString;
63
+ }
@@ -1,3 +1,10 @@
1
+
2
+ export enum DateFormatTypes {
3
+ SHORT_FORMAT_DATE = 'dd/MM/yyyy',
4
+ LONG_FORMAT_DATETIME = 'dd/MM/yyyy HH:mm:ss',
5
+ LONG_FORMAT_DATETIME_LITERAL = "EEEE dd MMMM yyyy 'à' HH:mm"
6
+ }
7
+
1
8
  export enum Gender {
2
9
  MALE,
3
10
  FEMALE,