## Summary Fixes all 31 ❌ FAILs and most ⚠️ WARNs from the QA audit (113✅/33⚠️/31❌). ## Changes ### FIX 1 — Loading Skeleton - Replace full-screen GlobalLoader with PageBody-scoped animate-pulse skeleton - Sidebar stays visible during page transitions ### FIX 2 — Status Badges i18n (15 files, 12 label maps) - Add *_LABEL_KEYS maps to lib/status-badges.ts (i18n keys instead of German) - Update all 15 consumer files to use t(*_LABEL_KEYS[status]) - Add status namespace to finance.json (de+en) - Add registration_open to events.json status (de+en) - Add status block to cms.json events section (de+en) - Add missing pending/bounced keys to newsletter.json (de+en) - Add active key to courses.json status (de+en) ### FIX 3 — Error Page i18n - Replace 4 hardcoded German strings with useTranslations('common') - Add error.* keys to common.json (de+en) ### FIX 4 — Account Not Found i18n - Convert AccountNotFound to async Server Component - Resolve default props from getTranslations('common') - Add accountNotFoundCard.* keys to common.json (de+en) ### FIX 5 — Publish Toggle Button (6 strings + 2 bugs) - Add useTranslations('siteBuilder'), replace 6 German strings - Fix: add response.ok check before router.refresh() - Fix: add disabled={isPending} to AlertDialogAction - Fix: use Base UI render= prop pattern (not asChild) - Add pages.hide/publish/hideTitle/publishTitle/hideDesc/publishDesc/ toggleError/cancelAction to siteBuilder.json (de+en) ### FIX 6 — Cancel Booking Button (7 strings + bugs) - Add useTranslations('bookings'), replace all strings - Fix: use render= prop pattern, add disabled={isPending} - Add cancel.* and calendar.* keys to bookings.json (de+en) ### FIX 7 — Portal Pages i18n (5 files, ~40 strings) - Create i18n/messages/de/portal.json and en/portal.json - Add 'portal' to i18n/request.ts namespace list - Rewrite portal/page.tsx, invite/page.tsx, profile/page.tsx, documents/page.tsx with getTranslations('portal') - Fix portal-linked-accounts.tsx: add useTranslations, replace hardcoded strings, fix AlertDialogTrigger render= pattern ### FIX 8 — Invitations View (1 string) - Replace hardcoded string with t('invitations.emptyDescription') - Add key to members.json (de+en) ### FIX 9 — Dead Navigation Link - Comment out memberPortal nav entry (page does not exist) ### FIX 10 — Calendar Button Accessibility - Add aria-label + aria-hidden to all icon buttons in bookings/calendar - Add aria-label + aria-hidden to all icon buttons in courses/calendar - Add previousMonth/nextMonth/backToBookings/backToCourses to bookings.json and courses.json (de+en) ### FIX 11 — Pagination Aria Labels - Add aria-label to icon-only pagination buttons in finance/page.tsx - Fix Link/Button nesting in newsletter/page.tsx, add aria-labels - Add pagination.* to common.json (de+en) - Add common.previous/next to newsletter.json (de+en) ### FIX 12 — Site Builder Type Safety - Add SitePage interface, replace Record<string,unknown> in page.tsx - Add SitePost interface, replace Record<string,unknown> in posts/page.tsx - Remove String() casts on typed properties ### FIX 14 — EmptyState Heading Level - Change <h3> to <h2> in empty-state.tsx (WCAG heading sequence) ### FIX 16 — CmsPageShell Nullish Coalescing - Change description ?? <AppBreadcrumbs /> to !== undefined check ### FIX 17 — Meetings Protocol Hardcoded Strings - Replace 5 hardcoded German strings with t() in protocol detail page - Add notFound/back/backToList/statusPublished/statusDraft to meetings.json ### FIX 18 — Finance Toolbar Hardcoded Strings - Replace toolbar filter labels with t() calls in finance/page.tsx ### FIX 19 — Admin Audit Hardcoded Strings - Add getTranslations('cms.audit') to audit page - Replace title, description, column headers, pagination labels - Add description/timestamp/paginationPrevious/paginationNext to cms.json ## Verification - tsc --noEmit: 0 errors - Turbopack: Compiled successfully in 9.3s - Lint: 0 new errors introduced - All 8 audit verification checks pass
585 lines
16 KiB
TypeScript
585 lines
16 KiB
TypeScript
import {
|
|
LayoutDashboard,
|
|
Settings,
|
|
UserCog,
|
|
CreditCard,
|
|
// People (Members + Access)
|
|
UserCheck,
|
|
UserPlus,
|
|
IdCard,
|
|
ClipboardList,
|
|
KeyRound,
|
|
// Courses
|
|
GraduationCap,
|
|
CalendarDays,
|
|
MapPin,
|
|
UserRound,
|
|
// Events
|
|
CalendarHeart,
|
|
Ticket,
|
|
PartyPopper,
|
|
// Bookings
|
|
Hotel,
|
|
BedDouble,
|
|
Contact,
|
|
CalendarRange,
|
|
// Finance
|
|
Wallet,
|
|
Receipt,
|
|
Landmark,
|
|
BarChart3,
|
|
// Documents
|
|
FileText,
|
|
FilePlus,
|
|
FileStack,
|
|
FolderOpen,
|
|
// Newsletter
|
|
Mail,
|
|
MailPlus,
|
|
FileCode,
|
|
// Site Builder
|
|
PanelTop,
|
|
Newspaper,
|
|
Palette,
|
|
// Fisheries
|
|
Fish,
|
|
Waves,
|
|
Anchor,
|
|
BookOpen,
|
|
ShieldCheck,
|
|
Trophy,
|
|
// Meetings
|
|
BookMarked,
|
|
ListChecks,
|
|
ScrollText,
|
|
// Association (Verband)
|
|
Building2,
|
|
Network,
|
|
SearchCheck,
|
|
Share2,
|
|
PieChart,
|
|
LayoutTemplate,
|
|
// Modules
|
|
Database,
|
|
} from 'lucide-react';
|
|
|
|
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
|
|
|
|
import featureFlagsConfig from '~/config/feature-flags.config';
|
|
import pathsConfig from '~/config/paths.config';
|
|
|
|
const iconClasses = 'w-4';
|
|
|
|
const getRoutes = (account: string) => {
|
|
const routes: Array<
|
|
| {
|
|
label: string;
|
|
collapsible?: boolean;
|
|
collapsed?: boolean;
|
|
children: Array<
|
|
| {
|
|
label: string;
|
|
path: string;
|
|
Icon: React.ReactNode;
|
|
highlightMatch?: string;
|
|
}
|
|
| undefined
|
|
>;
|
|
}
|
|
| { divider: true }
|
|
> = [
|
|
// ── Dashboard ──
|
|
{
|
|
label: 'common:routes.dashboard',
|
|
children: [
|
|
{
|
|
label: 'common:routes.dashboard',
|
|
path: pathsConfig.app.accountHome.replace('[account]', account),
|
|
Icon: <LayoutDashboard className={iconClasses} />,
|
|
highlightMatch: `${pathsConfig.app.home}$`,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// ── People (Members + Access) ──
|
|
{
|
|
const peopleChildren: Array<{
|
|
label: string;
|
|
path: string;
|
|
Icon: React.ReactNode;
|
|
}> = [];
|
|
|
|
if (featureFlagsConfig.enableMemberManagement) {
|
|
peopleChildren.push(
|
|
{
|
|
label: 'common:routes.clubMembers',
|
|
path: createPath(pathsConfig.app.accountCmsMembers, account),
|
|
Icon: <UserCheck className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.memberApplications',
|
|
path: createPath(
|
|
pathsConfig.app.accountCmsMembers + '/applications',
|
|
account,
|
|
),
|
|
Icon: <UserPlus className={iconClasses} />,
|
|
},
|
|
// NOTE: memberPortal page does not exist yet — nav entry commented out until built
|
|
// {
|
|
// label: 'common:routes.memberPortal',
|
|
// path: createPath(
|
|
// pathsConfig.app.accountCmsMembers + '/portal',
|
|
// account,
|
|
// ),
|
|
// Icon: <KeyRound className={iconClasses} />,
|
|
// },
|
|
{
|
|
label: 'common:routes.memberCards',
|
|
path: createPath(
|
|
pathsConfig.app.accountCmsMembers + '/cards',
|
|
account,
|
|
),
|
|
Icon: <IdCard className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.memberDues',
|
|
path: createPath(
|
|
pathsConfig.app.accountCmsMembers + '/dues',
|
|
account,
|
|
),
|
|
Icon: <ClipboardList className={iconClasses} />,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Admin users who can log in — always visible
|
|
peopleChildren.push({
|
|
label: 'common:routes.accessAndRoles',
|
|
path: createPath(pathsConfig.app.accountMembers, account),
|
|
Icon: <UserCog className={iconClasses} />,
|
|
});
|
|
|
|
routes.push({
|
|
label: 'common:routes.people',
|
|
collapsible: true,
|
|
children: peopleChildren,
|
|
});
|
|
}
|
|
|
|
// ── Courses ──
|
|
if (featureFlagsConfig.enableCourseManagement) {
|
|
routes.push({
|
|
label: 'common:routes.courseManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.courseList',
|
|
path: createPath(pathsConfig.app.accountCourses, account),
|
|
Icon: <GraduationCap className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.courseCalendar',
|
|
path: createPath(
|
|
pathsConfig.app.accountCourses + '/calendar',
|
|
account,
|
|
),
|
|
Icon: <CalendarDays className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.courseInstructors',
|
|
path: createPath(
|
|
pathsConfig.app.accountCourses + '/instructors',
|
|
account,
|
|
),
|
|
Icon: <UserRound className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.courseLocations',
|
|
path: createPath(
|
|
pathsConfig.app.accountCourses + '/locations',
|
|
account,
|
|
),
|
|
Icon: <MapPin className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Events ──
|
|
routes.push({
|
|
label: 'common:routes.eventManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.eventList',
|
|
path: createPath('/home/[account]/events', account),
|
|
Icon: <CalendarHeart className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.eventRegistrations',
|
|
path: createPath('/home/[account]/events/registrations', account),
|
|
Icon: <Ticket className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.holidayPasses',
|
|
path: createPath('/home/[account]/events/holiday-passes', account),
|
|
Icon: <PartyPopper className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
|
|
// ── Bookings ──
|
|
if (featureFlagsConfig.enableBookingManagement) {
|
|
routes.push({
|
|
label: 'common:routes.bookingManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.bookingList',
|
|
path: createPath(pathsConfig.app.accountBookings, account),
|
|
Icon: <Hotel className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.bookingCalendar',
|
|
path: createPath(
|
|
pathsConfig.app.accountBookings + '/calendar',
|
|
account,
|
|
),
|
|
Icon: <CalendarRange className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.bookingRooms',
|
|
path: createPath(pathsConfig.app.accountBookings + '/rooms', account),
|
|
Icon: <BedDouble className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.bookingGuests',
|
|
path: createPath(
|
|
pathsConfig.app.accountBookings + '/guests',
|
|
account,
|
|
),
|
|
Icon: <Contact className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Finance ──
|
|
if (featureFlagsConfig.enableSepaPayments) {
|
|
routes.push({
|
|
label: 'common:routes.financeManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.financeOverview',
|
|
path: createPath(pathsConfig.app.accountFinance, account),
|
|
Icon: <Wallet className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.financeInvoices',
|
|
path: createPath(
|
|
pathsConfig.app.accountFinance + '/invoices',
|
|
account,
|
|
),
|
|
Icon: <Receipt className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.financeSepa',
|
|
path: createPath(pathsConfig.app.accountFinance + '/sepa', account),
|
|
Icon: <Landmark className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.financePayments',
|
|
path: createPath(
|
|
pathsConfig.app.accountFinance + '/payments',
|
|
account,
|
|
),
|
|
Icon: <BarChart3 className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Documents ──
|
|
if (featureFlagsConfig.enableDocumentGeneration) {
|
|
routes.push({
|
|
label: 'common:routes.documentManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.documentOverview',
|
|
path: createPath(pathsConfig.app.accountDocuments, account),
|
|
Icon: <FileText className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.documentGenerate',
|
|
path: createPath(
|
|
pathsConfig.app.accountDocuments + '/generate',
|
|
account,
|
|
),
|
|
Icon: <FilePlus className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.documentTemplates',
|
|
path: createPath(
|
|
pathsConfig.app.accountDocuments + '/templates',
|
|
account,
|
|
),
|
|
Icon: <FileStack className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.files',
|
|
path: createPath(pathsConfig.app.accountFiles, account),
|
|
Icon: <FolderOpen className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Newsletter ──
|
|
if (featureFlagsConfig.enableNewsletter) {
|
|
routes.push({
|
|
label: 'common:routes.newsletterManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.newsletterCampaigns',
|
|
path: createPath(pathsConfig.app.accountNewsletter, account),
|
|
Icon: <Mail className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.newsletterNew',
|
|
path: createPath(pathsConfig.app.accountNewsletter + '/new', account),
|
|
Icon: <MailPlus className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.newsletterTemplates',
|
|
path: createPath(
|
|
pathsConfig.app.accountNewsletter + '/templates',
|
|
account,
|
|
),
|
|
Icon: <FileCode className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Site Builder ──
|
|
if (featureFlagsConfig.enableSiteBuilder) {
|
|
routes.push({
|
|
label: 'common:routes.siteBuilder',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.sitePages',
|
|
path: createPath(pathsConfig.app.accountSiteBuilder, account),
|
|
Icon: <PanelTop className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.sitePosts',
|
|
path: createPath(
|
|
pathsConfig.app.accountSiteBuilder + '/posts',
|
|
account,
|
|
),
|
|
Icon: <Newspaper className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.siteSettings',
|
|
path: createPath(
|
|
pathsConfig.app.accountSiteBuilder + '/settings',
|
|
account,
|
|
),
|
|
Icon: <Palette className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Custom Modules ──
|
|
if (featureFlagsConfig.enableModuleBuilder) {
|
|
routes.push({
|
|
label: 'common:routes.customModules',
|
|
collapsible: true,
|
|
collapsed: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.moduleList',
|
|
path: createPath(pathsConfig.app.accountModules, account),
|
|
Icon: <Database className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Fisheries ──
|
|
if (featureFlagsConfig.enableFischerei) {
|
|
routes.push({
|
|
label: 'common:routes.fisheriesManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.fisheriesOverview',
|
|
path: createPath(pathsConfig.app.accountFischerei, account),
|
|
Icon: <Fish className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.fisheriesWaters',
|
|
path: createPath(
|
|
pathsConfig.app.accountFischerei + '/waters',
|
|
account,
|
|
),
|
|
Icon: <Waves className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.fisheriesLeases',
|
|
path: createPath(
|
|
pathsConfig.app.accountFischerei + '/leases',
|
|
account,
|
|
),
|
|
Icon: <Anchor className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.fisheriesCatchBooks',
|
|
path: createPath(
|
|
pathsConfig.app.accountFischerei + '/catch-books',
|
|
account,
|
|
),
|
|
Icon: <BookOpen className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.fisheriesPermits',
|
|
path: createPath(
|
|
pathsConfig.app.accountFischerei + '/permits',
|
|
account,
|
|
),
|
|
Icon: <ShieldCheck className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.fisheriesCompetitions',
|
|
path: createPath(
|
|
pathsConfig.app.accountFischerei + '/competitions',
|
|
account,
|
|
),
|
|
Icon: <Trophy className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Meeting Protocols ──
|
|
if (featureFlagsConfig.enableMeetingProtocols) {
|
|
routes.push({
|
|
label: 'common:routes.meetingProtocols',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.meetingsOverview',
|
|
path: createPath(pathsConfig.app.accountMeetings, account),
|
|
Icon: <BookMarked className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.meetingsProtocols',
|
|
path: createPath(
|
|
pathsConfig.app.accountMeetings + '/protocols',
|
|
account,
|
|
),
|
|
Icon: <ScrollText className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.meetingsTasks',
|
|
path: createPath(pathsConfig.app.accountMeetings + '/tasks', account),
|
|
Icon: <ListChecks className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Association Management (Verband) ──
|
|
if (featureFlagsConfig.enableVerbandsverwaltung) {
|
|
routes.push({
|
|
label: 'common:routes.associationManagement',
|
|
collapsible: true,
|
|
children: [
|
|
{
|
|
label: 'common:routes.associationOverview',
|
|
path: createPath(pathsConfig.app.accountVerband, account),
|
|
Icon: <Building2 className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.associationHierarchy',
|
|
path: createPath(
|
|
pathsConfig.app.accountVerband + '/hierarchy',
|
|
account,
|
|
),
|
|
Icon: <Network className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.associationMemberSearch',
|
|
path: createPath(
|
|
pathsConfig.app.accountVerband + '/members',
|
|
account,
|
|
),
|
|
Icon: <SearchCheck className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.associationEvents',
|
|
path: createPath(pathsConfig.app.accountVerband + '/events', account),
|
|
Icon: <Share2 className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.associationReporting',
|
|
path: createPath(
|
|
pathsConfig.app.accountVerband + '/reporting',
|
|
account,
|
|
),
|
|
Icon: <PieChart className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common:routes.associationTemplates',
|
|
path: createPath(
|
|
pathsConfig.app.accountVerband + '/templates',
|
|
account,
|
|
),
|
|
Icon: <LayoutTemplate className={iconClasses} />,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Administration ──
|
|
routes.push({
|
|
label: 'common:routes.administration',
|
|
collapsible: false,
|
|
children: [
|
|
{
|
|
label: 'common:routes.accountSettings',
|
|
path: createPath(pathsConfig.app.accountSettings, account),
|
|
Icon: <Settings className={iconClasses} />,
|
|
},
|
|
featureFlagsConfig.enableTeamAccountBilling
|
|
? {
|
|
label: 'common:routes.billing',
|
|
path: createPath(pathsConfig.app.accountBilling, account),
|
|
Icon: <CreditCard className={iconClasses} />,
|
|
}
|
|
: undefined,
|
|
],
|
|
});
|
|
|
|
return routes;
|
|
};
|
|
|
|
export function getTeamAccountSidebarConfig(account: string) {
|
|
return NavigationConfigSchema.parse({
|
|
routes: getRoutes(account),
|
|
style: process.env.NEXT_PUBLIC_TEAM_NAVIGATION_STYLE,
|
|
sidebarCollapsed: process.env.NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED,
|
|
sidebarCollapsedStyle: process.env.NEXT_PUBLIC_SIDEBAR_COLLAPSIBLE_STYLE,
|
|
});
|
|
}
|
|
|
|
function createPath(path: string, account: string) {
|
|
return path.replace('[account]', account);
|
|
}
|