Files
TREK/client/src/components/Admin/addons/useAddonManager.ts
T
Maurice f1c437b8b8 Keep api/client.ts and trips.service.ts within their size baselines
The admin switch for wanderer moves to api/wanderer.ts and the recorded
track restore to api/recordedTrack.ts; trips.service.ts keeps dev's
formatting with only the derived-from remap on copy.
2026-10-15 18:16:25 +02:00

111 lines
4.1 KiB
TypeScript

import { useEffect, useEffectEvent, useState } from 'react';
import { adminApi } from '../../../api/client';
import { wandererAdminApi } from '../../../api/wanderer';
import { useTranslation } from '../../../i18n';
import { useAddonStore } from '../../../store/addonStore';
import { useToast } from '../../shared/Toast';
import { type Addon, type ProviderOption, isPhotosAddon } from './addonModel';
/**
* The addon switches behind both admin shells: the addon list loaded on mount, an
* optimistic toggle with a per row rollback, and the lists each shell groups its tiles
* and provider shelves from. The desktop columns and the phone cards render their own
* markup over it.
*/
export function useAddonManager() {
const { t } = useTranslation();
const toast = useToast();
const refreshGlobalAddons = useAddonStore((s) => s.loadAddons);
const [addons, setAddons] = useState<Addon[]>([]);
const [loading, setLoading] = useState(true);
const [toursWanderer, setToursWanderer] = useState(false);
const loadAddons = async () => {
try {
const data = await adminApi.addons();
setAddons(data.addons);
} catch {
toast.error(t('admin.addons.toast.error'));
}
};
const loadOnMount = useEffectEvent(() => {
void loadAddons().finally(() => setLoading(false));
wandererAdminApi
.get()
.then((flag) => setToursWanderer(flag.enabled))
.catch(() => toast.error(t('admin.addons.toast.error')));
});
useEffect(() => {
loadOnMount();
}, []);
/** Optimistic flip, per-row rollback on failure. Rolling the whole addons
* snapshot back instead would undo a toggle the user hit in parallel. */
const handleToggle = async (addon: Addon) => {
const newEnabled = !addon.enabled;
setAddons((prev) => prev.map((a) => (a.id === addon.id ? { ...a, enabled: newEnabled } : a)));
try {
await adminApi.updateAddon(addon.id, { enabled: newEnabled });
} catch {
setAddons((prev) => prev.map((a) => (a.id === addon.id ? { ...a, enabled: !newEnabled } : a)));
toast.error(t('admin.addons.toast.error'));
return;
}
refreshGlobalAddons();
// Journey off disables every photo provider with it, and the response carries
// only Journey. Without re-reading, switching Journey back on brings the shelf
// up with providers the database has long since turned off.
// Same for Documents: switching it off disables every document provider in
// the database, and the response carries only the addon itself.
if (addon.id === 'journey' || addon.id === 'documents') await loadAddons();
toast.success(t('admin.addons.toast.updated'));
};
/** wanderer under Tours: flipped at once, put back when the save fails. */
const toggleToursWanderer = async () => {
const next = !toursWanderer;
setToursWanderer(next);
try {
await wandererAdminApi.update(next);
} catch {
setToursWanderer(!next);
toast.error(t('admin.addons.toast.error'));
return;
}
void refreshGlobalAddons();
toast.success(t('admin.addons.toast.updated'));
};
const asOption = (provider: Addon): ProviderOption => ({
key: provider.id,
label: provider.name,
description: provider.description,
enabled: provider.enabled,
toggle: () => handleToggle(provider),
});
const tripAddons = addons.filter((a) => a.type === 'trip' && !isPhotosAddon(a));
const globalAddons = addons.filter((a) => a.type === 'global');
const integrationAddons = addons.filter((a) => a.type === 'integration');
const providerOptions = addons.filter((a) => a.type === 'photo_provider').map(asOption);
// No credential form under these, unlike the photo providers: a document connection
// belongs to a trip and is entered there. The admin only decides whether a provider
// may be offered at all.
const documentProviderOptions = addons.filter((a) => a.type === 'document_provider').map(asOption);
return {
addons,
loading,
handleToggle,
tripAddons,
globalAddons,
integrationAddons,
providerOptions,
documentProviderOptions,
toursWanderer,
toggleToursWanderer,
};
}