This commit is contained in:
Marek Lenczewski
2026-03-31 18:09:15 +02:00
parent b6a4548732
commit b998940caa
48 changed files with 717 additions and 816 deletions

View File

@@ -24,9 +24,9 @@ function formatDate(dateStr) {
<div class="day-tasks">
<TaskCard
v-for="task in tasks"
:key="`${task.id}-${date}`"
:key="task.id"
:task="task"
@toggle="(taskId, deadline) => emit('toggle', taskId, deadline || date)"
@toggle="(taskId) => emit('toggle', taskId)"
/>
</div>
</section>

View File

@@ -14,17 +14,17 @@ const router = useRouter()
<div
class="card"
:class="{
'card--completed': task.status === 'erledigt',
'card--completed': task.status === 'done',
'card--past': task.isPast,
}"
@click="emit('toggle', task.schemaId, task.deadline)"
@click="emit('toggle', task.id)"
>
<div class="card-left">
<span class="task-name">{{ task.name }}</span>
<CategoryBadge :category="task.category" />
</div>
<div class="card-right" @click.stop>
<button @click="router.push({ name: 'task-detail', params: { id: task.taskId } })">
<button @click="router.push({ name: 'task-detail', params: { id: task.id } })">
Anzeigen
</button>
</div>

View File

@@ -22,22 +22,19 @@ export const updateCategory = (id, data) => request(`/categories/${id}`, { metho
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 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) => 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')
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' })

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAllSchemas, getAllTasks, toggleTask } from '../services/api'
import { getSchemas, getTasks, toggleTask } from '../services/api'
import CategoryBadge from '../components/CategoryBadge.vue'
import Icon from '../components/Icon.vue'
@@ -13,8 +13,8 @@ const mode = ref('schemas')
async function fetchData(showLoading = true) {
if (showLoading) loading.value = true
items.value = mode.value === 'schemas'
? await getAllSchemas()
: await getAllTasks()
? await getSchemas()
: await getTasks()
loading.value = false
}
@@ -29,17 +29,29 @@ function formatDate(dateStr) {
})
}
function getDateKey(item) {
if (!item.date) return null
return item.date.split('T')[0]
}
const groupedItems = computed(() => {
const today = new Date().toISOString().split('T')[0]
const noDate = items.value.filter(i => !i.deadline)
const current = items.value.filter(i => i.deadline && i.deadline >= today)
const past = items.value.filter(i => i.deadline && i.deadline < today)
const noDate = items.value.filter(i => !getDateKey(i))
const current = items.value.filter(i => {
const d = getDateKey(i)
return d && d >= today
})
const past = items.value.filter(i => {
const d = getDateKey(i)
return d && d < today
})
const monthMap = {}
const months = []
for (const item of past) {
const d = new Date(item.deadline + 'T00:00:00')
const sortKey = item.deadline.substring(0, 7)
const dateKey = getDateKey(item)
const d = new Date(dateKey + 'T00:00:00')
const sortKey = dateKey.substring(0, 7)
const label = d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })
if (!monthMap[sortKey]) {
monthMap[sortKey] = { sortKey, label, items: [] }
@@ -53,7 +65,7 @@ const groupedItems = computed(() => {
})
async function handleToggle(item) {
await toggleTask(item.schemaId, item.deadline)
await toggleTask(item.id)
await fetchData(false)
}
@@ -108,11 +120,11 @@ onMounted(fetchData)
@click="handleToggle(item)"
>
<div class="task-left">
<span class="task-name" :class="{ 'task--completed': item.status === 'erledigt' }">{{ item.name }}</span>
<span class="task-name" :class="{ 'task--completed': item.status === 'done' }">{{ item.name }}</span>
<CategoryBadge :category="item.category" />
</div>
<div class="task-right" @click.stop>
<button @click="router.push({ name: 'task-detail', params: { id: item.taskId } })">Anzeigen</button>
<button @click="router.push({ name: 'task-detail', params: { id: item.id } })">Anzeigen</button>
</div>
</li>
@@ -125,12 +137,12 @@ onMounted(fetchData)
@click="handleToggle(item)"
>
<div class="task-left">
<span class="task-name" :class="{ 'task--completed': item.status === 'erledigt' }">{{ item.name }}</span>
<span class="task-name" :class="{ 'task--completed': item.status === 'done' }">{{ item.name }}</span>
<CategoryBadge :category="item.category" />
</div>
<div class="task-right" @click.stop>
<span class="task-date">{{ formatDate(item.deadline) }}</span>
<button @click="router.push({ name: 'task-detail', params: { id: item.taskId } })">Anzeigen</button>
<span class="task-date">{{ formatDate(getDateKey(item)) }}</span>
<button @click="router.push({ name: 'task-detail', params: { id: item.id } })">Anzeigen</button>
</div>
</li>
@@ -144,10 +156,10 @@ onMounted(fetchData)
@click="handleToggle(item)"
>
<div class="task-left">
<span class="task-name" :class="{ 'task--completed': item.status === 'erledigt' }">{{ item.name }}</span>
<span class="task-name" :class="{ 'task--completed': item.status === 'done' }">{{ item.name }}</span>
<CategoryBadge :category="item.category" />
</div>
<span class="task-date">{{ formatDate(item.deadline) }}</span>
<span class="task-date">{{ formatDate(getDateKey(item)) }}</span>
</li>
</template>
</template>

View File

@@ -2,7 +2,7 @@
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useCategoriesStore } from '../stores/categories'
import { getWeekTasks, toggleTask } from '../services/api'
import { getTasks, toggleTask } from '../services/api'
import DayColumn from '../components/DayColumn.vue'
import TaskCard from '../components/TaskCard.vue'
import Icon from '../components/Icon.vue'
@@ -10,41 +10,63 @@ import Icon from '../components/Icon.vue'
const router = useRouter()
const categoriesStore = useCategoriesStore()
const rawDays = ref([])
const rawTasksWithoutDeadline = ref([])
const allTasks = ref([])
const loading = ref(true)
const showCompleted = ref(true)
function filterTasks(tasks) {
if (showCompleted.value) return tasks
return tasks.filter(t => t.status !== 'erledigt')
function getWeekDates() {
const now = new Date()
const day = now.getDay()
const monday = new Date(now)
monday.setDate(now.getDate() - ((day + 6) % 7))
const dates = []
for (let i = 0; i < 7; i++) {
const d = new Date(monday)
d.setDate(monday.getDate() + i)
dates.push(d.toISOString().split('T')[0])
}
return dates
}
const tasksWithoutDeadline = computed(() => filterTasks(rawTasksWithoutDeadline.value))
const days = computed(() => rawDays.value.map(d => ({
...d,
tasks: filterTasks(d.tasks),
})))
const weekDates = getWeekDates()
async function fetchWeek(showLoading = true) {
function filterTasks(tasks) {
if (showCompleted.value) return tasks
return tasks.filter(t => t.status !== 'done')
}
const tasksWithoutDeadline = computed(() => {
const tasks = allTasks.value.filter(t => t.date === null)
return filterTasks(tasks)
})
const days = computed(() => {
return weekDates.map(date => {
const tasks = allTasks.value.filter(t => {
if (!t.date) return false
return t.date.split('T')[0] === date
})
return { date, tasks: filterTasks(tasks) }
})
})
async function fetchTasks(showLoading = true) {
if (showLoading) loading.value = true
try {
const data = await getWeekTasks()
rawDays.value = data.days
rawTasksWithoutDeadline.value = data.tasksWithoutDeadline
allTasks.value = await getTasks()
} finally {
loading.value = false
}
}
async function handleToggle(taskId, date) {
await toggleTask(taskId, date)
await fetchWeek(false)
async function handleToggle(taskId) {
await toggleTask(taskId)
await fetchTasks(false)
}
onMounted(() => {
categoriesStore.fetchCategories()
fetchWeek()
fetchTasks()
})
</script>

View File

@@ -17,18 +17,20 @@ const isEdit = computed(() => !!route.params.id)
const loading = ref(false)
const saving = ref(false)
const error = ref(null)
const deleteTasksOnRemove = ref(false)
const form = ref({
name: '',
categoryId: null,
status: 'aktiv',
taskType: 'einzel',
deadline: null,
status: 'active',
type: 'single',
startDate: null,
endDate: null,
weekdays: [],
monthDays: [],
yearDays: [],
days: {
week: [],
month: [],
year: [],
},
})
function formatDateForInput(dateStr) {
@@ -44,13 +46,14 @@ async function loadTask() {
name: task.name,
categoryId: task.category?.id ?? null,
status: task.status,
taskType: task.taskType,
deadline: formatDateForInput(task.deadline),
type: task.type,
startDate: formatDateForInput(task.startDate),
endDate: formatDateForInput(task.endDate),
weekdays: task.weekdays || [],
monthDays: task.monthDays || [],
yearDays: task.yearDays || [],
days: {
week: task.days?.week || [],
month: task.days?.month || [],
year: task.days?.year || [],
},
}
setDynamicLabel(task.name)
} finally {
@@ -64,23 +67,25 @@ function buildPayload() {
name: f.name,
categoryId: f.categoryId,
status: f.status,
taskType: f.taskType,
deadline: null,
type: f.type,
startDate: null,
endDate: null,
weekdays: null,
monthDays: null,
yearDays: null,
days: null,
}
if (f.taskType === 'einzel') {
payload.deadline = f.deadline
} else {
if (f.type === 'single') {
payload.days = { year: f.days.year }
} else if (f.type === 'daily') {
payload.startDate = f.startDate
payload.endDate = f.endDate
if (f.taskType === 'woechentlich') payload.weekdays = f.weekdays
if (f.taskType === 'monatlich') payload.monthDays = f.monthDays
if (f.taskType === 'multi' || f.taskType === 'jaehrlich') payload.yearDays = f.yearDays
} else if (f.type === 'custom') {
payload.startDate = f.startDate
payload.endDate = f.endDate
const days = {}
if (f.days.week.length) days.week = f.days.week
if (f.days.month.length) days.month = f.days.month
if (f.days.year.length) days.year = f.days.year
payload.days = Object.keys(days).length ? days : null
}
return payload
@@ -105,8 +110,8 @@ async function save() {
}
async function remove() {
if (!confirm('Aufgabe wirklich löschen?')) return
await deleteSchema(route.params.id)
if (!confirm('Schema wirklich löschen?')) return
await deleteSchema(route.params.id, deleteTasksOnRemove.value)
router.back()
}
@@ -151,9 +156,8 @@ onMounted(() => {
<div v-if="isEdit" class="form-group">
<label for="status">Status</label>
<select id="status" v-model="form.status">
<option value="aktiv">Aktiv</option>
<option value="erledigt">Erledigt</option>
<option value="inaktiv">Inaktiv</option>
<option value="active">Aktiv</option>
<option value="disabled">Inaktiv</option>
</select>
</div>
@@ -162,34 +166,25 @@ onMounted(() => {
<label>Aufgabentyp</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" v-model="form.taskType" value="einzel" /> Einzel
<input type="radio" v-model="form.type" value="single" /> Einzel
</label>
<label class="radio-label">
<input type="radio" v-model="form.taskType" value="taeglich" /> Täglich
<input type="radio" v-model="form.type" value="daily" /> Täglich
</label>
<label class="radio-label">
<input type="radio" v-model="form.taskType" value="multi" /> Multi
</label>
<label class="radio-label">
<input type="radio" v-model="form.taskType" value="woechentlich" /> Wöchentlich
</label>
<label class="radio-label">
<input type="radio" v-model="form.taskType" value="monatlich" /> Monatlich
</label>
<label class="radio-label">
<input type="radio" v-model="form.taskType" value="jaehrlich" /> Jährlich
<input type="radio" v-model="form.type" value="custom" /> Benutzerdefiniert
</label>
</div>
</div>
<!-- Einzel: Datum -->
<div v-if="form.taskType === 'einzel'" class="form-group">
<label for="deadline">Datum (optional)</label>
<input id="deadline" v-model="form.deadline" type="date" />
<!-- Einzel: Jahresansicht -->
<div v-if="form.type === 'single'" class="form-group">
<label>Tage auswählen</label>
<YearPicker v-model="form.days.year" />
</div>
<!-- Alle außer Einzel: Start + Enddatum nebeneinander -->
<template v-if="form.taskType !== 'einzel'">
<!-- Daily + Custom: Start + Enddatum -->
<template v-if="form.type === 'daily' || form.type === 'custom'">
<div class="form-row">
<div class="form-group flex-1">
<label for="startDate">Startdatum (leer = heute)</label>
@@ -202,30 +197,36 @@ onMounted(() => {
</div>
</template>
<!-- Wöchentlich -->
<div v-if="form.taskType === 'woechentlich'" class="form-group">
<label>Wochentage</label>
<WeekdayPicker v-model="form.weekdays" />
</div>
<!-- Custom: Alle Picker -->
<template v-if="form.type === 'custom'">
<div class="form-group">
<label>Wochentage</label>
<WeekdayPicker v-model="form.days.week" />
</div>
<!-- Monatlich -->
<div v-if="form.taskType === 'monatlich'" class="form-group">
<label>Monatstage</label>
<MonthdayPicker v-model="form.monthDays" />
</div>
<div class="form-group">
<label>Monatstage</label>
<MonthdayPicker v-model="form.days.month" />
</div>
<!-- Multi + Jährlich -->
<div v-if="form.taskType === 'multi' || form.taskType === 'jaehrlich'" class="form-group">
<label>Jahresansicht</label>
<YearPicker v-model="form.yearDays" />
</div>
<div class="form-group">
<label>Jahresansicht</label>
<YearPicker v-model="form.days.year" />
</div>
</template>
<p v-if="error" class="error">{{ error }}</p>
<div v-if="isEdit" class="form-actions">
<button type="button" class="btn-danger" @click="remove">
<Icon name="trash" />
</button>
<div class="delete-section">
<label class="checkbox-label">
<input type="checkbox" v-model="deleteTasksOnRemove" />
Aufgaben löschen
</label>
<button type="button" class="btn-danger" @click="remove">
<Icon name="trash" />
</button>
</div>
</div>
</form>
</div>
@@ -266,6 +267,21 @@ onMounted(() => {
margin-top: 1.5rem;
}
.delete-section {
display: flex;
align-items: center;
gap: 1rem;
}
.checkbox-label {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-weight: normal;
cursor: pointer;
font-size: 0.875rem;
}
.loading {
text-align: center;
color: var(--text);

View File

@@ -13,7 +13,7 @@ const categoriesStore = useCategoriesStore()
const loading = ref(true)
const saving = ref(false)
const occurrence = ref(null)
const form = ref({ name: '', status: 'aktiv', date: '', categoryId: null })
const form = ref({ name: '', status: 'active', date: '', categoryId: null })
async function load() {
loading.value = true
@@ -85,8 +85,8 @@ onMounted(() => {
<div class="form-group flex-1">
<label for="status">Status</label>
<select id="status" v-model="form.status">
<option value="aktiv">Aktiv</option>
<option value="erledigt">Erledigt</option>
<option value="active">Aktiv</option>
<option value="done">Erledigt</option>
</select>
</div>
<div class="form-group flex-1">