update task and schema module

This commit is contained in:
Marek Lenczewski
2026-04-14 18:15:05 +02:00
parent d576747155
commit 0d028beda8
23 changed files with 227 additions and 59 deletions

1
app/.gitignore vendored
View File

@@ -10,4 +10,3 @@
.cxx .cxx
local.properties local.properties
app/build app/build
*.jks

View File

@@ -13,8 +13,8 @@ android {
applicationId = "de.haushalt.app" applicationId = "de.haushalt.app"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 4 versionCode = 5
versionName = "0.1.0" versionName = "0.1.1"
} }
signingConfigs { signingConfigs {
@@ -23,6 +23,9 @@ android {
storePassword = "haushalt123" storePassword = "haushalt123"
keyAlias = "haushalt" keyAlias = "haushalt"
keyPassword = "haushalt123" keyPassword = "haushalt123"
enableV1Signing = true
enableV2Signing = true
enableV3Signing = true
} }
} }

View File

@@ -100,6 +100,7 @@ private fun SchemaRow(
schema.repeat.containsKey("2week") -> "2-Wöchentlich" schema.repeat.containsKey("2week") -> "2-Wöchentlich"
schema.repeat.containsKey("4week") -> "4-Wöchentlich" schema.repeat.containsKey("4week") -> "4-Wöchentlich"
schema.repeat.containsKey("monthly") -> "Monatlich" schema.repeat.containsKey("monthly") -> "Monatlich"
schema.repeat.containsKey("days") -> "Mehrere Tage"
else -> "" else -> ""
} }

View File

@@ -5,16 +5,23 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -23,10 +30,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.haushalt.app.data.TaskSchemaStatus import de.haushalt.app.data.TaskSchemaStatus
import de.haushalt.app.data.TaskStatus import de.haushalt.app.data.TaskStatus
import de.haushalt.app.ui.task.DatePickerField
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -91,7 +100,7 @@ fun RepeatTypeDropdown(
current: String, current: String,
onChange: (String) -> Unit, onChange: (String) -> Unit,
) { ) {
val options = listOf("none" to "Keine (Einmalig)", "daily" to "Täglich", "weekly" to "Wöchentlich", "2week" to "2-Wöchentlich", "4week" to "4-Wöchentlich", "monthly" to "Monatlich") val options = listOf("none" to "Keine (Einmalig)", "daily" to "Täglich", "weekly" to "Wöchentlich", "2week" to "2-Wöchentlich", "4week" to "4-Wöchentlich", "monthly" to "Monatlich", "days" to "Mehrere Tage")
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
OutlinedTextField( OutlinedTextField(
@@ -172,3 +181,31 @@ fun MonthdaySelector(
} }
} }
} }
@Composable
fun DaysSelector(
days: List<String>,
onChange: (List<String>) -> Unit,
) {
Text("Termine", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
days.forEachIndexed { i, day ->
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
DatePickerField(
value = day,
onChange = { newVal -> onChange(days.toMutableList().also { it[i] = newVal }) },
label = "Datum",
modifier = Modifier.weight(1f),
)
IconButton(onClick = { onChange(days.toMutableList().also { it.removeAt(i) }) }) {
Icon(Icons.Filled.Delete, contentDescription = "Entfernen")
}
}
}
OutlinedButton(onClick = { onChange(days + "") }) {
Icon(Icons.Filled.Add, contentDescription = null)
Text("Termin")
}
}

View File

@@ -78,6 +78,13 @@ fun SchemaCreateScreen(
) )
} }
if (viewModel.repeatType == "days") {
DaysSelector(
days = viewModel.days,
onChange = { viewModel.days = it },
)
}
if (viewModel.repeatType == "monthly") { if (viewModel.repeatType == "monthly") {
MonthdaySelector( MonthdaySelector(
selected = viewModel.monthly, selected = viewModel.monthly,
@@ -85,7 +92,7 @@ fun SchemaCreateScreen(
) )
} }
if (viewModel.repeatType != "none") { if (viewModel.repeatType !in listOf("none", "days")) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),

View File

@@ -24,6 +24,7 @@ class SchemaCreateViewModel : ViewModel() {
var end by mutableStateOf("") var end by mutableStateOf("")
var weekly by mutableStateOf(List(7) { false }) var weekly by mutableStateOf(List(7) { false })
var monthly by mutableStateOf(List(31) { false }) var monthly by mutableStateOf(List(31) { false })
var days by mutableStateOf(listOf<String>())
var isSubmitting by mutableStateOf(false) var isSubmitting by mutableStateOf(false)
private set private set
var error by mutableStateOf<String?>(null) var error by mutableStateOf<String?>(null)
@@ -51,6 +52,7 @@ class SchemaCreateViewModel : ViewModel() {
"2week" -> JsonObject(mapOf("2week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } })) "2week" -> JsonObject(mapOf("2week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } }))
"4week" -> JsonObject(mapOf("4week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } })) "4week" -> JsonObject(mapOf("4week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } }))
"monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } })) "monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } }))
"days" -> JsonObject(mapOf("days" to buildJsonArray { days.filter { it.isNotBlank() }.forEach { add(JsonPrimitive(it)) } }))
else -> null else -> null
} }

View File

@@ -87,6 +87,13 @@ fun SchemaEditScreen(
) )
} }
if (viewModel.repeatType == "days") {
DaysSelector(
days = viewModel.days,
onChange = { viewModel.days = it },
)
}
if (viewModel.repeatType == "monthly") { if (viewModel.repeatType == "monthly") {
MonthdaySelector( MonthdaySelector(
selected = viewModel.monthly, selected = viewModel.monthly,
@@ -94,7 +101,7 @@ fun SchemaEditScreen(
) )
} }
if (viewModel.repeatType != "none") { if (viewModel.repeatType !in listOf("none", "days")) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -125,9 +132,6 @@ fun SchemaEditScreen(
) { ) {
Text("Aktualisieren") Text("Aktualisieren")
} }
OutlinedButton(onClick = { viewModel.reset() }) {
Text("Zurücksetzen")
}
OutlinedButton(onClick = { navController.popBackStack() }) { OutlinedButton(onClick = { navController.popBackStack() }) {
Text("Abbrechen") Text("Abbrechen")
} }

View File

@@ -16,6 +16,7 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
class SchemaEditViewModel : ViewModel() { class SchemaEditViewModel : ViewModel() {
@@ -28,6 +29,7 @@ class SchemaEditViewModel : ViewModel() {
var end by mutableStateOf("") var end by mutableStateOf("")
var weekly by mutableStateOf(List(7) { false }) var weekly by mutableStateOf(List(7) { false })
var monthly by mutableStateOf(List(31) { false }) var monthly by mutableStateOf(List(31) { false })
var days by mutableStateOf(listOf<String>())
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
private set private set
var isSubmitting by mutableStateOf(false) var isSubmitting by mutableStateOf(false)
@@ -35,15 +37,12 @@ class SchemaEditViewModel : ViewModel() {
var error by mutableStateOf<String?>(null) var error by mutableStateOf<String?>(null)
private set private set
private var original: TaskSchema? = null
fun load(id: Int) { fun load(id: Int) {
viewModelScope.launch { viewModelScope.launch {
isLoading = true isLoading = true
error = null error = null
try { try {
val schema = ApiClient.schemaApi.get(id) val schema = ApiClient.schemaApi.get(id)
original = schema
applySchema(schema) applySchema(schema)
} catch (e: Exception) { } catch (e: Exception) {
error = e.message error = e.message
@@ -68,10 +67,6 @@ class SchemaEditViewModel : ViewModel() {
} }
} }
fun reset() {
original?.let { applySchema(it) }
}
private fun applySchema(schema: TaskSchema) { private fun applySchema(schema: TaskSchema) {
name = schema.name name = schema.name
status = schema.status status = schema.status
@@ -85,31 +80,43 @@ class SchemaEditViewModel : ViewModel() {
repeatType = "none" repeatType = "none"
weekly = List(7) { false } weekly = List(7) { false }
monthly = List(31) { false } monthly = List(31) { false }
days = listOf()
} }
schema.repeat.containsKey("daily") -> { schema.repeat.containsKey("daily") -> {
repeatType = "daily" repeatType = "daily"
weekly = List(7) { false } weekly = List(7) { false }
monthly = List(31) { false } monthly = List(31) { false }
days = listOf()
} }
schema.repeat.containsKey("weekly") -> { schema.repeat.containsKey("weekly") -> {
repeatType = "weekly" repeatType = "weekly"
weekly = schema.repeat["weekly"]!!.jsonArray.map { it.jsonPrimitive.boolean } weekly = schema.repeat["weekly"]!!.jsonArray.map { it.jsonPrimitive.boolean }
monthly = List(31) { false } monthly = List(31) { false }
days = listOf()
} }
schema.repeat.containsKey("2week") -> { schema.repeat.containsKey("2week") -> {
repeatType = "2week" repeatType = "2week"
weekly = schema.repeat["2week"]!!.jsonArray.map { it.jsonPrimitive.boolean } weekly = schema.repeat["2week"]!!.jsonArray.map { it.jsonPrimitive.boolean }
monthly = List(31) { false } monthly = List(31) { false }
days = listOf()
} }
schema.repeat.containsKey("4week") -> { schema.repeat.containsKey("4week") -> {
repeatType = "4week" repeatType = "4week"
weekly = schema.repeat["4week"]!!.jsonArray.map { it.jsonPrimitive.boolean } weekly = schema.repeat["4week"]!!.jsonArray.map { it.jsonPrimitive.boolean }
monthly = List(31) { false } monthly = List(31) { false }
days = listOf()
} }
schema.repeat.containsKey("monthly") -> { schema.repeat.containsKey("monthly") -> {
repeatType = "monthly" repeatType = "monthly"
weekly = List(7) { false } weekly = List(7) { false }
monthly = schema.repeat["monthly"]!!.jsonArray.map { it.jsonPrimitive.boolean } monthly = schema.repeat["monthly"]!!.jsonArray.map { it.jsonPrimitive.boolean }
days = listOf()
}
schema.repeat.containsKey("days") -> {
repeatType = "days"
weekly = List(7) { false }
monthly = List(31) { false }
days = schema.repeat["days"]!!.jsonArray.map { it.jsonPrimitive.contentOrNull ?: "" }
} }
} }
} }
@@ -121,6 +128,7 @@ class SchemaEditViewModel : ViewModel() {
"2week" -> JsonObject(mapOf("2week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } })) "2week" -> JsonObject(mapOf("2week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } }))
"4week" -> JsonObject(mapOf("4week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } })) "4week" -> JsonObject(mapOf("4week" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } }))
"monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } })) "monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } }))
"days" -> JsonObject(mapOf("days" to buildJsonArray { days.filter { it.isNotBlank() }.forEach { add(JsonPrimitive(it)) } }))
else -> null else -> null
} }

View File

@@ -69,9 +69,6 @@ fun TaskEditScreen(
) { ) {
Text("Aktualisieren") Text("Aktualisieren")
} }
OutlinedButton(onClick = { viewModel.reset() }) {
Text("Zurücksetzen")
}
OutlinedButton(onClick = { navController.popBackStack() }) { OutlinedButton(onClick = { navController.popBackStack() }) {
Text("Abbrechen") Text("Abbrechen")
} }

View File

@@ -12,8 +12,6 @@ import de.haushalt.app.data.TaskStatus
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TaskEditViewModel : ViewModel() { class TaskEditViewModel : ViewModel() {
private var original: Task? = null
var name by mutableStateOf("") var name by mutableStateOf("")
var date by mutableStateOf("") var date by mutableStateOf("")
var status by mutableStateOf(TaskStatus.active) var status by mutableStateOf(TaskStatus.active)
@@ -42,7 +40,6 @@ class TaskEditViewModel : ViewModel() {
error = null error = null
try { try {
val task = ApiClient.taskApi.get(id) val task = ApiClient.taskApi.get(id)
original = task
applyTask(task) applyTask(task)
} catch (e: Exception) { } catch (e: Exception) {
error = e.message error = e.message
@@ -74,10 +71,6 @@ class TaskEditViewModel : ViewModel() {
} }
} }
fun reset() {
original?.let { applyTask(it) }
}
private fun applyTask(task: Task) { private fun applyTask(task: Task) {
name = task.name name = task.name
date = task.date?.take(10) ?: "" date = task.date?.take(10) ?: ""

BIN
app/haushalt.jks Executable file

Binary file not shown.

View File

@@ -2,7 +2,3 @@ framework:
messenger: messenger:
transports: transports:
sync: 'sync://' sync: 'sync://'
scheduler_default: 'scheduler://default'
routing:
App\Message\GenerateTasksMessage: scheduler_default

Binary file not shown.

View File

@@ -1,4 +1,4 @@
{ {
"versionCode": 4, "versionCode": 5,
"apkFile": "haushalt.apk" "apkFile": "haushalt.apk"
} }

View File

@@ -24,7 +24,7 @@ class TaskSchemaRequest
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])] #[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
public readonly ?\DateTimeImmutable $date = null, public readonly ?\DateTimeImmutable $date = null,
public readonly ?array $repeat = null, public ?array $repeat = null,
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])] #[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
public readonly ?\DateTimeImmutable $start = null, public readonly ?\DateTimeImmutable $start = null,
@@ -32,5 +32,8 @@ class TaskSchemaRequest
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])] #[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
public readonly ?\DateTimeImmutable $end = null, public readonly ?\DateTimeImmutable $end = null,
) { ) {
if (isset($this->repeat['days']) && is_array($this->repeat['days'])) {
$this->repeat['days'] = array_values(array_unique($this->repeat['days']));
}
} }
} }

View File

@@ -32,4 +32,15 @@ class TaskSchemaRepository extends ServiceEntityRepository
->getQuery() ->getQuery()
->getResult(); ->getResult();
} }
/** @return list<TaskSchema> */
public function findExpired(): array
{
return $this->createQueryBuilder('s')
->andWhere('s.end IS NOT NULL')
->andWhere('s.end < :today')
->setParameter('today', new \DateTimeImmutable('today'))
->getQuery()
->getResult();
}
} }

View File

@@ -22,6 +22,6 @@ class Schedule implements ScheduleProviderInterface
return (new SymfonySchedule()) return (new SymfonySchedule())
->stateful($this->cache) ->stateful($this->cache)
->processOnlyLastMissedRun(true) ->processOnlyLastMissedRun(true)
->add(RecurringMessage::cron('0 3 * * *', new GenerateTasksMessage())); ->add(RecurringMessage::every('1 day', new GenerateTasksMessage(), from: new \DateTimeImmutable('03:00')));
} }
} }

View File

@@ -19,11 +19,20 @@ class TaskGenerator
public function generateNewTasks(): void public function generateNewTasks(): void
{ {
$this->deleteExpiredSchemas();
$today = new \DateTimeImmutable('today');
$schemas = $this->schemaRepo->findActive(); $schemas = $this->schemaRepo->findActive();
foreach ($schemas as $schema) { foreach ($schemas as $schema) {
$this->removeTasks($schema); if ($schema->getRepeatType() === null) {
$this->generateTasks($schema); $date = $schema->getDate();
if ($date !== null && $date->format('Y-m-d') === $today->format('Y-m-d')) {
$this->createTask($schema, $today);
}
} elseif ($this->matchesDate($schema, $today)) {
$this->createTask($schema, $today);
}
} }
$this->em->flush(); $this->em->flush();
@@ -42,6 +51,12 @@ class TaskGenerator
$dates = $this->getDates($schema); $dates = $this->getDates($schema);
foreach ($dates as $date) { foreach ($dates as $date) {
$this->createTask($schema, $date);
}
}
private function createTask(TaskSchema $schema, \DateTimeImmutable $date): void
{
$task = new Task(); $task = new Task();
$task->setName($schema->getName()); $task->setName($schema->getName());
$task->setDate($date); $task->setDate($date);
@@ -49,6 +64,14 @@ class TaskGenerator
$task->setSchema($schema); $task->setSchema($schema);
$this->em->persist($task); $this->em->persist($task);
} }
private function deleteExpiredSchemas(): void
{
$expired = $this->schemaRepo->findExpired();
foreach ($expired as $schema) {
$this->removeTasks($schema);
$this->em->remove($schema);
}
} }
/** @return list<\DateTimeImmutable> */ /** @return list<\DateTimeImmutable> */
@@ -94,6 +117,10 @@ class TaskGenerator
return $repeat['monthly'][$monthday]; return $repeat['monthly'][$monthday];
} }
if ($type === 'days') {
return in_array($date->format('Y-m-d'), $repeat['days'], true);
}
return false; return false;
} }
} }

View File

@@ -14,6 +14,7 @@ function repeatLabel(schema) {
if (schema.repeat['2week']) return '2-Wöchentlich' if (schema.repeat['2week']) return '2-Wöchentlich'
if (schema.repeat['4week']) return '4-Wöchentlich' if (schema.repeat['4week']) return '4-Wöchentlich'
if (schema.repeat.monthly) return 'Monatlich' if (schema.repeat.monthly) return 'Monatlich'
if (schema.repeat.days) return 'Mehrere Tage'
return '' return ''
} }

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useSchemasStore } from '../stores/schemas' import { useSchemasStore } from '../stores/schemas'
import Icon from '../components/Icon.vue'
const router = useRouter() const router = useRouter()
const store = useSchemasStore() const store = useSchemasStore()
@@ -16,6 +17,7 @@ const form = ref({
end: '', end: '',
weekly: Array(7).fill(false), weekly: Array(7).fill(false),
monthly: Array(31).fill(false), monthly: Array(31).fill(false),
days: [],
}) })
const submitting = ref(false) const submitting = ref(false)
@@ -46,6 +48,8 @@ function buildPayload() {
data.repeat = { '4week': [...form.value.weekly] } data.repeat = { '4week': [...form.value.weekly] }
} else if (form.value.repeatType === 'monthly') { } else if (form.value.repeatType === 'monthly') {
data.repeat = { monthly: [...form.value.monthly] } data.repeat = { monthly: [...form.value.monthly] }
} else if (form.value.repeatType === 'days') {
data.repeat = { days: form.value.days.filter(d => d) }
} }
} }
@@ -100,6 +104,7 @@ async function onSave() {
<option value="2week">2-Wöchentlich</option> <option value="2week">2-Wöchentlich</option>
<option value="4week">4-Wöchentlich</option> <option value="4week">4-Wöchentlich</option>
<option value="monthly">Monatlich</option> <option value="monthly">Monatlich</option>
<option value="days">Mehrere Tage</option>
</select> </select>
</div> </div>
@@ -128,7 +133,18 @@ async function onSave() {
</div> </div>
</div> </div>
<div v-if="form.repeatType !== 'none'" class="field-row"> <div v-if="form.repeatType === 'days'" class="field">
<label>Termine</label>
<div v-for="(d, i) in form.days" :key="i" class="days-row">
<input type="date" v-model="form.days[i]" />
<button type="button" class="icon-btn" @click="form.days.splice(i, 1)" title="Entfernen">
<Icon name="trash" />
</button>
</div>
<button type="button" class="add-termin" @click="form.days.push('')">+ Termin</button>
</div>
<div v-if="!['none', 'days'].includes(form.repeatType)" class="field-row">
<div class="field"> <div class="field">
<label for="start">Start</label> <label for="start">Start</label>
<input id="start" v-model="form.start" type="date" /> <input id="start" v-model="form.start" type="date" />
@@ -249,6 +265,35 @@ button:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.days-row {
display: flex;
gap: 0.5rem;
}
.days-row input {
flex: 1;
}
.icon-btn {
width: 2.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: 1px solid var(--border);
border-radius: 0.375rem;
cursor: pointer;
}
.icon-btn:hover {
background: var(--breadcrumb-bg);
}
.add-termin {
align-self: flex-start;
}
.error { .error {
color: #dc2626; color: #dc2626;
} }

View File

@@ -2,6 +2,7 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useSchemasStore } from '../stores/schemas' import { useSchemasStore } from '../stores/schemas'
import Icon from '../components/Icon.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -19,6 +20,7 @@ const form = ref({
end: '', end: '',
weekly: Array(7).fill(false), weekly: Array(7).fill(false),
monthly: Array(31).fill(false), monthly: Array(31).fill(false),
days: [],
}) })
const submitting = ref(false) const submitting = ref(false)
const error = ref(null) const error = ref(null)
@@ -37,6 +39,7 @@ function detectRepeatType(schema) {
if (schema.repeat['2week']) return '2week' if (schema.repeat['2week']) return '2week'
if (schema.repeat['4week']) return '4week' if (schema.repeat['4week']) return '4week'
if (schema.repeat.monthly) return 'monthly' if (schema.repeat.monthly) return 'monthly'
if (schema.repeat.days) return 'days'
return 'none' return 'none'
} }
@@ -55,6 +58,7 @@ function loadFromSchema(schema) {
: schema.repeat?.['4week'] ? [...schema.repeat['4week']] : schema.repeat?.['4week'] ? [...schema.repeat['4week']]
: Array(7).fill(false), : Array(7).fill(false),
monthly: schema.repeat?.monthly ? [...schema.repeat.monthly] : Array(31).fill(false), monthly: schema.repeat?.monthly ? [...schema.repeat.monthly] : Array(31).fill(false),
days: schema.repeat?.days ? [...schema.repeat.days] : [],
} }
} }
@@ -81,6 +85,8 @@ function buildPayload() {
data.repeat = { '4week': [...form.value.weekly] } data.repeat = { '4week': [...form.value.weekly] }
} else if (form.value.repeatType === 'monthly') { } else if (form.value.repeatType === 'monthly') {
data.repeat = { monthly: [...form.value.monthly] } data.repeat = { monthly: [...form.value.monthly] }
} else if (form.value.repeatType === 'days') {
data.repeat = { days: form.value.days.filter(d => d) }
} }
} }
@@ -110,11 +116,6 @@ async function onUpdate() {
} }
} }
function onReset() {
if (original.value) {
form.value = loadFromSchema(original.value)
}
}
</script> </script>
<template> <template>
@@ -151,6 +152,7 @@ function onReset() {
<option value="2week">2-Wöchentlich</option> <option value="2week">2-Wöchentlich</option>
<option value="4week">4-Wöchentlich</option> <option value="4week">4-Wöchentlich</option>
<option value="monthly">Monatlich</option> <option value="monthly">Monatlich</option>
<option value="days">Mehrere Tage</option>
</select> </select>
</div> </div>
@@ -179,7 +181,18 @@ function onReset() {
</div> </div>
</div> </div>
<div v-if="form.repeatType !== 'none'" class="field-row"> <div v-if="form.repeatType === 'days'" class="field">
<label>Termine</label>
<div v-for="(d, i) in form.days" :key="i" class="days-row">
<input type="date" v-model="form.days[i]" />
<button type="button" class="icon-btn" @click="form.days.splice(i, 1)" title="Entfernen">
<Icon name="trash" />
</button>
</div>
<button type="button" class="add-termin" @click="form.days.push('')">+ Termin</button>
</div>
<div v-if="!['none', 'days'].includes(form.repeatType)" class="field-row">
<div class="field"> <div class="field">
<label for="start">Start</label> <label for="start">Start</label>
<input id="start" v-model="form.start" type="date" /> <input id="start" v-model="form.start" type="date" />
@@ -194,7 +207,6 @@ function onReset() {
<div class="form-actions"> <div class="form-actions">
<button type="submit" :disabled="submitting || !form.name">Aktualisieren</button> <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> <button type="button" @click="router.back()">Abbrechen</button>
</div> </div>
</form> </form>
@@ -303,6 +315,35 @@ button:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.days-row {
display: flex;
gap: 0.5rem;
}
.days-row input {
flex: 1;
}
.icon-btn {
width: 2.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: 1px solid var(--border);
border-radius: 0.375rem;
cursor: pointer;
}
.icon-btn:hover {
background: var(--breadcrumb-bg);
}
.add-termin {
align-self: flex-start;
}
.error { .error {
color: #dc2626; color: #dc2626;
} }

View File

@@ -54,12 +54,6 @@ async function onUpdate() {
} }
} }
function onReset() {
if (original.value) {
form.value = loadFromTask(original.value)
}
}
function onAbort() { function onAbort() {
router.back() router.back()
} }
@@ -90,7 +84,6 @@ function onAbort() {
<div class="form-actions"> <div class="form-actions">
<button type="submit" :disabled="submitting">Aktualisieren</button> <button type="submit" :disabled="submitting">Aktualisieren</button>
<button type="button" @click="onReset">Zurücksetzen</button>
<button type="button" @click="onAbort">Abbrechen</button> <button type="button" @click="onAbort">Abbrechen</button>
</div> </div>
</form> </form>

View File

@@ -20,8 +20,8 @@
- start page: /tasks button - start page: /tasks button
- tasks page: list tasks (name, toggle onclick), only period, group by date, no-date first, hide inactive, navigation (schemas, create, list, toggle, refresh) - tasks page: list tasks (name, toggle onclick), only period, group by date, no-date first, hide inactive, navigation (schemas, create, list, toggle, refresh)
- all tasks page: list all task (name, toggle onclick, edit, delete), sort by date desc, no-date-first, navigation (schemas, create, refresh) - all tasks page: list all task (name, toggle onclick, edit, delete), sort by date desc, no-date-first, navigation (schemas, create, refresh)
- edit page: form (name, date, status), current values, buttons(save, reset, abort) remove schema on update - edit page: form (name, date, status), current values, buttons(save, abort) remove schema on update
- navigation: calender icon (schemas), + icon (create), list icon (all tasks), eye icon (toggle), arrow (refresh), pencil icon (edit), bin icon (delete), save icon (save), reset icon (reset), abort icon (abort) - navigation: calender icon (schemas), + icon (create), list icon (all tasks), eye icon (toggle), arrow (refresh), pencil icon (edit), bin icon (delete), save icon (save), abort icon (abort)
# AppUpdate module # AppUpdate module
- version: public/app/version.json - version: public/app/version.json
@@ -35,7 +35,7 @@
- type(single, !date): just create task - type(single, !date): just create task
- type(single, date): create schema, schema creates task - type(single, date): create schema, schema creates task
- type(repeat, daily/weekly/2weekly/4weekly/monthly): create schema, schema creates tasks - type(repeat, daily/weekly/2weekly/4weekly/monthly): create schema, schema creates tasks
- type(repeat, days): + icon (add input:date), add multiple dates, like single+date in bulk, create schema, schema creates tasks - type(repeat, days): + icon (add input:date), add multiple dates, like single+date in bulk, create schema, schema creates tasks, bin icon (remove date)
- schema: creates tasks in period, schema update and delete affects only tasks in period (no-past), start(today if null), delete schema (if end < today) - schema: creates tasks in period, schema update and delete affects only tasks in period (no-past), start(today if null), delete schema (if end < today)
- schema create: create tasks in period - schema create: create tasks in period
- schema update: remove and create task in period (if task ref schema) - schema update: remove and create task in period (if task ref schema)