diff --git a/CLAUDE.md b/CLAUDE.md index 98aba74..c2a9268 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,158 +1,50 @@ -# Haushalt — Aufgabenverwaltung +# Haushalt + +Basis-Software mit 3 geplanten Apps: Task Manager, Shopping List, Meal Planner. + +Aktueller Stand: **Setup module** (siehe `module.md`) — minimales Symfony + Vue Gerüst, kein Feature-Code. ## Tech-Stack | Schicht | Technologie | |---------|------------| -| Backend | Symfony 7.4, PHP 8.2+, Doctrine ORM 3.6 | -| Frontend Web | Vue 3 (Composition API), Vite 8, Pinia, Vue Router 4 | -| Datenbank | MySQL/MariaDB (utf8mb4) | +| Backend | Symfony 7.4, PHP 8.3, Doctrine ORM | +| Frontend Web | Vue 3 (Composition API), Vite, Pinia, Vue Router 4 | +| Frontend Mobile | Kotlin + Jetpack Compose (noch nicht aufgesetzt) | +| Datenbank | MariaDB 10.11 (utf8mb4) | | CORS | Nelmio CORS Bundle | | Umgebung | DDEV | -## Domain-Modell - -**Task** — Einzelne Aufgabe -- Felder: id, name, status, date, schema (nullable FK), category (nullable FK), createdAt -- TaskStatus: Active (`active`), Done (`done`) -- Tasks mit schema=null sind standalone (z.B. aus Single-Schemas erstellt) -- Tasks mit schema!=null gehören zu einem wiederkehrenden Schema - -**TaskSchema** — Template für wiederkehrende Aufgaben -- Felder: id, name, status, taskType, category, startDate, endDate, days, createdAt -- TaskSchemaStatus: Active (`active`), Disabled (`disabled`) -- TaskSchemaType: Single (`single`), Daily (`daily`), Custom (`custom`) -- `days`: JSON-Feld mit optionalen Keys: `week` (1-7), `month` (1-31), `year` ([{month, day}]) -- Single: Erstellt Tasks via YearPicker, Tasks werden detached (schema=null), Schema löscht sich automatisch -- Daily: Erstellt Tasks für jeden Tag im Zeitraum start–end -- Custom: Erstellt Tasks basierend auf days-Konfiguration im Zeitraum start–end -- Disabled: Generiert keine neuen Tasks, bestehende bleiben - -**Category** — Farbkodierte Kategorie -- Felder: id, name, color (Hex #RRGGBB) - -Enum-Case-Namen und DB-Werte sind Englisch. - -## Architektur (Backend) +## Struktur ``` -Controller → Service (Manager) → Repository → Entity - ↓ - DTO (Request) +backend/ + src/ + Kernel.php — Standard-Symfony-Kernel, keine App-Klassen + config/ — Symfony-Config (nelmio_cors, doctrine, framework, ...) + migrations/ — leer + public/index.php — Symfony-Einstieg +frontend/ + src/ + main.js — Vue-Init mit Pinia + Router + App.vue — RouterView, kein Content + router/index.js — Router mit leerem routes-Array + style.css — leer + index.html, vite.config.js ``` -**Prinzipien:** -- Controller: nur Routing + Response, keine Geschäftslogik -- Manager-Services: Geschäftslogik (CRUD, Validierung, Toggle) -- DTOs: typisierter Input (Request) -- Validierung: nur auf Request-DTOs (`#[Assert\...]`), nicht auf Entities -- Entities: Doctrine-Mapping + Getter/Setter, Serializer-Groups für API-Output -- Task-Generierung: Lazy beim Aufruf von GET /api/tasks (nicht per Cronjob) +## Dokumentation -## Verzeichnisstruktur (Backend) - -``` -backend/src/ - Controller/Api/ - CategoryController.php — Category CRUD - TaskController.php — Task CRUD + index + toggle - TaskSchemaController.php — Schema CRUD - DTO/ - Request/ — CreateSchemaRequest, UpdateSchemaRequest, CreateTaskRequest, - UpdateTaskRequest, CreateCategoryRequest, UpdateCategoryRequest, - SchemaValidationTrait - Entity/ - Category.php, Task.php, TaskSchema.php - Enum/ - TaskStatus.php, TaskSchemaStatus.php, TaskSchemaType.php - Repository/ - CategoryRepository.php, TaskRepository.php, TaskSchemaRepository.php - Service/ - CategoryManager.php — Category CRUD-Logik - TaskManager.php — Task Create/Update/Delete/Toggle - TaskSchemaManager.php — Schema CRUD + Single-Flow + Sync-Auslösung - TaskGenerator.php — Erzeugt Task-Instanzen für Zeiträume (lazy) - TaskSynchronizer.php — Sync nach Schema-Update (löschen/erstellen/reset) - DeadlineCalculator.php — Berechnet Fälligkeitsdaten für Daily/Custom-Schemas -``` - -## API-Routen - -### Tasks (`/api/tasks`) -``` -GET / — Alle Tasks (triggert Task-Generierung) -GET /{id} — Task-Details -POST / — Task direkt erstellen (schema=null) -PUT /{id} — Task aktualisieren -DELETE /{id} — Task löschen -PATCH /{id}/toggle — Status Active↔Done umschalten -``` - -### Schemas (`/api/schemas`) -``` -GET / — Alle Schemas -GET /{id} — Einzelnes Schema -POST / — Erstellen (Single: erstellt Tasks + löscht Schema) -PUT /{id} — Aktualisieren (synchronisiert zukünftige Tasks) -DELETE /{id} — Löschen (?deleteTasks=1 für Task-Löschung) -``` - -### Categories (`/api/categories`) -``` -GET / — Alle Kategorien -GET /{id} — Einzelne Kategorie -POST / — Erstellen -PUT /{id} — Aktualisieren -DELETE /{id} — Löschen -``` - -## Verzeichnisstruktur (Frontend) - -``` -frontend/src/ - views/ - HomeView.vue — Startseite, Wochenansicht (client-seitig berechnet) - AllTasksView.vue — Übersicht aller Schemas/Tasks - SchemaView.vue — Schema erstellen/bearbeiten (3 Typen) - TaskDetailView.vue — Einzelne Aufgabe bearbeiten - CategoriesView.vue — Kategorien verwalten - components/ - TaskCard.vue — Aufgaben-Karte (klickbar zum Toggle) - DayColumn.vue — Tages-Spalte in Wochenansicht - CategoryBadge.vue — Kategorie-Badge mit Farbe - Icon.vue — SVG-Icons (eyeOpen, eyeClosed, plus, arrowLeft, save, trash, edit, close) - WeekdayPicker.vue — Wochentag-Auswahl (Mo-So) - MonthdayPicker.vue — Monatstag-Auswahl (Kalender-Grid) - YearPicker.vue — Jahreskalender-Auswahl - router/index.js — Routen mit Breadcrumb-Meta - services/api.js — REST-Client (fetch-basiert) - stores/categories.js — Pinia Store für Kategorien - style.css — Globale Styles, CSS-Variablen, Light/Dark Mode - App.vue — Root-Layout mit Breadcrumb-Navigation - main.js — Vue-App-Init (Pinia + Router) -``` - -## Frontend-Routen - -``` -/ → HomeView (Wochenansicht) -/tasks/all → AllTasksView (Übersicht) -/tasks/new → SchemaView (Schema erstellen) -/tasks/:id → TaskDetailView (Aufgabe bearbeiten) -/schemas/:id → SchemaView (Schema bearbeiten) -/categories → CategoriesView -``` +- **`base.md`** — Vision: was gebaut wird (3 Apps, Systeme, Datenbank-Skizze) +- **`module.md`** — Implementierungs-Schritte als Feature-Module (Backend + Frontend end-to-end pro Modul) +- **`CLAUDE.md`** (diese Datei) — Ist-Zustand des Codes ## Code-Konventionen -- **Sprache Code**: Englisch (Klassen, Methoden, Variablen, CSS-Klassen) -- **Sprache UI**: Deutsch (Labels, Fehlermeldungen, Platzhalter) -- **Enum-Werte**: Englisch in DB und Code (`active`, `done`, `single`, `daily`, `custom`, `disabled`) -- **API-Serialisierung**: Symfony Serializer-Groups auf Entities (`schema:read`, `task:read`, `category:read`) -- **Validierung**: Auf Request-DTOs, nicht auf Entities -- **Error-Handling**: `HttpException` → Symfony built-in -- **Frontend**: Vue 3 Composition API mit ` - - diff --git a/frontend/src/components/CategoryBadge.vue b/frontend/src/components/CategoryBadge.vue deleted file mode 100644 index fe6f9dc..0000000 --- a/frontend/src/components/CategoryBadge.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/frontend/src/components/DayColumn.vue b/frontend/src/components/DayColumn.vue deleted file mode 100644 index f398164..0000000 --- a/frontend/src/components/DayColumn.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/frontend/src/components/Icon.vue b/frontend/src/components/Icon.vue deleted file mode 100644 index 30c14ad..0000000 --- a/frontend/src/components/Icon.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/frontend/src/components/MonthdayPicker.vue b/frontend/src/components/MonthdayPicker.vue deleted file mode 100644 index 8c406c6..0000000 --- a/frontend/src/components/MonthdayPicker.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/frontend/src/components/TaskCard.vue b/frontend/src/components/TaskCard.vue deleted file mode 100644 index 099be35..0000000 --- a/frontend/src/components/TaskCard.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - diff --git a/frontend/src/components/WeekdayPicker.vue b/frontend/src/components/WeekdayPicker.vue deleted file mode 100644 index f8250d2..0000000 --- a/frontend/src/components/WeekdayPicker.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - diff --git a/frontend/src/components/YearPicker.vue b/frontend/src/components/YearPicker.vue deleted file mode 100644 index 8d2671f..0000000 --- a/frontend/src/components/YearPicker.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 365942d..835241d 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,46 +1,8 @@ import { createRouter, createWebHistory } from 'vue-router' -import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(), - routes: [ - { - path: '/', - name: 'home', - component: HomeView, - meta: { breadcrumb: [] }, - }, - { - path: '/tasks/all', - name: 'tasks-all', - component: () => import('../views/AllTasksView.vue'), - meta: { breadcrumb: [{ label: 'Übersicht' }] }, - }, - { - path: '/tasks/new', - name: 'schema-create', - component: () => import('../views/SchemaView.vue'), - meta: { breadcrumb: [{ label: 'Übersicht', to: '/tasks/all' }, { label: 'Neue Aufgabe' }] }, - }, - { - path: '/tasks/:id', - name: 'task-detail', - component: () => import('../views/TaskDetailView.vue'), - meta: { breadcrumb: [{ label: 'Übersicht', to: '/tasks/all' }, { label: '', dynamic: true }] }, - }, - { - path: '/schemas/:id', - name: 'schema-detail', - component: () => import('../views/SchemaView.vue'), - meta: { breadcrumb: [{ label: 'Übersicht', to: '/tasks/all' }, { label: '', dynamic: true }] }, - }, - { - path: '/categories', - name: 'categories', - component: () => import('../views/CategoriesView.vue'), - meta: { breadcrumb: [{ label: 'Kategorien' }] }, - }, - ], + routes: [], }) export default router diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js deleted file mode 100644 index b8a6c81..0000000 --- a/frontend/src/services/api.js +++ /dev/null @@ -1,40 +0,0 @@ -const API_BASE = `${location.protocol}//${location.hostname}/api` - -async function request(path, options = {}) { - const url = API_BASE + path - const res = await fetch(url, { - headers: { 'Content-Type': 'application/json', ...options.headers }, - ...options, - }) - if (res.status === 204) return null - if (!res.ok) { - const err = await res.json().catch(() => ({})) - throw { status: res.status, body: err } - } - return res.json() -} - -// Categories -export const getCategories = () => request('/categories') -export const getCategory = (id) => request(`/categories/${id}`) -export const createCategory = (data) => request('/categories', { method: 'POST', body: JSON.stringify(data) }) -export const updateCategory = (id, data) => request(`/categories/${id}`, { method: 'PUT', body: JSON.stringify(data) }) -export const deleteCategory = (id) => request(`/categories/${id}`, { method: 'DELETE' }) - -// Schemas -export const getSchemas = () => request('/schemas') -export const getSchema = (id) => request(`/schemas/${id}`) -export const createSchema = (data) => request('/schemas', { method: 'POST', body: JSON.stringify(data) }) -export const updateSchema = (id, data) => request(`/schemas/${id}`, { method: 'PUT', body: JSON.stringify(data) }) -export const deleteSchema = (id, deleteTasks = false) => { - const params = deleteTasks ? '?deleteTasks=1' : '' - return request(`/schemas/${id}${params}`, { method: 'DELETE' }) -} - -// Tasks -export const getTasks = () => request('/tasks') -export const getTask = (id) => request(`/tasks/${id}`) -export const createTask = (data) => request('/tasks', { method: 'POST', body: JSON.stringify(data) }) -export const updateTask = (id, data) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }) -export const deleteTask = (id) => request(`/tasks/${id}`, { method: 'DELETE' }) -export const toggleTask = (id) => request(`/tasks/${id}/toggle`, { method: 'PATCH' }) diff --git a/frontend/src/stores/categories.js b/frontend/src/stores/categories.js deleted file mode 100644 index f1c2601..0000000 --- a/frontend/src/stores/categories.js +++ /dev/null @@ -1,34 +0,0 @@ -import { defineStore } from 'pinia' -import { ref } from 'vue' -import * as api from '../services/api' - -export const useCategoriesStore = defineStore('categories', () => { - const items = ref([]) - const loaded = ref(false) - - async function fetchCategories(force = false) { - if (loaded.value && !force) return - items.value = await api.getCategories() - loaded.value = true - } - - async function addCategory(data) { - const category = await api.createCategory(data) - items.value.push(category) - return category - } - - async function editCategory(id, data) { - const updated = await api.updateCategory(id, data) - const index = items.value.findIndex((c) => c.id === id) - if (index !== -1) items.value[index] = updated - return updated - } - - async function removeCategory(id) { - await api.deleteCategory(id) - items.value = items.value.filter((c) => c.id !== id) - } - - return { items, loaded, fetchCategories, addCategory, editCategory, removeCategory } -}) diff --git a/frontend/src/style.css b/frontend/src/style.css index 704282d..e69de29 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,140 +0,0 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --danger: #dc2626; - --danger-bg: rgba(220, 38, 38, 0.1); - --warning: #f59e0b; - --warning-bg: rgba(245, 158, 11, 0.1); - --success: #16a34a; - --shadow: rgba(0, 0, 0, 0.1) 0 2px 8px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - - font: 16px/1.5 var(--sans); - color-scheme: light dark; - color: var(--text); - background: var(--bg); - -webkit-font-smoothing: antialiased; -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --danger: #ef4444; - --danger-bg: rgba(239, 68, 68, 0.15); - --warning: #fbbf24; - --warning-bg: rgba(251, 191, 36, 0.15); - --success: #22c55e; - --shadow: rgba(0, 0, 0, 0.3) 0 2px 8px; - } -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; -} - -h1, h2, h3 { - font-weight: 600; - color: var(--text-h); - margin: 0 0 0.5rem; -} - -h1 { font-size: 1.5rem; } -h2 { font-size: 1.25rem; } -h3 { font-size: 1rem; } - -a { - color: var(--accent); - text-decoration: none; -} - -button { - font-family: var(--sans); - font-size: 0.875rem; - padding: 0.5rem 1rem; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text-h); - cursor: pointer; - transition: background 0.15s, border-color 0.15s; -} - -button:hover { - border-color: var(--accent); -} - -button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-primary { - background: var(--accent); - color: #fff; - border-color: var(--accent); -} - -.btn-primary:hover { - opacity: 0.9; -} - -.btn-danger { - color: var(--danger); - border-color: var(--danger); -} - -.btn-danger:hover { - background: var(--danger-bg); -} - -input[type="text"], -input[type="date"], -select { - font-family: var(--sans); - font-size: 0.875rem; - padding: 0.5rem; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text-h); - width: 100%; -} - -input[type="text"]:focus, -input[type="date"]:focus, -select:focus { - outline: 2px solid var(--accent); - outline-offset: -1px; -} - -label { - display: block; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-h); - margin-bottom: 0.25rem; -} - -.form-group { - margin-bottom: 1rem; -} - -.btn-row { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} diff --git a/frontend/src/views/AllTasksView.vue b/frontend/src/views/AllTasksView.vue deleted file mode 100644 index 2813531..0000000 --- a/frontend/src/views/AllTasksView.vue +++ /dev/null @@ -1,288 +0,0 @@ - - - - - diff --git a/frontend/src/views/CategoriesView.vue b/frontend/src/views/CategoriesView.vue deleted file mode 100644 index a8bf4f5..0000000 --- a/frontend/src/views/CategoriesView.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue deleted file mode 100644 index 0af0c1e..0000000 --- a/frontend/src/views/HomeView.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - - diff --git a/frontend/src/views/SchemaView.vue b/frontend/src/views/SchemaView.vue deleted file mode 100644 index 34771e8..0000000 --- a/frontend/src/views/SchemaView.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - - - diff --git a/frontend/src/views/TaskDetailView.vue b/frontend/src/views/TaskDetailView.vue deleted file mode 100644 index 063d088..0000000 --- a/frontend/src/views/TaskDetailView.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - - -