Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
✨ Changements majeurs: - Suppression complète du code Flask legacy - Migration backend FastAPI vers racine /backend - Migration frontend Vue.js vers racine /frontend - Suppression de notytex-v2/ (code monté à la racine) ✅ Validations: - Backend démarre correctement (port 8000) - API /api/v2/health répond healthy - 99/99 tests unitaires passent - Frontend configuré avec proxy Vite 📝 Documentation: - README.md réécrit pour v2 - Instructions de démarrage mises à jour - .gitignore adapté pour backend/frontend/ 🎯 Architecture finale: notytex/ ├── backend/ # FastAPI + SQLAlchemy + Pydantic ├── frontend/ # Vue 3 + Vite + TailwindCSS ├── docs/ # Documentation └── school_management.db # Base de données (inchangée) Jalon 6 complété: Application v2 prête pour utilisation!
This commit is contained in:
227
frontend/src/components/config/ConfigDomainsTab.vue
Normal file
227
frontend/src/components/config/ConfigDomainsTab.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Liste des domaines -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="font-medium">Domaines configures</h3>
|
||||
<p class="text-sm text-gray-500">{{ domains.length }} domaine(s)</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div v-if="domains.length" class="space-y-3">
|
||||
<div
|
||||
v-for="domain in domains"
|
||||
:key="domain.id"
|
||||
class="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full border-2 border-white shadow-sm"
|
||||
:style="{ backgroundColor: domain.color }"
|
||||
></div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900">{{ domain.name }}</h4>
|
||||
<p v-if="domain.description" class="text-xs text-gray-500">{{ domain.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
@click="editDomain(domain)"
|
||||
class="p-2 text-gray-400 hover:text-primary-600 transition-colors"
|
||||
title="Modifier"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="confirmDelete(domain)"
|
||||
class="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-else
|
||||
title="Aucun domaine"
|
||||
description="Ajoutez votre premier domaine"
|
||||
icon="tag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'ajout/modification -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="card sticky top-4">
|
||||
<div class="card-header">
|
||||
<h3 class="font-medium">
|
||||
{{ editingDomain ? 'Modifier' : 'Ajouter' }} un domaine
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveDomain" class="card-body space-y-4">
|
||||
<div>
|
||||
<label for="domain-name" class="label">Nom</label>
|
||||
<input
|
||||
type="text"
|
||||
id="domain-name"
|
||||
v-model="form.name"
|
||||
placeholder="Ex: Geometrie, Algebre..."
|
||||
class="input"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">Couleur</label>
|
||||
<ColorPicker v-model="form.color" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="domain-desc" class="label">Description (optionnel)</label>
|
||||
<textarea
|
||||
id="domain-desc"
|
||||
v-model="form.description"
|
||||
rows="2"
|
||||
class="input"
|
||||
placeholder="Description du domaine..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button
|
||||
v-if="editingDomain"
|
||||
type="button"
|
||||
@click="cancelEdit"
|
||||
class="btn btn-secondary flex-1"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary flex-1"
|
||||
:disabled="saving"
|
||||
>
|
||||
{{ editingDomain ? 'Modifier' : 'Ajouter' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de confirmation de suppression -->
|
||||
<Modal v-model="showDeleteModal" title="Confirmer la suppression">
|
||||
<p class="text-gray-600">
|
||||
Etes-vous sur de vouloir supprimer le domaine
|
||||
<strong>{{ domainToDelete?.name }}</strong> ?
|
||||
</p>
|
||||
|
||||
<template #footer>
|
||||
<button @click="showDeleteModal = false" class="btn btn-secondary">
|
||||
Annuler
|
||||
</button>
|
||||
<button @click="deleteConfirmed" class="btn btn-danger">
|
||||
Supprimer
|
||||
</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useNotificationsStore } from '@/stores/notifications'
|
||||
import ColorPicker from '@/components/common/ColorPicker.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const notifications = useNotificationsStore()
|
||||
|
||||
const domains = computed(() => configStore.domains)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
color: '#6b7280',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const editingDomain = ref(null)
|
||||
const saving = ref(false)
|
||||
const showDeleteModal = ref(false)
|
||||
const domainToDelete = ref(null)
|
||||
|
||||
function editDomain(domain) {
|
||||
editingDomain.value = domain
|
||||
form.value = {
|
||||
name: domain.name,
|
||||
color: domain.color,
|
||||
description: domain.description || ''
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingDomain.value = null
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
name: '',
|
||||
color: '#6b7280',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDomain() {
|
||||
if (!form.value.name.trim()) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingDomain.value) {
|
||||
await configStore.updateDomain(editingDomain.value.id, form.value)
|
||||
notifications.success('Domaine modifie')
|
||||
editingDomain.value = null
|
||||
} else {
|
||||
await configStore.createDomain(form.value)
|
||||
notifications.success('Domaine ajoute')
|
||||
}
|
||||
resetForm()
|
||||
} catch (e) {
|
||||
notifications.error(e.response?.data?.detail || 'Erreur lors de l\'enregistrement')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(domain) {
|
||||
domainToDelete.value = domain
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteConfirmed() {
|
||||
if (!domainToDelete.value) return
|
||||
|
||||
try {
|
||||
await configStore.deleteDomain(domainToDelete.value.id)
|
||||
notifications.success('Domaine supprime')
|
||||
} catch (e) {
|
||||
notifications.error('Erreur lors de la suppression')
|
||||
} finally {
|
||||
showDeleteModal.value = false
|
||||
domainToDelete.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user