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);
+ }
+}