update task and schema module
This commit is contained in:
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -10,4 +10,3 @@
|
|||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
app/build
|
app/build
|
||||||
*.jks
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
BIN
app/haushalt.jks
Executable file
Binary file not shown.
@@ -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.
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"versionCode": 4,
|
"versionCode": 5,
|
||||||
"apkFile": "haushalt.apk"
|
"apkFile": "haushalt.apk"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +51,26 @@ class TaskGenerator
|
|||||||
$dates = $this->getDates($schema);
|
$dates = $this->getDates($schema);
|
||||||
|
|
||||||
foreach ($dates as $date) {
|
foreach ($dates as $date) {
|
||||||
$task = new Task();
|
$this->createTask($schema, $date);
|
||||||
$task->setName($schema->getName());
|
}
|
||||||
$task->setDate($date);
|
}
|
||||||
$task->setStatus($schema->getTaskStatus());
|
|
||||||
$task->setSchema($schema);
|
private function createTask(TaskSchema $schema, \DateTimeImmutable $date): void
|
||||||
$this->em->persist($task);
|
{
|
||||||
|
$task = new Task();
|
||||||
|
$task->setName($schema->getName());
|
||||||
|
$task->setDate($date);
|
||||||
|
$task->setStatus($schema->getTaskStatus());
|
||||||
|
$task->setSchema($schema);
|
||||||
|
$this->em->persist($task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteExpiredSchemas(): void
|
||||||
|
{
|
||||||
|
$expired = $this->schemaRepo->findExpired();
|
||||||
|
foreach ($expired as $schema) {
|
||||||
|
$this->removeTasks($schema);
|
||||||
|
$this->em->remove($schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user