feat(quick-261003-387): Verwaltungsseite Kategorien, Marktplatz-Reihenfolge, Formular, Texte, CHANGELOG und Handbuch

- Administrator -> Module -> Kategorien: anlegen, umbenennen, sortieren, Eintraege zuordnen und sortieren, loeschen mit Zielauswahl
- Marktplatz-Filter folgen der Kategorienreihenfolge, Kategorieseite und Modulliste zeigen gespeicherte Namen
- Formular fuer eigene Module bietet alle Kategorien der Organisation an
- Texte de/en, CHANGELOG, Handbuch Administration und Anwender

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 02:48:04 +02:00
parent af1878d5ee
commit f2c0a896e8
16 changed files with 1173 additions and 14 deletions
+99
View File
@@ -17,9 +17,108 @@ export interface ModuleCategoryInfo {
isSystem: boolean;
}
/** Ein Eintrag einer Kategorie in der Verwaltungsuebersicht. */
export interface ModuleCategoryOverviewItem {
/** `module` = Marktplatz-Modul, `custom` = gemeinsames eigenes Modul. */
type: 'module' | 'custom';
id: string;
name: string;
slug?: string;
}
/** Eine Kategorie samt Eintraegen fuer die Verwaltungsseite. */
export interface ModuleCategoryOverview extends ModuleCategoryInfo {
items: ModuleCategoryOverviewItem[];
/** Anzahl persoenlicher eigener Module von Benutzern (nie einzeln benannt). */
personalCount: number;
}
/** Fehler mit HTTP-Status und Servermeldung (falls vorhanden). */
export class ModuleCategoryRequestError extends Error {
constructor(
readonly status: number,
message: string,
) {
super(message);
this.name = 'ModuleCategoryRequestError';
}
}
async function failure(res: Response): Promise<ModuleCategoryRequestError> {
let message = `Request failed (${res.status})`;
try {
const body = await res.json();
const raw = body?.message;
if (Array.isArray(raw)) message = raw.join(' ');
else if (typeof raw === 'string') message = raw;
} catch {
// Antwort ohne JSON-Koerper — Standardmeldung bleibt.
}
return new ModuleCategoryRequestError(res.status, message);
}
async function send<T>(path: string, method: string, body?: unknown): Promise<T> {
const res = await fetch(`${API_URL}/module-categories${path}`, {
method,
credentials: 'include',
...(body === undefined
? {}
: { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
});
if (!res.ok) throw await failure(res);
return res.json();
}
/** Alle Kategorien der Organisation in ihrer Reihenfolge (jeder Angemeldete). */
export async function listModuleCategories(): Promise<ModuleCategoryInfo[]> {
const res = await fetch(`${API_URL}/module-categories`, { credentials: 'include' });
if (!res.ok) throw new Error(`Request failed (${res.status})`);
return res.json();
}
/** Kategorien mit ihren Eintraegen (nur Administratoren). */
export function getModuleCategoryOverview(): Promise<ModuleCategoryOverview[]> {
return send('/overview', 'GET');
}
export function createModuleCategory(name: string): Promise<ModuleCategoryInfo> {
return send('', 'POST', { name });
}
export function renameModuleCategory(key: string, name: string): Promise<ModuleCategoryInfo> {
return send(`/${encodeURIComponent(key)}`, 'PATCH', { name });
}
/** Neue Reihenfolge ALLER Kategorien (Kennungen). */
export function reorderModuleCategories(keys: string[]): Promise<ModuleCategoryInfo[]> {
return send('/order', 'PUT', { keys });
}
/** Neue Reihenfolge ALLER nicht persoenlichen Eintraege einer Kategorie. */
export function reorderModuleCategoryItems(
key: string,
items: Array<Pick<ModuleCategoryOverviewItem, 'type' | 'id'>>,
): Promise<{ ok: true }> {
return send(`/${encodeURIComponent(key)}/items`, 'PUT', { items });
}
/** Einen Eintrag einer Kategorie zuordnen (hinten angehaengt). */
export function assignModuleCategory(input: {
type: 'module' | 'custom';
id: string;
categoryKey: string;
}): Promise<unknown> {
return send('/assignment', 'PUT', input);
}
/**
* Kategorie loeschen. Eine nicht leere Kategorie braucht `moveTo`, sonst
* antwortet die API mit 409 und loescht nichts.
*/
export function deleteModuleCategory(
key: string,
moveTo?: string,
): Promise<{ deleted: true; moved: number }> {
const query = moveTo ? `?moveTo=${encodeURIComponent(moveTo)}` : '';
return send(`/${encodeURIComponent(key)}${query}`, 'DELETE');
}
@@ -21,7 +21,8 @@ export const useModuleCategoryStore = create<ModuleCategoryState>()((set, get) =
load: async () => {
try {
const categories = await listModuleCategories();
set({ categories, loaded: true });
// Eine unerwartete Antwort (kein Feld) laesst den bisherigen Stand stehen.
if (Array.isArray(categories)) set({ categories, loaded: true });
} catch {
// silently fail
}