diff --git a/backend/src/main/java/com/storycove/controller/StoryController.java b/backend/src/main/java/com/storycove/controller/StoryController.java index f0ec16a..b2d26ca 100644 --- a/backend/src/main/java/com/storycove/controller/StoryController.java +++ b/backend/src/main/java/com/storycove/controller/StoryController.java @@ -691,8 +691,9 @@ public class StoryController { // Reading progress fields dto.setIsRead(story.getIsRead()); dto.setReadingPosition(story.getReadingPosition()); + dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story)); dto.setLastReadAt(story.getLastReadAt()); - + if (story.getAuthor() != null) { dto.setAuthorId(story.getAuthor().getId()); dto.setAuthorName(story.getAuthor().getName()); diff --git a/backend/src/main/java/com/storycove/dto/StorySummaryDto.java b/backend/src/main/java/com/storycove/dto/StorySummaryDto.java index 5d9bdc3..8e10dab 100644 --- a/backend/src/main/java/com/storycove/dto/StorySummaryDto.java +++ b/backend/src/main/java/com/storycove/dto/StorySummaryDto.java @@ -23,6 +23,7 @@ public class StorySummaryDto { // Reading progress fields private Boolean isRead; private Integer readingPosition; + private Integer readingProgressPercentage; // Pre-calculated percentage (0-100) private LocalDateTime lastReadAt; // Related entities as simple references @@ -122,11 +123,19 @@ public class StorySummaryDto { public Integer getReadingPosition() { return readingPosition; } - + public void setReadingPosition(Integer readingPosition) { this.readingPosition = readingPosition; } - + + public Integer getReadingProgressPercentage() { + return readingProgressPercentage; + } + + public void setReadingProgressPercentage(Integer readingProgressPercentage) { + this.readingProgressPercentage = readingProgressPercentage; + } + public LocalDateTime getLastReadAt() { return lastReadAt; } diff --git a/backend/src/main/java/com/storycove/service/TagService.java b/backend/src/main/java/com/storycove/service/TagService.java index dffe50f..f142271 100644 --- a/backend/src/main/java/com/storycove/service/TagService.java +++ b/backend/src/main/java/com/storycove/service/TagService.java @@ -28,11 +28,12 @@ import java.util.UUID; @Validated @Transactional public class TagService { - + private static final Logger logger = LoggerFactory.getLogger(TagService.class); private final TagRepository tagRepository; private final TagAliasRepository tagAliasRepository; + private SolrService solrService; @Autowired public TagService(TagRepository tagRepository, TagAliasRepository tagAliasRepository) { @@ -40,6 +41,11 @@ public class TagService { this.tagAliasRepository = tagAliasRepository; } + @Autowired(required = false) + public void setSolrService(SolrService solrService) { + this.solrService = solrService; + } + @Transactional(readOnly = true) public List findAll() { return tagRepository.findAll(); @@ -142,13 +148,39 @@ public class TagService { public void delete(UUID id) { Tag tag = findById(id); - - // Check if tag is used by any stories + + // Remove tag from all stories before deletion and track for reindexing + List storiesToReindex = new ArrayList<>(); if (!tag.getStories().isEmpty()) { - throw new IllegalStateException("Cannot delete tag that is used by stories. Remove tag from all stories first."); + // Create a copy to avoid ConcurrentModificationException + List storiesToUpdate = new ArrayList<>(tag.getStories()); + storiesToUpdate.forEach(story -> { + story.removeTag(tag); + storiesToReindex.add(story); + }); + logger.info("Removed tag '{}' from {} stories before deletion", tag.getName(), storiesToUpdate.size()); } - + + // Remove tag from all collections before deletion + if (tag.getCollections() != null && !tag.getCollections().isEmpty()) { + tag.getCollections().forEach(collection -> collection.getTags().remove(tag)); + logger.info("Removed tag '{}' from {} collections before deletion", tag.getName(), tag.getCollections().size()); + } + tagRepository.delete(tag); + logger.info("Deleted tag '{}'", tag.getName()); + + // Reindex affected stories in Solr + if (solrService != null && !storiesToReindex.isEmpty()) { + try { + for (Story story : storiesToReindex) { + solrService.indexStory(story); + } + logger.info("Reindexed {} stories after tag deletion", storiesToReindex.size()); + } catch (Exception e) { + logger.error("Failed to reindex stories after tag deletion", e); + } + } } public List deleteUnusedTags() { diff --git a/frontend/src/app/settings/tag-maintenance/page.tsx b/frontend/src/app/settings/tag-maintenance/page.tsx index 1b3ae38..0da059a 100644 --- a/frontend/src/app/settings/tag-maintenance/page.tsx +++ b/frontend/src/app/settings/tag-maintenance/page.tsx @@ -120,26 +120,27 @@ export default function TagMaintenancePage() { const handleDeleteSelected = async () => { if (selectedTagIds.size === 0) return; - + const confirmation = confirm( `Are you sure you want to delete ${selectedTagIds.size} selected tag(s)? This action cannot be undone.` ); - + if (!confirmation) return; - + try { - const deletePromises = Array.from(selectedTagIds).map(tagId => + const deletePromises = Array.from(selectedTagIds).map(tagId => tagApi.deleteTag(tagId) ); - + await Promise.all(deletePromises); - + // Reload tags and reset selection await loadTags(); setSelectedTagIds(new Set()); - } catch (error) { + } catch (error: any) { console.error('Failed to delete tags:', error); - alert('Failed to delete some tags. Please try again.'); + const errorMessage = error.response?.data?.error || error.message || 'Failed to delete some tags. Please try again.'; + alert(errorMessage); } }; diff --git a/frontend/src/components/tags/TagEditModal.tsx b/frontend/src/components/tags/TagEditModal.tsx index 7d61282..92c8dd2 100644 --- a/frontend/src/components/tags/TagEditModal.tsx +++ b/frontend/src/components/tags/TagEditModal.tsx @@ -129,7 +129,8 @@ export default function TagEditModal({ tag, isOpen, onClose, onSave, onDelete }: onDelete(tag); onClose(); } catch (error: any) { - setErrors({ submit: error.message }); + const errorMessage = error.response?.data?.error || error.message || 'Failed to delete tag'; + setErrors({ submit: errorMessage }); } finally { setSaving(false); }