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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user