parent
21dbf6199f
commit
e5903774e3
@ -0,0 +1,82 @@
|
|||||||
|
.import-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 20px 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-container {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section pre {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-snackbar {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-snackbar {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
<div class="p-6 max-w-7xl mx-auto">
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">Import de données</h2>
|
||||||
|
<p class="text-gray-600">Importez des données depuis une API Geoapify</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form [formGroup]="importForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="apiUrl" class="block text-sm font-medium text-gray-700">URL de l'API</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="apiUrl"
|
||||||
|
formControlName="apiUrl"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="https://api.geoapify.com/..."
|
||||||
|
>
|
||||||
|
<div *ngIf="importForm.get('apiUrl')?.hasError('required') && importForm.get('apiUrl')?.touched"
|
||||||
|
class="text-red-500 text-sm">
|
||||||
|
L'URL est requise
|
||||||
|
</div>
|
||||||
|
<div *ngIf="importForm.get('apiUrl')?.hasError('pattern') && importForm.get('apiUrl')?.touched"
|
||||||
|
class="text-red-500 text-sm">
|
||||||
|
L'URL doit commencer par http:// ou https://
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="!importForm.valid || isLoading"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<svg *ngIf="isLoading" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span *ngIf="!isLoading">Importer</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div *ngIf="data" class="mt-8 space-y-6">
|
||||||
|
<div class="bg-gray-50 rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Correspondance des champs</h3>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
Sélectionnez les champs de l'API qui correspondent à vos champs système
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div *ngFor="let targetField of targetFields" class="space-y-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-700">
|
||||||
|
{{targetField}}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
[value]="fieldMappings[targetField]"
|
||||||
|
(change)="updateFieldMapping(targetField, $any($event.target).value)"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Aucune correspondance</option>
|
||||||
|
<option *ngFor="let sourceField of availableFields" [value]="sourceField">
|
||||||
|
{{sourceField}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="data.features.length > 0" class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Aperçu des données</h3>
|
||||||
|
<pre class="bg-gray-50 p-4 rounded-md overflow-x-auto text-sm">{{data.features[0] | json}}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
(click)="processImport()"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
Valider l'import
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,129 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { ImportService, GeoapifyResponse, GeoapifyFeature } from '../../services/import.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { POIService } from '../../services/poi.service';
|
||||||
|
import { POI } from '../../model/POI';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-import',
|
||||||
|
templateUrl: './import.component.html',
|
||||||
|
styleUrls: ['./import.component.css'],
|
||||||
|
imports: [CommonModule, ReactiveFormsModule]
|
||||||
|
})
|
||||||
|
export class ImportComponent {
|
||||||
|
importForm: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
data: GeoapifyResponse | null = null;
|
||||||
|
fieldMappings: { [key: string]: string } = {};
|
||||||
|
availableFields: string[] = [];
|
||||||
|
targetFields = [
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'complete_address',
|
||||||
|
'latitude',
|
||||||
|
'longitude'
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private importService: ImportService,
|
||||||
|
private poiService: POIService
|
||||||
|
) {
|
||||||
|
this.importForm = this.fb.group({
|
||||||
|
apiUrl: ['', [Validators.required, Validators.pattern('^https?://.+')]]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.importForm.valid) {
|
||||||
|
this.isLoading = true;
|
||||||
|
const url = this.importForm.get('apiUrl')?.value;
|
||||||
|
|
||||||
|
this.importService.fetchDataFromUrl(url).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
if (this.importService.validateGeoapifyData(response)) {
|
||||||
|
this.data = response;
|
||||||
|
this.extractAvailableFields(response.features[0]);
|
||||||
|
this.initializeFieldMappings();
|
||||||
|
} else {
|
||||||
|
this.showError('Format de données invalide');
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.showError(error.message);
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractAvailableFields(feature: GeoapifyFeature): void {
|
||||||
|
this.availableFields = Object.keys(feature.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeFieldMappings(): void {
|
||||||
|
this.fieldMappings = {};
|
||||||
|
this.targetFields.forEach(field => {
|
||||||
|
this.fieldMappings[field] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFieldMapping(targetField: string, sourceField: string): void {
|
||||||
|
this.fieldMappings[targetField] = sourceField;
|
||||||
|
}
|
||||||
|
|
||||||
|
processImport(): void {
|
||||||
|
if (!this.data) return;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
// Traiter chaque feature
|
||||||
|
this.data.features.forEach(feature => {
|
||||||
|
const poi = this.importService.convertToPOI(feature, this.fieldMappings);
|
||||||
|
|
||||||
|
this.poiService.addPOI(poi).subscribe({
|
||||||
|
next: () => {
|
||||||
|
successCount++;
|
||||||
|
if (successCount + errorCount === this.data!.features.length) {
|
||||||
|
this.showSuccess(`${successCount} POIs importés avec succès`);
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Erreur lors de l\'import du POI:', error);
|
||||||
|
errorCount++;
|
||||||
|
if (successCount + errorCount === this.data!.features.length) {
|
||||||
|
this.showError(`${errorCount} erreurs lors de l'import`);
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showError(message: string): void {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue