Add deletedAt timestamps to delete operations for better audit tracking

- Updated delete methods across hooks and components to include `deletedAt: System.timeStampInSeconds()`.
- Refactored synchronized delete logic to pass `deletedAt` for both offline and online states.
- Improved synchronization workflows to include `deletedAt` in server and IPC requests.
- Enhanced destructuring patterns for cleaner and more consistent request data.
This commit is contained in:
natreex
2026-02-09 17:12:03 -05:00
parent 209dc6f85a
commit 49bb6e06f5
14 changed files with 146 additions and 92 deletions

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import {useContext, useEffect, useState} from 'react'; import {useCallback, useContext, useEffect, useRef, useState} from 'react';
import {BookContext} from "@/context/BookContext"; import {BookContext} from "@/context/BookContext";
import {ChapterProps} from "@/lib/models/Chapter"; import {ChapterProps} from "@/lib/models/Chapter";
import {ChapterContext} from '@/context/ChapterContext'; import {ChapterContext} from '@/context/ChapterContext';
@@ -67,59 +67,57 @@ const messagesMap = {
function AutoSyncOnReconnect() { function AutoSyncOnReconnect() {
const {offlineMode} = useContext(OfflineContext); const {offlineMode} = useContext(OfflineContext);
const {syncAllToServer: syncAllBooksToServer, refreshBooks, booksToSyncToServer} = useSyncBooks(); const {session} = useContext(SessionContext);
const {syncAllToServer: syncAllSeriesToServer, refreshSeries, seriesToSyncToServer} = useSyncSeries(); const {syncAllToServer: syncAllBooksToServer, syncAllFromServer: syncAllBooksFromServer, refreshBooks, booksToSyncToServer, booksToSyncFromServer} = useSyncBooks();
const [pendingSync, setPendingSync] = useState<boolean>(false); const {syncAllToServer: syncAllSeriesToServer, syncAllFromServer: syncAllSeriesFromServer, refreshSeries, seriesToSyncToServer, seriesToSyncFromServer} = useSyncSeries();
const [isRefreshing, setIsRefreshing] = useState<boolean>(false); const isSyncingRef = useRef<boolean>(false);
const hasRefreshedRef = useRef<boolean>(false);
const saveLastOnlineTimestamp = (): void => { const saveLastOnlineTimestamp = useCallback((): void => {
const timestamp: number = Math.floor(Date.now() / 1000); const timestamp: number = Math.floor(Date.now() / 1000);
localStorage.setItem('lastOnlineTimestamp', timestamp.toString()); localStorage.setItem('lastOnlineTimestamp', timestamp.toString());
}; }, []);
// Refresh sync data when online + authenticated + DB ready
useEffect((): void => { useEffect((): void => {
if (!offlineMode.isOffline) { if (!offlineMode.isOffline && session.isConnected && offlineMode.isDatabaseInitialized) {
setPendingSync(true); hasRefreshedRef.current = true;
setIsRefreshing(true); Promise.all([refreshBooks(), refreshSeries()]);
Promise.all([refreshBooks(), refreshSeries()]).then(() => { }
setIsRefreshing(false); }, [offlineMode.isOffline, session.isConnected, offlineMode.isDatabaseInitialized]);
// Auto-sync when diffs become available (reactive, no flags)
useEffect((): void => {
if (offlineMode.isOffline || !session.isConnected || isSyncingRef.current || !hasRefreshedRef.current) return;
const syncPromises: Promise<void>[] = [];
if (booksToSyncToServer.length > 0) syncPromises.push(syncAllBooksToServer());
if (booksToSyncFromServer.length > 0) syncPromises.push(syncAllBooksFromServer());
if (seriesToSyncToServer.length > 0) syncPromises.push(syncAllSeriesToServer());
if (seriesToSyncFromServer.length > 0) syncPromises.push(syncAllSeriesFromServer());
if (syncPromises.length > 0) {
isSyncingRef.current = true;
Promise.all(syncPromises).then((): void => {
saveLastOnlineTimestamp();
isSyncingRef.current = false;
}).catch((): void => {
isSyncingRef.current = false;
}); });
} }
}, [offlineMode.isOffline]); }, [booksToSyncToServer, booksToSyncFromServer, seriesToSyncToServer, seriesToSyncFromServer]);
useEffect((): void => {
if (pendingSync && !isRefreshing) {
const syncPromises: Promise<void>[] = [];
if (booksToSyncToServer.length > 0) {
syncPromises.push(syncAllBooksToServer());
}
if (seriesToSyncToServer.length > 0) {
syncPromises.push(syncAllSeriesToServer());
}
if (syncPromises.length > 0) {
Promise.all(syncPromises).then(() => {
saveLastOnlineTimestamp();
});
} else {
saveLastOnlineTimestamp();
}
setPendingSync(false);
}
}, [booksToSyncToServer, seriesToSyncToServer, pendingSync, isRefreshing]);
// Update lastOnlineTimestamp every 5 minutes while online // Update lastOnlineTimestamp every 5 minutes while online
useEffect((): (() => void) | void => { useEffect((): (() => void) | void => {
if (!offlineMode.isOffline) { if (!offlineMode.isOffline && session.isConnected) {
const intervalId: NodeJS.Timeout = setInterval((): void => { const intervalId: NodeJS.Timeout = setInterval((): void => {
saveLastOnlineTimestamp(); saveLastOnlineTimestamp();
}, 5 * 60 * 1000); // 5 minutes }, 5 * 60 * 1000);
return (): void => clearInterval(intervalId); return (): void => clearInterval(intervalId);
} }
}, [offlineMode.isOffline]); }, [offlineMode.isOffline, session.isConnected, saveLastOnlineTimestamp]);
return null; return null;
} }
@@ -284,8 +282,6 @@ function ScribeContent() {
useEffect((): void => { useEffect((): void => {
if (session.isConnected) { if (session.isConnected) {
refreshBooks().then()
refreshSeries().then()
setIsTermsAccepted(session.user?.termsAccepted ?? false); setIsTermsAccepted(session.user?.termsAccepted ?? false);
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic')); setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
setIsLoading(false); setIsLoading(false);

View File

@@ -1,7 +1,7 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome/free-solid-svg-icons"; import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome/free-solid-svg-icons";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {useState, useContext} from "react"; import {useState, useEffect, useContext} from "react";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {SyncType} from "@/context/BooksSyncContext"; import {SyncType} from "@/context/BooksSyncContext";
import useSyncBooks from "@/hooks/useSyncBooks"; import useSyncBooks from "@/hooks/useSyncBooks";
@@ -18,6 +18,10 @@ export default function SyncBook({bookId, status}: SyncBookProps) {
const [currentStatus, setCurrentStatus] = useState<SyncType>(status); const [currentStatus, setCurrentStatus] = useState<SyncType>(status);
const {upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer} = useSyncBooks(); const {upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer} = useSyncBooks();
useEffect((): void => {
setCurrentStatus(status);
}, [status]);
const isOffline: boolean = isCurrentlyOffline(); const isOffline: boolean = isCurrentlyOffline();
async function upload(): Promise<void> { async function upload(): Promise<void> {

View File

@@ -486,8 +486,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
let response: boolean; let response: boolean;
const elementId = sections.find((section: LocationProps): boolean => section.id === sectionId) const elementId = sections.find((section: LocationProps): boolean => section.id === sectionId)
?.elements[elementIndex].id; ?.elements[elementIndex].id;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
const deleteData = {elementId: elementId}; const deleteData = {elementId: elementId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData); response = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
} else { } else {
@@ -498,16 +499,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
} }
} else if (isCurrentlyOffline() || book?.localBook) { } else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:element:delete', { response = await window.electron.invoke<boolean>('db:location:element:delete', {
elementId: elementId, elementId: elementId, bookId: currentEntityId, deletedAt,
}); });
} else { } else {
response = await System.authDeleteToServer<boolean>(`location/element/delete`, { response = await System.authDeleteToServer<boolean>(`location/element/delete`, {
elementId: elementId, elementId: elementId, bookId: currentEntityId, deletedAt,
}, token, lang); }, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
addToQueue('db:location:element:delete', { addToQueue('db:location:element:delete', {
elementId: elementId, elementId: elementId, bookId: currentEntityId, deletedAt,
}); });
} }
} }
@@ -536,8 +537,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
try { try {
let response: boolean; let response: boolean;
const subElementId = sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id; const subElementId = sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
const deleteData = {subElementId: subElementId}; const deleteData = {subElementId: subElementId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData); response = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
} else { } else {
@@ -548,16 +550,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
} }
} else if (isCurrentlyOffline() || book?.localBook) { } else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:subelement:delete', { response = await window.electron.invoke<boolean>('db:location:subelement:delete', {
subElementId: subElementId, subElementId: subElementId, bookId: currentEntityId, deletedAt,
}); });
} else { } else {
response = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, { response = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, {
subElementId: subElementId, subElementId: subElementId, bookId: currentEntityId, deletedAt,
}, token, lang); }, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
addToQueue('db:location:subelement:delete', { addToQueue('db:location:subelement:delete', {
subElementId: subElementId, subElementId: subElementId, bookId: currentEntityId, deletedAt,
}); });
} }
} }
@@ -581,8 +583,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function handleRemoveSection(sectionId: string): Promise<void> { async function handleRemoveSection(sectionId: string): Promise<void> {
try { try {
let response: boolean; let response: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
const deleteData = {locationId: sectionId}; const deleteData = {locationId: sectionId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:location:delete', deleteData); response = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
} else { } else {
@@ -593,16 +596,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
} }
} else if (isCurrentlyOffline() || book?.localBook) { } else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:delete', { response = await window.electron.invoke<boolean>('db:location:delete', {
locationId: sectionId, locationId: sectionId, bookId: currentEntityId, deletedAt,
}); });
} else { } else {
response = await System.authDeleteToServer<boolean>(`location/delete`, { response = await System.authDeleteToServer<boolean>(`location/delete`, {
locationId: sectionId, locationId: sectionId, bookId: currentEntityId, deletedAt,
}, token, lang); }, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
addToQueue('db:location:delete', { addToQueue('db:location:delete', {
locationId: sectionId, locationId: sectionId, bookId: currentEntityId, deletedAt,
}); });
} }
} }

View File

@@ -132,7 +132,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
async function deleteIncident(actId: number, incidentId: string): Promise<void> { async function deleteIncident(actId: number, incidentId: string): Promise<void> {
try { try {
let response: boolean; let response: boolean;
const deleteData = { bookId, incidentId }; const deleteData = { bookId, incidentId, deletedAt: System.timeStampInSeconds() };
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:incident:remove', deleteData); response = await window.electron.invoke<boolean>('db:book:incident:remove', deleteData);
} else { } else {
@@ -223,7 +223,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
async function deletePlotPoint(actId: number, plotPointId: string): Promise<void> { async function deletePlotPoint(actId: number, plotPointId: string): Promise<void> {
try { try {
let response: boolean; let response: boolean;
const deleteData = { plotId: plotPointId }; const deleteData = { plotId: plotPointId, bookId, deletedAt: System.timeStampInSeconds() };
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:plot:remove', deleteData); response = await window.electron.invoke<boolean>('db:book:plot:remove', deleteData);
} else { } else {
@@ -365,7 +365,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
): Promise<void> { ): Promise<void> {
try { try {
let response: boolean; let response: boolean;
const unlinkData = { chapterInfoId }; const unlinkData = { chapterInfoId, bookId, deletedAt: System.timeStampInSeconds() };
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:information:remove', unlinkData); response = await window.electron.invoke<boolean>('db:chapter:information:remove', unlinkData);
} else { } else {

View File

@@ -88,10 +88,12 @@ export default function Issues({issues, setIssues}: IssuesProps) {
try { try {
let response: boolean; let response: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:issue:remove', { response = await window.electron.invoke<boolean>('db:book:issue:remove', {
bookId, bookId,
issueId, issueId,
deletedAt,
}); });
} else { } else {
response = await System.authDeleteToServer<boolean>( response = await System.authDeleteToServer<boolean>(
@@ -99,6 +101,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
{ {
bookId, bookId,
issueId, issueId,
deletedAt,
}, },
token, token,
lang lang
@@ -108,6 +111,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
addToQueue('db:book:issue:remove', { addToQueue('db:book:issue:remove', {
bookId, bookId,
issueId, issueId,
deletedAt,
}); });
} }
} }

View File

@@ -65,8 +65,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
try { try {
let response: boolean; let response: boolean;
const elementId = (worlds[selectedWorldIndex][section] as WorldElement[])[index].id; const elementId = (worlds[selectedWorldIndex][section] as WorldElement[])[index].id;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
const deleteData = {elementId: elementId}; const deleteData = {elementId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:world:element:delete', deleteData); response = await window.electron.invoke<boolean>('db:series:world:element:delete', deleteData);
} else { } else {
@@ -77,16 +78,16 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
} }
} else if (isCurrentlyOffline() || book?.localBook) { } else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:world:element:remove', { response = await window.electron.invoke<boolean>('db:book:world:element:remove', {
elementId: elementId, elementId, bookId: book?.bookId, deletedAt,
}); });
} else { } else {
response = await System.authDeleteToServer<boolean>('book/world/element/delete', { response = await System.authDeleteToServer<boolean>('book/world/element/delete', {
elementId: elementId, elementId, bookId: book?.bookId, deletedAt,
}, session.accessToken, lang); }, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:book:world:element:remove', { addToQueue('db:book:world:element:remove', {
elementId: elementId, elementId: elementId, bookId: book?.bookId, deletedAt,
}); });
} }
} }

View File

@@ -188,16 +188,25 @@ export default function ScribeChapterComponent() {
try { try {
setDeleteConfirmationMessage(false); setDeleteConfirmationMessage(false);
let response:boolean = false; let response:boolean = false;
const deletedAt: number = System.timeStampInSeconds();
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:remove', removeChapterId); response = await window.electron.invoke<boolean>('db:chapter:remove', {
chapterId: removeChapterId,
bookId: book?.bookId,
deletedAt,
});
} else { } else {
response = await System.authDeleteToServer<boolean>('chapter/remove', { response = await System.authDeleteToServer<boolean>('chapter/remove', {
chapterId: removeChapterId, chapterId: removeChapterId,
bookId: book?.bookId,
deletedAt,
}, userToken, lang); }, userToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:chapter:remove', { addToQueue('db:chapter:remove', {
chapterId: removeChapterId, chapterId: removeChapterId,
bookId: book?.bookId,
deletedAt,
}); });
} }
} }

View File

@@ -58,7 +58,7 @@ export default function SeriesSettingSidebar(
async function handleDeleteSeries(): Promise<void> { async function handleDeleteSeries(): Promise<void> {
try { try {
const deleteData = {seriesId: seriesId}; const deleteData = {seriesId: seriesId, deletedAt: System.timeStampInSeconds()};
let success: boolean; let success: boolean;
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {

View File

@@ -174,7 +174,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
try { try {
const removeData = { const removeData = {
seriesId: seriesId, seriesId: seriesId,
bookId: bookId bookId: bookId,
deletedAt: System.timeStampInSeconds(),
}; };
let response: boolean; let response: boolean;

View File

@@ -523,10 +523,11 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
const deleteCharacter = useCallback(async function (characterId: string): Promise<void> { const deleteCharacter = useCallback(async function (characterId: string): Promise<void> {
try { try {
let response: boolean; let response: boolean;
const requestData = {characterId: characterId}; const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const requestData = {characterId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:character:delete', requestData); response = await window.electron.invoke<boolean>('db:series:character:delete', requestData);
} else { } else {
@@ -537,6 +538,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
} }
} else { } else {
// Pattern A: mutations // Pattern A: mutations
const requestData = {characterId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
// Offline OU livre local → IPC // Offline OU livre local → IPC
response = await window.electron.invoke<boolean>('db:character:delete', requestData); response = await window.electron.invoke<boolean>('db:character:delete', requestData);
@@ -649,11 +651,12 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
setSelectedCharacter({...selectedCharacter, [section]: updatedSection}); setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
} else { } else {
try { try {
const requestData = {attributeId: attrId}; const deletedAt: number = System.timeStampInSeconds();
let response: boolean; let response: boolean;
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const requestData = {attributeId: attrId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:character:attribute:delete', requestData); response = await window.electron.invoke<boolean>('db:series:character:attribute:delete', requestData);
} else { } else {
@@ -664,6 +667,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
} }
} else { } else {
// Pattern A: mutations // Pattern A: mutations
const requestData = {attributeId: attrId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
// Offline OU livre local → IPC // Offline OU livre local → IPC
response = await window.electron.invoke<boolean>('db:character:attribute:delete', requestData); response = await window.electron.invoke<boolean>('db:character:attribute:delete', requestData);

View File

@@ -514,9 +514,10 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
const removeSection = useCallback(async function (sectionId: string): Promise<void> { const removeSection = useCallback(async function (sectionId: string): Promise<void> {
try { try {
let success: boolean; let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const deleteData = {locationId: sectionId}; const deleteData = {locationId: sectionId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:location:delete', deleteData); success = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
} else { } else {
@@ -527,7 +528,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
} }
} else { } else {
const requestData = { const requestData = {
locationId: sectionId, locationId: sectionId, bookId: entityId, deletedAt,
}; };
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:location:delete', requestData); success = await window.electron.invoke<boolean>('db:location:delete', requestData);
@@ -563,9 +564,10 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
return section.id === sectionId; return section.id === sectionId;
})?.elements[elementIndex].id; })?.elements[elementIndex].id;
let success: boolean; let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const deleteData = {elementId: elementId}; const deleteData = {elementId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData); success = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
} else { } else {
@@ -576,7 +578,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
} }
} else { } else {
const requestData = { const requestData = {
elementId: elementId, elementId, bookId: entityId, deletedAt,
}; };
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:location:element:delete', requestData); success = await window.electron.invoke<boolean>('db:location:element:delete', requestData);
@@ -615,9 +617,10 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
return section.id === sectionId; return section.id === sectionId;
})?.elements[elementIndex].subElements[subElementIndex].id; })?.elements[elementIndex].subElements[subElementIndex].id;
let success: boolean; let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const deleteData = {subElementId: subElementId}; const deleteData = {subElementId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData); success = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
} else { } else {
@@ -628,7 +631,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
} }
} else { } else {
const requestData = { const requestData = {
subElementId: subElementId, subElementId, bookId: entityId, deletedAt,
}; };
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:location:subelement:delete', requestData); success = await window.electron.invoke<boolean>('db:location:subelement:delete', requestData);

View File

@@ -296,12 +296,20 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
}); });
if (response.seriesSpellId) { if (response.seriesSpellId) {
const seriesSpellResponse: SeriesSpellDetailResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>( let seriesSpellResponse: SeriesSpellDetailResponse;
'series/spell/detail', if (isCurrentlyOffline() || book?.localBook) {
userToken, seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
lang, 'db:series:spell:detail',
{spellid: response.seriesSpellId} {spellId: response.seriesSpellId}
); );
} else {
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
'series/spell/detail',
userToken,
lang,
{spellid: response.seriesSpellId}
);
}
if (seriesSpellResponse) { if (seriesSpellResponse) {
setSelectedSeriesSpell(seriesSpellResponse); setSelectedSeriesSpell(seriesSpellResponse);
} }
@@ -536,9 +544,10 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
const deleteSpell = useCallback(async function (spellId: string): Promise<void> { const deleteSpell = useCallback(async function (spellId: string): Promise<void> {
try { try {
let success: boolean; let success: boolean;
const requestData = {spellId}; const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const requestData = {spellId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:spell:delete', requestData); success = await window.electron.invoke<boolean>('db:series:spell:delete', requestData);
} else { } else {
@@ -548,6 +557,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
} }
} }
} else { } else {
const requestData = {spellId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:spell:delete', requestData); success = await window.electron.invoke<boolean>('db:spell:delete', requestData);
} else { } else {
@@ -844,9 +854,10 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
const deleteTag = useCallback(async function (tagId: string): Promise<boolean> { const deleteTag = useCallback(async function (tagId: string): Promise<boolean> {
try { try {
let success: boolean; let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) { if (isSeriesMode) {
// Series mode - dual logic // Series mode - dual logic
const deleteData = {tagId}; const deleteData = {tagId, deletedAt};
if (isCurrentlyOffline() || localSeries) { if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:spell:tag:delete', deleteData); success = await window.electron.invoke<boolean>('db:series:spell:tag:delete', deleteData);
} else { } else {
@@ -856,7 +867,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
} }
} }
} else { } else {
const requestData = {tagId, bookId: entityId}; const requestData = {tagId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) { if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:spell:tag:delete', requestData); success = await window.electron.invoke<boolean>('db:spell:tag:delete', requestData);
} else { } else {
@@ -885,17 +896,25 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
const handleSyncComplete = useCallback(async function (): Promise<void> { const handleSyncComplete = useCallback(async function (): Promise<void> {
if (selectedSpell?.seriesSpellId) { if (selectedSpell?.seriesSpellId) {
const seriesSpellResponse: SeriesSpellDetailResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>( let seriesSpellResponse: SeriesSpellDetailResponse;
'series/spell/detail', if (isCurrentlyOffline() || (isSeriesMode ? localSeries : book?.localBook)) {
userToken, seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
lang, 'db:series:spell:detail',
{spellid: selectedSpell.seriesSpellId} {spellId: selectedSpell.seriesSpellId}
); );
} else {
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
'series/spell/detail',
userToken,
lang,
{spellid: selectedSpell.seriesSpellId}
);
}
if (seriesSpellResponse) { if (seriesSpellResponse) {
setSelectedSeriesSpell(seriesSpellResponse); setSelectedSeriesSpell(seriesSpellResponse);
} }
} }
}, [selectedSpell?.seriesSpellId, userToken, lang]); }, [selectedSpell?.seriesSpellId, userToken, lang, isCurrentlyOffline, isSeriesMode, localSeries, book?.localBook]);
const enterDetailMode = useCallback(async function (spell: SpellListItem): Promise<void> { const enterDetailMode = useCallback(async function (spell: SpellListItem): Promise<void> {
await selectSpell(spell); await selectSpell(spell);

View File

@@ -186,6 +186,12 @@ export default function useSyncBooks() {
} }
} }
async function syncAllFromServer(): Promise<void> {
for (const diff of booksToSyncFromServer) {
await syncFromServer(diff.id);
}
}
async function refreshBooks(): Promise<void> { async function refreshBooks(): Promise<void> {
try { try {
let localBooksResponse: SyncedBook[] = []; let localBooksResponse: SyncedBook[] = [];
@@ -250,6 +256,7 @@ export default function useSyncBooks() {
syncFromServer, syncFromServer,
syncToServer, syncToServer,
syncAllToServer, syncAllToServer,
syncAllFromServer,
refreshBooks, refreshBooks,
localOnlyBooks, localOnlyBooks,
serverOnlyBooks, serverOnlyBooks,

View File

@@ -275,10 +275,12 @@ export default function useSyncSeries() {
} }
} }
/** async function syncAllFromServer(): Promise<void> {
* Refreshes the sync status of all series by comparing local and server data. for (const diff of seriesToSyncFromServer) {
* Updates the context with the latest sync information. await syncFromServer(diff.id);
*/ }
}
async function refreshSeries(): Promise<void> { async function refreshSeries(): Promise<void> {
try { try {
let localSeriesResponse: SyncedSeries[] = []; let localSeriesResponse: SyncedSeries[] = [];
@@ -343,6 +345,7 @@ export default function useSyncSeries() {
syncFromServer, syncFromServer,
syncToServer, syncToServer,
syncAllToServer, syncAllToServer,
syncAllFromServer,
refreshSeries, refreshSeries,
localOnlySeries, localOnlySeries,
serverOnlySeries, serverOnlySeries,