diff --git a/src/api.ts b/src/api.ts index 8771019..ac02d60 100644 --- a/src/api.ts +++ b/src/api.ts @@ -46,7 +46,14 @@ import { getApiBaseUrl } from '../config/config'; const api = axios.create({ baseURL: getApiBaseUrl(), - withCredentials: true + withCredentials: true, + // FastAPI expects repeat-style array query params (``?ids=1&ids=2``). + // Axios v1.x default would render ``?ids[]=1&ids[]=2``, which FastAPI + // silently drops -- e.g. ``trackerIds`` filters on the Redmine stats + // endpoint never reach the route. Setting ``indexes: null`` switches + // the URLSearchParams visitor to repeat format. Applies globally so + // every endpoint with array query params gets it for free. + paramsSerializer: { indexes: null }, }); // Add a request interceptor to add the auth token, context headers, and log backend IP diff --git a/src/api/trusteeApi.ts b/src/api/trusteeApi.ts index c3eb5da..f21a369 100644 --- a/src/api/trusteeApi.ts +++ b/src/api/trusteeApi.ts @@ -853,16 +853,46 @@ export async function fetchChartOfAccounts( }); } +/** + * Submits a background job that pushes positions to the accounting system and + * polls `/api/jobs/{jobId}` until the job reaches a terminal status. Returns + * the same `{ total, success, skipped, errors, results }` payload that the + * legacy synchronous endpoint used to return -- but does NOT block the user + * while the (potentially long) external accounting calls run in the worker. + */ export async function syncPositionsToAccounting( request: ApiRequestFunction, instanceId: string, - positionIds: string[] -): Promise<{ total: number; success: number; errors: number; results: any[] }> { - return await request({ + positionIds: string[], + opts?: { pollMs?: number; onProgress?: (progress: number, message?: string | null) => void } +): Promise<{ total: number; success: number; skipped?: number; errors: number; results: any[] }> { + const submission = await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync`, method: 'post', data: { positionIds } }); + + const jobId: string | undefined = submission?.jobId; + if (!jobId) { + throw new Error('Background job could not be started (missing jobId).'); + } + + const pollMs = opts?.pollMs ?? 1500; + const TERMINAL = new Set(['SUCCESS', 'ERROR', 'CANCELLED']); + + while (true) { + const job = await request({ url: `/api/jobs/${jobId}`, method: 'get' }); + if (opts?.onProgress) { + opts.onProgress(Number(job?.progress ?? 0), job?.progressMessage ?? null); + } + if (job?.status && TERMINAL.has(job.status)) { + if (job.status === 'SUCCESS' && job.result) { + return job.result; + } + throw new Error(job?.errorMessage || 'Sync-Job fehlgeschlagen'); + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + } } export async function fetchSyncStatus( diff --git a/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.module.css b/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.module.css index 10a5d63..f516be0 100644 --- a/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.module.css +++ b/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.module.css @@ -109,6 +109,53 @@ border-color: var(--primary-color, #f25843); } +/* --- Multiselect chip group --- */ + +.chipGroup { + display: inline-flex; + align-items: center; + gap: 0.375rem; + flex-wrap: wrap; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.625rem; + border: 1px solid var(--border-color, #333); + border-radius: 999px; + background: var(--bg-secondary, #2a2a2a); + color: var(--text-secondary, #888); + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + user-select: none; + transition: background 120ms ease, color 120ms ease, border-color 120ms ease; +} + +.chip:hover { + border-color: var(--primary-color, #f25843); + color: var(--text-primary, #e0e0e0); +} + +.chipActive { + background: var(--primary-color, #4A6FA5); + border-color: var(--primary-color, #4A6FA5); + color: #fff; +} + +.chipActive:hover { + color: #fff; + filter: brightness(0.95); +} + +.chipMeta { + font-size: 0.7rem; + color: var(--text-secondary, #888); + margin-left: 0.25rem; +} + /* --- Sections Grid --- */ .sectionsGrid { diff --git a/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.tsx b/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.tsx index 8f3ff5f..81513fb 100644 --- a/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.tsx +++ b/src/components/FormGenerator/FormGeneratorReport/FormGeneratorReport.tsx @@ -667,6 +667,12 @@ const _Toolbar: React.FC = ({ value={(filterState.filters[filter.key] as string) || ''} onChange={(e) => _handleFilterChange(filter.key, e.target.value)} /> + ) : filter.type === 'multiselect' ? ( + <_MultiselectChips + filter={filter} + value={filterState.filters[filter.key]} + onChange={(next) => _handleFilterChange(filter.key, next)} + /> ) : ( `` ``min``/``max`` attributes so the browser +// refuses invalid years instead of us silently falling back to the default +// preset afterwards. +export function clampIsoDate(iso: string | undefined, cfg: PeriodConstraints, side: 'min' | 'max'): string | undefined { + const today = toIsoDate(todayDate()); + let lo: string | undefined = cfg.minDate; + let hi: string | undefined = cfg.maxDate; + if (cfg.direction === 'past') hi = hi && hi < today ? hi : today; + if (cfg.direction === 'future') lo = lo && lo > today ? lo : today; + if (side === 'min') return lo; + return hi; +} + // --------------------------------------------------------------------------- // Label formatting // --------------------------------------------------------------------------- @@ -174,6 +198,7 @@ export function isValueAllowed(value: PeriodValue | null, cfg: PeriodConstraints */ export function presetLiteralKey(kind: PeriodPresetKind): string { switch (kind) { + case 'allTime': return 'Alle'; case 'ytd': return 'Laufendes Jahr'; case 'lastYear': return 'Letztes Jahr'; case 'nextYear': return 'Nächstes Jahr'; diff --git a/src/components/PeriodPicker/PeriodPickerPopover.tsx b/src/components/PeriodPicker/PeriodPickerPopover.tsx index a1c521a..f21dd67 100644 --- a/src/components/PeriodPicker/PeriodPickerPopover.tsx +++ b/src/components/PeriodPicker/PeriodPickerPopover.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLanguage } from '../../providers/language/LanguageContext'; import PeriodPickerCalendar from './PeriodPickerCalendar'; import { + clampIsoDate, fromIsoDate, isPresetDisabled, presetLiteralKey, @@ -27,6 +28,7 @@ import type { import styles from './PeriodPicker.module.css'; const PRESETS_ORDER: PeriodPresetKind[] = [ + 'allTime', 'ytd', 'lastYear', 'nextYear', @@ -41,6 +43,7 @@ const PRESETS_ORDER: PeriodPresetKind[] = [ function _presetLabel(kind: PeriodPresetKind, t: (k: string) => string): string { switch (kind) { + case 'allTime': return t('Alle'); case 'ytd': return t('Laufendes Jahr'); case 'lastYear': return t('Letztes Jahr'); case 'nextYear': return t('Nächstes Jahr'); @@ -107,13 +110,21 @@ const PeriodPickerPopover: React.FC = (props) => { const _selectPreset = useCallback((kind: PeriodPresetKind) => { if (isPresetDisabled(kind, constraints)) return; if (kind === 'custom') { + // Switching from ``allTime`` back to custom: don't carry the 1970-2999 + // sentinel. Seed with today/today so the user gets a sensible starting + // point and the calendar has a real anchor. + const isFromAllTime = draft.preset.kind === 'allTime'; + const seedFrom = isFromAllTime ? toIsoDate(todayDate()) : draft.fromDate; + const seedTo = isFromAllTime ? toIsoDate(todayDate()) : draft.toDate; const next: PeriodValue = { preset: { kind: 'custom' }, - fromDate: draft.fromDate, - toDate: draft.toDate, + fromDate: seedFrom, + toDate: seedTo, }; setDraft(next); setRangePick({ from: fromIsoDate(next.fromDate), to: fromIsoDate(next.toDate) }); + const anchor = fromIsoDate(seedFrom); + if (anchor) setCalAnchor(startOfMonth(anchor)); return; } const preset: PeriodPreset = { kind } as PeriodPreset; @@ -152,15 +163,32 @@ const PeriodPickerPopover: React.FC = (props) => { }, [rangePick]); const _onFooterFromChange = useCallback((iso: string) => { + // Empty string = user cleared the input; ignore so ``draft`` keeps a valid ISO. + if (!iso) return; + const d = fromIsoDate(iso); setDraft((prev) => ({ ...prev, preset: { kind: 'custom' }, fromDate: iso })); - setRangePick((prev) => ({ from: fromIsoDate(iso), to: prev.to })); + setRangePick((prev) => ({ from: d, to: prev.to })); + // Jump the calendar to the typed month so the user immediately sees the + // selection move. Without this, the calendar stays on the current month + // and it *looks* like the input was ignored. + if (d) setCalAnchor(startOfMonth(d)); }, []); const _onFooterToChange = useCallback((iso: string) => { + if (!iso) return; + const d = fromIsoDate(iso); setDraft((prev) => ({ ...prev, preset: { kind: 'custom' }, toDate: iso })); - setRangePick((prev) => ({ from: prev.from, to: fromIsoDate(iso) })); + setRangePick((prev) => ({ from: prev.from, to: d })); + if (d) setCalAnchor(startOfMonth(d)); }, []); + // ``min``/``max`` on the native date inputs — prevents the user from typing + // a date that would be silently reverted by the parent's + // ``isValueAllowed`` fallback (which would replace it with ``defaultPreset`` + // and lose the custom year). + const footerMin = clampIsoDate(undefined, constraints, 'min'); + const footerMax = clampIsoDate(undefined, constraints, 'max'); + // Keyboard: Esc cancels, Enter applies const popRef = useRef(null); useEffect(() => { @@ -269,18 +297,20 @@ const PeriodPickerPopover: React.FC = (props) => { _onFooterFromChange(e.target.value)} /> {t('Bis')} _onFooterToChange(e.target.value)} /> diff --git a/src/components/PeriodPicker/PeriodPickerTypes.ts b/src/components/PeriodPicker/PeriodPickerTypes.ts index e4fd4fe..48d9a17 100644 --- a/src/components/PeriodPicker/PeriodPickerTypes.ts +++ b/src/components/PeriodPicker/PeriodPickerTypes.ts @@ -9,6 +9,7 @@ export type PeriodUnit = 'day' | 'week' | 'month' | 'year'; export type PeriodPresetKind = + | 'allTime' | 'ytd' | 'lastYear' | 'nextYear' @@ -23,6 +24,7 @@ export type PeriodPresetKind = | 'custom'; export type PeriodPreset = + | { kind: 'allTime' } | { kind: 'ytd' } | { kind: 'lastYear' } | { kind: 'nextYear' } diff --git a/src/pages/admin/AdminDemoConfigPage.module.css b/src/pages/admin/AdminDemoConfigPage.module.css index d11aa0f..65a291f 100644 --- a/src/pages/admin/AdminDemoConfigPage.module.css +++ b/src/pages/admin/AdminDemoConfigPage.module.css @@ -156,3 +156,110 @@ .spin { animation: spin 1s linear infinite; } + +.credentialsBox { + margin-top: 0.75rem; + padding: 0.75rem 0.85rem; + border-radius: var(--object-radius-small, 6px); + background: var(--bg-secondary, #fff); + border: 1px dashed var(--border-color, #cbd5e1); + color: var(--text-primary); +} + +.credentialsBoxCompact { + margin-top: 0.6rem; + padding: 0.5rem 0.65rem; + border-radius: var(--object-radius-small, 6px); + background: var(--bg-tertiary, #f7f7f8); + border: 1px dashed var(--border-color, #cbd5e1); + color: var(--text-primary); + font-size: 0.78rem; +} + +:global(.dark-theme) .credentialsBox, +:global(.dark-theme) .credentialsBoxCompact { + background: var(--bg-tertiary, #2a2a3a); + border-color: var(--border-color, #3d3d4d); +} + +.credentialsHeader { + display: flex; + align-items: center; + gap: 0.4rem; + font-weight: 600; + font-size: 0.78rem; + color: var(--text-secondary); + margin-bottom: 0.4rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.credentialsRow { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.credentialsRow + .credentialsRow { + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--border-color, #e2e8f0); +} + +.credentialsRole { + font-size: 0.72rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.credentialsField { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; +} + +.credentialsField code { + font-family: var(--font-mono, monospace); + background: var(--bg-tertiary, #f7f7f8); + padding: 1px 6px; + border-radius: 4px; + font-size: 0.8rem; + color: var(--text-primary); + word-break: break-all; +} + +:global(.dark-theme) .credentialsField code { + background: var(--bg-secondary, #1e1e2e); +} + +.credentialsLabel { + min-width: 60px; + color: var(--text-secondary); + font-size: 0.75rem; +} + +.copyButton { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid var(--border-color, #cbd5e1); + background: transparent; + color: var(--text-secondary); + cursor: pointer; + font-size: 0.7rem; +} + +.copyButton:hover:not(:disabled) { + background: var(--bg-secondary, #f7f7f8); + color: var(--text-primary); +} + +.copyButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} diff --git a/src/pages/admin/AdminDemoConfigPage.tsx b/src/pages/admin/AdminDemoConfigPage.tsx index dcbac8d..0c683c8 100644 --- a/src/pages/admin/AdminDemoConfigPage.tsx +++ b/src/pages/admin/AdminDemoConfigPage.tsx @@ -6,17 +6,25 @@ */ import React, { useState, useEffect, useCallback } from 'react'; -import { FaPlay, FaTrash, FaSync, FaCubes } from 'react-icons/fa'; +import { FaPlay, FaTrash, FaSync, FaCubes, FaCopy, FaKey } from 'react-icons/fa'; import api from '../../api'; import styles from './Admin.module.css'; import demoStyles from './AdminDemoConfigPage.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; import { useConfirm } from '../../hooks/useConfirm'; +interface _DemoCredential { + role?: string; + username?: string; + email?: string; + password?: string; +} + interface _DemoConfig { code: string; label: string; description: string; + credentials?: _DemoCredential[]; } interface _ActionResult { @@ -24,6 +32,7 @@ interface _ActionResult { action: 'load' | 'remove'; status: 'ok' | 'error'; summary?: Record; + credentials?: _DemoCredential[]; error?: string; } @@ -59,7 +68,18 @@ export const AdminDemoConfigPage: React.FC = () => { setLastResult(null); try { const response = await api.post(`/api/admin/demo-config/${code}/load`); - setLastResult({ code, action: 'load', status: 'ok', summary: response.data.summary }); + const summary = (response.data?.summary || {}) as Record; + const credsFromSummary = Array.isArray(summary.credentials) + ? (summary.credentials as _DemoCredential[]) + : undefined; + const credsFromConfig = configs.find((c) => c.code === code)?.credentials; + setLastResult({ + code, + action: 'load', + status: 'ok', + summary, + credentials: credsFromSummary ?? credsFromConfig, + }); } catch (err: any) { setLastResult({ code, action: 'load', status: 'error', error: err.response?.data?.detail || String(err) }); } finally { @@ -110,6 +130,9 @@ export const AdminDemoConfigPage: React.FC = () => { ) : ( {lastResult.error} )} + {lastResult.status === 'ok' && lastResult.action === 'load' && lastResult.credentials && lastResult.credentials.length > 0 && ( + <_CredentialsBox credentials={lastResult.credentials} /> + )} )} @@ -126,6 +149,9 @@ export const AdminDemoConfigPage: React.FC = () => {

{cfg.label}

{cfg.description}

{cfg.code} + {cfg.credentials && cfg.credentials.length > 0 && ( + <_CredentialsBox credentials={cfg.credentials} compact /> + )}
+
+
+ {t('Passwort')}: + {pwd} + +
+ {cred.email && ( +
+ {t('E-Mail')}: + {cred.email} +
+ )} + + ); + })} + + ); +}; diff --git a/src/pages/admin/AdminLanguagesPage.tsx b/src/pages/admin/AdminLanguagesPage.tsx index beb771d..5e74919 100644 --- a/src/pages/admin/AdminLanguagesPage.tsx +++ b/src/pages/admin/AdminLanguagesPage.tsx @@ -77,7 +77,13 @@ function _getColumns(t: (key: string) => string): ColumnConfig[] { ]; } -const _PRIORITY_CODES = ['de', 'gsw', 'en', 'fr', 'it']; +// ISO 639 catalog (codes + native labels + priority order) is provided by the +// gateway via GET /api/i18n/iso-choices. We must NOT keep a local copy here -- +// any divergence between frontend and backend caused subtle bugs (e.g. user +// could create a language code that the AI translation prompt did not know how +// to label). The catalog is fetched once on mount and held in component state. +type IsoChoice = { value: string; label: string }; +type IsoCatalogResponse = { priorityCodes: string[]; choices: IsoChoice[] }; function _isAbortError(e: unknown): boolean { if (axios.isCancel(e)) return true; @@ -88,55 +94,6 @@ function _isAbortError(e: unknown): boolean { return false; } -const _isoChoices: { value: string; label: string }[] = [ - { value: 'de', label: 'de — Deutsch' }, - { value: 'gsw', label: 'gsw — Schweizerdeutsch' }, - { value: 'en', label: 'en — English' }, - { value: 'fr', label: 'fr — Français' }, { value: 'it', label: 'it — Italiano' }, - { value: 'es', label: 'es — Español' }, { value: 'pt', label: 'pt — Português' }, - { value: 'nl', label: 'nl — Nederlands' }, { value: 'pl', label: 'pl — Polski' }, - { value: 'cs', label: 'cs — Čeština' }, { value: 'sk', label: 'sk — Slovenčina' }, - { value: 'sv', label: 'sv — Svenska' }, { value: 'no', label: 'no — Norsk' }, - { value: 'da', label: 'da — Dansk' }, { value: 'fi', label: 'fi — Suomi' }, - { value: 'hu', label: 'hu — Magyar' }, { value: 'ro', label: 'ro — Română' }, - { value: 'bg', label: 'bg — Български' }, { value: 'hr', label: 'hr — Hrvatski' }, - { value: 'sl', label: 'sl — Slovenščina' }, { value: 'et', label: 'et — Eesti' }, - { value: 'lv', label: 'lv — Latviešu' }, { value: 'lt', label: 'lt — Lietuvių' }, - { value: 'el', label: 'el — Ελληνικά' }, { value: 'tr', label: 'tr — Türkçe' }, - { value: 'ru', label: 'ru — Русский' }, { value: 'uk', label: 'uk — Українська' }, - { value: 'ar', label: 'ar — العربية' }, { value: 'he', label: 'he — עברית' }, - { value: 'zh', label: 'zh — 中文' }, { value: 'ja', label: 'ja — 日本語' }, - { value: 'ko', label: 'ko — 한국어' }, { value: 'hi', label: 'hi — हिन्दी' }, - { value: 'th', label: 'th — ไทย' }, { value: 'vi', label: 'vi — Tiếng Việt' }, - { value: 'id', label: 'id — Bahasa Indonesia' }, { value: 'ms', label: 'ms — Bahasa Melayu' }, - { value: 'tl', label: 'tl — Filipino' }, { value: 'sw', label: 'sw — Kiswahili' }, - { value: 'af', label: 'af — Afrikaans' }, { value: 'sq', label: 'sq — Shqip' }, - { value: 'am', label: 'am — አማርኛ' }, { value: 'hy', label: 'hy — Հայերեն' }, - { value: 'az', label: 'az — Azərbaycan' }, { value: 'eu', label: 'eu — Euskara' }, - { value: 'be', label: 'be — Беларуская' }, { value: 'bn', label: 'bn — বাংলা' }, - { value: 'bs', label: 'bs — Bosanski' }, { value: 'ca', label: 'ca — Català' }, - { value: 'cy', label: 'cy — Cymraeg' }, { value: 'eo', label: 'eo — Esperanto' }, - { value: 'fa', label: 'fa — فارسی' }, { value: 'ga', label: 'ga — Gaeilge' }, - { value: 'gl', label: 'gl — Galego' }, { value: 'gu', label: 'gu — ગુજરાતી' }, - { value: 'ha', label: 'ha — Hausa' }, { value: 'is', label: 'is — Íslenska' }, - { value: 'jv', label: 'jv — Basa Jawa' }, { value: 'ka', label: 'ka — ქართული' }, - { value: 'kk', label: 'kk — Қазақ' }, { value: 'km', label: 'km — ខ្មែរ' }, - { value: 'kn', label: 'kn — ಕನ್ನಡ' }, { value: 'ku', label: 'ku — Kurdî' }, - { value: 'ky', label: 'ky — Кыргызча' }, { value: 'la', label: 'la — Latina' }, - { value: 'lb', label: 'lb — Lëtzebuergesch' }, { value: 'lo', label: 'lo — ລາວ' }, - { value: 'mk', label: 'mk — Македонски' }, { value: 'ml', label: 'ml — മലയാളം' }, - { value: 'mn', label: 'mn — Монгол' }, { value: 'mr', label: 'mr — मराठी' }, - { value: 'mt', label: 'mt — Malti' }, { value: 'my', label: 'my — မြန်မာ' }, - { value: 'ne', label: 'ne — नेपाली' }, { value: 'or', label: 'or — ଓଡ଼ିଆ' }, - { value: 'pa', label: 'pa — ਪੰਜਾਬੀ' }, { value: 'ps', label: 'ps — پښتو' }, - { value: 'si', label: 'si — සිංහල' }, { value: 'so', label: 'so — Soomaali' }, - { value: 'sr', label: 'sr — Српски' }, { value: 'su', label: 'su — Basa Sunda' }, - { value: 'ta', label: 'ta — தமிழ்' }, { value: 'te', label: 'te — తెలుగు' }, - { value: 'tg', label: 'tg — Тоҷикӣ' }, { value: 'tk', label: 'tk — Türkmen' }, - { value: 'ur', label: 'ur — اردو' }, { value: 'uz', label: 'uz — Oʻzbek' }, - { value: 'yo', label: 'yo — Yorùbá' }, { value: 'zu', label: 'zu — isiZulu' }, -]; - // --------------------------------------------------------------------------- // Progress overlay component // --------------------------------------------------------------------------- @@ -327,9 +284,30 @@ export const AdminLanguagesPage: React.FC = () => { const [addCode, setAddCode] = useState(''); const [progress, setProgress] = useState(null); const [search, setSearch] = useState(''); + const [isoCatalog, setIsoCatalog] = useState({ priorityCodes: [], choices: [] }); const busyRef = useRef(false); const abortRef = useRef(null); + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await api.get('/api/i18n/iso-choices'); + if (cancelled) return; + const data = res.data as IsoCatalogResponse; + setIsoCatalog({ + priorityCodes: Array.isArray(data?.priorityCodes) ? data.priorityCodes : [], + choices: Array.isArray(data?.choices) ? data.choices : [], + }); + } catch (e) { + console.error('Failed to load ISO language catalog from /api/i18n/iso-choices:', e); + } + })(); + return () => { + cancelled = true; + }; + }, []); + const _endProgressSoon = useCallback((ms: number) => { window.setTimeout(() => { setProgress(null); @@ -418,17 +396,18 @@ export const AdminLanguagesPage: React.FC = () => { const existingCodes = useMemo(() => new Set(rows.map((r) => r.id)), [rows]); const addChoices = useMemo(() => { - const available = _isoChoices.filter((c) => !existingCodes.has(c.value)); + const available = isoCatalog.choices.filter((c) => !existingCodes.has(c.value)); + const priority = isoCatalog.priorityCodes; available.sort((a, b) => { - const aPrio = _PRIORITY_CODES.indexOf(a.value); - const bPrio = _PRIORITY_CODES.indexOf(b.value); + const aPrio = priority.indexOf(a.value); + const bPrio = priority.indexOf(b.value); if (aPrio !== -1 && bPrio !== -1) return aPrio - bPrio; if (aPrio !== -1) return -1; if (bPrio !== -1) return 1; return a.label.localeCompare(b.label); }); return available; - }, [existingCodes]); + }, [existingCodes, isoCatalog]); useEffect(() => { if (addChoices.length > 0 && (!addCode || !addChoices.find((c) => c.value === addCode))) { diff --git a/src/pages/views/redmine/RedmineStatsView.tsx b/src/pages/views/redmine/RedmineStatsView.tsx index e8cbfb7..d98f1fe 100644 --- a/src/pages/views/redmine/RedmineStatsView.tsx +++ b/src/pages/views/redmine/RedmineStatsView.tsx @@ -273,6 +273,7 @@ export const RedmineStatsView: React.FC = () => { direction: 'past', defaultPresetKind: 'thisQuarter', enabledPresets: [ + 'allTime', 'thisMonth', 'lastMonth', 'thisQuarter', 'lastQuarter', 'ytd', 'lastYear', 'last12Months', 'lastN', 'custom', ], @@ -332,8 +333,15 @@ export const RedmineStatsView: React.FC = () => { const _handleFilterChange = useCallback((filterState: ReportFilterState) => { if (filterState.periodValue) { - setDateFrom(filterState.periodValue.fromDate); - setDateTo(filterState.periodValue.toDate); + // "Alle" = no date filter. Drop the sentinel range so the backend + // aggregates over the full history instead of clamping to 1970--2999. + if (filterState.periodValue.preset.kind === 'allTime') { + setDateFrom(undefined); + setDateTo(undefined); + } else { + setDateFrom(filterState.periodValue.fromDate); + setDateTo(filterState.periodValue.toDate); + } } else if (filterState.dateRange) { setDateFrom(toIsoDate(filterState.dateRange.from)); setDateTo(toIsoDate(filterState.dateRange.to)); diff --git a/src/pages/views/trustee/TrusteeDocumentsView.tsx b/src/pages/views/trustee/TrusteeDocumentsView.tsx index bb3b5df..90ca654 100644 --- a/src/pages/views/trustee/TrusteeDocumentsView.tsx +++ b/src/pages/views/trustee/TrusteeDocumentsView.tsx @@ -1,8 +1,15 @@ /** * TrusteeDocumentsView - * + * * Dokument-Verwaltung für eine Trustee-Instanz. * Verwendet FormGeneratorTable für konsistentes UI. + * + * NOTE: Mounted only as a tab inside `TrusteeDataTablesView` (Tab `documents` + * unter `/mandates/{m}/trustee/{i}/data-tables?tab=documents`). Es gibt keine + * eigenständige Top-Level-Route mehr (`/trustee/{i}/documents` wurde entfernt + * -- siehe `wiki/c-work/4-done/2026-04-trustee-cleanup-positions-documents.md`). + * Direkt-Import durch `TrusteeDataTablesView`; kein Re-Export über + * `views/trustee/index.ts`. */ import React, { useState, useMemo, useEffect } from 'react'; diff --git a/src/pages/views/trustee/TrusteePositionsView.tsx b/src/pages/views/trustee/TrusteePositionsView.tsx index 4ee6626..3fedbf7 100644 --- a/src/pages/views/trustee/TrusteePositionsView.tsx +++ b/src/pages/views/trustee/TrusteePositionsView.tsx @@ -1,8 +1,15 @@ /** * TrusteePositionsView - * + * * Positions-Verwaltung für eine Trustee-Instanz. * Verwendet FormGeneratorTable für konsistentes UI. + * + * NOTE: Mounted only as a tab inside `TrusteeDataTablesView` (Tab `positions` + * unter `/mandates/{m}/trustee/{i}/data-tables?tab=positions`). Es gibt keine + * eigenständige Top-Level-Route mehr (`/trustee/{i}/positions` wurde entfernt + * -- siehe `wiki/c-work/4-done/2026-04-trustee-cleanup-positions-documents.md`). + * Direkt-Import durch `TrusteeDataTablesView`; kein Re-Export über + * `views/trustee/index.ts`. */ import React, { useState, useMemo, useEffect, useCallback } from 'react'; diff --git a/src/pages/views/trustee/index.ts b/src/pages/views/trustee/index.ts index 625ce78..2cbeafa 100644 --- a/src/pages/views/trustee/index.ts +++ b/src/pages/views/trustee/index.ts @@ -1,10 +1,15 @@ /** * Trustee Views Export + * + * NOTE: `TrusteePositionsView` und `TrusteeDocumentsView` werden hier + * ABSICHTLICH NICHT mehr re-exportiert. Beide Komponenten sind nur noch + * Tab-Bodies innerhalb von `TrusteeDataTablesView` und werden dort per + * Direkt-Import (`./TrusteePositionsView`, `./TrusteeDocumentsView`) geladen. + * Externe Importe ueber dieses Index-File sind nicht mehr unterstuetzt -- + * siehe `wiki/c-work/4-done/2026-04-trustee-cleanup-positions-documents.md`. */ export { TrusteeDashboardView } from './TrusteeDashboardView'; -export { TrusteeDocumentsView } from './TrusteeDocumentsView'; -export { TrusteePositionsView } from './TrusteePositionsView'; export { TrusteeInstanceRolesView } from './TrusteeInstanceRolesView'; export { TrusteeExpenseImportView } from './TrusteeExpenseImportView'; export { TrusteeScanUploadView } from './TrusteeScanUploadView';