TaskSchema module
This commit is contained in:
@@ -48,6 +48,12 @@ const props = defineProps({
|
||||
<line x1="10" y1="11" x2="10" y2="17" />
|
||||
<line x1="14" y1="11" x2="14" y2="17" />
|
||||
</template>
|
||||
<template v-else-if="name === 'calendar'">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</template>
|
||||
<template v-else-if="name === 'refresh'">
|
||||
<polyline points="23 4 23 10 17 10" />
|
||||
<polyline points="1 20 1 14 7 14" />
|
||||
|
||||
@@ -27,16 +27,40 @@ const router = createRouter({
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tasks/create',
|
||||
name: 'tasks-create',
|
||||
component: () => import('../views/TaskCreate.vue'),
|
||||
path: '/schemas',
|
||||
name: 'schemas',
|
||||
component: () => import('../views/SchemaAll.vue'),
|
||||
meta: {
|
||||
breadcrumb: [
|
||||
{ label: 'Tasks', to: '/tasks' },
|
||||
{ label: 'Schemas' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/schemas/create',
|
||||
name: 'schemas-create',
|
||||
component: () => import('../views/SchemaCreate.vue'),
|
||||
meta: {
|
||||
breadcrumb: [
|
||||
{ label: 'Tasks', to: '/tasks' },
|
||||
{ label: 'Schemas', to: '/schemas' },
|
||||
{ label: 'Create' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/schemas/:id(\\d+)',
|
||||
name: 'schemas-edit',
|
||||
component: () => import('../views/SchemaEdit.vue'),
|
||||
meta: {
|
||||
breadcrumb: [
|
||||
{ label: 'Tasks', to: '/tasks' },
|
||||
{ label: 'Schemas', to: '/schemas' },
|
||||
{ label: 'Edit' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tasks/:id(\\d+)',
|
||||
name: 'tasks-edit',
|
||||
|
||||
@@ -12,10 +12,17 @@ async function request(path, opts = {}) {
|
||||
return res.status === 204 ? null : res.json()
|
||||
}
|
||||
|
||||
export const schemaApi = {
|
||||
list: () => request('/task-schemas'),
|
||||
get: (id) => request(`/task-schemas/${id}`),
|
||||
create: (data) => request('/task-schemas', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => request(`/task-schemas/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
remove: (id) => request(`/task-schemas/${id}`, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
export const taskApi = {
|
||||
list: (filter) => request(`/tasks${filter ? `?filter=${filter}` : ''}`),
|
||||
get: (id) => request(`/tasks/${id}`),
|
||||
create: (data) => request('/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
remove: (id) => request(`/tasks/${id}`, { method: 'DELETE' }),
|
||||
toggle: (id) => request(`/tasks/${id}/toggle`, { method: 'PATCH' }),
|
||||
|
||||
44
frontend/src/stores/schemas.js
Normal file
44
frontend/src/stores/schemas.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { schemaApi } from '../services/api'
|
||||
|
||||
export const useSchemasStore = defineStore('schemas', {
|
||||
state: () => ({
|
||||
schemas: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchAll() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
this.schemas = await schemaApi.list()
|
||||
} catch (e) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async get(id) {
|
||||
return schemaApi.get(id)
|
||||
},
|
||||
|
||||
async create(data) {
|
||||
const result = await schemaApi.create(data)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(id, data) {
|
||||
const updated = await schemaApi.update(id, data)
|
||||
this.schemas = this.schemas.map((s) => (s.id === id ? updated : s))
|
||||
return updated
|
||||
},
|
||||
|
||||
async remove(id) {
|
||||
await schemaApi.remove(id)
|
||||
this.schemas = this.schemas.filter((s) => s.id !== id)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -44,12 +44,6 @@ export const useTasksStore = defineStore('tasks', {
|
||||
this.availableStatuses = await taskApi.statuses()
|
||||
},
|
||||
|
||||
async create(data) {
|
||||
const task = await taskApi.create(data)
|
||||
this.tasks.push(task)
|
||||
return task
|
||||
},
|
||||
|
||||
async update(id, data) {
|
||||
const updated = await taskApi.update(id, data)
|
||||
this.replaceLocal(updated)
|
||||
|
||||
145
frontend/src/views/SchemaAll.vue
Normal file
145
frontend/src/views/SchemaAll.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSchemasStore } from '../stores/schemas'
|
||||
import Icon from '../components/Icon.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useSchemasStore()
|
||||
|
||||
function repeatLabel(schema) {
|
||||
if (!schema.repeat) return 'Einmalig'
|
||||
if (schema.repeat.daily) return 'Täglich'
|
||||
if (schema.repeat.weekly) return 'Wöchentlich'
|
||||
if (schema.repeat.monthly) return 'Monatlich'
|
||||
return ''
|
||||
}
|
||||
|
||||
async function onDelete(id) {
|
||||
await store.remove(id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.fetchAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="schema-all-view">
|
||||
<div class="actions">
|
||||
<button class="icon-btn" @click="router.push('/schemas/create')" title="Neues Schema">
|
||||
<Icon name="plus" />
|
||||
</button>
|
||||
<button class="icon-btn" @click="store.fetchAll()" title="Neu laden">
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="store.loading && store.schemas.length === 0" class="hint">Lädt…</p>
|
||||
<p v-else-if="store.error" class="error">{{ store.error }}</p>
|
||||
<p v-else-if="store.schemas.length === 0" class="hint">Keine Schemas.</p>
|
||||
|
||||
<ul v-else class="schema-list">
|
||||
<li
|
||||
v-for="schema in store.schemas"
|
||||
:key="schema.id"
|
||||
class="schema-item"
|
||||
:class="{ inactive: schema.status === 'inactive' }"
|
||||
>
|
||||
<div class="schema-info">
|
||||
<span class="schema-name">{{ schema.name }}</span>
|
||||
<span class="schema-repeat">{{ repeatLabel(schema) }}</span>
|
||||
</div>
|
||||
<div class="schema-actions">
|
||||
<button class="icon-btn" @click="router.push(`/schemas/${schema.id}`)" title="Bearbeiten">
|
||||
<Icon name="pencil" />
|
||||
</button>
|
||||
<button class="icon-btn" @click="onDelete(schema.id)" title="Löschen">
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--breadcrumb-bg);
|
||||
}
|
||||
|
||||
.schema-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.schema-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.schema-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.schema-item:hover {
|
||||
background: var(--breadcrumb-bg);
|
||||
}
|
||||
|
||||
.schema-item.inactive {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.schema-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.schema-repeat {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.schema-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
249
frontend/src/views/SchemaCreate.vue
Normal file
249
frontend/src/views/SchemaCreate.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSchemasStore } from '../stores/schemas'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useSchemasStore()
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
status: 'active',
|
||||
taskStatus: 'active',
|
||||
repeatType: 'none',
|
||||
date: '',
|
||||
start: '',
|
||||
end: '',
|
||||
weekly: Array(7).fill(false),
|
||||
monthly: Array(31).fill(false),
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
||||
|
||||
function buildPayload() {
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
status: form.value.status,
|
||||
taskStatus: form.value.taskStatus,
|
||||
}
|
||||
|
||||
if (form.value.repeatType === 'none') {
|
||||
data.date = form.value.date || null
|
||||
} else {
|
||||
data.start = form.value.start || null
|
||||
data.end = form.value.end || null
|
||||
|
||||
if (form.value.repeatType === 'daily') {
|
||||
data.repeat = { daily: true }
|
||||
} else if (form.value.repeatType === 'weekly') {
|
||||
data.repeat = { weekly: [...form.value.weekly] }
|
||||
} else if (form.value.repeatType === 'monthly') {
|
||||
data.repeat = { monthly: [...form.value.monthly] }
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
submitting.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await store.create(buildPayload())
|
||||
router.push('/schemas')
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="schema-form" @submit.prevent="onSave">
|
||||
<div class="field">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" v-model="form.name" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="status">Schema Status</label>
|
||||
<select id="status" v-model="form.status">
|
||||
<option value="active">active</option>
|
||||
<option value="inactive">inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="taskStatus">Task Status</label>
|
||||
<select id="taskStatus" v-model="form.taskStatus">
|
||||
<option value="active">active</option>
|
||||
<option value="done">done</option>
|
||||
<option value="inactive">inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="repeatType">Wiederholung</label>
|
||||
<select id="repeatType" v-model="form.repeatType">
|
||||
<option value="none">Keine (Einmalig)</option>
|
||||
<option value="daily">Täglich</option>
|
||||
<option value="weekly">Wöchentlich</option>
|
||||
<option value="monthly">Monatlich</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType === 'none'" class="field">
|
||||
<label for="date">Datum</label>
|
||||
<input id="date" v-model="form.date" type="date" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType === 'weekly'" class="field">
|
||||
<label>Wochentage</label>
|
||||
<div class="day-grid">
|
||||
<label v-for="(day, i) in weekdays" :key="i" class="day-label" :class="{ selected: form.weekly[i] }">
|
||||
<input type="checkbox" v-model="form.weekly[i]" class="sr-only" />
|
||||
{{ day }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType === 'monthly'" class="field">
|
||||
<label>Monatstage</label>
|
||||
<div class="day-grid">
|
||||
<label v-for="d in 31" :key="d" class="day-label" :class="{ selected: form.monthly[d - 1] }">
|
||||
<input type="checkbox" v-model="form.monthly[d - 1]" class="sr-only" />
|
||||
{{ d }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType !== 'none'" class="field-row">
|
||||
<div class="field">
|
||||
<label for="start">Start</label>
|
||||
<input id="start" v-model="form.start" type="date" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="end">Ende</label>
|
||||
<input id="end" v-model="form.end" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" :disabled="submitting || !form.name">Erstellen</button>
|
||||
<button type="button" @click="router.back()">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.schema-form {
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.field-row .field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.day-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.day-label.selected {
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--breadcrumb-bg);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
302
frontend/src/views/SchemaEdit.vue
Normal file
302
frontend/src/views/SchemaEdit.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useSchemasStore } from '../stores/schemas'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useSchemasStore()
|
||||
|
||||
const id = Number(route.params.id)
|
||||
const original = ref(null)
|
||||
const form = ref({
|
||||
name: '',
|
||||
status: 'active',
|
||||
taskStatus: 'active',
|
||||
repeatType: 'none',
|
||||
date: '',
|
||||
start: '',
|
||||
end: '',
|
||||
weekly: Array(7).fill(false),
|
||||
monthly: Array(31).fill(false),
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
||||
|
||||
function toFormDate(iso) {
|
||||
if (!iso) return ''
|
||||
return iso.slice(0, 10)
|
||||
}
|
||||
|
||||
function detectRepeatType(schema) {
|
||||
if (!schema.repeat) return 'none'
|
||||
if (schema.repeat.daily) return 'daily'
|
||||
if (schema.repeat.weekly) return 'weekly'
|
||||
if (schema.repeat.monthly) return 'monthly'
|
||||
return 'none'
|
||||
}
|
||||
|
||||
function loadFromSchema(schema) {
|
||||
const repeatType = detectRepeatType(schema)
|
||||
return {
|
||||
name: schema.name,
|
||||
status: schema.status,
|
||||
taskStatus: schema.taskStatus,
|
||||
repeatType,
|
||||
date: toFormDate(schema.date),
|
||||
start: toFormDate(schema.start),
|
||||
end: toFormDate(schema.end),
|
||||
weekly: schema.repeat?.weekly ? [...schema.repeat.weekly] : Array(7).fill(false),
|
||||
monthly: schema.repeat?.monthly ? [...schema.repeat.monthly] : Array(31).fill(false),
|
||||
}
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
status: form.value.status,
|
||||
taskStatus: form.value.taskStatus,
|
||||
}
|
||||
|
||||
if (form.value.repeatType === 'none') {
|
||||
data.date = form.value.date || null
|
||||
} else {
|
||||
data.start = form.value.start || null
|
||||
data.end = form.value.end || null
|
||||
|
||||
if (form.value.repeatType === 'daily') {
|
||||
data.repeat = { daily: true }
|
||||
} else if (form.value.repeatType === 'weekly') {
|
||||
data.repeat = { weekly: [...form.value.weekly] }
|
||||
} else if (form.value.repeatType === 'monthly') {
|
||||
data.repeat = { monthly: [...form.value.monthly] }
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const schema = await store.get(id)
|
||||
original.value = schema
|
||||
form.value = loadFromSchema(schema)
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
}
|
||||
})
|
||||
|
||||
async function onUpdate() {
|
||||
submitting.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await store.update(id, buildPayload())
|
||||
router.push('/schemas')
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
if (original.value) {
|
||||
form.value = loadFromSchema(original.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form v-if="original" class="schema-form" @submit.prevent="onUpdate">
|
||||
<div class="field">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" v-model="form.name" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="status">Schema Status</label>
|
||||
<select id="status" v-model="form.status">
|
||||
<option value="active">active</option>
|
||||
<option value="inactive">inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="taskStatus">Task Status</label>
|
||||
<select id="taskStatus" v-model="form.taskStatus">
|
||||
<option value="active">active</option>
|
||||
<option value="done">done</option>
|
||||
<option value="inactive">inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="repeatType">Wiederholung</label>
|
||||
<select id="repeatType" v-model="form.repeatType">
|
||||
<option value="none">Keine (Einmalig)</option>
|
||||
<option value="daily">Täglich</option>
|
||||
<option value="weekly">Wöchentlich</option>
|
||||
<option value="monthly">Monatlich</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType === 'none'" class="field">
|
||||
<label for="date">Datum</label>
|
||||
<input id="date" v-model="form.date" type="date" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType === 'weekly'" class="field">
|
||||
<label>Wochentage</label>
|
||||
<div class="day-grid">
|
||||
<label v-for="(day, i) in weekdays" :key="i" class="day-label" :class="{ selected: form.weekly[i] }">
|
||||
<input type="checkbox" v-model="form.weekly[i]" class="sr-only" />
|
||||
{{ day }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType === 'monthly'" class="field">
|
||||
<label>Monatstage</label>
|
||||
<div class="day-grid">
|
||||
<label v-for="d in 31" :key="d" class="day-label" :class="{ selected: form.monthly[d - 1] }">
|
||||
<input type="checkbox" v-model="form.monthly[d - 1]" class="sr-only" />
|
||||
{{ d }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.repeatType !== 'none'" class="field-row">
|
||||
<div class="field">
|
||||
<label for="start">Start</label>
|
||||
<input id="start" v-model="form.start" type="date" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="end">Ende</label>
|
||||
<input id="end" v-model="form.end" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" :disabled="submitting || !form.name">Aktualisieren</button>
|
||||
<button type="button" @click="onReset">Zurücksetzen</button>
|
||||
<button type="button" @click="router.back()">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
<p v-else-if="error" class="error">{{ error }}</p>
|
||||
<p v-else class="hint">Lädt…</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.schema-form {
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.field-row .field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.day-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.day-label.selected {
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--breadcrumb-bg);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,10 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="task-view">
|
||||
<div class="actions">
|
||||
<button class="icon-btn" @click="router.push('/tasks/create')" title="Neuer Task">
|
||||
<button class="icon-btn" @click="router.push('/schemas')" title="Schemas">
|
||||
<Icon name="calendar" />
|
||||
</button>
|
||||
<button class="icon-btn" @click="router.push('/schemas/create')" title="Neues Schema">
|
||||
<Icon name="plus" />
|
||||
</button>
|
||||
<button class="icon-btn" @click="router.push('/tasks/all')" title="Alle Tasks">
|
||||
|
||||
@@ -24,7 +24,10 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="task-all-view">
|
||||
<div class="actions">
|
||||
<button class="icon-btn" @click="router.push('/tasks/create')" title="Neuer Task">
|
||||
<button class="icon-btn" @click="router.push('/schemas')" title="Schemas">
|
||||
<Icon name="calendar" />
|
||||
</button>
|
||||
<button class="icon-btn" @click="router.push('/schemas/create')" title="Neues Schema">
|
||||
<Icon name="plus" />
|
||||
</button>
|
||||
<button class="icon-btn" @click="store.fetchAll()" title="Neu laden">
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTasksStore } from '../stores/tasks'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useTasksStore()
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
date: '',
|
||||
status: 'active',
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
store.fetchStatuses()
|
||||
})
|
||||
|
||||
async function onSave() {
|
||||
submitting.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await store.create({
|
||||
name: form.value.name,
|
||||
date: form.value.date || null,
|
||||
status: form.value.status,
|
||||
})
|
||||
router.push('/tasks')
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onAbort() {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="task-form" @submit.prevent="onSave">
|
||||
<div class="field">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" v-model="form.name" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="date">Datum</label>
|
||||
<input id="date" v-model="form.date" type="date" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" v-model="form.status">
|
||||
<option v-for="key in store.availableStatuses" :key="key" :value="key">
|
||||
{{ key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" :disabled="submitting">Speichern</button>
|
||||
<button type="button" @click="onAbort">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-form {
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--breadcrumb-bg);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
background: var(--text);
|
||||
color: var(--bg);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user