update schema module

This commit is contained in:
Marek Lenczewski
2026-04-14 00:43:47 +02:00
parent 4a053c5ee7
commit d576747155
14 changed files with 148 additions and 142 deletions

View File

@@ -97,6 +97,8 @@ private fun SchemaRow(
schema.repeat == null -> "Einmalig"
schema.repeat.containsKey("daily") -> "Täglich"
schema.repeat.containsKey("weekly") -> "Wöchentlich"
schema.repeat.containsKey("2week") -> "2-Wöchentlich"
schema.repeat.containsKey("4week") -> "4-Wöchentlich"
schema.repeat.containsKey("monthly") -> "Monatlich"
else -> ""
}

View File

@@ -91,7 +91,7 @@ fun RepeatTypeDropdown(
current: String,
onChange: (String) -> Unit,
) {
val options = listOf("none" to "Keine (Einmalig)", "daily" to "Täglich", "weekly" to "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")
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
OutlinedTextField(

View File

@@ -71,7 +71,7 @@ fun SchemaCreateScreen(
)
}
if (viewModel.repeatType == "weekly") {
if (viewModel.repeatType in listOf("weekly", "2week", "4week")) {
WeekdaySelector(
selected = viewModel.weekly,
onChange = { viewModel.weekly = it },

View File

@@ -48,6 +48,8 @@ class SchemaCreateViewModel : ViewModel() {
val repeat = when (repeatType) {
"daily" -> JsonObject(mapOf("daily" to JsonPrimitive(true)))
"weekly" -> JsonObject(mapOf("weekly" 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)) } }))
"monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } }))
else -> null
}

View File

@@ -80,7 +80,7 @@ fun SchemaEditScreen(
)
}
if (viewModel.repeatType == "weekly") {
if (viewModel.repeatType in listOf("weekly", "2week", "4week")) {
WeekdaySelector(
selected = viewModel.weekly,
onChange = { viewModel.weekly = it },

View File

@@ -96,6 +96,16 @@ class SchemaEditViewModel : ViewModel() {
weekly = schema.repeat["weekly"]!!.jsonArray.map { it.jsonPrimitive.boolean }
monthly = List(31) { false }
}
schema.repeat.containsKey("2week") -> {
repeatType = "2week"
weekly = schema.repeat["2week"]!!.jsonArray.map { it.jsonPrimitive.boolean }
monthly = List(31) { false }
}
schema.repeat.containsKey("4week") -> {
repeatType = "4week"
weekly = schema.repeat["4week"]!!.jsonArray.map { it.jsonPrimitive.boolean }
monthly = List(31) { false }
}
schema.repeat.containsKey("monthly") -> {
repeatType = "monthly"
weekly = List(7) { false }
@@ -108,6 +118,8 @@ class SchemaEditViewModel : ViewModel() {
val repeat = when (repeatType) {
"daily" -> JsonObject(mapOf("daily" to JsonPrimitive(true)))
"weekly" -> JsonObject(mapOf("weekly" 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)) } }))
"monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } }))
else -> null
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Helper;
class DateHelper
{
public static function isInRange(\DateTimeImmutable $date): bool
{
$today = new \DateTimeImmutable('today');
return $date >= $today && $date <= $today->modify('+14 days');
}
/** @return array{\DateTimeImmutable, \DateTimeImmutable} */
public static function getDateRange(?\DateTimeImmutable $start, ?\DateTimeImmutable $end): array
{
$today = new \DateTimeImmutable('today');
$from = max($today, $start ?? $today);
$to = min($today->modify('+14 days'), $end ?? $today->modify('+14 days'));
return [$from, $to];
}
public static function getWeeksDiff(\DateTimeImmutable $start, \DateTimeImmutable $date): int
{
return (int) ($start->modify('monday this week')->diff($date->modify('monday this week'))->days / 7);
}
}

View File

@@ -24,11 +24,10 @@ class TaskSchemaRepository extends ServiceEntityRepository
}
/** @return list<TaskSchema> */
public function findActiveWithRepeat(): array
public function findActive(): array
{
return $this->createQueryBuilder('s')
->andWhere('s.status = :status')
->andWhere('s.repeat IS NOT NULL')
->setParameter('status', TaskSchemaStatus::Active)
->getQuery()
->getResult();

View File

@@ -5,6 +5,7 @@ namespace App\Service;
use App\Entity\Task;
use App\Entity\TaskSchema;
use App\Enum\TaskStatus;
use App\Helper\DateHelper;
use App\Repository\TaskSchemaRepository;
use Doctrine\ORM\EntityManagerInterface;
@@ -18,7 +19,7 @@ class TaskGenerator
public function generateNewTasks(): void
{
$schemas = $this->schemaRepo->findActiveWithRepeat();
$schemas = $this->schemaRepo->findActive();
foreach ($schemas as $schema) {
$this->removeTasks($schema);
@@ -50,38 +51,49 @@ class TaskGenerator
}
}
/** @return array{\DateTimeImmutable, \DateTimeImmutable} */
private function getDateRange(TaskSchema $schema): array
{
$today = new \DateTimeImmutable('today');
$from = max($today, $schema->getStart() ?? $today);
$end = min($today->modify('+14 days'), $schema->getEnd() ?? $today->modify('+14 days'));
return [$from, $end];
}
/** @return list<\DateTimeImmutable> */
private function getDates(TaskSchema $schema): array
{
[$from, $end] = $this->getDateRange($schema);
$type = $schema->getRepeatType();
$repeat = $schema->getRepeat();
if ($schema->getRepeatType() === null) {
$date = $schema->getDate();
return DateHelper::isInRange($date) ? [$date] : [];
}
[$from, $end] = DateHelper::getDateRange($schema->getStart(), $schema->getEnd());
$dates = [];
for ($date = $from; $date <= $end; $date = $date->modify('+1 day')) {
if ($type === 'weekly') {
$weekday = (int) $date->format('N') - 1;
if(!$repeat['weekly'][$weekday]) continue;
}
if ($type === 'monthly') {
$monthday = (int) $date->format('j') - 1;
if(!$repeat['monthly'][$monthday]) continue;
}
if ($this->matchesDate($schema, $date)) {
$dates[] = $date;
}
}
return $dates;
}
private function matchesDate(TaskSchema $schema, \DateTimeImmutable $date): bool
{
$type = $schema->getRepeatType();
$repeat = $schema->getRepeat();
if ($type === 'daily') {
return true;
}
if ($type === 'weekly' || $type === '2week' || $type === '4week') {
$weekday = (int) $date->format('N') - 1;
if (!$repeat[$type][$weekday]) return false;
if ($type === 'weekly') return true;
$start = $schema->getStart() ?? new \DateTimeImmutable('today');
return DateHelper::getWeeksDiff($start, $date) % ($type === '2week' ? 2 : 4) === 0;
}
if ($type === 'monthly') {
$monthday = (int) $date->format('j') - 1;
return $repeat['monthly'][$monthday];
}
return false;
}
}

View File

@@ -18,10 +18,9 @@ class TaskSchemaManager
public function create(TaskSchemaRequest $req): void
{
if ($req->repeat === null) {
if ($req->repeat === null && $req->date === null) {
$task = new Task();
$task->setName($req->name);
$task->setDate($req->date);
$task->setStatus($req->taskStatus);
$this->em->persist($task);
$this->em->flush();

View File

@@ -11,6 +11,8 @@ 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['2week']) return '2-Wöchentlich'
if (schema.repeat['4week']) return '4-Wöchentlich'
if (schema.repeat.monthly) return 'Monatlich'
return ''
}

View File

@@ -40,6 +40,10 @@ function buildPayload() {
data.repeat = { daily: true }
} else if (form.value.repeatType === 'weekly') {
data.repeat = { weekly: [...form.value.weekly] }
} else if (form.value.repeatType === '2week') {
data.repeat = { '2week': [...form.value.weekly] }
} else if (form.value.repeatType === '4week') {
data.repeat = { '4week': [...form.value.weekly] }
} else if (form.value.repeatType === 'monthly') {
data.repeat = { monthly: [...form.value.monthly] }
}
@@ -93,6 +97,8 @@ async function onSave() {
<option value="none">Keine (Einmalig)</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
<option value="2week">2-Wöchentlich</option>
<option value="4week">4-Wöchentlich</option>
<option value="monthly">Monatlich</option>
</select>
</div>
@@ -102,7 +108,7 @@ async function onSave() {
<input id="date" v-model="form.date" type="date" />
</div>
<div v-if="form.repeatType === 'weekly'" class="field">
<div v-if="['weekly', '2week', '4week'].includes(form.repeatType)" 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] }">

View File

@@ -34,6 +34,8 @@ function detectRepeatType(schema) {
if (!schema.repeat) return 'none'
if (schema.repeat.daily) return 'daily'
if (schema.repeat.weekly) return 'weekly'
if (schema.repeat['2week']) return '2week'
if (schema.repeat['4week']) return '4week'
if (schema.repeat.monthly) return 'monthly'
return 'none'
}
@@ -48,7 +50,10 @@ function loadFromSchema(schema) {
date: toFormDate(schema.date),
start: toFormDate(schema.start),
end: toFormDate(schema.end),
weekly: schema.repeat?.weekly ? [...schema.repeat.weekly] : Array(7).fill(false),
weekly: schema.repeat?.weekly ? [...schema.repeat.weekly]
: schema.repeat?.['2week'] ? [...schema.repeat['2week']]
: schema.repeat?.['4week'] ? [...schema.repeat['4week']]
: Array(7).fill(false),
monthly: schema.repeat?.monthly ? [...schema.repeat.monthly] : Array(31).fill(false),
}
}
@@ -70,6 +75,10 @@ function buildPayload() {
data.repeat = { daily: true }
} else if (form.value.repeatType === 'weekly') {
data.repeat = { weekly: [...form.value.weekly] }
} else if (form.value.repeatType === '2week') {
data.repeat = { '2week': [...form.value.weekly] }
} else if (form.value.repeatType === '4week') {
data.repeat = { '4week': [...form.value.weekly] }
} else if (form.value.repeatType === 'monthly') {
data.repeat = { monthly: [...form.value.monthly] }
}
@@ -139,6 +148,8 @@ function onReset() {
<option value="none">Keine (Einmalig)</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
<option value="2week">2-Wöchentlich</option>
<option value="4week">4-Wöchentlich</option>
<option value="monthly">Monatlich</option>
</select>
</div>
@@ -148,7 +159,7 @@ function onReset() {
<input id="date" v-model="form.date" type="date" />
</div>
<div v-if="form.repeatType === 'weekly'" class="field">
<div v-if="['weekly', '2week', '4week'].includes(form.repeatType)" 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] }">

148
module.md
View File

@@ -1,115 +1,49 @@
# Datei
Implementierungs-Schritte als Feature-Module - WIE es gebaut wird
# Setup module
## Backend
- Setup Symfony ./backend
## Frontend
- Setup Vue ./frontend, router, pinia
- App.vue - no content
## App
- Setup Kotlin ./app, navigation compose
- MainScreen.kt - no content
## Features
- Symfony, Vue and Kotlin minimal setup, no content
- Symfony ./backend
- Vue ./frontend
- Kotlin ./app
- Kotlin copy vue changes
- Symfony, Vue and Kotlin can start
# Base module
## Backend
- nothing
## Frontend
- App.vue - layout: breadcrumb, main area
- router - / start page route
- Startpage.vue - start page, no content
## App
- MainScreen.kt - layout: breadcrumb, main area
- StartScreen.kt - start page, no content
## Features
- Standard layout for all pages: breadcrumb, main area
- layout: Breadcrumb, main area
- start page: /, empty
# Task module
## Backend
- Task - Task entity
- id, name, date (due), status
- TaskStatus - Enum for task status
- active, done, inactive
- TaskController - Task routes
- index, show, create, update, delete, toggle (active/done)
- TaskManager - Task CRUD
- create, update, delete, toggle
- TaskRepository - Default task queries
- currentTasks()
- TaskDto - Dto for create and update task
## Frontend
- Startpage.vue - quader button for tasks
- App.vue - register task routes in breadcrumb.
- router - tasks routes /tasks, /tasks/all, /tasks/create, /tasks/:id
- Task.vue
- Display current tasks (now to +2 weeks and without date) as list with name (done strikethrough), onclick toggle status, order by date (no-date then date asc, hide inactive)
- top right nav - list icon (all tasks), + icon (create), eye icon (toggle task visibility by active/done)
- TaskAll.vue
- Display all tasks as list with name (done strikethrough, past faded), pencil icon (edit), bin icon (delete), onclick toggle status, order by date (no-date then date asc)
- top right nav - + icon (create)
- TaskCreate.vue - Display form with name-text, date-date, status-select, save-button, abort-button
- TaskEdit.vue
- Display form with name-text, date-date, status-select, update-button, reset-button, abort-button, use current values
- api.js - API routes to symfony
## App
- StartScreen.kt - quader button for tasks
- MainScreen.kt - register task routes in breadcrumb.
- NavHost - tasks routes /tasks, /tasks/all, /tasks/create, /tasks/:id
- TaskScreen.kt
- Display current tasks (now to +2 weeks and without date) as list with name (done strikethrough), onclick toggle status, order by date (no-date then date asc, hide inactive)
- top right nav - list icon (all tasks), + icon (create), eye icon (toggle task visibility by active/done)
- TaskAllScreen.kt
- Display all tasks as list with name (done strikethrough, past faded), pencil icon (edit), bin icon (delete), onclick toggle status, order by date (no-date then date asc)
- top right nav - + icon (create)
- TaskCreateScreen.kt - Display form with name-text, date-date, status-select, save-button, abort-button
- TaskEditScreen.kt
- Display form with name-text, date-date, status-select, update-button, reset-button, abort-button, use current values
- TaskApi.kt - API calls to symfony
## Features
- Start page: task button
- Task page: current tasks ordered by date, filter done
- TaskAll page: all tasks ordered by date, past faded, delete task
- TaskCreate page: create task
- TaskEdit page: update task
- task: id, name, date?, status, schema?
- status: active, done, inactive, past (< today)
- breadcrumb: /tasks, /tasks/all, /tasks/:id
- period: today to +2 weeks
- done task: strikethrough, less opacity, default eye hide
- inactive task: less opacity, smaller
# App update module
## Backend
- public/app/version.json - version info (versionCode, apkFile)
- public/app/haushalt.apk - current APK (manually deployed)
## App
- AppUpdateApi.kt - version check endpoint
- AppUpdater.kt - check version, download APK, trigger install
- StartScreen.kt - "Update prüfen" button with state feedback
- AndroidManifest.xml - REQUEST_INSTALL_PACKAGES, FileProvider
- file_paths.xml - cache path for downloaded APK
## Features
- Manual update check from start screen
- Download and install new APK from server
- No Play Store required
- 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)
- 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
- 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)
# AppUpdate module
- version: public/app/version.json
- apk: public/app/haushalt.apk
- start page: button check version, download apk, trigger install
# TaskSchema module
## Backend
- Task - add schema (n:1)
- TaskSchema - id, name, status, taskStatus, date, repeat (json), start, end
- repeat=null → single, repeat={"daily"/"weekly"/"monthly":...} → repeating
- TaskSchemaStatus - active, inactive
- TaskController - remove create route
- TaskManager - remove create
- TaskSchemaController - index, show, create, update, delete
- TaskSchemaManager - create (single=task only, repeat=schema+generate), update (remove+generate), delete (remove+schema)
- TaskGenerator - generateTasks, removeTasks, generateNewTasks (scheduler)
- Scheduler - daily at 03:00, messenger:consume via DDEV daemon
- Migration - task_schema table + schema_id FK
## Frontend
- TaskCreate.vue removed, SchemaCreate/SchemaEdit/SchemaAll added
- Task.vue + TaskAll.vue - calendar + plus icons → /schemas, /schemas/create
- Form: name, (status + taskStatus), repeat, weekday/monthday grid, (start + end)
## App
- same changes as Frontend
## Features
- Single schema: task directly, no schema persisted
- Repeat schema: tasks for period (max 14 days), scheduler fills daily
- Update: remove non-past tasks + regenerate
- Delete: remove non-past tasks + schema
- task schema: id, name, status, taskStatus, type, date?, repeat?, start?, end?
- status: active, inactive
- type: single (show date), repeat (show repeat, start, end)
- type(single, !date): just create task
- type(single, date): create schema, schema creates task
- 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
- 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 update: remove and create task in period (if task ref schema)
- schema delete: remove tasks in period (if task ref schema)
- scheduler: execute 3:00, create task (if date=today), remove schemas (if end < today)
- router - /schemas, /schemas/create, /schemas/:id
- list page: list all schemas (name, repeat label, edit, delete), navigation (create, refresh)
- create page: form (name, status, taskStatus, type, date, repeat, weekday/monthday, start, end), buttons(save, abort)
- edit page: like create but with current values