feat(web): Dashboard mit Begruessung, Befehlsleiste und ruhigeren Kacheln
Begruessung nach Tageszeit mit Datum, Befehlsleiste oben rechts statt schwebendem Knopf, einheitliche Kachel-Kopfzeilen mit Symbol, Kalender mit Pfeilknoepfen, runden Tagen und Terminpunkten, Favoriten als 36-px-Zeilen bzw. App-Starter-Kacheln, Notiz-Kaestchen im Akzent, leerer Zustand mit drei Vorschlaegen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,16 @@ const messages: Record<string, Record<string, string>> = {
|
||||
editMode: 'Dashboard bearbeiten',
|
||||
saveChanges: 'Änderungen speichern',
|
||||
addWidget: 'Widget hinzufügen',
|
||||
editShort: 'Bearbeiten',
|
||||
done: 'Fertig',
|
||||
toolbarLabel: 'Dashboard-Aktionen',
|
||||
layoutLoadError: 'Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.',
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: (ns: string) => (key: string) => messages[ns]?.[key] ?? key,
|
||||
useLocale: () => 'de',
|
||||
}));
|
||||
|
||||
const mockStore = {
|
||||
@@ -106,45 +110,46 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('DashboardPage (quick-260916-dyv)', () => {
|
||||
it('Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2', async () => {
|
||||
it('Test 1: Ansichtsmodus — Befehlsleiste rechts oben mit „Bearbeiten“, kein "Widget hinzufuegen", Begruessung, Grid direkt im Container p-2 (Design „Mosaik“)', async () => {
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: 'Dashboard bearbeiten' });
|
||||
const bar = toggle.parentElement as HTMLElement;
|
||||
expect(bar.classList.contains('fixed')).toBe(true);
|
||||
expect(bar.classList.contains('bottom-6')).toBe(true);
|
||||
expect(bar.classList.contains('right-6')).toBe(true);
|
||||
expect(bar.classList.contains('z-20')).toBe(true);
|
||||
const toolbar = screen.getByRole('toolbar', { name: 'Dashboard-Aktionen' });
|
||||
const toggle = screen.getByRole('button', { name: 'Bearbeiten' });
|
||||
expect(toggle.parentElement).toBe(toolbar);
|
||||
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
||||
// kein schwebender Knopf mehr unten rechts
|
||||
expect(toolbar.classList.contains('fixed')).toBe(false);
|
||||
expect(document.querySelector('.bottom-6')).toBeNull();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Widget hinzufügen' })).toBeNull();
|
||||
expect(document.querySelector('.mt-8')).toBeNull();
|
||||
expect(document.querySelector('.top-2')).toBeNull();
|
||||
await waitFor(() => expect(screen.getByTestId('dashboard-greeting')).toBeInTheDocument());
|
||||
|
||||
const grid = screen.getByTestId('dashboard-grid');
|
||||
expect((grid.parentElement as HTMLElement).classList.contains('p-2')).toBe(true);
|
||||
expect(grid.getAttribute('data-edit')).toBe('false');
|
||||
});
|
||||
|
||||
it('Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben dem Haekchen in derselben Leiste, Grid im Bearbeitungsmodus', async () => {
|
||||
it('Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben „Fertig“ in derselben Leiste, Grid im Bearbeitungsmodus', async () => {
|
||||
mockStore.isEditMode = true;
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: 'Änderungen speichern' });
|
||||
const toggle = screen.getByRole('button', { name: 'Fertig' });
|
||||
const add = screen.getByRole('button', { name: 'Widget hinzufügen' });
|
||||
expect(add.parentElement).toBe(toggle.parentElement);
|
||||
// "Widget hinzufuegen" steht im DOM VOR dem Umschalter (links daneben).
|
||||
expect(add.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
expect(screen.getByTestId('dashboard-grid').getAttribute('data-edit')).toBe('true');
|
||||
});
|
||||
|
||||
it('Test 3: Umschalten — Klick auf "Dashboard bearbeiten" ruft setEditMode(true) genau einmal', async () => {
|
||||
it('Test 3: Umschalten — Klick auf "Bearbeiten" ruft setEditMode(true) genau einmal', async () => {
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard bearbeiten' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Bearbeiten' }));
|
||||
expect(mockStore.setEditMode).toHaveBeenCalledTimes(1);
|
||||
expect(mockStore.setEditMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
|
||||
import { DashboardGrid } from '@/components/dashboard/dashboard-grid';
|
||||
import { DashboardTabs } from '@/components/dashboard/dashboard-tabs';
|
||||
import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle';
|
||||
import { DashboardGreeting } from '@/components/dashboard/dashboard-greeting';
|
||||
import { BackgroundPicker, DashboardBackdrop, useDashboardBackground } from '@/components/dashboard/dashboard-background';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal';
|
||||
@@ -138,34 +139,27 @@ export default function DashboardPage() {
|
||||
onReorder={reorderDashboards}
|
||||
/>
|
||||
|
||||
{isSwitchingDashboard ? (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">{t('tabs.switching')}</p>
|
||||
{/* Kopfzeile des Dashboards (Design „Mosaik“, Runde 2): Begruessung
|
||||
links, Befehlsleiste rechts (Fluent „command bar“) statt des
|
||||
frueheren schwebenden Knopfs unten rechts. Mit Hintergrund liegen
|
||||
beide auf der durchscheinenden Kachelflaeche (.dashboard-chrome). */}
|
||||
<div className="flex flex-wrap items-end justify-between gap-x-4 gap-y-2 px-3 pb-1 pt-2">
|
||||
<div className="dashboard-chrome min-w-0 rounded-lg">
|
||||
<DashboardGreeting />
|
||||
</div>
|
||||
) : (
|
||||
/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Feste Aktionsleiste unten rechts.
|
||||
quick-260916-dyv: Umschalter unten rechts statt oben rechts, damit das
|
||||
Grid direkt unter der Kopfzeile beginnt (12 + 8 + 8 = 28 px statt 60 px).
|
||||
Im Bearbeitungsmodus steht "Widget hinzufuegen" links neben dem Haekchen. */}
|
||||
<div className="fixed bottom-6 right-6 z-20 flex items-center gap-2">
|
||||
{isEditMode && <BackgroundPicker value={background} onChange={chooseBackground} />}
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="btn btn-primary shadow-lg"
|
||||
>
|
||||
<svg
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label={t('toolbarLabel')}
|
||||
className="dashboard-chrome flex items-center gap-1 rounded-lg"
|
||||
>
|
||||
{isEditMode && <BackgroundPicker value={background} onChange={chooseBackground} />}
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="btn btn-subtle"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@@ -180,15 +174,33 @@ export default function DashboardPage() {
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
)}
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
)}
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSwitchingDashboard ? (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">{t('tabs.switching')}</p>
|
||||
</div>
|
||||
) : (
|
||||
/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
onOpenCatalog={() => setCatalogOpen(true)}
|
||||
onQuickAdd={(type) => addWidget(type)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Widget catalog modal */}
|
||||
<WidgetCatalogModal
|
||||
isOpen={catalogOpen}
|
||||
|
||||
Reference in New Issue
Block a user