@osfarm/itineraire-technique 1.1.20 → 1.2.0
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/README.md +34 -2
- package/examples/README.md +137 -0
- package/examples/nextjs-_document.tsx +66 -0
- package/examples/nextjs-api-route.ts +122 -0
- package/examples/nextjs-app-router-editor.tsx +304 -0
- package/examples/nextjs-app-router-viewer.tsx +90 -0
- package/package.json +59 -6
- package/react/QUICKSTART.md +172 -0
- package/react/README.md +305 -0
- package/react/TikaEditor.jsx +212 -0
- package/react/TikaRenderer.jsx +116 -0
- package/react/hooks.ts +217 -0
- package/react/index.ts +19 -0
- package/react/types.ts +152 -0
- package/.github/copilot-instructions.md +0 -56
- package/.github/workflows/publish.yml +0 -34
- package/scss/styles-editor.scss +0 -149
- package/scss/styles-rendering.scss +0 -184
package/README.md
CHANGED
|
@@ -24,10 +24,42 @@ Le visualisateur est fourni avec un éditeur qui permet de créer son propre iti
|
|
|
24
24
|
Ce visualisateur est avant tout conçu pour être utilisé sur [Triple Performance](https://wiki.tripleperformance.fr/). Vous y trouverez de [nombreux](https://wiki.tripleperformance.fr/wiki/Retours_d%27exp%C3%A9rience) [retours d'expérience](https://wiki.tripleperformance.fr/wiki/Ferme_de_Longueil) documentés avec des données technico-économiques ainsi que les itinéraires techniques associés. Les itinéraires peuvent être créés alors directement dans [Google Spreadsheet](https://wiki.tripleperformance.fr/wiki/Aide:Ins%C3%A9rer_des_graphiques_dans_une_page) grâce à l'[add-on](https://workspace.google.com/marketplace/app/triple_performance/427792115089) spécifiquement conçu pour Google Workspace.
|
|
25
25
|
|
|
26
26
|
## Utilisation dans un autre contexte / logiciel
|
|
27
|
+
|
|
27
28
|
Il est possible d'utiliser cette librairie très facilement dans n'importe quel outil. Le visualisateur a été conçu pour être très facile à intégrer dans une page HTML, il ne dépend que de briques Javascript (Apache Echarts, JQuery et Bootstrap). N'hésitez pas à nous contacter si vous décidez de l'utiliser et à contribuer si vous faites des évolutions !
|
|
28
|
-
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
### 🆕 Composants React/Next.js
|
|
31
|
+
|
|
32
|
+
**Nouveauté version 1.2.0** : Le projet inclut désormais des composants React/Next.js prêts à l'emploi !
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm i @osfarm/itineraire-technique
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Utilisation rapide avec React/Next.js :**
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { TikaRenderer } from '@osfarm/itineraire-technique/react';
|
|
42
|
+
|
|
43
|
+
function MyComponent() {
|
|
44
|
+
const data = { /* vos données JSON */ };
|
|
45
|
+
return <TikaRenderer data={data} />;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**📚 [Documentation complète React/Next.js](react/README.md)**
|
|
50
|
+
|
|
51
|
+
Consultez le guide complet avec :
|
|
52
|
+
- Composants `TikaRenderer` et `TikaEditor`
|
|
53
|
+
- Hooks personnalisés (`useItineraire`, etc.)
|
|
54
|
+
- Types TypeScript
|
|
55
|
+
- Exemples Next.js App Router et Pages Router
|
|
56
|
+
- Configuration et intégration
|
|
57
|
+
|
|
58
|
+
**🔗 [Exemples d'intégration](examples/)**
|
|
59
|
+
|
|
60
|
+
### Utilisation vanilla JS/HTML
|
|
61
|
+
|
|
62
|
+
Pour utiliser le package en JavaScript vanilla, le plus simple est d'utiliser npm :
|
|
31
63
|
|
|
32
64
|
```
|
|
33
65
|
npm i @osfarm/itineraire-technique
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Exemples d'utilisation
|
|
2
|
+
|
|
3
|
+
Ce dossier contient des exemples d'intégration du visualisateur d'itinéraires techniques TIKA dans différents contextes.
|
|
4
|
+
|
|
5
|
+
## Fichiers disponibles
|
|
6
|
+
|
|
7
|
+
### Next.js
|
|
8
|
+
|
|
9
|
+
- **`nextjs-app-router-viewer.tsx`** - Exemple de page de visualisation avec Next.js App Router (13+)
|
|
10
|
+
- Affichage d'un itinéraire technique
|
|
11
|
+
- Gestion du chargement des données
|
|
12
|
+
- Événements de clic et survol
|
|
13
|
+
|
|
14
|
+
- **`nextjs-app-router-editor.tsx`** - Exemple d'éditeur complet avec Next.js App Router
|
|
15
|
+
- Interface complète d'édition
|
|
16
|
+
- Ajout/modification/suppression d'étapes
|
|
17
|
+
- Gestion des interventions
|
|
18
|
+
- Sauvegarde et export
|
|
19
|
+
|
|
20
|
+
- **`nextjs-_document.tsx`** - Configuration du document Next.js
|
|
21
|
+
- Chargement des dépendances CDN
|
|
22
|
+
- Configuration des scripts et styles
|
|
23
|
+
- Compatible Pages Router
|
|
24
|
+
|
|
25
|
+
- **`nextjs-api-route.ts`** - Exemple d'API Routes
|
|
26
|
+
- Sauvegarde et chargement d'itinéraires
|
|
27
|
+
- CRUD complet
|
|
28
|
+
- Gestion des fichiers JSON
|
|
29
|
+
|
|
30
|
+
## Utilisation
|
|
31
|
+
|
|
32
|
+
### 1. Copier les exemples
|
|
33
|
+
|
|
34
|
+
Copiez les fichiers dans votre projet Next.js :
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Pour le visualiseur
|
|
38
|
+
cp examples/nextjs-app-router-viewer.tsx app/itineraire/page.tsx
|
|
39
|
+
|
|
40
|
+
# Pour l'éditeur
|
|
41
|
+
cp examples/nextjs-app-router-editor.tsx app/editor/page.tsx
|
|
42
|
+
|
|
43
|
+
# Pour la configuration
|
|
44
|
+
cp examples/nextjs-_document.tsx pages/_document.tsx # Pages Router
|
|
45
|
+
# ou adaptez pour app/layout.tsx (App Router)
|
|
46
|
+
|
|
47
|
+
# Pour l'API
|
|
48
|
+
cp examples/nextjs-api-route.ts app/api/itineraire/route.ts # App Router
|
|
49
|
+
# ou pages/api/itineraire.ts (Pages Router)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Installer les dépendances
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @osfarm/itineraire-technique
|
|
56
|
+
npm run setup:react
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Adapter à votre projet
|
|
60
|
+
|
|
61
|
+
Les exemples sont commentés et peuvent être adaptés à vos besoins spécifiques :
|
|
62
|
+
|
|
63
|
+
- Personnalisez les styles
|
|
64
|
+
- Ajoutez votre logique métier
|
|
65
|
+
- Intégrez avec votre base de données
|
|
66
|
+
- Ajoutez l'authentification
|
|
67
|
+
|
|
68
|
+
## Structure d'un projet Next.js complet
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
my-app/
|
|
72
|
+
├── app/ # App Router (Next.js 13+)
|
|
73
|
+
│ ├── layout.tsx # Layout principal avec scripts CDN
|
|
74
|
+
│ ├── itineraire/
|
|
75
|
+
│ │ └── page.tsx # Page de visualisation
|
|
76
|
+
│ ├── editor/
|
|
77
|
+
│ │ └── page.tsx # Page d'édition
|
|
78
|
+
│ └── api/
|
|
79
|
+
│ └── itineraire/
|
|
80
|
+
│ └── route.ts # API Routes
|
|
81
|
+
├── public/ # Fichiers statiques
|
|
82
|
+
│ ├── js/
|
|
83
|
+
│ │ ├── chart-render.js
|
|
84
|
+
│ │ └── editor-*.js
|
|
85
|
+
│ ├── css/
|
|
86
|
+
│ │ ├── styles-rendering.css
|
|
87
|
+
│ │ └── styles-editor.css
|
|
88
|
+
│ └── test/
|
|
89
|
+
│ └── test.json
|
|
90
|
+
├── package.json
|
|
91
|
+
└── tsconfig.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Frameworks alternatifs
|
|
95
|
+
|
|
96
|
+
Bien que ces exemples soient pour Next.js, les composants React sont compatibles avec :
|
|
97
|
+
|
|
98
|
+
- **Create React App** - Ajoutez les scripts CDN dans `public/index.html`
|
|
99
|
+
- **Vite** - Ajoutez les scripts CDN dans `index.html`
|
|
100
|
+
- **Remix** - Ajoutez les scripts dans `root.tsx`
|
|
101
|
+
- **Gatsby** - Utilisez `gatsby-ssr.js` pour ajouter les scripts
|
|
102
|
+
|
|
103
|
+
## Support TypeScript
|
|
104
|
+
|
|
105
|
+
Tous les exemples sont fournis en TypeScript avec les types complets. Si vous utilisez JavaScript, supprimez simplement les annotations de type.
|
|
106
|
+
|
|
107
|
+
## Personnalisation
|
|
108
|
+
|
|
109
|
+
### Thème et styles
|
|
110
|
+
|
|
111
|
+
Vous pouvez personnaliser les styles en :
|
|
112
|
+
|
|
113
|
+
1. Surchargeant les classes CSS existantes
|
|
114
|
+
2. Modifiant les fichiers SCSS sources
|
|
115
|
+
3. Utilisant la prop `className` des composants
|
|
116
|
+
|
|
117
|
+
### Événements
|
|
118
|
+
|
|
119
|
+
Les composants exposent des callbacks pour interagir avec eux :
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<TikaRenderer
|
|
123
|
+
data={data}
|
|
124
|
+
onItemClick={(id, event) => {
|
|
125
|
+
// Votre logique
|
|
126
|
+
}}
|
|
127
|
+
onItemHover={(id, event) => {
|
|
128
|
+
// Votre logique
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Besoin d'aide ?
|
|
134
|
+
|
|
135
|
+
- Consultez la [documentation React](../react/README.md)
|
|
136
|
+
- Consultez le [Quick Start](../react/QUICKSTART.md)
|
|
137
|
+
- Ouvrez une [issue sur GitHub](https://github.com/osfarm/itineraire-technique/issues)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemple de _document.tsx pour Next.js Pages Router
|
|
3
|
+
* Fichier: pages/_document.tsx
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Html, Head, Main, NextScript } from 'next/document';
|
|
7
|
+
|
|
8
|
+
export default function Document() {
|
|
9
|
+
return (
|
|
10
|
+
<Html lang="fr">
|
|
11
|
+
<Head>
|
|
12
|
+
{/* ECharts - Bibliothèque de graphiques */}
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0/dist/echarts.js"></script>
|
|
14
|
+
|
|
15
|
+
{/* jQuery et jQuery UI */}
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/dist/jquery-ui.min.js"></script>
|
|
18
|
+
<link
|
|
19
|
+
rel="stylesheet"
|
|
20
|
+
href="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/themes/base/jquery-ui.css"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
{/* Underscore.js */}
|
|
24
|
+
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.7/underscore-umd-min.js"></script>
|
|
25
|
+
|
|
26
|
+
{/* Bootstrap */}
|
|
27
|
+
<script
|
|
28
|
+
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
|
29
|
+
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
|
30
|
+
crossOrigin="anonymous"
|
|
31
|
+
></script>
|
|
32
|
+
<link
|
|
33
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
|
|
34
|
+
rel="stylesheet"
|
|
35
|
+
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
|
|
36
|
+
crossOrigin="anonymous"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{/* Font Awesome */}
|
|
40
|
+
<link
|
|
41
|
+
href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
|
|
42
|
+
rel="stylesheet"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
{/*
|
|
46
|
+
Scripts TIKA - À copier dans le dossier public/
|
|
47
|
+
Voir instructions dans react/README.md
|
|
48
|
+
*/}
|
|
49
|
+
<script src="/chart-render.js"></script>
|
|
50
|
+
<script src="/editor-attributes.js"></script>
|
|
51
|
+
<script src="/editor-interventions.js"></script>
|
|
52
|
+
<script src="/editor-crops.js"></script>
|
|
53
|
+
<script src="/editor-export.js"></script>
|
|
54
|
+
<script src="/editor-wiki-editor.js"></script>
|
|
55
|
+
|
|
56
|
+
{/* Styles TIKA - À copier dans le dossier public/ */}
|
|
57
|
+
<link href="/styles-rendering.css" rel="stylesheet" />
|
|
58
|
+
<link href="/styles-editor.css" rel="stylesheet" />
|
|
59
|
+
</Head>
|
|
60
|
+
<body>
|
|
61
|
+
<Main />
|
|
62
|
+
<NextScript />
|
|
63
|
+
</body>
|
|
64
|
+
</Html>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemple d'API route pour sauvegarder/charger des itinéraires
|
|
3
|
+
* Fichier: app/api/itineraire/route.ts (Next.js App Router)
|
|
4
|
+
* ou pages/api/itineraire.ts (Next.js Pages Router)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
// Dossier de stockage des itinéraires
|
|
12
|
+
const ITINERAIRES_DIR = path.join(process.cwd(), 'data', 'itineraires');
|
|
13
|
+
|
|
14
|
+
// Créer le dossier s'il n'existe pas
|
|
15
|
+
async function ensureDir() {
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(ITINERAIRES_DIR);
|
|
18
|
+
} catch {
|
|
19
|
+
await fs.mkdir(ITINERAIRES_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GET - Récupérer un itinéraire
|
|
24
|
+
export async function GET(request: NextRequest) {
|
|
25
|
+
const { searchParams } = new URL(request.url);
|
|
26
|
+
const id = searchParams.get('id');
|
|
27
|
+
|
|
28
|
+
if (!id) {
|
|
29
|
+
return NextResponse.json({ error: 'ID manquant' }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await ensureDir();
|
|
34
|
+
const filePath = path.join(ITINERAIRES_DIR, `${id}.json`);
|
|
35
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
36
|
+
return NextResponse.json(JSON.parse(data));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{ error: 'Itinéraire non trouvé' },
|
|
40
|
+
{ status: 404 }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// POST - Sauvegarder un itinéraire
|
|
46
|
+
export async function POST(request: NextRequest) {
|
|
47
|
+
try {
|
|
48
|
+
const data = await request.json();
|
|
49
|
+
|
|
50
|
+
// Validation basique
|
|
51
|
+
if (!data.title || !data.options || !data.steps) {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Données invalides' },
|
|
54
|
+
{ status: 400 }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await ensureDir();
|
|
59
|
+
|
|
60
|
+
// Générer un ID si nécessaire
|
|
61
|
+
const id = data.id || `itk-${Date.now()}`;
|
|
62
|
+
const filePath = path.join(ITINERAIRES_DIR, `${id}.json`);
|
|
63
|
+
|
|
64
|
+
// Sauvegarder
|
|
65
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
66
|
+
|
|
67
|
+
return NextResponse.json({ success: true, id });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Erreur de sauvegarde:', error);
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: 'Erreur de sauvegarde' },
|
|
72
|
+
{ status: 500 }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// DELETE - Supprimer un itinéraire
|
|
78
|
+
export async function DELETE(request: NextRequest) {
|
|
79
|
+
const { searchParams } = new URL(request.url);
|
|
80
|
+
const id = searchParams.get('id');
|
|
81
|
+
|
|
82
|
+
if (!id) {
|
|
83
|
+
return NextResponse.json({ error: 'ID manquant' }, { status: 400 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const filePath = path.join(ITINERAIRES_DIR, `${id}.json`);
|
|
88
|
+
await fs.unlink(filePath);
|
|
89
|
+
return NextResponse.json({ success: true });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{ error: 'Erreur de suppression' },
|
|
93
|
+
{ status: 500 }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Liste de tous les itinéraires (pour Pages Router)
|
|
99
|
+
// Fichier: pages/api/itineraires/list.ts
|
|
100
|
+
export async function listItineraires() {
|
|
101
|
+
await ensureDir();
|
|
102
|
+
const files = await fs.readdir(ITINERAIRES_DIR);
|
|
103
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
104
|
+
|
|
105
|
+
const itineraires = await Promise.all(
|
|
106
|
+
jsonFiles.map(async (file) => {
|
|
107
|
+
const content = await fs.readFile(
|
|
108
|
+
path.join(ITINERAIRES_DIR, file),
|
|
109
|
+
'utf-8'
|
|
110
|
+
);
|
|
111
|
+
const data = JSON.parse(content);
|
|
112
|
+
return {
|
|
113
|
+
id: file.replace('.json', ''),
|
|
114
|
+
title: data.title,
|
|
115
|
+
stepsCount: data.steps?.length || 0,
|
|
116
|
+
updatedAt: (await fs.stat(path.join(ITINERAIRES_DIR, file))).mtime
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return itineraires;
|
|
122
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemple d'utilisation de l'éditeur avec Next.js App Router
|
|
3
|
+
* Fichier: app/editor/page.tsx
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { useItineraire, useItineraireDependencies } from '@osfarm/itineraire-technique/react';
|
|
9
|
+
import { TikaRenderer } from '@osfarm/itineraire-technique/react';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import type { ItineraireData, Step, Intervention } from '@osfarm/itineraire-technique/react';
|
|
12
|
+
|
|
13
|
+
export default function EditorPage() {
|
|
14
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
15
|
+
const [saveMessage, setSaveMessage] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
// Vérifier que les dépendances sont chargées
|
|
18
|
+
const { loaded: depsLoaded, error: depsError } = useItineraireDependencies();
|
|
19
|
+
|
|
20
|
+
// Données initiales
|
|
21
|
+
const initialData: ItineraireData = {
|
|
22
|
+
title: "Nouvelle rotation",
|
|
23
|
+
options: {
|
|
24
|
+
view: "horizontal",
|
|
25
|
+
show_transcript: true,
|
|
26
|
+
title_top_interventions: "Cultures principales",
|
|
27
|
+
title_bottom_interventions: "Couverts et CIVE",
|
|
28
|
+
title_steps: "Rotation",
|
|
29
|
+
region: "France",
|
|
30
|
+
show_climate_diagram: false
|
|
31
|
+
},
|
|
32
|
+
steps: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
data,
|
|
37
|
+
loading,
|
|
38
|
+
error,
|
|
39
|
+
addStep,
|
|
40
|
+
updateStep,
|
|
41
|
+
deleteStep,
|
|
42
|
+
addIntervention,
|
|
43
|
+
deleteIntervention,
|
|
44
|
+
exportToJson,
|
|
45
|
+
importFromJson
|
|
46
|
+
} = useItineraire(initialData);
|
|
47
|
+
|
|
48
|
+
// Sauvegarder vers une API
|
|
49
|
+
const handleSave = async () => {
|
|
50
|
+
if (!data) return;
|
|
51
|
+
|
|
52
|
+
setIsSaving(true);
|
|
53
|
+
setSaveMessage(null);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch('/api/itineraire', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(data),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) throw new Error('Erreur de sauvegarde');
|
|
65
|
+
|
|
66
|
+
setSaveMessage('Sauvegarde réussie !');
|
|
67
|
+
setTimeout(() => setSaveMessage(null), 3000);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
setSaveMessage(`Erreur: ${err instanceof Error ? err.message : 'Erreur inconnue'}`);
|
|
70
|
+
} finally {
|
|
71
|
+
setIsSaving(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Exporter en JSON
|
|
76
|
+
const handleExport = () => {
|
|
77
|
+
const json = exportToJson();
|
|
78
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
79
|
+
const url = URL.createObjectURL(blob);
|
|
80
|
+
const a = document.createElement('a');
|
|
81
|
+
a.href = url;
|
|
82
|
+
a.download = `itineraire-${Date.now()}.json`;
|
|
83
|
+
document.body.appendChild(a);
|
|
84
|
+
a.click();
|
|
85
|
+
document.body.removeChild(a);
|
|
86
|
+
URL.revokeObjectURL(url);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Importer depuis JSON
|
|
90
|
+
const handleImport = () => {
|
|
91
|
+
const input = document.createElement('input');
|
|
92
|
+
input.type = 'file';
|
|
93
|
+
input.accept = 'application/json';
|
|
94
|
+
input.onchange = (e) => {
|
|
95
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
96
|
+
if (file) {
|
|
97
|
+
const reader = new FileReader();
|
|
98
|
+
reader.onload = (e) => {
|
|
99
|
+
const content = e.target?.result as string;
|
|
100
|
+
try {
|
|
101
|
+
importFromJson(content);
|
|
102
|
+
setSaveMessage('Import réussi !');
|
|
103
|
+
setTimeout(() => setSaveMessage(null), 3000);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
setSaveMessage(`Erreur d'import: ${err instanceof Error ? err.message : 'Fichier invalide'}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
reader.readAsText(file);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
input.click();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Ajouter une nouvelle étape
|
|
115
|
+
const handleAddStep = () => {
|
|
116
|
+
const now = new Date();
|
|
117
|
+
const endDate = new Date(now);
|
|
118
|
+
endDate.setMonth(endDate.getMonth() + 3);
|
|
119
|
+
|
|
120
|
+
const newStep: Step = {
|
|
121
|
+
id: crypto.randomUUID(),
|
|
122
|
+
name: `Nouvelle culture ${data?.steps.length ? data.steps.length + 1 : 1}`,
|
|
123
|
+
startDate: now.toISOString(),
|
|
124
|
+
endDate: endDate.toISOString(),
|
|
125
|
+
color: `#${Math.floor(Math.random()*16777215).toString(16)}`,
|
|
126
|
+
description: '',
|
|
127
|
+
interventions: []
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
addStep(newStep);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Ajouter une intervention à une étape
|
|
134
|
+
const handleAddIntervention = (stepId: string) => {
|
|
135
|
+
const newIntervention: Intervention = {
|
|
136
|
+
id: crypto.randomUUID(),
|
|
137
|
+
day: '0',
|
|
138
|
+
name: 'Nouvelle intervention',
|
|
139
|
+
type: 'intervention_top',
|
|
140
|
+
description: ''
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
addIntervention(stepId, newIntervention);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!depsLoaded) {
|
|
147
|
+
return (
|
|
148
|
+
<div className="container py-5 text-center">
|
|
149
|
+
<div className="spinner-border" role="status">
|
|
150
|
+
<span className="visually-hidden">Chargement des dépendances...</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (depsError) {
|
|
157
|
+
return (
|
|
158
|
+
<div className="container py-5">
|
|
159
|
+
<div className="alert alert-danger">
|
|
160
|
+
Erreur de chargement: {depsError.message}
|
|
161
|
+
<br />
|
|
162
|
+
<small>Assurez-vous que les scripts sont bien chargés dans _document.tsx</small>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (loading) {
|
|
169
|
+
return (
|
|
170
|
+
<div className="container py-5 text-center">
|
|
171
|
+
<div className="spinner-border" role="status">
|
|
172
|
+
<span className="visually-hidden">Chargement...</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (error) {
|
|
179
|
+
return (
|
|
180
|
+
<div className="container py-5">
|
|
181
|
+
<div className="alert alert-danger">
|
|
182
|
+
Erreur: {error.message}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="container-fluid py-4">
|
|
190
|
+
{/* En-tête avec boutons d'action */}
|
|
191
|
+
<div className="row mb-4">
|
|
192
|
+
<div className="col-12">
|
|
193
|
+
<div className="d-flex justify-content-between align-items-center">
|
|
194
|
+
<h1>Éditeur d'Itinéraire Technique</h1>
|
|
195
|
+
|
|
196
|
+
<div className="btn-group" role="group">
|
|
197
|
+
<button
|
|
198
|
+
className="btn btn-primary"
|
|
199
|
+
onClick={handleSave}
|
|
200
|
+
disabled={isSaving}
|
|
201
|
+
>
|
|
202
|
+
<i className="fa fa-save"></i>
|
|
203
|
+
{isSaving ? ' Sauvegarde...' : ' Sauvegarder'}
|
|
204
|
+
</button>
|
|
205
|
+
<button
|
|
206
|
+
className="btn btn-outline-secondary"
|
|
207
|
+
onClick={handleExport}
|
|
208
|
+
>
|
|
209
|
+
<i className="fa fa-download"></i> Exporter JSON
|
|
210
|
+
</button>
|
|
211
|
+
<button
|
|
212
|
+
className="btn btn-outline-secondary"
|
|
213
|
+
onClick={handleImport}
|
|
214
|
+
>
|
|
215
|
+
<i className="fa fa-upload"></i> Importer JSON
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{saveMessage && (
|
|
221
|
+
<div className={`alert mt-3 ${saveMessage.includes('Erreur') ? 'alert-danger' : 'alert-success'}`}>
|
|
222
|
+
{saveMessage}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* Zone d'édition */}
|
|
229
|
+
<div className="row">
|
|
230
|
+
<div className="col-lg-3">
|
|
231
|
+
<div className="card">
|
|
232
|
+
<div className="card-header d-flex justify-content-between align-items-center">
|
|
233
|
+
<h5 className="mb-0">Étapes</h5>
|
|
234
|
+
<button
|
|
235
|
+
className="btn btn-sm btn-success"
|
|
236
|
+
onClick={handleAddStep}
|
|
237
|
+
>
|
|
238
|
+
<i className="fa fa-plus"></i>
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="card-body">
|
|
242
|
+
{data?.steps.length === 0 ? (
|
|
243
|
+
<p className="text-muted text-center">Aucune étape</p>
|
|
244
|
+
) : (
|
|
245
|
+
<div className="list-group">
|
|
246
|
+
{data?.steps.map((step) => (
|
|
247
|
+
<div key={step.id} className="list-group-item">
|
|
248
|
+
<div className="d-flex justify-content-between align-items-center mb-2">
|
|
249
|
+
<strong>{step.name}</strong>
|
|
250
|
+
<div className="btn-group btn-group-sm">
|
|
251
|
+
<button
|
|
252
|
+
className="btn btn-outline-primary"
|
|
253
|
+
onClick={() => handleAddIntervention(step.id)}
|
|
254
|
+
title="Ajouter intervention"
|
|
255
|
+
>
|
|
256
|
+
<i className="fa fa-plus"></i>
|
|
257
|
+
</button>
|
|
258
|
+
<button
|
|
259
|
+
className="btn btn-outline-danger"
|
|
260
|
+
onClick={() => deleteStep(step.id)}
|
|
261
|
+
title="Supprimer"
|
|
262
|
+
>
|
|
263
|
+
<i className="fa fa-trash"></i>
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
<small className="text-muted">
|
|
268
|
+
{new Date(step.startDate).toLocaleDateString()} -
|
|
269
|
+
{new Date(step.endDate).toLocaleDateString()}
|
|
270
|
+
</small>
|
|
271
|
+
{step.interventions && step.interventions.length > 0 && (
|
|
272
|
+
<div className="mt-2">
|
|
273
|
+
<small>{step.interventions.length} intervention(s)</small>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div className="col-lg-9">
|
|
285
|
+
<div className="card">
|
|
286
|
+
<div className="card-header">
|
|
287
|
+
<h5 className="mb-0">Aperçu</h5>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="card-body">
|
|
290
|
+
{data && data.steps.length > 0 ? (
|
|
291
|
+
<TikaRenderer data={data} width="100%" height="auto" />
|
|
292
|
+
) : (
|
|
293
|
+
<div className="text-center text-muted py-5">
|
|
294
|
+
<i className="fa fa-info-circle fa-3x mb-3"></i>
|
|
295
|
+
<p>Ajoutez des étapes pour voir l'aperçu</p>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|