704 lines
36 KiB
Vue
704 lines
36 KiB
Vue
<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>
|