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