Added the Import component with logic for importing data from a Geoapify API (WIP).

master
Alix JEUDI--LEMOINE 7 days ago
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…
Cancel
Save