Remove CharacterComponent and CharacterDetail components
- Deleted `CharacterComponent` and `CharacterDetail` files from the project. - Refactored related logic to improve code maintainability and reduce redundancy.
This commit is contained in:
982
hooks/settings/useLocations.ts
Normal file
982
hooks/settings/useLocations.ts
Normal file
@@ -0,0 +1,982 @@
|
||||
'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<void>;
|
||||
addElement: (sectionId: string) => Promise<void>;
|
||||
addSubElement: (sectionId: string, elementIndex: number) => Promise<void>;
|
||||
removeSection: (sectionId: string) => Promise<void>;
|
||||
removeElement: (sectionId: string, elementIndex: number) => Promise<void>;
|
||||
removeSubElement: (sectionId: string, elementIndex: number, subElementIndex: number) => Promise<void>;
|
||||
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<boolean>;
|
||||
toggleTool: (enabled: boolean) => Promise<void>;
|
||||
importFromSeries: (seriesLocationId: string) => Promise<void>;
|
||||
exportToSeries: (section: LocationProps) => Promise<void>;
|
||||
refreshLocations: () => Promise<void>;
|
||||
refreshSeriesLocations: () => Promise<void>;
|
||||
setNewSectionName: (name: string) => void;
|
||||
setNewElementNames: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
||||
setNewSubElementNames: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
||||
|
||||
// Navigation actions
|
||||
enterDetailMode: (sectionIndex: number) => void;
|
||||
enterEditMode: () => void;
|
||||
exitEditMode: (save: boolean) => Promise<void>;
|
||||
backToList: () => void;
|
||||
}
|
||||
|
||||
export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
const {entityType, entityId} = config;
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {book, setBook} = useContext(BookContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const {localSeries} = useContext<SeriesContextProps>(SeriesContext);
|
||||
const {localSyncedSeries} = useContext<SeriesSyncContextProps>(SeriesSyncContext);
|
||||
|
||||
const [sections, setSections] = useState<LocationProps[]>([]);
|
||||
const [seriesLocations, setSeriesLocations] = useState<SeriesLocationItem[]>([]);
|
||||
const [toolEnabled, setToolEnabled] = useState<boolean>(entityType === 'series' || (book?.tools?.locations ?? false));
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [newSectionName, setNewSectionName] = useState<string>('');
|
||||
const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({});
|
||||
const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({});
|
||||
const [isAddingElement, setIsAddingElement] = useState<boolean>(false);
|
||||
const [isAddingSubElement, setIsAddingSubElement] = useState<boolean>(false);
|
||||
const [isAddingSection, setIsAddingSection] = useState<boolean>(false);
|
||||
|
||||
// Navigation state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [selectedSectionIndex, setSelectedSectionIndex] = useState<number>(-1);
|
||||
const [sectionsBackup, setSectionsBackup] = useState<LocationProps[] | null>(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<void> {
|
||||
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<SeriesLocationItem[]>(
|
||||
'db:series:location:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'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<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesLocationItem[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesLocationItem[]>(
|
||||
'db:series:location:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'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<LocationListResponse>('db:location:all', {bookid: entityId});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: entityId});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<LocationListResponse>(
|
||||
'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<void> {
|
||||
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<boolean>('db:book:tool:update', requestData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('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<void> {
|
||||
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<string>('db:series:location:section:add', addData);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>(
|
||||
'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<string>('db:location:section:add', requestData);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>('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<void> {
|
||||
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<string>('db:series:location:element:add', addData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>(
|
||||
'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<string>('db:location:element:add', requestData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>('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<void> {
|
||||
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<string>('db:series:location:subelement:add', addData);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>(
|
||||
'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<string>('db:location:subelement:add', requestData);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>('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<void> {
|
||||
try {
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {locationId: sectionId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('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,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('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<void> {
|
||||
try {
|
||||
const elementId: string | undefined = sections.find(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
})?.elements[elementIndex].id;
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {elementId: elementId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('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: elementId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:element:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('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<void> {
|
||||
try {
|
||||
const subElementId: string | undefined = sections.find(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
})?.elements[elementIndex].subElements[subElementIndex].id;
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {subElementId: subElementId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('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: subElementId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:subelement:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('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<boolean> {
|
||||
try {
|
||||
const requestData = {
|
||||
locations: sections,
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:update', requestData);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>('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<void> {
|
||||
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<string>('db:series:location:section:add', seriesLocationData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesLocationId = await System.authPostToServer<string>('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<boolean>('db:location:section:update', updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('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<void> {
|
||||
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<string>('db:location:section:add', sectionData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
sectionId = await System.authPostToServer<string>('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<string>('db:location:element:add', elementData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
elementId = await System.authPostToServer<string>('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<string>('db:location:subelement:add', subElementData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
subElementId = await System.authPostToServer<string>('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<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user