'use client' import {useCallback, useContext, useEffect, useState} from 'react'; import {SeriesLocationItem, SeriesLocationElement, SeriesLocationSubElement} from '@/lib/models/Series'; import {SessionContext} from '@/context/SessionContext'; import {BookContext} from '@/context/BookContext'; import {AlertContext} from '@/context/AlertContext'; import {LangContext, LangContextProps} from '@/context/LangContext'; import System from '@/lib/models/System'; import {useTranslations} from 'next-intl'; import {ViewMode} from '@/shared/interface'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; import {SyncedBook} from '@/lib/models/SyncedBook'; import {SeriesContext, SeriesContextProps} from '@/context/SeriesContext'; import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext'; import {SyncedSeries} from '@/lib/models/SyncedSeries'; export interface SubElement { id: string; name: string; description: string; } export interface Element { id: string; name: string; description: string; subElements: SubElement[]; } export interface LocationProps { id: string; name: string; elements: Element[]; seriesLocationId?: string | null; } interface LocationListResponse { locations: LocationProps[]; enabled: boolean; } export interface UseLocationsConfig { entityType: 'book' | 'series'; entityId: string; } export interface UseLocationsReturn { // State sections: LocationProps[]; seriesLocations: SeriesLocationItem[]; toolEnabled: boolean; isLoading: boolean; isSeriesMode: boolean; bookSeriesId: string | null; newSectionName: string; newElementNames: { [key: string]: string }; newSubElementNames: { [key: string]: string }; // Navigation state viewMode: ViewMode; selectedSectionIndex: number; sectionsBackup: LocationProps[] | null; // Actions addSection: () => Promise; addElement: (sectionId: string) => Promise; addSubElement: (sectionId: string, elementIndex: number) => Promise; removeSection: (sectionId: string) => Promise; removeElement: (sectionId: string, elementIndex: number) => Promise; removeSubElement: (sectionId: string, elementIndex: number, subElementIndex: number) => Promise; updateElement: (sectionId: string, elementIndex: number, field: keyof Element, value: string) => void; updateSubElement: (sectionId: string, elementIndex: number, subElementIndex: number, field: keyof SubElement, value: string) => void; saveLocations: () => Promise; toggleTool: (enabled: boolean) => Promise; importFromSeries: (seriesLocationId: string) => Promise; exportToSeries: (section: LocationProps) => Promise; refreshLocations: () => Promise; refreshSeriesLocations: () => Promise; setNewSectionName: (name: string) => void; setNewElementNames: React.Dispatch>; setNewSubElementNames: React.Dispatch>; // Navigation actions enterDetailMode: (sectionIndex: number) => void; enterEditMode: () => void; exitEditMode: (save: boolean) => Promise; backToList: () => void; } export function useLocations(config: UseLocationsConfig): UseLocationsReturn { const {entityType, entityId} = config; const t = useTranslations(); const {lang} = useContext(LangContext); const {session} = useContext(SessionContext); const {book, setBook} = useContext(BookContext); const {errorMessage, successMessage} = useContext(AlertContext); const {isCurrentlyOffline} = useContext(OfflineContext); const {addToQueue} = useContext(LocalSyncQueueContext); const {localSyncedBooks} = useContext(BooksSyncContext); const {localSeries} = useContext(SeriesContext); const {localSyncedSeries} = useContext(SeriesSyncContext); const [sections, setSections] = useState([]); const [seriesLocations, setSeriesLocations] = useState([]); const [toolEnabled, setToolEnabled] = useState(entityType === 'series' || (book?.tools?.locations ?? false)); const [isLoading, setIsLoading] = useState(true); const [newSectionName, setNewSectionName] = useState(''); const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({}); const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({}); const [isAddingElement, setIsAddingElement] = useState(false); const [isAddingSubElement, setIsAddingSubElement] = useState(false); const [isAddingSection, setIsAddingSection] = useState(false); // Navigation state const [viewMode, setViewMode] = useState('list'); const [selectedSectionIndex, setSelectedSectionIndex] = useState(-1); const [sectionsBackup, setSectionsBackup] = useState(null); const isSeriesMode: boolean = entityType === 'series'; const bookSeriesId: string | null = book?.seriesId || null; const userToken: string = session?.accessToken || ''; // Load locations on mount useEffect(function (): void { if (entityId) { refreshLocations(); } }, [entityId]); // Load series locations for book mode useEffect(function (): void { if (bookSeriesId && !isSeriesMode) { refreshSeriesLocations(); } }, [bookSeriesId, isSeriesMode]); const refreshSeriesLocations = useCallback(async function (): Promise { if (!bookSeriesId) return; try { let response: SeriesLocationItem[]; // Dual logic: offline ou livre local → IPC, sinon serveur if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke( 'db:series:location:list', {seriesId: bookSeriesId} ); } else { response = await System.authGetQueryToServer( 'series/location/list', userToken, lang, {seriesid: bookSeriesId} ); } if (response) { setSeriesLocations(response); } } catch (e: unknown) { if (e instanceof Error) { console.error('Error loading series locations:', e.message); } } }, [bookSeriesId, userToken, lang, isCurrentlyOffline, book?.localBook]); const refreshLocations = useCallback(async function (): Promise { setIsLoading(true); try { if (isSeriesMode) { // Series mode - dual logic let response: SeriesLocationItem[]; if (isCurrentlyOffline() || localSeries) { response = await window.electron.invoke( 'db:series:location:list', {seriesId: entityId} ); } else { response = await System.authGetQueryToServer( 'series/location/list', userToken, lang, {seriesid: entityId} ); } if (response) { const mappedLocations: LocationProps[] = response.map(function (loc: SeriesLocationItem): LocationProps { return { id: loc.id, name: loc.name, elements: loc.elements.map(function (elem: SeriesLocationElement): Element { return { id: elem.id, name: elem.name, description: elem.description, subElements: elem.subElements.map(function (sub: SeriesLocationSubElement): SubElement { return { id: sub.id, name: sub.name, description: sub.description, }; }), }; }), }; }); setSections(mappedLocations); } } else { let response: LocationListResponse; if (isCurrentlyOffline()) { response = await window.electron.invoke('db:location:all', {bookid: entityId}); } else if (book?.localBook) { response = await window.electron.invoke('db:location:all', {bookid: entityId}); } else { response = await System.authGetQueryToServer( 'location/all', userToken, lang, {bookid: entityId} ); } if (response) { setSections(response.locations); setToolEnabled(response.enabled); if (setBook && book) { setBook({ ...book, tools: { characters: book.tools?.characters ?? false, worlds: book.tools?.worlds ?? false, locations: response.enabled, spells: book.tools?.spells ?? false } }); } } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownFetchLocations')); } } finally { setIsLoading(false); } }, [entityId, isSeriesMode, userToken, lang, book, setBook, errorMessage, t, isCurrentlyOffline]); const toggleTool = useCallback(async function (enabled: boolean): Promise { if (isSeriesMode) return; try { const requestData = { bookId: book?.bookId, toolName: 'locations', enabled: enabled }; let response: boolean; if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:tool:update', requestData); } else { response = await System.authPatchToServer('book/tool-setting', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { addToQueue('db:book:tool:update', requestData); } } if (response && setBook && book) { setToolEnabled(enabled); setBook({ ...book, tools: { characters: book.tools?.characters ?? false, worlds: book.tools?.worlds ?? false, locations: enabled, spells: book.tools?.spells ?? false } }); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [isSeriesMode, book, setBook, userToken, lang, errorMessage, isCurrentlyOffline, addToQueue, localSyncedBooks]); const addSection = useCallback(async function (): Promise { if (isAddingSection) return; if (!newSectionName.trim()) { errorMessage(t('locationComponent.errorSectionNameEmpty')); return; } setIsAddingSection(true); try { let sectionId: string; if (isSeriesMode) { // Series mode - dual logic const addData = { seriesId: entityId, name: newSectionName, }; if (isCurrentlyOffline() || localSeries) { sectionId = await window.electron.invoke('db:series:location:section:add', addData); } else { sectionId = await System.authPostToServer( 'series/location/section/add', addData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('db:series:location:section:add', {...addData, id: sectionId}); } } if (!sectionId) { errorMessage(t('locationComponent.errorUnknownAddSection')); return; } } else { const requestData = { bookId: entityId, locationName: newSectionName, }; if (isCurrentlyOffline() || book?.localBook) { sectionId = await window.electron.invoke('db:location:section:add', requestData); } else { sectionId = await System.authPostToServer('location/section/add', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:section:add', {...requestData, id: sectionId}); } } if (!sectionId) { errorMessage(t('locationComponent.errorUnknownAddSection')); return; } } const newLocation: LocationProps = { id: sectionId, name: newSectionName, elements: [], }; setSections(function (prev: LocationProps[]): LocationProps[] { return [...prev, newLocation]; }); setNewSectionName(''); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownAddSection')); } } finally { setIsAddingSection(false); } }, [newSectionName, isSeriesMode, entityId, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, book, isAddingSection]); const addElement = useCallback(async function (sectionId: string): Promise { if (isAddingElement) return; if (!newElementNames[sectionId]?.trim()) { errorMessage(t('locationComponent.errorElementNameEmpty')); return; } setIsAddingElement(true); try { let elementId: string; if (isSeriesMode) { // Series mode - dual logic const addData = { locationId: sectionId, name: newElementNames[sectionId], }; if (isCurrentlyOffline() || localSeries) { elementId = await window.electron.invoke('db:series:location:element:add', addData); } else { elementId = await System.authPostToServer( 'series/location/element/add', addData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('db:series:location:element:add', {...addData, id: elementId}); } } if (!elementId) { errorMessage(t('locationComponent.errorUnknownAddElement')); return; } } else { const requestData = { bookId: entityId, locationId: sectionId, elementName: newElementNames[sectionId], }; if (isCurrentlyOffline() || book?.localBook) { elementId = await window.electron.invoke('db:location:element:add', requestData); } else { elementId = await System.authPostToServer('location/element/add', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:element:add', {...requestData, id: elementId}); } } if (!elementId) { errorMessage(t('locationComponent.errorUnknownAddElement')); return; } } setSections(function (prev: LocationProps[]): LocationProps[] { const updated: LocationProps[] = [...prev]; const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean { return section.id === sectionId; }); updated[sectionIndex].elements.push({ id: elementId, name: newElementNames[sectionId], description: '', subElements: [], }); return updated; }); setNewElementNames(function (prev: { [key: string]: string }): { [key: string]: string } { return {...prev, [sectionId]: ''}; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownAddElement')); } } finally { setIsAddingElement(false); } }, [newElementNames, isSeriesMode, entityId, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, book, isAddingElement]); const addSubElement = useCallback(async function (sectionId: string, elementIndex: number): Promise { if (isAddingSubElement) return; if (!newSubElementNames[elementIndex]?.trim()) { errorMessage(t('locationComponent.errorSubElementNameEmpty')); return; } setIsAddingSubElement(true); const sectionIndex: number = sections.findIndex(function (section: LocationProps): boolean { return section.id === sectionId; }); try { let subElementId: string; if (isSeriesMode) { // Series mode - dual logic const addData = { elementId: sections[sectionIndex].elements[elementIndex].id, name: newSubElementNames[elementIndex], }; if (isCurrentlyOffline() || localSeries) { subElementId = await window.electron.invoke('db:series:location:subelement:add', addData); } else { subElementId = await System.authPostToServer( 'series/location/sub-element/add', addData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('db:series:location:subelement:add', {...addData, id: subElementId}); } } if (!subElementId) { errorMessage(t('locationComponent.errorUnknownAddSubElement')); return; } } else { const requestData = { elementId: sections[sectionIndex].elements[elementIndex].id, subElementName: newSubElementNames[elementIndex], }; if (isCurrentlyOffline() || book?.localBook) { subElementId = await window.electron.invoke('db:location:subelement:add', requestData); } else { subElementId = await System.authPostToServer('location/sub-element/add', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:subelement:add', {...requestData, id: subElementId}); } } if (!subElementId) { errorMessage(t('locationComponent.errorUnknownAddSubElement')); return; } } setSections(function (prev: LocationProps[]): LocationProps[] { const updated: LocationProps[] = [...prev]; updated[sectionIndex].elements[elementIndex].subElements.push({ id: subElementId, name: newSubElementNames[elementIndex], description: '', }); return updated; }); setNewSubElementNames(function (prev: { [key: string]: string }): { [key: string]: string } { return {...prev, [elementIndex]: ''}; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownAddSubElement')); } } finally { setIsAddingSubElement(false); } }, [sections, newSubElementNames, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book, isAddingSubElement]); const removeSection = useCallback(async function (sectionId: string): Promise { try { let success: boolean; const deletedAt: number = System.timeStampInSeconds(); if (isSeriesMode) { // Series mode - dual logic const deleteData = {locationId: sectionId, deletedAt}; if (isCurrentlyOffline() || localSeries) { success = await window.electron.invoke('db:series:location:delete', deleteData); } else { success = await System.authDeleteToServer('series/location/delete', deleteData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('db:series:location:delete', deleteData); } } } else { const requestData = { locationId: sectionId, bookId: entityId, deletedAt, }; if (isCurrentlyOffline() || book?.localBook) { success = await window.electron.invoke('db:location:delete', requestData); } else { success = await System.authDeleteToServer('location/delete', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:delete', requestData); } } } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteSection')); return; } setSections(function (prev: LocationProps[]): LocationProps[] { return prev.filter(function (section: LocationProps): boolean { return section.id !== sectionId; }); }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownDeleteSection')); } } }, [isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]); const removeElement = useCallback(async function (sectionId: string, elementIndex: number): Promise { try { const elementId: string | undefined = sections.find(function (section: LocationProps): boolean { return section.id === sectionId; })?.elements[elementIndex].id; let success: boolean; const deletedAt: number = System.timeStampInSeconds(); if (isSeriesMode) { // Series mode - dual logic const deleteData = {elementId, deletedAt}; if (isCurrentlyOffline() || localSeries) { success = await window.electron.invoke('db:series:location:element:delete', deleteData); } else { success = await System.authDeleteToServer('series/location/element/delete', deleteData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('db:series:location:element:delete', deleteData); } } } else { const requestData = { elementId, bookId: entityId, deletedAt, }; if (isCurrentlyOffline() || book?.localBook) { success = await window.electron.invoke('db:location:element:delete', requestData); } else { success = await System.authDeleteToServer('location/element/delete', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:element:delete', requestData); } } } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteElement')); return; } setSections(function (prev: LocationProps[]): LocationProps[] { const updated: LocationProps[] = [...prev]; const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean { return section.id === sectionId; }); updated[sectionIndex].elements.splice(elementIndex, 1); return updated; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownDeleteElement')); } } }, [sections, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]); const removeSubElement = useCallback(async function (sectionId: string, elementIndex: number, subElementIndex: number): Promise { try { const subElementId: string | undefined = sections.find(function (section: LocationProps): boolean { return section.id === sectionId; })?.elements[elementIndex].subElements[subElementIndex].id; let success: boolean; const deletedAt: number = System.timeStampInSeconds(); if (isSeriesMode) { // Series mode - dual logic const deleteData = {subElementId, deletedAt}; if (isCurrentlyOffline() || localSeries) { success = await window.electron.invoke('db:series:location:subelement:delete', deleteData); } else { success = await System.authDeleteToServer('series/location/sub-element/delete', deleteData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('db:series:location:subelement:delete', deleteData); } } } else { const requestData = { subElementId, bookId: entityId, deletedAt, }; if (isCurrentlyOffline() || book?.localBook) { success = await window.electron.invoke('db:location:subelement:delete', requestData); } else { success = await System.authDeleteToServer('location/sub-element/delete', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:subelement:delete', requestData); } } } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteSubElement')); return; } setSections(function (prev: LocationProps[]): LocationProps[] { const updated: LocationProps[] = [...prev]; const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean { return section.id === sectionId; }); updated[sectionIndex].elements[elementIndex].subElements.splice(subElementIndex, 1); return updated; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownDeleteSubElement')); } } }, [sections, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]); const updateElement = useCallback(function (sectionId: string, elementIndex: number, field: keyof Element, value: string): void { setSections(function (prev: LocationProps[]): LocationProps[] { const updated: LocationProps[] = [...prev]; const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean { return section.id === sectionId; }); // @ts-ignore updated[sectionIndex].elements[elementIndex][field] = value; return updated; }); }, []); const updateSubElement = useCallback(function (sectionId: string, elementIndex: number, subElementIndex: number, field: keyof SubElement, value: string): void { setSections(function (prev: LocationProps[]): LocationProps[] { const updated: LocationProps[] = [...prev]; const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean { return section.id === sectionId; }); updated[sectionIndex].elements[elementIndex].subElements[subElementIndex][field] = value; return updated; }); }, []); const saveLocations = useCallback(async function (): Promise { try { const requestData = { locations: sections, }; let response: boolean; if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:location:update', requestData); } else { response = await System.authPostToServer('location/update', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:update', requestData); } } if (!response) { errorMessage(t('locationComponent.errorUnknownSave')); return false; } successMessage(t('locationComponent.successSave')); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownSave')); } return false; } }, [sections, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]); const exportToSeries = useCallback(async function (section: LocationProps): Promise { if (!bookSeriesId) return; try { const seriesLocationData = { seriesId: bookSeriesId, name: section.name, }; let seriesLocationId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → IPC seriesLocationId = await window.electron.invoke('db:series:location:section:add', seriesLocationData); } else { // Mode online → Serveur seriesLocationId = await System.authPostToServer('series/location/section/add', seriesLocationData, userToken, lang); // Si la série a une copie locale → addToQueue avec l'ID du serveur if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) { addToQueue('db:series:location:section:add', {...seriesLocationData, id: seriesLocationId}); } } if (seriesLocationId) { const updateData = { sectionId: section.id, sectionName: section.name, seriesLocationId: seriesLocationId, }; let updateResponse: boolean; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → IPC updateResponse = await window.electron.invoke('db:location:section:update', updateData); } else { // Mode online → Serveur updateResponse = await System.authPostToServer('location/section/update', updateData, userToken, lang); // Si le livre a une copie locale → addToQueue if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:section:update', updateData); } } if (updateResponse) { setSections(function (prev: LocationProps[]): LocationProps[] { return prev.map(function (s: LocationProps): LocationProps { return s.id === section.id ? {...s, seriesLocationId: seriesLocationId} : s; }); }); const newSeriesLocation: SeriesLocationItem = { id: seriesLocationId, name: section.name, elements: [], }; setSeriesLocations(function (prev: SeriesLocationItem[]): SeriesLocationItem[] { return [...prev, newSeriesLocation]; }); successMessage(t("locationComponent.exportSuccess")); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [bookSeriesId, userToken, lang, successMessage, errorMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, localSyncedSeries, entityId]); const importFromSeries = useCallback(async function (seriesLocationId: string): Promise { const seriesLocation: SeriesLocationItem | undefined = seriesLocations.find(function (location: SeriesLocationItem): boolean { return location.id === seriesLocationId; }); if (!seriesLocation) return; try { const sectionData = { bookId: entityId, locationName: seriesLocation.name, seriesLocationId: seriesLocationId, }; let sectionId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → IPC sectionId = await window.electron.invoke('db:location:section:add', sectionData); } else { // Mode online → Serveur sectionId = await System.authPostToServer('location/section/add', sectionData, userToken, lang); // Si le livre a une copie locale → addToQueue avec l'ID du serveur if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:section:add', {...sectionData, id: sectionId}); } } if (!sectionId) { errorMessage(t('locationComponent.importError')); return; } const importedElements: Element[] = []; for (const seriesElement of seriesLocation.elements) { const elementData = { bookId: entityId, locationId: sectionId, elementName: seriesElement.name, }; let elementId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → IPC elementId = await window.electron.invoke('db:location:element:add', elementData); } else { // Mode online → Serveur elementId = await System.authPostToServer('location/element/add', elementData, userToken, lang); // Si le livre a une copie locale → addToQueue avec l'ID du serveur if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:element:add', {...elementData, id: elementId}); } } if (!elementId) continue; const importedSubElements: SubElement[] = []; for (const seriesSubElement of seriesElement.subElements) { const subElementData = { elementId: elementId, subElementName: seriesSubElement.name, }; let subElementId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → IPC subElementId = await window.electron.invoke('db:location:subelement:add', subElementData); } else { // Mode online → Serveur subElementId = await System.authPostToServer('location/sub-element/add', subElementData, userToken, lang); // Si le livre a une copie locale → addToQueue avec l'ID du serveur if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('db:location:subelement:add', {...subElementData, id: subElementId}); } } if (subElementId) { importedSubElements.push({ id: subElementId, name: seriesSubElement.name, description: seriesSubElement.description, }); } } importedElements.push({ id: elementId, name: seriesElement.name, description: seriesElement.description, subElements: importedSubElements, }); } const newLocation: LocationProps = { id: sectionId, name: seriesLocation.name, elements: importedElements, seriesLocationId: seriesLocationId, }; setSections(function (prev: LocationProps[]): LocationProps[] { return [...prev, newLocation]; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [seriesLocations, entityId, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks]); // Navigation functions const enterDetailMode = useCallback(function (sectionIndex: number): void { setSelectedSectionIndex(sectionIndex); setViewMode('detail'); setSectionsBackup(null); }, []); const enterEditMode = useCallback(function (): void { setSectionsBackup(sections.map(function (section: LocationProps): LocationProps { return { ...section, elements: section.elements.map(function (element: Element): Element { return { ...element, subElements: [...element.subElements] }; }) }; })); setViewMode('edit'); }, [sections]); const exitEditMode = useCallback(async function (save: boolean): Promise { if (save) { const success: boolean = await saveLocations(); if (!success) { // Stay in edit mode on error return; } setViewMode('detail'); } else { if (sectionsBackup) { setSections(sectionsBackup); setViewMode('detail'); } else { setViewMode('list'); } } setSectionsBackup(null); }, [saveLocations, sectionsBackup]); const backToList = useCallback(function (): void { setSelectedSectionIndex(-1); setSectionsBackup(null); setViewMode('list'); }, []); return { // State sections, seriesLocations, toolEnabled, isLoading, isSeriesMode, bookSeriesId, newSectionName, newElementNames, newSubElementNames, // Navigation state viewMode, selectedSectionIndex, sectionsBackup, // Actions addSection, addElement, addSubElement, removeSection, removeElement, removeSubElement, updateElement, updateSubElement, saveLocations, toggleTool, importFromSeries, exportToSeries, refreshLocations, refreshSeriesLocations, setNewSectionName, setNewElementNames, setNewSubElementNames, // Navigation actions enterDetailMode, enterEditMode, exitEditMode, backToList, }; }