diff --git a/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js b/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js index 4eac98c..0d4a338 100644 --- a/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js +++ b/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js @@ -377,4 +377,24 @@ export class ApiMangaRepository { throw error; } } + + async deleteManga(mangaId) { + try { + const response = await fetch(`/api/mangas/${mangaId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (!response.ok) { + throw new Error('Failed to delete manga'); + } + + return true; + } catch (error) { + console.error('API Error:', error); + throw error; + } + } } diff --git a/assets/vue/app/domain/manga/presentation/components/MangaDeleteModal.vue b/assets/vue/app/domain/manga/presentation/components/MangaDeleteModal.vue new file mode 100644 index 0000000..b83d659 --- /dev/null +++ b/assets/vue/app/domain/manga/presentation/components/MangaDeleteModal.vue @@ -0,0 +1,128 @@ + + + diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaDelete.js b/assets/vue/app/domain/manga/presentation/composables/useMangaDelete.js new file mode 100644 index 0000000..bfcc301 --- /dev/null +++ b/assets/vue/app/domain/manga/presentation/composables/useMangaDelete.js @@ -0,0 +1,49 @@ +import { useMutation, useQueryClient } from '@tanstack/vue-query'; +import { ref } from 'vue'; +import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository'; + +export function useMangaDelete() { + const mangaRepository = new ApiMangaRepository(); + const queryClient = useQueryClient(); + const isDeleteModalOpen = ref(false); + + const deleteMutation = useMutation({ + mutationFn: ({ mangaId }) => { + return mangaRepository.deleteManga(mangaId); + }, + onSuccess: (data, variables) => { + // Invalider et refetch les listes de mangas + queryClient.invalidateQueries({ queryKey: ['mangas'] }); + queryClient.invalidateQueries({ queryKey: ['manga-search'] }); + } + }); + + const openDeleteModal = () => { + isDeleteModalOpen.value = true; + }; + + const closeDeleteModal = () => { + isDeleteModalOpen.value = false; + }; + + const deleteManga = async (mangaId) => { + try { + await deleteMutation.mutateAsync({ mangaId }); + closeDeleteModal(); + return true; + } catch (error) { + console.error('Erreur lors de la suppression du manga:', error); + throw error; + } + }; + + return { + isDeleteModalOpen, + openDeleteModal, + closeDeleteModal, + deleteManga, + isLoading: deleteMutation.isPending, + error: deleteMutation.error, + isSuccess: deleteMutation.isSuccess + }; +} diff --git a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue index 5ba9664..13064fc 100644 --- a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue +++ b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue @@ -68,6 +68,16 @@ @close="closeManageChaptersModal" @save="saveChaptersChanges" /> + + +
@@ -93,9 +103,10 @@ WrenchIcon } from '@heroicons/vue/24/outline'; import { computed, onUnmounted, ref, watch } from 'vue'; -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; - import { useMangaDetails } from '../composables/useMangaDetails'; + import { useMangaDelete } from '../composables/useMangaDelete'; +import { useMangaDetails } from '../composables/useMangaDetails'; import { useMangaEdit } from '../composables/useMangaEdit'; import { useMangaMonitoring } from '../composables/useMangaMonitoring'; import { useMangaPreferredSources } from '../composables/useMangaPreferredSources'; @@ -103,6 +114,7 @@ import { useMangaRefresh } from '../composables/useMangaRefresh'; import { useMangaVolumes } from '../composables/useMangaVolumes'; import ManageChaptersModal from '../components/ManageChaptersModal.vue'; +import MangaDeleteModal from '../components/MangaDeleteModal.vue'; import MangaEditModal from '../components/MangaEditModal.vue'; import MangaHeader from '../components/MangaHeader.vue'; import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue'; @@ -114,6 +126,7 @@ import Toolbar from '../../../../shared/components/ui/Toolbar.vue'; import { useMangaStore } from '../../application/store/mangaStore'; const route = useRoute(); + const router = useRouter(); const mangaStore = useMangaStore(); const mangaId = computed(() => route.params.id || null); @@ -174,6 +187,16 @@ import { useMangaStore } from '../../application/store/mangaStore'; toggleError: monitoringError } = useMangaMonitoring(); + // Composable pour la suppression + const { + isDeleteModalOpen, + openDeleteModal, + closeDeleteModal, + deleteManga, + isLoading: isDeleting, + error: deleteError + } = useMangaDelete(); + // Charger les chapitres dans le store quand le manga est chargé watch( mangaId, @@ -202,6 +225,18 @@ import { useMangaStore } from '../../application/store/mangaStore'; chaptersError.value = null; }; + const confirmDeleteManga = async () => { + if (!mangaId.value) return; + + try { + await deleteManga(mangaId.value); + // Rediriger vers la liste des mangas après suppression + router.push('/manga'); + } catch (error) { + console.error('Erreur lors de la suppression du manga:', error); + } + }; + const savePreferredSources = async (sourceIds) => { try { await saveSourcesOrder(sourceIds); @@ -334,7 +369,7 @@ import { useMangaStore } from '../../application/store/mangaStore'; icon: TrashIcon, label: 'Delete', type: 'button', - onClick: () => console.log('Delete') + onClick: openDeleteModal }, { icon: isAllExpanded.value ? ChevronDoubleUpIcon : ChevronDoubleDownIcon, diff --git a/src/Domain/Manga/Application/Command/DeleteManga.php b/src/Domain/Manga/Application/Command/DeleteManga.php new file mode 100644 index 0000000..a97eeb5 --- /dev/null +++ b/src/Domain/Manga/Application/Command/DeleteManga.php @@ -0,0 +1,12 @@ +mangaRepository->findById($command->mangaId); + + if (!$manga) { + throw new MangaNotFoundException(sprintf('Manga with id %s not found', $command->mangaId)); + } + + $this->mangaRepository->delete($manga); + } +} diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/Resource/DeleteMangaResource.php b/src/Domain/Manga/Infrastructure/ApiPlatform/Resource/DeleteMangaResource.php new file mode 100644 index 0000000..b9bd278 --- /dev/null +++ b/src/Domain/Manga/Infrastructure/ApiPlatform/Resource/DeleteMangaResource.php @@ -0,0 +1,48 @@ + 'Delete a manga', + 'description' => 'Permanently deletes a manga and all its associated chapters', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'string' + ], + 'description' => 'The manga ID' + ] + ], + 'responses' => [ + '204' => [ + 'description' => 'Manga successfully deleted' + ], + '404' => [ + 'description' => 'Manga not found' + ] + ] + ] + ) + ] +)] +class DeleteMangaResource +{ + public function __construct( + public string $id + ) {} +} diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/DeleteMangaProcessor.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/DeleteMangaProcessor.php new file mode 100644 index 0000000..93eb239 --- /dev/null +++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/DeleteMangaProcessor.php @@ -0,0 +1,34 @@ +handler->handle($command); + + return Response::HTTP_NO_CONTENT; + } catch (MangaNotFoundException $e) { + throw new NotFoundHttpException('Manga not found'); + } + } +} diff --git a/src/Domain/Scraping/Domain/Model/Manga.php b/src/Domain/Scraping/Domain/Model/Manga.php index e522f45..466ec41 100644 --- a/src/Domain/Scraping/Domain/Model/Manga.php +++ b/src/Domain/Scraping/Domain/Model/Manga.php @@ -15,6 +15,7 @@ class Manga private readonly string $description, private readonly string $author, private readonly string $publicationYear, + private readonly bool $monitored = false, private readonly array $preferredSources = [], private readonly array $alternativeSlugs = [], ) { @@ -73,4 +74,9 @@ class Manga { return $this->alternativeSlugs; } + + public function isMonitored(): bool + { + return $this->monitored; + } } diff --git a/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php b/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php index 150abbb..fa98be7 100644 --- a/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php +++ b/src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php @@ -5,17 +5,26 @@ namespace App\Domain\Scraping\Infrastructure\EventListener; use App\Domain\Manga\Domain\Event\ChapterReadyForScraping; use App\Domain\Scraping\Application\Command\ScrapeChapter; use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler; +use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface; +use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; class AutoScrapingListener { public function __construct( - private readonly ScrapeChapterHandler $scrapeChapterHandler + private readonly ScrapeChapterHandler $scrapeChapterHandler, + private readonly ChapterRepositoryInterface $chapterRepository, + private readonly MangaRepositoryInterface $mangaRepository, ) {} #[AsMessageHandler] public function onChapterReadyForScraping(ChapterReadyForScraping $event): void { - $this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue())); + $chapter = $this->chapterRepository->getById($event->chapterId->getValue()); + $manga = $this->mangaRepository->getById($chapter->mangaId); + + if ($manga->isMonitored()) { + $this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue())); + } } } diff --git a/src/Domain/Scraping/Infrastructure/Persistence/LegacyMangaRepository.php b/src/Domain/Scraping/Infrastructure/Persistence/LegacyMangaRepository.php index 0b21713..1cf2458 100644 --- a/src/Domain/Scraping/Infrastructure/Persistence/LegacyMangaRepository.php +++ b/src/Domain/Scraping/Infrastructure/Persistence/LegacyMangaRepository.php @@ -35,14 +35,15 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface } return new Manga( - (string) $mangaEntity->getId(), - $mangaEntity->getTitle(), - $mangaEntity->getSlug(), - $mangaEntity->getDescription() ?? '', - $mangaEntity->getAuthor() ?? '', - (string) ($mangaEntity->getPublicationYear() ?? ''), - $preferredSourceIds, - $mangaEntity->getAlternativeSlugs() ?? [] + id: (string) $mangaEntity->getId(), + title: $mangaEntity->getTitle(), + slug: $mangaEntity->getSlug(), + description: $mangaEntity->getDescription() ?? '', + author: $mangaEntity->getAuthor() ?? '', + publicationYear: (string) ($mangaEntity->getPublicationYear() ?? ''), + monitored: $mangaEntity->isMonitored() ?? false, + preferredSources: $preferredSourceIds, + alternativeSlugs: $mangaEntity->getAlternativeSlugs() ?? [] ); } diff --git a/tests/Feature/Manga/DeleteMangaTest.php b/tests/Feature/Manga/DeleteMangaTest.php new file mode 100644 index 0000000..59b223d --- /dev/null +++ b/tests/Feature/Manga/DeleteMangaTest.php @@ -0,0 +1,86 @@ + 'One Piece', + 'slug' => 'one-piece' + ]); + + // Create chapters for the manga + ChapterFactory::createMany(3, [ + 'manga' => $manga, + 'number' => 1.0, + 'title' => 'Chapter 1', + 'visible' => true + ]); + + ChapterFactory::createMany(2, [ + 'manga' => $manga, + 'number' => 2.0, + 'title' => 'Chapter 2', + 'visible' => true + ]); + + ChapterFactory::createMany(1, [ + 'manga' => $manga, + 'number' => 3.0, + 'title' => 'Chapter 3', + 'visible' => true + ]); + + $mangaId = $manga->getId(); + + // Verify chapters exist before deletion + $chaptersBefore = $this->entityManager->getRepository(Chapter::class)->findBy(['manga' => $mangaId]); + $this->assertCount(6, $chaptersBefore); + + // Act + static::createClient()->request('DELETE', "/api/mangas/{$mangaId}"); + + // Then + $this->assertResponseStatusCodeSame(204); + + // Verify the manga was deleted + $freshManga = $this->entityManager->find(Manga::class, $mangaId); + $this->assertNull($freshManga); + + // Verify all chapters were also deleted (cascade) + $chaptersAfter = $this->entityManager->getRepository(Chapter::class)->findBy(['manga' => $mangaId]); + $this->assertCount(0, $chaptersAfter); + } + + public function test_it_returns_404_for_non_existent_manga(): void + { + // When + static::createClient()->request('DELETE', '/api/mangas/999999'); + + // Then + $this->assertResponseStatusCodeSame(404); + } + + public function test_it_returns_404_for_missing_id(): void + { + // When + static::createClient()->request('DELETE', '/api/mangas/'); + + // Then + $this->assertResponseStatusCodeSame(404); + } +}