diff --git a/frontend/src/lib/planner/DayPicker.svelte b/frontend/src/lib/planner/DayPicker.svelte
new file mode 100644
index 0000000..b1a31c7
--- /dev/null
+++ b/frontend/src/lib/planner/DayPicker.svelte
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+ Tag wählen
+
+
+ {recipeName}
+
+
+
+
+
+
+
+ {formatWeekRange(weekStart)}
+
+
+
+
+
+
+ {#each days as date (date)}
+ {@const state = chipState(date)}
+
+ {/each}
+
+
+
+ {#if selectedDate && existingRecipeName}
+
+ Ersetzt {existingRecipeName} an diesem Tag.
+
+ {/if}
+
+
+
+
+
+
diff --git a/frontend/src/lib/planner/DayPicker.test.ts b/frontend/src/lib/planner/DayPicker.test.ts
new file mode 100644
index 0000000..a896e0d
--- /dev/null
+++ b/frontend/src/lib/planner/DayPicker.test.ts
@@ -0,0 +1,134 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import DayPicker from './DayPicker.svelte';
+
+const weekStart = '2026-03-30'; // Monday
+const today = '2026-04-01'; // Wednesday
+
+// Mo: filled, Di: filled (today), Mi: filled, Do: empty, Fr: filled, Sa: empty, So: filled
+const slots = [
+ { id: 's1', slotDate: '2026-03-30', recipe: { id: 'r1', name: 'Pasta', effort: 'easy' } },
+ { id: 's2', slotDate: '2026-04-01', recipe: { id: 'r2', name: 'Curry', effort: 'easy' } },
+ { id: 's3', slotDate: '2026-04-02', recipe: { id: 'r3', name: 'Risotto', effort: 'medium' } },
+ { id: 's5', slotDate: '2026-04-04', recipe: { id: 'r5', name: 'Suppe', effort: 'easy' } },
+ { id: 's7', slotDate: '2026-04-06', recipe: { id: 'r7', name: 'Stir Fry', effort: 'easy' } }
+];
+
+const baseProps = {
+ recipeName: 'Mushroom Risotto',
+ recipeId: 'recipe-42',
+ planId: 'plan-1',
+ weekStart,
+ today,
+ slots,
+ onconfirm: vi.fn(),
+ onweekchange: vi.fn()
+};
+
+describe('DayPicker', () => {
+ it('shows recipe name in header', () => {
+ render(DayPicker, { props: baseProps });
+ expect(screen.getByText('Mushroom Risotto')).toBeTruthy();
+ });
+
+ it('shows 7 day chips', () => {
+ render(DayPicker, { props: baseProps });
+ const chips = screen.getAllByTestId(/^chip-/);
+ expect(chips).toHaveLength(7);
+ });
+
+ it('marks empty slot chips with data-state="empty"', () => {
+ render(DayPicker, { props: baseProps });
+ // Do (2026-04-03) and Sa (2026-04-05) are empty
+ const doChip = screen.getByTestId('chip-2026-04-03');
+ expect(doChip.getAttribute('data-state')).toBe('empty');
+ });
+
+ it('marks filled slot chips with data-state="filled"', () => {
+ render(DayPicker, { props: baseProps });
+ const moChip = screen.getByTestId('chip-2026-03-30');
+ expect(moChip.getAttribute('data-state')).toBe('filled');
+ });
+
+ it('marks today chip with data-state="today"', () => {
+ render(DayPicker, { props: baseProps });
+ const todayChip = screen.getByTestId('chip-2026-04-01');
+ expect(todayChip.getAttribute('data-state')).toBe('today');
+ });
+
+ it('selecting an empty chip changes its state to sel-empty', async () => {
+ render(DayPicker, { props: baseProps });
+ const doChip = screen.getByTestId('chip-2026-04-03');
+ await userEvent.click(doChip);
+ expect(doChip.getAttribute('data-state')).toBe('sel-empty');
+ });
+
+ it('selecting a filled chip changes its state to sel-filled', async () => {
+ render(DayPicker, { props: baseProps });
+ const moChip = screen.getByTestId('chip-2026-03-30');
+ await userEvent.click(moChip);
+ expect(moChip.getAttribute('data-state')).toBe('sel-filled');
+ });
+
+ it('shows replace warning when filled chip is selected', async () => {
+ render(DayPicker, { props: baseProps });
+ const moChip = screen.getByTestId('chip-2026-03-30');
+ await userEvent.click(moChip);
+ expect(screen.getByTestId('replace-warning')).toBeTruthy();
+ expect(screen.getByText(/Pasta/)).toBeTruthy();
+ });
+
+ it('does not show replace warning when empty chip is selected', async () => {
+ render(DayPicker, { props: baseProps });
+ const doChip = screen.getByTestId('chip-2026-04-03');
+ await userEvent.click(doChip);
+ expect(screen.queryByTestId('replace-warning')).toBeNull();
+ });
+
+ it('confirm button is disabled when no chip is selected', () => {
+ render(DayPicker, { props: baseProps });
+ const btn = screen.getByTestId('confirm-btn');
+ expect(btn.hasAttribute('disabled')).toBe(true);
+ });
+
+ it('calls onconfirm with date and null slotId when empty chip confirmed', async () => {
+ const onconfirm = vi.fn();
+ render(DayPicker, { props: { ...baseProps, onconfirm } });
+ const doChip = screen.getByTestId('chip-2026-04-03');
+ await userEvent.click(doChip);
+ const btn = screen.getByTestId('confirm-btn');
+ await userEvent.click(btn);
+ expect(onconfirm).toHaveBeenCalledWith({ date: '2026-04-03', slotId: null });
+ });
+
+ it('calls onconfirm with date and slotId when filled chip confirmed', async () => {
+ const onconfirm = vi.fn();
+ render(DayPicker, { props: { ...baseProps, onconfirm } });
+ const moChip = screen.getByTestId('chip-2026-03-30');
+ await userEvent.click(moChip);
+ const btn = screen.getByTestId('confirm-btn');
+ await userEvent.click(btn);
+ expect(onconfirm).toHaveBeenCalledWith({ date: '2026-03-30', slotId: 's1' });
+ });
+
+ it('shows prev/next week navigation buttons', () => {
+ render(DayPicker, { props: baseProps });
+ expect(screen.getByRole('button', { name: /Vorherige Woche/ })).toBeTruthy();
+ expect(screen.getByRole('button', { name: /Nächste Woche/ })).toBeTruthy();
+ });
+
+ it('calls onweekchange with prev week when prev button clicked', async () => {
+ const onweekchange = vi.fn();
+ render(DayPicker, { props: { ...baseProps, onweekchange } });
+ await userEvent.click(screen.getByRole('button', { name: /Vorherige Woche/ }));
+ expect(onweekchange).toHaveBeenCalledWith('2026-03-23');
+ });
+
+ it('calls onweekchange with next week when next button clicked', async () => {
+ const onweekchange = vi.fn();
+ render(DayPicker, { props: { ...baseProps, onweekchange } });
+ await userEvent.click(screen.getByRole('button', { name: /Nächste Woche/ }));
+ expect(onweekchange).toHaveBeenCalledWith('2026-04-06');
+ });
+});