diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d013356 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,175 @@ +# Haushalt — Aufgabenverwaltung + +## 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) | +| CORS | Nelmio CORS Bundle | +| Umgebung | DDEV | + +## Domain-Modell + +**TaskSchema** — Vorlage/Template für Aufgaben +- Felder: name, status, taskType, category, deadline, startDate, endDate, weekdays, monthDays, yearDays +- TaskSchemaStatus: Active (`aktiv`), Completed (`erledigt`), Inactive (`inaktiv`) +- TaskSchemaType: Single (`einzel`), Daily (`taeglich`), Multi (`multi`), Weekly (`woechentlich`), Monthly (`monatlich`), Yearly (`jaehrlich`) + +**Task** — Einzelne Aufgabe (Instanz eines Schemas) +- Felder: schema (FK), name (Override), category (Override), categoryOverridden, date, status, createdAt +- TaskStatus: Active (`aktiv`), Completed (`erledigt`) +- Kann Name und Kategorie pro Instanz überschreiben (`getEffectiveName()`, `getEffectiveCategory()`) +- Unique Constraint: (schema_id, date) + +**Category** — Farbkodierte Kategorie +- Felder: id, name, color (Hex #RRGGBB) + +Enum-Case-Namen sind Englisch, String-Werte bleiben Deutsch (in DB gespeichert). + +## Architektur (Backend) + +``` +Controller → Service (Manager) → Repository → Entity + ↓ + DTO (Request/Response) +``` + +**Prinzipien:** +- Controller: nur Routing + Response, keine Geschäftslogik +- Manager-Services: Geschäftslogik (CRUD, Validierung, Toggle) +- DTOs: typisierter Input (Request) und Output (Response) +- Validierung: nur auf Request-DTOs (`#[Assert\...]`), nicht auf Entities +- Exceptions: `ValidationException` (eigener Listener), `HttpException` (Symfony built-in) +- Entities: nur Doctrine-Mapping + Getter/Setter + +## Verzeichnisstruktur (Backend) + +``` +backend/src/ + Controller/Api/ + CategoryController.php — Category CRUD + TaskController.php — Task show/update/delete + TaskSchemaController.php — Schema CRUD + week view + toggle + DTO/ + Request/ — CreateSchemaRequest, UpdateSchemaRequest, UpdateTaskRequest, + ToggleRequest, CreateCategoryRequest, UpdateCategoryRequest + Response/ — TaskResponse, CategoryResponse, WeekViewResponse, + DayResponse, ToggleResponse + Entity/ + Category.php, Task.php, TaskSchema.php + Enum/ + TaskStatus.php, TaskSchemaStatus.php, TaskSchemaType.php + Exception/ + ValidationException.php — Wraps ConstraintViolationList, handled by ExceptionListener + EventListener/ + ExceptionListener.php — Fängt ValidationException, erzeugt JSON 422 + Repository/ + CategoryRepository.php, TaskRepository.php, TaskSchemaRepository.php + Service/ + CategoryManager.php — Category CRUD-Logik + TaskManager.php — Task Update/Delete + TaskSchemaManager.php — Schema CRUD + Toggle + Sync-Auslösung + TaskGenerator.php — Erzeugt Task-Instanzen für Zeiträume + TaskSynchronizer.php — Sync nach Schema-Update (löschen/erstellen/reset) + DeadlineCalculator.php — Berechnet Fälligkeitsdaten für ein Schema + TaskViewBuilder.php — Baut Wochenansicht + Alle-Aufgaben-View + TaskSerializer.php — Task/Category → Response-DTOs +``` + +## API-Routen + +### Categories (`/api/categories`) +``` +GET / — Alle Kategorien +GET /{id} — Einzelne Kategorie +POST / — Erstellen +PUT /{id} — Aktualisieren +DELETE /{id} — Löschen +``` + +### Schemas (`/api/schemas`) +``` +GET / — Alle Schemas +GET /week?start=DATE — Wochenansicht (7 Tage) +GET /all — Alle Schemas sortiert +GET /all-tasks — Alle Tasks über alle Schemas +GET /{id} — Einzelnes Schema +POST / — Erstellen +PUT /{id} — Aktualisieren (synchronisiert Tasks) +DELETE /{id} — Löschen +PATCH /{id}/toggle — Task-Status umschalten +``` + +### Tasks (`/api/tasks`) +``` +GET /{id} — Task-Details +PUT /{id} — Task aktualisieren +DELETE /{id} — Task löschen +``` + +## Verzeichnisstruktur (Frontend) + +``` +frontend/src/ + views/ + HomeView.vue — Startseite, Wochenansicht + AllTasksView.vue — Übersicht aller Schemas/Tasks + SchemaView.vue — Schema erstellen/bearbeiten + 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 +``` + +## Code-Konventionen + +- **Sprache Code**: Englisch (Klassen, Methoden, Variablen, CSS-Klassen) +- **Sprache UI**: Deutsch (Labels, Fehlermeldungen, Platzhalter) +- **Enum-Werte**: Deutsch in DB (`aktiv`, `erledigt`, `einzel` etc.), Englisch als PHP-Case-Namen (`Active`, `Completed`, `Single`) +- **API-Serialisierung**: Symfony Serializer-Groups für Schema/Category Entities (`schema:read`, `schema:write`, `category:read`, `category:write`), TaskSerializer-Service für Tasks +- **Validierung**: Auf Request-DTOs, nicht auf Entities +- **Error-Handling**: `ValidationException` → ExceptionListener → JSON 422; `HttpException` → Symfony built-in +- **Frontend**: Vue 3 Composition API mit ` - + + + Haushalt + + > + {{ crumb.label }} + {{ crumb.label }} + + + + + + + + diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index cc51a3d..0000000 Binary files a/frontend/src/assets/hero.png and /dev/null differ diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg deleted file mode 100644 index 770e9d3..0000000 --- a/frontend/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/CategoryBadge.vue b/frontend/src/components/CategoryBadge.vue new file mode 100644 index 0000000..fe6f9dc --- /dev/null +++ b/frontend/src/components/CategoryBadge.vue @@ -0,0 +1,26 @@ + + + + + {{ category?.name || 'Allgemein' }} + + + + diff --git a/frontend/src/components/DayColumn.vue b/frontend/src/components/DayColumn.vue new file mode 100644 index 0000000..661fc7f --- /dev/null +++ b/frontend/src/components/DayColumn.vue @@ -0,0 +1,65 @@ + + + + + {{ formatDate(date) }} + + emit('toggle', taskId, deadline || date)" + /> + + + + + diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue deleted file mode 100644 index 3a7b808..0000000 --- a/frontend/src/components/HelloWorld.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - Get started - Edit src/App.vue and save to test HMR - - Count is {{ count }} - - - - - - - - - - Documentation - Your questions, answered - - - - - Explore Vite - - - - - - Learn more - - - - - - - - - Connect with us - Join the Vite community - - - - - - - GitHub - - - - - - - - Discord - - - - - - - - X.com - - - - - - - - Bluesky - - - - - - - - - diff --git a/frontend/src/components/Icon.vue b/frontend/src/components/Icon.vue new file mode 100644 index 0000000..30c14ad --- /dev/null +++ b/frontend/src/components/Icon.vue @@ -0,0 +1,38 @@ + + + + + + + diff --git a/frontend/src/components/MonthdayPicker.vue b/frontend/src/components/MonthdayPicker.vue new file mode 100644 index 0000000..8c406c6 --- /dev/null +++ b/frontend/src/components/MonthdayPicker.vue @@ -0,0 +1,96 @@ + + + + + Mo + Di + Mi + Do + Fr + Sa + So + + + + {{ day }} + + + + + diff --git a/frontend/src/components/TaskCard.vue b/frontend/src/components/TaskCard.vue new file mode 100644 index 0000000..0dfa387 --- /dev/null +++ b/frontend/src/components/TaskCard.vue @@ -0,0 +1,83 @@ + + + + + + {{ task.name }} + + + + + Anzeigen + + + + + + diff --git a/frontend/src/components/WeekdayPicker.vue b/frontend/src/components/WeekdayPicker.vue new file mode 100644 index 0000000..f8250d2 --- /dev/null +++ b/frontend/src/components/WeekdayPicker.vue @@ -0,0 +1,84 @@ + + + + + + + {{ day.label }} + + + + + diff --git a/frontend/src/components/YearPicker.vue b/frontend/src/components/YearPicker.vue new file mode 100644 index 0000000..8d2671f --- /dev/null +++ b/frontend/src/components/YearPicker.vue @@ -0,0 +1,185 @@ + + + + + + + Mo + Di + Mi + Do + Fr + Sa + So + + + + {{ cell.name }} + + + + {{ cell.day }} + + + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js index 2425c0f..c4b3a5f 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,5 +1,10 @@ import { createApp } from 'vue' -import './style.css' +import { createPinia } from 'pinia' +import router from './router' import App from './App.vue' +import './style.css' -createApp(App).mount('#app') +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..365942d --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,46 @@ +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' }] }, + }, + ], +}) + +export default router diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..1dca356 --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,43 @@ +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 getWeekTasks = (start) => { + const params = start ? `?start=${start}` : '' + return request(`/schemas/week${params}`) +} +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) => request(`/schemas/${id}`, { method: 'DELETE' }) +export const toggleTask = (id, date) => request(`/schemas/${id}/toggle`, { + method: 'PATCH', + body: JSON.stringify(date ? { date } : {}), +}) +export const getAllSchemas = () => request('/schemas/all') +export const getAllTasks = () => request('/schemas/all-tasks') + +// Tasks +export const getTask = (id) => request(`/tasks/${id}`) +export const updateTask = (id, data) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }) +export const deleteTask = (id) => request(`/tasks/${id}`, { method: 'DELETE' }) diff --git a/frontend/src/stores/categories.js b/frontend/src/stores/categories.js new file mode 100644 index 0000000..f1c2601 --- /dev/null +++ b/frontend/src/stores/categories.js @@ -0,0 +1,34 @@ +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 527d4fb..704282d 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -3,31 +3,22 @@ --text-h: #08060d; --bg: #fff; --border: #e5e4e7; - --code-bg: #f4f3ec; --accent: #aa3bff; --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + --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; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; + font: 16px/1.5 var(--sans); color-scheme: light dark; color: var(--text); background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } } @media (prefers-color-scheme: dark) { @@ -36,261 +27,114 @@ --text-h: #f3f4f6; --bg: #16171d; --border: #2e303a; - --code-bg: #1f2028; --accent: #c084fc; --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + --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; } +} - #social .button-icon { - filter: invert(1) brightness(2); - } +* { + box-sizing: border-box; } body { margin: 0; } -h1, -h2 { - font-family: var(--heading); +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; } -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; +.form-group { + margin-bottom: 1rem; } -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} - -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#app { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; +.btn-row { display: flex; - flex-direction: column; - box-sizing: border-box; -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } + gap: 0.5rem; + flex-wrap: wrap; } diff --git a/frontend/src/views/AllTasksView.vue b/frontend/src/views/AllTasksView.vue new file mode 100644 index 0000000..cc22ae4 --- /dev/null +++ b/frontend/src/views/AllTasksView.vue @@ -0,0 +1,276 @@ + + + + + + Kategorien + + + + + + + + + + + + Alle Schemas + + + Alle Aufgaben + + + + + Laden... + + + + + + + {{ item.name }} + + + + Anzeigen + + + + + + + + + {{ item.name }} + + + + Anzeigen + + + + + Aktuell + + + {{ item.name }} + + + + {{ formatDate(item.deadline) }} + Anzeigen + + + + + + {{ month.label }} + + + {{ item.name }} + + + {{ formatDate(item.deadline) }} + + + + + + Keine Einträge. + + + + diff --git a/frontend/src/views/CategoriesView.vue b/frontend/src/views/CategoriesView.vue new file mode 100644 index 0000000..a8bf4f5 --- /dev/null +++ b/frontend/src/views/CategoriesView.vue @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ cat.name }} + + + + + + + + + + Keine Kategorien vorhanden. + + + + + diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue new file mode 100644 index 0000000..0a6b55f --- /dev/null +++ b/frontend/src/views/HomeView.vue @@ -0,0 +1,107 @@ + + + + + + + + Übersicht + + + + + + + Laden... + + + + + + + + + + + + diff --git a/frontend/src/views/SchemaView.vue b/frontend/src/views/SchemaView.vue new file mode 100644 index 0000000..3b7edce --- /dev/null +++ b/frontend/src/views/SchemaView.vue @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + Laden... + + + + + + Name + + + + Kategorie + + Keine Kategorie + + {{ cat.name }} + + + + + + + Status + + Aktiv + Erledigt + Inaktiv + + + + + + Aufgabentyp + + + Einzel + + + Täglich + + + Multi + + + Wöchentlich + + + Monatlich + + + Jährlich + + + + + + + Datum (optional) + + + + + + + + Startdatum (leer = heute) + + + + Enddatum (optional) + + + + + + + + Wochentage + + + + + + Monatstage + + + + + + Jahresansicht + + + + {{ error }} + + + + + + + + + + + diff --git a/frontend/src/views/TaskDetailView.vue b/frontend/src/views/TaskDetailView.vue new file mode 100644 index 0000000..333557a --- /dev/null +++ b/frontend/src/views/TaskDetailView.vue @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + Laden... + + + + + Name + + + + Kategorie + + Keine Kategorie + + {{ cat.name }} + + + + + + + + Status + + Aktiv + Erledigt + + + + Datum + + + + + + + +
Edit src/App.vue and save to test HMR
src/App.vue
HMR
Your questions, answered
Join the Vite community
Laden...
Keine Einträge.
+ Keine Kategorien vorhanden. +
{{ error }}
Connect with us
-Join the Vite community
---
-
-
- GitHub
-
-
- -
-
-
- Discord
-
-
- -
-
-
- X.com
-
-
- -
-
-
- Bluesky
-
-
-
-