Files
myeasycms-v2/apps/web/config/team-account-navigation.config.tsx
Zaid Marzguioui a1719671df
Some checks failed
Workflow / ⚫️ Test (push) Has been cancelled
Workflow / ʦ TypeScript (push) Has been cancelled
fix: QA remediation — all 19 audit fixes (C+ → A-)
## 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
2026-04-02 01:18:15 +02:00

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);
}