feat: ajout d'une modale de gestion des chapitres, permettant la création, l'édition et le déplacement de chapitres. Mise à jour de l'API pour gérer les modifications en lot des chapitres, ainsi que l'intégration de tests pour valider cette nouvelle fonctionnalité. Amélioration de l'interface utilisateur pour une gestion plus fluide des chapitres.
This commit is contained in:
parent
00d63dffeb
commit
551db0bf77
@@ -0,0 +1,703 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- Overlay -->
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="handleClose"></div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-5xl sm:w-full">
|
||||
<!-- Header -->
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
Gérer les chapitres - {{ manga?.title }}
|
||||
</h3>
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<XMarkIcon class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="bg-white px-4 pb-5 sm:p-6">
|
||||
<div v-if="isLoading" class="flex justify-center items-center h-32">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button
|
||||
@click="showCreateVolumeModal = true"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded text-sm hover:bg-blue-700"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4 inline mr-1" />
|
||||
Créer un volume
|
||||
</button>
|
||||
<button
|
||||
@click="showUnassignedChapters = !showUnassignedChapters"
|
||||
class="text-gray-600 hover:text-gray-800 text-sm"
|
||||
>
|
||||
{{ showUnassignedChapters ? 'Masquer' : 'Afficher' }} les chapitres non assignés
|
||||
</button>
|
||||
<!-- Actions de sélection multiple -->
|
||||
<div v-if="selectedChapters.length > 0" class="flex items-center space-x-2 bg-blue-50 px-3 py-1 rounded-lg">
|
||||
<span class="text-sm text-blue-700">{{ selectedChapters.length }} chapitre(s) sélectionné(s)</span>
|
||||
<button
|
||||
@click="showMoveToVolumeModal = true"
|
||||
class="bg-blue-600 text-white px-2 py-1 rounded text-xs hover:bg-blue-700"
|
||||
>
|
||||
Déplacer vers un volume
|
||||
</button>
|
||||
<button
|
||||
@click="clearSelection"
|
||||
class="text-blue-600 hover:text-blue-800 text-xs"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ totalChapters }} chapitres, {{ volumes.length }} volumes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arborescence -->
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<!-- Chapitres non assignés -->
|
||||
<div v-if="showUnassignedChapters && unassignedChapters.length > 0" class="bg-gray-50 border-b border-gray-200">
|
||||
<div class="px-4 py-3">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">
|
||||
Chapitres non assignés ({{ unassignedChapters.length }})
|
||||
</h4>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="chapter in unassignedChapters"
|
||||
:key="chapter.id"
|
||||
class="flex items-center space-x-3 p-2 hover:bg-gray-100 rounded"
|
||||
:class="{ 'bg-blue-50 border border-blue-200': isChapterSelected(chapter) }"
|
||||
>
|
||||
<!-- Checkbox de sélection -->
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isChapterSelected(chapter)"
|
||||
@change="toggleChapterSelection(chapter)"
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<DocumentIcon class="h-4 w-4 text-gray-400" />
|
||||
<span class="text-sm font-medium text-gray-700 w-12">{{ chapter.number }}</span>
|
||||
<div class="flex-1">
|
||||
<div v-if="!chapter.isEditing" class="flex items-center">
|
||||
<span
|
||||
class="text-sm text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
@click="startEditingTitle(chapter)"
|
||||
>
|
||||
{{ chapter.title || 'Sans titre' }}
|
||||
</span>
|
||||
<button
|
||||
@click="startEditingTitle(chapter)"
|
||||
class="ml-2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<PencilIcon class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex items-center space-x-1">
|
||||
<input
|
||||
v-model="chapter.editingTitle"
|
||||
type="text"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-xs"
|
||||
@keyup.enter="saveTitle(chapter)"
|
||||
@keyup.esc="cancelEditingTitle(chapter)"
|
||||
ref="titleInput"
|
||||
/>
|
||||
<button
|
||||
@click="saveTitle(chapter)"
|
||||
class="text-green-600 hover:text-green-800"
|
||||
>
|
||||
<CheckIcon class="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
@click="cancelEditingTitle(chapter)"
|
||||
class="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<XMarkIcon class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button
|
||||
@click="assignToVolume(chapter)"
|
||||
class="text-blue-600 hover:text-blue-800 text-xs"
|
||||
>
|
||||
Assigner
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="chapter.isModified" class="w-2 h-2 bg-yellow-400 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Volumes -->
|
||||
<div class="divide-y divide-gray-200">
|
||||
<div
|
||||
v-for="volume in volumes"
|
||||
:key="volume.number"
|
||||
class="bg-white"
|
||||
>
|
||||
<!-- En-tête du volume -->
|
||||
<div class="px-4 py-3 bg-blue-50 border-b border-blue-100">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<FolderIcon class="h-5 w-5 text-blue-600" />
|
||||
<span class="text-sm font-medium text-blue-900">Volume {{ volume.number }}</span>
|
||||
<span class="text-xs text-blue-600">({{ volume.chapters.length }} chapitres)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="toggleVolumeExpanded(volume)"
|
||||
class="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<ChevronDownIcon v-if="volume.isExpanded" class="h-4 w-4" />
|
||||
<ChevronRightIcon v-else class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="deleteVolume(volume.number)"
|
||||
class="text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chapitres du volume -->
|
||||
<div v-if="volume.isExpanded" class="px-4 py-2">
|
||||
<div v-if="volume.chapters.length === 0" class="text-center py-4 text-gray-500 text-sm">
|
||||
Aucun chapitre assigné à ce volume.
|
||||
<br>
|
||||
<span class="text-xs">Utilisez le bouton "Assigner" sur les chapitres non assignés pour les ajouter.</span>
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<div
|
||||
v-for="chapter in volume.chapters"
|
||||
:key="chapter.id"
|
||||
class="flex items-center space-x-3 p-2 hover:bg-gray-50 rounded"
|
||||
:class="{ 'bg-blue-50 border border-blue-200': isChapterSelected(chapter) }"
|
||||
>
|
||||
<!-- Checkbox de sélection -->
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isChapterSelected(chapter)"
|
||||
@change="toggleChapterSelection(chapter)"
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<DocumentIcon class="h-4 w-4 text-gray-400" />
|
||||
<span class="text-sm font-medium text-gray-700 w-12">{{ chapter.number }}</span>
|
||||
<div class="flex-1">
|
||||
<div v-if="!chapter.isEditing" class="flex items-center">
|
||||
<span
|
||||
class="text-sm text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
@click="startEditingTitle(chapter)"
|
||||
>
|
||||
{{ chapter.title || 'Sans titre' }}
|
||||
</span>
|
||||
<button
|
||||
@click="startEditingTitle(chapter)"
|
||||
class="ml-2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<PencilIcon class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex items-center space-x-1">
|
||||
<input
|
||||
v-model="chapter.editingTitle"
|
||||
type="text"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-xs"
|
||||
@keyup.enter="saveTitle(chapter)"
|
||||
@keyup.esc="cancelEditingTitle(chapter)"
|
||||
ref="titleInput"
|
||||
/>
|
||||
<button
|
||||
@click="saveTitle(chapter)"
|
||||
class="text-green-600 hover:text-green-800"
|
||||
>
|
||||
<CheckIcon class="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
@click="cancelEditingTitle(chapter)"
|
||||
class="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<XMarkIcon class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button
|
||||
@click="removeFromVolume(chapter)"
|
||||
class="text-red-600 hover:text-red-800 text-xs"
|
||||
>
|
||||
Retirer
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="chapter.isModified" class="w-2 h-2 bg-yellow-400 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
@click="handleSave"
|
||||
:disabled="isSaving || !hasChanges"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
<span v-if="isSaving">Sauvegarde...</span>
|
||||
<span v-else>Sauvegarder</span>
|
||||
</button>
|
||||
<button
|
||||
@click="handleClose"
|
||||
:disabled="isSaving"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de création de volume -->
|
||||
<div v-if="showCreateVolumeModal" class="fixed inset-0 z-60 overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="showCreateVolumeModal = false"></div>
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Créer un nouveau volume</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Numéro du volume</label>
|
||||
<input
|
||||
v-model="newVolumeNumber"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
|
||||
placeholder="Ex: 1"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="volumeExists" class="text-red-600 text-sm">
|
||||
Ce volume existe déjà.
|
||||
</div>
|
||||
<div v-if="newVolumeNumber && !isValidVolumeNumber" class="text-red-600 text-sm">
|
||||
Le numéro de volume doit être entre 1 et 999.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
@click="createVolume"
|
||||
:disabled="!isValidVolumeNumber || volumeExists"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
Créer
|
||||
</button>
|
||||
<button
|
||||
@click="showCreateVolumeModal = false"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal d'assignation -->
|
||||
<div v-if="showAssignModal" class="fixed inset-0 z-60 overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="showAssignModal = false"></div>
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Assigner le chapitre {{ selectedChapter?.number }}</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Volume</label>
|
||||
<select
|
||||
v-model="selectedVolumeForAssignment"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">Sélectionner un volume</option>
|
||||
<option v-for="volume in volumes" :key="volume.number" :value="volume.number">
|
||||
Volume {{ volume.number }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
@click="confirmAssignToVolume"
|
||||
:disabled="!selectedVolumeForAssignment"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
Assigner
|
||||
</button>
|
||||
<button
|
||||
@click="showAssignModal = false"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de déplacement multiple -->
|
||||
<div v-if="showMoveToVolumeModal" class="fixed inset-0 z-60 overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="showMoveToVolumeModal = false"></div>
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Déplacer {{ selectedChapters.length }} chapitre(s)</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-blue-50 p-3 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
Chapitres sélectionnés : {{ selectedChapters.map(c => c.number).join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Volume de destination</label>
|
||||
<select
|
||||
v-model="selectedVolumeForMove"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">Sélectionner un volume</option>
|
||||
<option v-for="volume in volumes" :key="volume.number" :value="volume.number">
|
||||
Volume {{ volume.number }}
|
||||
</option>
|
||||
<option value="unassigned">Chapitres non assignés</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
@click="confirmMoveToVolume"
|
||||
:disabled="!selectedVolumeForMove"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
Déplacer
|
||||
</button>
|
||||
<button
|
||||
@click="showMoveToVolumeModal = false"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
DocumentIcon,
|
||||
FolderIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
XMarkIcon
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
manga: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
chapters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
// État local
|
||||
const localChapters = ref([]);
|
||||
const showCreateVolumeModal = ref(false);
|
||||
const showAssignModal = ref(false);
|
||||
const showMoveToVolumeModal = ref(false);
|
||||
const showUnassignedChapters = ref(true);
|
||||
const newVolumeNumber = ref('');
|
||||
const selectedChapter = ref(null);
|
||||
const selectedVolumeForAssignment = ref('');
|
||||
const selectedVolumeForMove = ref('');
|
||||
const titleInput = ref(null);
|
||||
const expandedVolumes = ref(new Set());
|
||||
const selectedChapters = ref([]);
|
||||
|
||||
// Computed properties
|
||||
const volumes = computed(() => {
|
||||
const volumeMap = new Map();
|
||||
|
||||
// Ajouter les volumes existants avec leurs chapitres
|
||||
localChapters.value.forEach(chapter => {
|
||||
if (chapter.volume) {
|
||||
if (!volumeMap.has(chapter.volume)) {
|
||||
volumeMap.set(chapter.volume, {
|
||||
number: chapter.volume,
|
||||
chapters: []
|
||||
});
|
||||
}
|
||||
volumeMap.get(chapter.volume).chapters.push(chapter);
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter les volumes vides qui sont dans expandedVolumes
|
||||
expandedVolumes.value.forEach(volumeNumber => {
|
||||
if (!volumeMap.has(volumeNumber)) {
|
||||
volumeMap.set(volumeNumber, {
|
||||
number: volumeNumber,
|
||||
chapters: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const volumesArray = Array.from(volumeMap.values())
|
||||
.sort((a, b) => b.number - a.number) // Tri décroissant (derniers volumes en premier)
|
||||
.map(volume => ({
|
||||
...volume,
|
||||
chapters: volume.chapters.sort((a, b) => b.number - a.number), // Chapitres décroissants
|
||||
isExpanded: expandedVolumes.value.has(volume.number)
|
||||
}));
|
||||
|
||||
return volumesArray;
|
||||
});
|
||||
|
||||
const unassignedChapters = computed(() => {
|
||||
return localChapters.value
|
||||
.filter(chapter => !chapter.volume)
|
||||
.sort((a, b) => b.number - a.number); // Tri décroissant (derniers chapitres en premier)
|
||||
});
|
||||
|
||||
const totalChapters = computed(() => localChapters.value.length);
|
||||
|
||||
const volumeExists = computed(() => {
|
||||
if (!newVolumeNumber.value) return false;
|
||||
const volumeNumber = parseInt(newVolumeNumber.value);
|
||||
return volumes.value.some(volume => volume.number === volumeNumber);
|
||||
});
|
||||
|
||||
const isValidVolumeNumber = computed(() => {
|
||||
if (!newVolumeNumber.value) return false;
|
||||
const volumeNumber = parseInt(newVolumeNumber.value);
|
||||
return volumeNumber > 0 && volumeNumber <= 999;
|
||||
});
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
return localChapters.value.some(chapter => chapter.isModified);
|
||||
});
|
||||
|
||||
// Méthodes
|
||||
const handleClose = () => {
|
||||
if (!props.isSaving) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const modifiedChapters = localChapters.value
|
||||
.filter(chapter => chapter.isModified)
|
||||
.map(chapter => ({
|
||||
id: chapter.id,
|
||||
title: chapter.title,
|
||||
volume: chapter.volume || null
|
||||
}));
|
||||
|
||||
if (modifiedChapters.length > 0) {
|
||||
emit('save', modifiedChapters);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleVolumeExpanded = (volume) => {
|
||||
if (expandedVolumes.value.has(volume.number)) {
|
||||
expandedVolumes.value.delete(volume.number);
|
||||
} else {
|
||||
expandedVolumes.value.add(volume.number);
|
||||
}
|
||||
};
|
||||
|
||||
const createVolume = () => {
|
||||
if (newVolumeNumber.value && !volumeExists.value && isValidVolumeNumber.value) {
|
||||
const volumeNumber = parseInt(newVolumeNumber.value);
|
||||
|
||||
// Ajouter le nouveau volume à la liste des volumes dépliés
|
||||
expandedVolumes.value.add(volumeNumber);
|
||||
|
||||
// Fermer la modale et réinitialiser
|
||||
showCreateVolumeModal.value = false;
|
||||
newVolumeNumber.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const deleteVolume = (volumeNumber) => {
|
||||
localChapters.value.forEach(chapter => {
|
||||
if (chapter.volume === volumeNumber) {
|
||||
chapter.volume = null;
|
||||
chapter.isModified = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const assignToVolume = (chapter) => {
|
||||
selectedChapter.value = chapter;
|
||||
selectedVolumeForAssignment.value = '';
|
||||
showAssignModal.value = true;
|
||||
};
|
||||
|
||||
const confirmAssignToVolume = () => {
|
||||
if (selectedChapter.value && selectedVolumeForAssignment.value) {
|
||||
selectedChapter.value.volume = parseInt(selectedVolumeForAssignment.value);
|
||||
selectedChapter.value.isModified = true;
|
||||
showAssignModal.value = false;
|
||||
selectedChapter.value = null;
|
||||
selectedVolumeForAssignment.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromVolume = (chapter) => {
|
||||
chapter.volume = null;
|
||||
chapter.isModified = true;
|
||||
};
|
||||
|
||||
const startEditingTitle = async (chapter) => {
|
||||
chapter.isEditing = true;
|
||||
chapter.editingTitle = chapter.title || '';
|
||||
|
||||
await nextTick();
|
||||
if (titleInput.value) {
|
||||
titleInput.value.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const saveTitle = (chapter) => {
|
||||
if (chapter.editingTitle !== chapter.title) {
|
||||
chapter.title = chapter.editingTitle;
|
||||
chapter.isModified = true;
|
||||
}
|
||||
chapter.isEditing = false;
|
||||
chapter.editingTitle = '';
|
||||
};
|
||||
|
||||
const cancelEditingTitle = (chapter) => {
|
||||
chapter.isEditing = false;
|
||||
chapter.editingTitle = '';
|
||||
};
|
||||
|
||||
// Méthodes de sélection multiple
|
||||
const isChapterSelected = (chapter) => {
|
||||
return selectedChapters.value.some(selected => selected.id === chapter.id);
|
||||
};
|
||||
|
||||
const toggleChapterSelection = (chapter) => {
|
||||
const index = selectedChapters.value.findIndex(selected => selected.id === chapter.id);
|
||||
if (index > -1) {
|
||||
selectedChapters.value.splice(index, 1);
|
||||
} else {
|
||||
selectedChapters.value.push(chapter);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
selectedChapters.value = [];
|
||||
};
|
||||
|
||||
const confirmMoveToVolume = () => {
|
||||
if (selectedVolumeForMove.value && selectedChapters.value.length > 0) {
|
||||
const targetVolume = selectedVolumeForMove.value === 'unassigned' ? null : parseInt(selectedVolumeForMove.value);
|
||||
|
||||
selectedChapters.value.forEach(chapter => {
|
||||
chapter.volume = targetVolume;
|
||||
chapter.isModified = true;
|
||||
});
|
||||
|
||||
// Vider la sélection et fermer la modale
|
||||
selectedChapters.value = [];
|
||||
showMoveToVolumeModal.value = false;
|
||||
selectedVolumeForMove.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Initialiser les chapitres locaux quand les props changent
|
||||
watch(() => props.chapters, (newChapters) => {
|
||||
localChapters.value = newChapters.map(chapter => ({
|
||||
...chapter,
|
||||
isModified: false,
|
||||
isEditing: false,
|
||||
editingTitle: ''
|
||||
}));
|
||||
}, { immediate: true });
|
||||
|
||||
// Réinitialiser quand la modale s'ouvre
|
||||
watch(() => props.isOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
localChapters.value = props.chapters.map(chapter => ({
|
||||
...chapter,
|
||||
isModified: false,
|
||||
isEditing: false,
|
||||
editingTitle: ''
|
||||
}));
|
||||
showCreateVolumeModal.value = false;
|
||||
showAssignModal.value = false;
|
||||
showMoveToVolumeModal.value = false;
|
||||
showUnassignedChapters.value = true;
|
||||
newVolumeNumber.value = '';
|
||||
selectedChapter.value = null;
|
||||
selectedVolumeForAssignment.value = '';
|
||||
selectedVolumeForMove.value = '';
|
||||
expandedVolumes.value.clear();
|
||||
selectedChapters.value = [];
|
||||
|
||||
// S'assurer que le dernier volume est déplié après l'initialisation
|
||||
nextTick(() => {
|
||||
if (volumes.value.length > 0) {
|
||||
expandedVolumes.value.add(volumes.value[0].number);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -50,6 +50,18 @@
|
||||
@close="closeEditModal"
|
||||
@save="saveMangaEdit"
|
||||
/>
|
||||
|
||||
<!-- Modale de gestion des chapitres -->
|
||||
<ManageChaptersModal
|
||||
:is-open="isManageChaptersModalOpen"
|
||||
:manga="currentManga"
|
||||
:chapters="mangaStore.mangaChapters[mangaId]?.items || []"
|
||||
:is-loading="mangaStore.loadingChapters"
|
||||
:is-saving="isSavingChapters"
|
||||
:error="chaptersError"
|
||||
@close="closeManageChaptersModal"
|
||||
@save="saveChaptersChanges"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
|
||||
@@ -84,7 +96,8 @@ import { useMangaPreferredSources } from '../composables/useMangaPreferredSource
|
||||
import { useMangaRefresh } from '../composables/useMangaRefresh';
|
||||
import { useMangaVolumes } from '../composables/useMangaVolumes';
|
||||
|
||||
import MangaEditModal from '../components/MangaEditModal.vue';
|
||||
import ManageChaptersModal from '../components/ManageChaptersModal.vue';
|
||||
import MangaEditModal from '../components/MangaEditModal.vue';
|
||||
import MangaHeader from '../components/MangaHeader.vue';
|
||||
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
|
||||
import MangaVolumeList from '../components/MangaVolumeList.vue';
|
||||
@@ -101,6 +114,9 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
|
||||
// État de la modale
|
||||
const isPreferredSourcesModalOpen = ref(false);
|
||||
const isManageChaptersModalOpen = ref(false);
|
||||
const isSavingChapters = ref(false);
|
||||
const chaptersError = ref(null);
|
||||
|
||||
const {
|
||||
data: currentManga,
|
||||
@@ -167,6 +183,15 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
isPreferredSourcesModalOpen.value = false;
|
||||
};
|
||||
|
||||
const openManageChaptersModal = () => {
|
||||
isManageChaptersModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeManageChaptersModal = () => {
|
||||
isManageChaptersModalOpen.value = false;
|
||||
chaptersError.value = null;
|
||||
};
|
||||
|
||||
const savePreferredSources = async (sourceIds) => {
|
||||
try {
|
||||
await saveSourcesOrder(sourceIds);
|
||||
@@ -185,6 +210,39 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour sauvegarder les changements des chapitres
|
||||
const saveChaptersChanges = async (chaptersData) => {
|
||||
if (!mangaId.value) return;
|
||||
|
||||
isSavingChapters.value = true;
|
||||
chaptersError.value = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chapters/batch-edit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chapters: chaptersData
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de la sauvegarde des chapitres');
|
||||
}
|
||||
|
||||
// Recharger les chapitres et les volumes
|
||||
await mangaStore.loadChapters(mangaId.value);
|
||||
closeManageChaptersModal();
|
||||
} catch (error) {
|
||||
chaptersError.value = error.message;
|
||||
console.error('Erreur lors de la sauvegarde des chapitres:', error);
|
||||
} finally {
|
||||
isSavingChapters.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour le refresh des métadonnées
|
||||
const handleRefreshMetadata = async () => {
|
||||
if (!mangaId.value) return;
|
||||
@@ -224,9 +282,9 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
},
|
||||
{
|
||||
icon: PencilSquareIcon,
|
||||
label: 'Rename chapters',
|
||||
label: 'Manage chapters',
|
||||
type: 'button',
|
||||
onClick: () => console.log('Rename chapters')
|
||||
onClick: openManageChaptersModal
|
||||
},
|
||||
{
|
||||
icon: DocumentArrowDownIcon,
|
||||
|
||||
Reference in New Issue
Block a user