1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 17:49:57 +01:00

feat(frontend): TypeScript-Monorepo mit Terminal-UI für Effigenix ERP

Monorepo-Setup (pnpm workspaces) mit vier shared Packages und einer TUI-App:

Shared Packages:
- @effigenix/types: TypeScript-DTOs (UserDTO, RoleDTO, AuthDTO, Enums)
- @effigenix/config: API-Konfiguration und Shared Constants
- @effigenix/validation: Zod-Schemas für Username, E-Mail und Passwort
- @effigenix/api-client: axios-Client mit JWT-Handling (proaktiver + reaktiver
  Token-Refresh), AuthInterceptor, ErrorInterceptor, Resources für auth/users/roles

TUI (apps/cli, Ink 5 / React):
- Authentication: Login/Logout, Session-Restore beim Start, JWT-Refresh
- User Management: Liste, Anlage (Zod-Inline-Validation), Detailansicht,
  Passwort ändern, Sperren/Entsperren mit ConfirmDialog
- Role Management: Liste, Detailansicht, Zuweisen/Entfernen per RoleSelectList (↑↓)
- UX: SuccessDisplay (Auto-Dismiss 3 s), ConfirmDialog (J/N),
  FormInput mit Inline-Fehlern, StatusBar mit API-URL
- Layout: Fullscreen-Modus (alternate screen buffer), Header mit eingeloggtem User
- Tests: vitest + ink-testing-library (15 Tests)
This commit is contained in:
Sebastian Frick 2026-02-18 12:23:11 +01:00
parent 87123df2e4
commit bbe9e87c33
65 changed files with 6955 additions and 1 deletions

View file

@ -0,0 +1,37 @@
import { useState, useCallback } from 'react';
import type { RoleDTO } from '@effigenix/api-client';
import { client } from '../utils/api-client.js';
interface RolesState {
roles: RoleDTO[];
loading: boolean;
error: string | null;
}
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
}
export function useRoles() {
const [state, setState] = useState<RolesState>({
roles: [],
loading: false,
error: null,
});
const fetchRoles = useCallback(async () => {
setState((s) => ({ ...s, loading: true, error: null }));
try {
const roles = await client.roles.list();
setState({ roles, loading: false, error: null });
} catch (err) {
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
}
}, []);
const clearError = useCallback(() => {
setState((s) => ({ ...s, error: null }));
}, []);
return { ...state, fetchRoles, clearError };
}

View file

@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';
import { useStdout } from 'ink';
interface TerminalSize {
columns: number;
rows: number;
}
export function useTerminalSize(): TerminalSize {
const { stdout } = useStdout();
const [size, setSize] = useState<TerminalSize>({
columns: stdout?.columns ?? 80,
rows: stdout?.rows ?? 24,
});
useEffect(() => {
if (!stdout) return;
const handleResize = () => {
setSize({ columns: stdout.columns, rows: stdout.rows });
};
stdout.on('resize', handleResize);
return () => {
stdout.off('resize', handleResize);
};
}, [stdout]);
return size;
}

View file

@ -0,0 +1,137 @@
import { useState, useCallback } from 'react';
import type { UserDTO } from '@effigenix/api-client';
import { client } from '../utils/api-client.js';
interface UsersState {
users: UserDTO[];
loading: boolean;
error: string | null;
}
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
}
export function useUsers() {
const [state, setState] = useState<UsersState>({
users: [],
loading: false,
error: null,
});
const fetchUsers = useCallback(async () => {
setState((s) => ({ ...s, loading: true, error: null }));
try {
const users = await client.users.list();
setState({ users, loading: false, error: null });
} catch (err) {
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
}
}, []);
const createUser = useCallback(
async (username: string, email: string, password: string, roleName?: string) => {
setState((s) => ({ ...s, loading: true, error: null }));
try {
const user = await client.users.create({
username,
email,
password,
roleNames: roleName ? [roleName] : [],
});
setState((s) => ({ users: [...s.users, user], loading: false, error: null }));
return user;
} catch (err) {
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
return null;
}
},
[],
);
const lockUser = useCallback(async (id: string) => {
try {
const user = await client.users.lock(id);
setState((s) => ({
...s,
users: s.users.map((u) => (u.id === id ? user : u)),
}));
return user;
} catch (err) {
setState((s) => ({ ...s, error: errorMessage(err) }));
return null;
}
}, []);
const unlockUser = useCallback(async (id: string) => {
try {
const user = await client.users.unlock(id);
setState((s) => ({
...s,
users: s.users.map((u) => (u.id === id ? user : u)),
}));
return user;
} catch (err) {
setState((s) => ({ ...s, error: errorMessage(err) }));
return null;
}
}, []);
const assignRole = useCallback(async (id: string, roleName: string) => {
try {
const user = await client.users.assignRole(id, { roleName });
setState((s) => ({
...s,
users: s.users.map((u) => (u.id === id ? user : u)),
}));
return user;
} catch (err) {
setState((s) => ({ ...s, error: errorMessage(err) }));
return null;
}
}, []);
const removeRole = useCallback(async (id: string, roleName: string) => {
try {
await client.users.removeRole(id, roleName);
const updated = await client.users.getById(id);
setState((s) => ({
...s,
users: s.users.map((u) => (u.id === id ? updated : u)),
}));
return true;
} catch (err) {
setState((s) => ({ ...s, error: errorMessage(err) }));
return false;
}
}, []);
const changePassword = useCallback(
async (id: string, currentPassword: string, newPassword: string) => {
try {
await client.users.changePassword(id, { currentPassword, newPassword });
return true;
} catch (err) {
setState((s) => ({ ...s, error: errorMessage(err) }));
return false;
}
},
[],
);
const clearError = useCallback(() => {
setState((s) => ({ ...s, error: null }));
}, []);
return {
...state,
fetchUsers,
createUser,
lockUser,
unlockUser,
assignRole,
removeRole,
changePassword,
clearError,
};
}