TaskSchema module
This commit is contained in:
@@ -27,6 +27,9 @@ web_extra_daemons:
|
|||||||
- name: vite
|
- name: vite
|
||||||
command: "npm run dev"
|
command: "npm run dev"
|
||||||
directory: /var/www/html/frontend
|
directory: /var/www/html/frontend
|
||||||
|
- name: scheduler
|
||||||
|
command: "php /var/www/html/backend/bin/console messenger:consume scheduler_default --time-limit=3600"
|
||||||
|
directory: /var/www/html/backend
|
||||||
|
|
||||||
# Key features of DDEV's config.yaml:
|
# Key features of DDEV's config.yaml:
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
|
import de.haushalt.app.ui.schema.SchemaAllScreen
|
||||||
|
import de.haushalt.app.ui.schema.SchemaCreateScreen
|
||||||
|
import de.haushalt.app.ui.schema.SchemaEditScreen
|
||||||
import de.haushalt.app.ui.task.TaskAllScreen
|
import de.haushalt.app.ui.task.TaskAllScreen
|
||||||
import de.haushalt.app.ui.task.TaskCreateScreen
|
|
||||||
import de.haushalt.app.ui.task.TaskEditScreen
|
import de.haushalt.app.ui.task.TaskEditScreen
|
||||||
import de.haushalt.app.ui.task.TaskScreen
|
import de.haushalt.app.ui.task.TaskScreen
|
||||||
|
|
||||||
@@ -35,16 +37,28 @@ private val trailMap: Map<String, List<Pair<String, String>>> = mapOf(
|
|||||||
"tasks" to "Tasks",
|
"tasks" to "Tasks",
|
||||||
"tasks/all" to "All",
|
"tasks/all" to "All",
|
||||||
),
|
),
|
||||||
"tasks/create" to listOf(
|
|
||||||
"start" to "Haushalt",
|
|
||||||
"tasks" to "Tasks",
|
|
||||||
"tasks/create" to "Create",
|
|
||||||
),
|
|
||||||
"tasks/{id}" to listOf(
|
"tasks/{id}" to listOf(
|
||||||
"start" to "Haushalt",
|
"start" to "Haushalt",
|
||||||
"tasks" to "Tasks",
|
"tasks" to "Tasks",
|
||||||
"tasks/{id}" to "Edit",
|
"tasks/{id}" to "Edit",
|
||||||
),
|
),
|
||||||
|
"schemas" to listOf(
|
||||||
|
"start" to "Haushalt",
|
||||||
|
"tasks" to "Tasks",
|
||||||
|
"schemas" to "Schemas",
|
||||||
|
),
|
||||||
|
"schemas/create" to listOf(
|
||||||
|
"start" to "Haushalt",
|
||||||
|
"tasks" to "Tasks",
|
||||||
|
"schemas" to "Schemas",
|
||||||
|
"schemas/create" to "Create",
|
||||||
|
),
|
||||||
|
"schemas/{id}" to listOf(
|
||||||
|
"start" to "Haushalt",
|
||||||
|
"tasks" to "Tasks",
|
||||||
|
"schemas" to "Schemas",
|
||||||
|
"schemas/{id}" to "Edit",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -70,9 +84,6 @@ fun MainScreen() {
|
|||||||
composable("tasks/all") {
|
composable("tasks/all") {
|
||||||
TaskAllScreen(navController = navController)
|
TaskAllScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable("tasks/create") {
|
|
||||||
TaskCreateScreen(navController = navController)
|
|
||||||
}
|
|
||||||
composable(
|
composable(
|
||||||
route = "tasks/{id}",
|
route = "tasks/{id}",
|
||||||
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||||
@@ -80,6 +91,19 @@ fun MainScreen() {
|
|||||||
val id = entry.arguments?.getInt("id") ?: return@composable
|
val id = entry.arguments?.getInt("id") ?: return@composable
|
||||||
TaskEditScreen(navController = navController, taskId = id)
|
TaskEditScreen(navController = navController, taskId = id)
|
||||||
}
|
}
|
||||||
|
composable("schemas") {
|
||||||
|
SchemaAllScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("schemas/create") {
|
||||||
|
SchemaCreateScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
route = "schemas/{id}",
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||||
|
) { entry ->
|
||||||
|
val id = entry.arguments?.getInt("id") ?: return@composable
|
||||||
|
SchemaEditScreen(navController = navController, schemaId = id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ object ApiClient {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val taskApi: TaskApi = retrofit.create(TaskApi::class.java)
|
val taskApi: TaskApi = retrofit.create(TaskApi::class.java)
|
||||||
|
val schemaApi: TaskSchemaApi = retrofit.create(TaskSchemaApi::class.java)
|
||||||
|
|
||||||
val appUpdateApi: AppUpdateApi = Retrofit.Builder()
|
val appUpdateApi: AppUpdateApi = Retrofit.Builder()
|
||||||
.baseUrl(BASE_URL)
|
.baseUrl(BASE_URL)
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ interface TaskApi {
|
|||||||
@GET("tasks/{id}")
|
@GET("tasks/{id}")
|
||||||
suspend fun get(@Path("id") id: Int): Task
|
suspend fun get(@Path("id") id: Int): Task
|
||||||
|
|
||||||
@POST("tasks")
|
|
||||||
suspend fun create(@Body body: TaskRequest): Task
|
|
||||||
|
|
||||||
@PUT("tasks/{id}")
|
@PUT("tasks/{id}")
|
||||||
suspend fun update(@Path("id") id: Int, @Body body: TaskRequest): Task
|
suspend fun update(@Path("id") id: Int, @Body body: TaskRequest): Task
|
||||||
|
|
||||||
|
|||||||
32
app/app/src/main/java/de/haushalt/app/data/TaskSchema.kt
Normal file
32
app/app/src/main/java/de/haushalt/app/data/TaskSchema.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package de.haushalt.app.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class TaskSchemaStatus {
|
||||||
|
active, inactive,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TaskSchema(
|
||||||
|
val id: Int? = null,
|
||||||
|
val name: String,
|
||||||
|
val status: TaskSchemaStatus = TaskSchemaStatus.active,
|
||||||
|
val taskStatus: TaskStatus = TaskStatus.active,
|
||||||
|
val date: String? = null,
|
||||||
|
val repeat: JsonObject? = null,
|
||||||
|
val start: String? = null,
|
||||||
|
val end: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TaskSchemaRequest(
|
||||||
|
val name: String,
|
||||||
|
val status: TaskSchemaStatus = TaskSchemaStatus.active,
|
||||||
|
val taskStatus: TaskStatus = TaskStatus.active,
|
||||||
|
val date: String? = null,
|
||||||
|
val repeat: JsonObject? = null,
|
||||||
|
val start: String? = null,
|
||||||
|
val end: String? = null,
|
||||||
|
)
|
||||||
26
app/app/src/main/java/de/haushalt/app/data/TaskSchemaApi.kt
Normal file
26
app/app/src/main/java/de/haushalt/app/data/TaskSchemaApi.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package de.haushalt.app.data
|
||||||
|
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.DELETE
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface TaskSchemaApi {
|
||||||
|
@GET("task-schemas")
|
||||||
|
suspend fun list(): List<TaskSchema>
|
||||||
|
|
||||||
|
@GET("task-schemas/{id}")
|
||||||
|
suspend fun get(@Path("id") id: Int): TaskSchema
|
||||||
|
|
||||||
|
@POST("task-schemas")
|
||||||
|
suspend fun create(@Body body: TaskSchemaRequest): TaskSchema
|
||||||
|
|
||||||
|
@PUT("task-schemas/{id}")
|
||||||
|
suspend fun update(@Path("id") id: Int, @Body body: TaskSchemaRequest): TaskSchema
|
||||||
|
|
||||||
|
@DELETE("task-schemas/{id}")
|
||||||
|
suspend fun delete(@Path("id") id: Int)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import de.haushalt.app.data.TaskSchema
|
||||||
|
import de.haushalt.app.data.TaskSchemaStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SchemaAllScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: SchemaAllViewModel = viewModel(),
|
||||||
|
) {
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
DisposableEffect(lifecycleOwner) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { navController.navigate("schemas/create") }) {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = "Neues Schema")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { viewModel.refresh() }) {
|
||||||
|
Icon(Icons.Filled.Refresh, contentDescription = "Neu laden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.padding(4.dp))
|
||||||
|
|
||||||
|
when {
|
||||||
|
viewModel.isLoading && viewModel.schemas.isEmpty() -> Text("Lädt…")
|
||||||
|
viewModel.error != null -> Text(viewModel.error ?: "", color = MaterialTheme.colorScheme.error)
|
||||||
|
viewModel.schemas.isEmpty() -> Text("Keine Schemas.")
|
||||||
|
else -> LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(viewModel.schemas, key = { it.id ?: 0 }) { schema ->
|
||||||
|
SchemaRow(
|
||||||
|
schema = schema,
|
||||||
|
onEdit = { schema.id?.let { navController.navigate("schemas/$it") } },
|
||||||
|
onDelete = { schema.id?.let(viewModel::delete) },
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SchemaRow(
|
||||||
|
schema: TaskSchema,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
val repeatLabel = when {
|
||||||
|
schema.repeat == null -> "Einmalig"
|
||||||
|
schema.repeat.containsKey("daily") -> "Täglich"
|
||||||
|
schema.repeat.containsKey("weekly") -> "Wöchentlich"
|
||||||
|
schema.repeat.containsKey("monthly") -> "Monatlich"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.alpha(if (schema.status == TaskSchemaStatus.inactive) 0.5f else 1f)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = schema.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontStyle = if (schema.status == TaskSchemaStatus.inactive) FontStyle.Italic else null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = repeatLabel,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onEdit) {
|
||||||
|
Icon(Icons.Filled.Edit, contentDescription = "Bearbeiten")
|
||||||
|
}
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(Icons.Filled.Delete, contentDescription = "Löschen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.haushalt.app.data.ApiClient
|
||||||
|
import de.haushalt.app.data.TaskSchema
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SchemaAllViewModel : ViewModel() {
|
||||||
|
var schemas by mutableStateOf<List<TaskSchema>>(emptyList())
|
||||||
|
private set
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
try {
|
||||||
|
schemas = ApiClient.schemaApi.list()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(id: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
ApiClient.schemaApi.delete(id)
|
||||||
|
schemas = schemas.filter { it.id != id }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MenuAnchorType
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.haushalt.app.data.TaskSchemaStatus
|
||||||
|
import de.haushalt.app.data.TaskStatus
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SchemaStatusDropdown(
|
||||||
|
current: TaskSchemaStatus,
|
||||||
|
onChange: (TaskSchemaStatus) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }, modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = current.name,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Schema Status") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||||
|
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
TaskSchemaStatus.entries.forEach { status ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(status.name) },
|
||||||
|
onClick = { onChange(status); expanded = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TaskStatusDropdown(
|
||||||
|
current: TaskStatus,
|
||||||
|
onChange: (TaskStatus) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val selectable = TaskStatus.entries.filter { it != TaskStatus.past }
|
||||||
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }, modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = current.name,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Task Status") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||||
|
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
selectable.forEach { status ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(status.name) },
|
||||||
|
onClick = { onChange(status); expanded = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
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")
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = options.firstOrNull { it.first == current }?.second ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Wiederholung") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||||
|
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
options.forEach { (key, label) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(label) },
|
||||||
|
onClick = { onChange(key); expanded = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun WeekdaySelector(
|
||||||
|
selected: List<Boolean>,
|
||||||
|
onChange: (List<Boolean>) -> Unit,
|
||||||
|
) {
|
||||||
|
val days = listOf("Mo", "Di", "Mi", "Do", "Fr", "Sa", "So")
|
||||||
|
Text("Wochentage", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
days.forEachIndexed { i, day ->
|
||||||
|
val isSelected = selected[i]
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onChange(selected.toMutableList().also { it[i] = !it[i] })
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
border = BorderStroke(1.dp, if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline),
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = day,
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MonthdaySelector(
|
||||||
|
selected: List<Boolean>,
|
||||||
|
onChange: (List<Boolean>) -> Unit,
|
||||||
|
) {
|
||||||
|
Text("Monatstage", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
for (d in 1..31) {
|
||||||
|
val isSelected = selected[d - 1]
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(40.dp).clickable {
|
||||||
|
onChange(selected.toMutableList().also { it[d - 1] = !it[d - 1] })
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
border = BorderStroke(1.dp, if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline),
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$d",
|
||||||
|
modifier = Modifier.padding(4.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import de.haushalt.app.data.TaskSchemaStatus
|
||||||
|
import de.haushalt.app.data.TaskStatus
|
||||||
|
import de.haushalt.app.ui.task.DatePickerField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SchemaCreateScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: SchemaCreateViewModel = viewModel(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = viewModel.name,
|
||||||
|
onValueChange = { viewModel.name = it },
|
||||||
|
label = { Text("Name") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
SchemaStatusDropdown(
|
||||||
|
current = viewModel.status,
|
||||||
|
onChange = { viewModel.status = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
TaskStatusDropdown(
|
||||||
|
current = viewModel.taskStatus,
|
||||||
|
onChange = { viewModel.taskStatus = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RepeatTypeDropdown(
|
||||||
|
current = viewModel.repeatType,
|
||||||
|
onChange = { viewModel.repeatType = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewModel.repeatType == "none") {
|
||||||
|
DatePickerField(
|
||||||
|
value = viewModel.date,
|
||||||
|
onChange = { viewModel.date = it },
|
||||||
|
label = "Datum",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.repeatType == "weekly") {
|
||||||
|
WeekdaySelector(
|
||||||
|
selected = viewModel.weekly,
|
||||||
|
onChange = { viewModel.weekly = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.repeatType == "monthly") {
|
||||||
|
MonthdaySelector(
|
||||||
|
selected = viewModel.monthly,
|
||||||
|
onChange = { viewModel.monthly = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.repeatType != "none") {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
DatePickerField(
|
||||||
|
value = viewModel.start,
|
||||||
|
onChange = { viewModel.start = it },
|
||||||
|
label = "Start",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
DatePickerField(
|
||||||
|
value = viewModel.end,
|
||||||
|
onChange = { viewModel.end = it },
|
||||||
|
label = "Ende",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.error?.let {
|
||||||
|
Text(it, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Button(
|
||||||
|
enabled = !viewModel.isSubmitting && viewModel.name.isNotBlank(),
|
||||||
|
onClick = { viewModel.save { navController.popBackStack() } },
|
||||||
|
) {
|
||||||
|
Text("Erstellen")
|
||||||
|
}
|
||||||
|
OutlinedButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Text("Abbrechen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.haushalt.app.data.ApiClient
|
||||||
|
import de.haushalt.app.data.TaskSchemaRequest
|
||||||
|
import de.haushalt.app.data.TaskSchemaStatus
|
||||||
|
import de.haushalt.app.data.TaskStatus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
|
||||||
|
class SchemaCreateViewModel : ViewModel() {
|
||||||
|
var name by mutableStateOf("")
|
||||||
|
var status by mutableStateOf(TaskSchemaStatus.active)
|
||||||
|
var taskStatus by mutableStateOf(TaskStatus.active)
|
||||||
|
var repeatType by mutableStateOf("none")
|
||||||
|
var date by mutableStateOf("")
|
||||||
|
var start by mutableStateOf("")
|
||||||
|
var end by mutableStateOf("")
|
||||||
|
var weekly by mutableStateOf(List(7) { false })
|
||||||
|
var monthly by mutableStateOf(List(31) { false })
|
||||||
|
var isSubmitting by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun save(onSuccess: () -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isSubmitting = true
|
||||||
|
error = null
|
||||||
|
try {
|
||||||
|
ApiClient.schemaApi.create(buildRequest())
|
||||||
|
onSuccess()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRequest(): TaskSchemaRequest {
|
||||||
|
val repeat = when (repeatType) {
|
||||||
|
"daily" -> JsonObject(mapOf("daily" to JsonPrimitive(true)))
|
||||||
|
"weekly" -> JsonObject(mapOf("weekly" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } }))
|
||||||
|
"monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } }))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskSchemaRequest(
|
||||||
|
name = name,
|
||||||
|
status = status,
|
||||||
|
taskStatus = taskStatus,
|
||||||
|
date = if (repeatType == "none" && date.isNotBlank()) date else null,
|
||||||
|
repeat = repeat,
|
||||||
|
start = if (repeatType != "none" && start.isNotBlank()) start else null,
|
||||||
|
end = if (repeatType != "none" && end.isNotBlank()) end else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import de.haushalt.app.ui.task.DatePickerField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SchemaEditScreen(
|
||||||
|
navController: NavController,
|
||||||
|
schemaId: Int,
|
||||||
|
viewModel: SchemaEditViewModel = viewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(schemaId) {
|
||||||
|
viewModel.load(schemaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.isLoading) {
|
||||||
|
Text("Lädt…", modifier = Modifier.padding(16.dp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = viewModel.name,
|
||||||
|
onValueChange = { viewModel.name = it },
|
||||||
|
label = { Text("Name") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
SchemaStatusDropdown(
|
||||||
|
current = viewModel.status,
|
||||||
|
onChange = { viewModel.status = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
TaskStatusDropdown(
|
||||||
|
current = viewModel.taskStatus,
|
||||||
|
onChange = { viewModel.taskStatus = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RepeatTypeDropdown(
|
||||||
|
current = viewModel.repeatType,
|
||||||
|
onChange = { viewModel.repeatType = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewModel.repeatType == "none") {
|
||||||
|
DatePickerField(
|
||||||
|
value = viewModel.date,
|
||||||
|
onChange = { viewModel.date = it },
|
||||||
|
label = "Datum",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.repeatType == "weekly") {
|
||||||
|
WeekdaySelector(
|
||||||
|
selected = viewModel.weekly,
|
||||||
|
onChange = { viewModel.weekly = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.repeatType == "monthly") {
|
||||||
|
MonthdaySelector(
|
||||||
|
selected = viewModel.monthly,
|
||||||
|
onChange = { viewModel.monthly = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.repeatType != "none") {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
DatePickerField(
|
||||||
|
value = viewModel.start,
|
||||||
|
onChange = { viewModel.start = it },
|
||||||
|
label = "Start",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
DatePickerField(
|
||||||
|
value = viewModel.end,
|
||||||
|
onChange = { viewModel.end = it },
|
||||||
|
label = "Ende",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.error?.let {
|
||||||
|
Text(it, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Button(
|
||||||
|
enabled = !viewModel.isSubmitting && viewModel.name.isNotBlank(),
|
||||||
|
onClick = { viewModel.update(schemaId) { navController.popBackStack() } },
|
||||||
|
) {
|
||||||
|
Text("Aktualisieren")
|
||||||
|
}
|
||||||
|
OutlinedButton(onClick = { viewModel.reset() }) {
|
||||||
|
Text("Zurücksetzen")
|
||||||
|
}
|
||||||
|
OutlinedButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Text("Abbrechen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package de.haushalt.app.ui.schema
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.haushalt.app.data.ApiClient
|
||||||
|
import de.haushalt.app.data.TaskSchema
|
||||||
|
import de.haushalt.app.data.TaskSchemaRequest
|
||||||
|
import de.haushalt.app.data.TaskSchemaStatus
|
||||||
|
import de.haushalt.app.data.TaskStatus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
class SchemaEditViewModel : ViewModel() {
|
||||||
|
var name by mutableStateOf("")
|
||||||
|
var status by mutableStateOf(TaskSchemaStatus.active)
|
||||||
|
var taskStatus by mutableStateOf(TaskStatus.active)
|
||||||
|
var repeatType by mutableStateOf("none")
|
||||||
|
var date by mutableStateOf("")
|
||||||
|
var start by mutableStateOf("")
|
||||||
|
var end by mutableStateOf("")
|
||||||
|
var weekly by mutableStateOf(List(7) { false })
|
||||||
|
var monthly by mutableStateOf(List(31) { false })
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var isSubmitting by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var original: TaskSchema? = null
|
||||||
|
|
||||||
|
fun load(id: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
try {
|
||||||
|
val schema = ApiClient.schemaApi.get(id)
|
||||||
|
original = schema
|
||||||
|
applySchema(schema)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(id: Int, onSuccess: () -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isSubmitting = true
|
||||||
|
error = null
|
||||||
|
try {
|
||||||
|
ApiClient.schemaApi.update(id, buildRequest())
|
||||||
|
onSuccess()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
original?.let { applySchema(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applySchema(schema: TaskSchema) {
|
||||||
|
name = schema.name
|
||||||
|
status = schema.status
|
||||||
|
taskStatus = schema.taskStatus
|
||||||
|
date = schema.date?.take(10) ?: ""
|
||||||
|
start = schema.start?.take(10) ?: ""
|
||||||
|
end = schema.end?.take(10) ?: ""
|
||||||
|
|
||||||
|
when {
|
||||||
|
schema.repeat == null -> {
|
||||||
|
repeatType = "none"
|
||||||
|
weekly = List(7) { false }
|
||||||
|
monthly = List(31) { false }
|
||||||
|
}
|
||||||
|
schema.repeat.containsKey("daily") -> {
|
||||||
|
repeatType = "daily"
|
||||||
|
weekly = List(7) { false }
|
||||||
|
monthly = List(31) { false }
|
||||||
|
}
|
||||||
|
schema.repeat.containsKey("weekly") -> {
|
||||||
|
repeatType = "weekly"
|
||||||
|
weekly = schema.repeat["weekly"]!!.jsonArray.map { it.jsonPrimitive.boolean }
|
||||||
|
monthly = List(31) { false }
|
||||||
|
}
|
||||||
|
schema.repeat.containsKey("monthly") -> {
|
||||||
|
repeatType = "monthly"
|
||||||
|
weekly = List(7) { false }
|
||||||
|
monthly = schema.repeat["monthly"]!!.jsonArray.map { it.jsonPrimitive.boolean }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRequest(): TaskSchemaRequest {
|
||||||
|
val repeat = when (repeatType) {
|
||||||
|
"daily" -> JsonObject(mapOf("daily" to JsonPrimitive(true)))
|
||||||
|
"weekly" -> JsonObject(mapOf("weekly" to buildJsonArray { weekly.forEach { add(JsonPrimitive(it)) } }))
|
||||||
|
"monthly" -> JsonObject(mapOf("monthly" to buildJsonArray { monthly.forEach { add(JsonPrimitive(it)) } }))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskSchemaRequest(
|
||||||
|
name = name,
|
||||||
|
status = status,
|
||||||
|
taskStatus = taskStatus,
|
||||||
|
date = if (repeatType == "none" && date.isNotBlank()) date else null,
|
||||||
|
repeat = repeat,
|
||||||
|
start = if (repeatType != "none" && start.isNotBlank()) start else null,
|
||||||
|
end = if (repeatType != "none" && end.isNotBlank()) end else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.DateRange
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
@@ -65,8 +66,11 @@ fun TaskAllScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { navController.navigate("tasks/create") }) {
|
IconButton(onClick = { navController.navigate("schemas") }) {
|
||||||
Icon(Icons.Filled.Add, contentDescription = "Neuer Task")
|
Icon(Icons.Filled.DateRange, contentDescription = "Schemas")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { navController.navigate("schemas/create") }) {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = "Neues Schema")
|
||||||
}
|
}
|
||||||
IconButton(onClick = { viewModel.refresh() }) {
|
IconButton(onClick = { viewModel.refresh() }) {
|
||||||
Icon(Icons.Filled.Refresh, contentDescription = "Neu laden")
|
Icon(Icons.Filled.Refresh, contentDescription = "Neu laden")
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package de.haushalt.app.ui.task
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TaskCreateScreen(
|
|
||||||
navController: NavController,
|
|
||||||
viewModel: TaskCreateViewModel = viewModel(),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = viewModel.name,
|
|
||||||
onValueChange = { viewModel.name = it },
|
|
||||||
label = { Text("Name") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
DatePickerField(
|
|
||||||
value = viewModel.date,
|
|
||||||
onChange = { viewModel.date = it },
|
|
||||||
)
|
|
||||||
|
|
||||||
StatusDropdown(
|
|
||||||
current = viewModel.status,
|
|
||||||
selectable = viewModel.availableStatuses,
|
|
||||||
onChange = { viewModel.status = it },
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.error?.let {
|
|
||||||
Text(it, color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Button(
|
|
||||||
enabled = !viewModel.isSubmitting && viewModel.name.isNotBlank(),
|
|
||||||
onClick = { viewModel.save { navController.popBackStack() } },
|
|
||||||
) {
|
|
||||||
Text("Speichern")
|
|
||||||
}
|
|
||||||
OutlinedButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Text("Abbrechen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package de.haushalt.app.ui.task
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import de.haushalt.app.data.ApiClient
|
|
||||||
import de.haushalt.app.data.TaskRequest
|
|
||||||
import de.haushalt.app.data.TaskStatus
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class TaskCreateViewModel : ViewModel() {
|
|
||||||
var name by mutableStateOf("")
|
|
||||||
var date by mutableStateOf("")
|
|
||||||
var status by mutableStateOf(TaskStatus.active)
|
|
||||||
var availableStatuses by mutableStateOf<List<TaskStatus>>(emptyList())
|
|
||||||
private set
|
|
||||||
var isSubmitting by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
var error by mutableStateOf<String?>(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
availableStatuses = ApiClient.taskApi.statuses()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
availableStatuses = emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(onSuccess: () -> Unit) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
isSubmitting = true
|
|
||||||
error = null
|
|
||||||
try {
|
|
||||||
ApiClient.taskApi.create(
|
|
||||||
TaskRequest(
|
|
||||||
name = name,
|
|
||||||
date = date.ifBlank { null },
|
|
||||||
status = status,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
onSuccess()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = e.message
|
|
||||||
} finally {
|
|
||||||
isSubmitting = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.List
|
import androidx.compose.material.icons.automirrored.filled.List
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.DateRange
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
@@ -66,8 +67,11 @@ fun TaskScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { navController.navigate("tasks/create") }) {
|
IconButton(onClick = { navController.navigate("schemas") }) {
|
||||||
Icon(Icons.Filled.Add, contentDescription = "Neuer Task")
|
Icon(Icons.Filled.DateRange, contentDescription = "Schemas")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { navController.navigate("schemas/create") }) {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = "Neues Schema")
|
||||||
}
|
}
|
||||||
IconButton(onClick = { navController.navigate("tasks/all") }) {
|
IconButton(onClick = { navController.navigate("tasks/all") }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "Alle Tasks")
|
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "Alle Tasks")
|
||||||
|
|||||||
@@ -38,3 +38,10 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1|haushalt\.ddev\.site)(:[0-9
|
|||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||||
###< doctrine/doctrine-bundle ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> symfony/messenger ###
|
||||||
|
# Choose one of the transports below
|
||||||
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||||
|
###< symfony/messenger ###
|
||||||
|
|||||||
@@ -19,9 +19,11 @@
|
|||||||
"symfony/dotenv": "7.4.*",
|
"symfony/dotenv": "7.4.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "7.4.*",
|
"symfony/framework-bundle": "7.4.*",
|
||||||
|
"symfony/messenger": "7.4.*",
|
||||||
"symfony/property-access": "7.4.*",
|
"symfony/property-access": "7.4.*",
|
||||||
"symfony/property-info": "7.4.*",
|
"symfony/property-info": "7.4.*",
|
||||||
"symfony/runtime": "7.4.*",
|
"symfony/runtime": "7.4.*",
|
||||||
|
"symfony/scheduler": "7.4.*",
|
||||||
"symfony/serializer": "7.4.*",
|
"symfony/serializer": "7.4.*",
|
||||||
"symfony/validator": "7.4.*",
|
"symfony/validator": "7.4.*",
|
||||||
"symfony/yaml": "7.4.*"
|
"symfony/yaml": "7.4.*"
|
||||||
|
|||||||
307
backend/composer.lock
generated
307
backend/composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "36cb2b820400a518223222a28461ff75",
|
"content-hash": "3faf0714de84c701cf5b3c444ce28e8b",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "doctrine/collections",
|
"name": "doctrine/collections",
|
||||||
@@ -1456,6 +1456,54 @@
|
|||||||
},
|
},
|
||||||
"time": "2021-02-03T23:26:27+00:00"
|
"time": "2021-02-03T23:26:27+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/clock",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/clock.git",
|
||||||
|
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
|
||||||
|
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Clock\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for reading the clock.",
|
||||||
|
"homepage": "https://github.com/php-fig/clock",
|
||||||
|
"keywords": [
|
||||||
|
"clock",
|
||||||
|
"now",
|
||||||
|
"psr",
|
||||||
|
"psr-20",
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-fig/clock/issues",
|
||||||
|
"source": "https://github.com/php-fig/clock/tree/1.0.0"
|
||||||
|
},
|
||||||
|
"time": "2022-11-25T14:36:26+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/container",
|
"name": "psr/container",
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
@@ -1789,6 +1837,84 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-13T15:25:07+00:00"
|
"time": "2025-03-13T15:25:07+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/clock",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/clock.git",
|
||||||
|
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
|
||||||
|
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/clock": "^1.0",
|
||||||
|
"symfony/polyfill-php83": "^1.28"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"psr/clock-implementation": "1.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"Resources/now.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Clock\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Decouples applications from the system clock",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"clock",
|
||||||
|
"psr20",
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/clock/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-24T13:12:05+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/config",
|
"name": "symfony/config",
|
||||||
"version": "v7.4.7",
|
"version": "v7.4.7",
|
||||||
@@ -3121,6 +3247,100 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-03-06T16:33:18+00:00"
|
"time": "2026-03-06T16:33:18+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/messenger",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/messenger.git",
|
||||||
|
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/messenger/zipball/ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
|
||||||
|
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^1|^2|^3",
|
||||||
|
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/console": "<7.2",
|
||||||
|
"symfony/event-dispatcher": "<6.4",
|
||||||
|
"symfony/event-dispatcher-contracts": "<2.5",
|
||||||
|
"symfony/framework-bundle": "<6.4",
|
||||||
|
"symfony/http-kernel": "<7.3",
|
||||||
|
"symfony/lock": "<7.4",
|
||||||
|
"symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"psr/cache": "^1.0|^2.0|^3.0",
|
||||||
|
"symfony/console": "^7.2|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-kernel": "^7.3|^8.0",
|
||||||
|
"symfony/lock": "^7.4|^8.0",
|
||||||
|
"symfony/process": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/routing": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
|
||||||
|
"symfony/service-contracts": "^2.5|^3",
|
||||||
|
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/validator": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Messenger\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Samuel Roze",
|
||||||
|
"email": "samuel.roze@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Helps applications send and receive messages to/from other applications or via message queues",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/messenger/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-30T12:55:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-grapheme",
|
"name": "symfony/polyfill-intl-grapheme",
|
||||||
"version": "v1.33.0",
|
"version": "v1.33.0",
|
||||||
@@ -3952,6 +4172,91 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-05T14:04:53+00:00"
|
"time": "2025-12-05T14:04:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/scheduler",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/scheduler.git",
|
||||||
|
"reference": "f95e696edaad466db9b087a6480ef936c766c3de"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/scheduler/zipball/f95e696edaad466db9b087a6480ef936c766c3de",
|
||||||
|
"reference": "f95e696edaad466db9b087a6480ef936c766c3de",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/clock": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dragonmantank/cron-expression": "^3.1",
|
||||||
|
"symfony/cache": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/console": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/lock": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/serializer": "^6.4|^7.1|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Scheduler\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sergey Rabochiy",
|
||||||
|
"email": "upyx.00@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides scheduling through Symfony Messenger",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"cron",
|
||||||
|
"schedule",
|
||||||
|
"scheduler"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/scheduler/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-24T13:12:05+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/serializer",
|
"name": "symfony/serializer",
|
||||||
"version": "v7.4.7",
|
"version": "v7.4.7",
|
||||||
|
|||||||
8
backend/config/packages/messenger.yaml
Normal file
8
backend/config/packages/messenger.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
framework:
|
||||||
|
messenger:
|
||||||
|
transports:
|
||||||
|
sync: 'sync://'
|
||||||
|
scheduler_default: 'scheduler://default'
|
||||||
|
|
||||||
|
routing:
|
||||||
|
App\Message\GenerateTasksMessage: scheduler_default
|
||||||
@@ -426,7 +426,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* resources?: array<string, scalar|Param|null>,
|
* resources?: array<string, scalar|Param|null>,
|
||||||
* },
|
* },
|
||||||
* messenger?: bool|array{ // Messenger configuration
|
* messenger?: bool|array{ // Messenger configuration
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: true
|
||||||
* routing?: array<string, string|array{ // Default: []
|
* routing?: array<string, string|array{ // Default: []
|
||||||
* senders?: list<scalar|Param|null>,
|
* senders?: list<scalar|Param|null>,
|
||||||
* }>,
|
* }>,
|
||||||
@@ -468,7 +468,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* }>,
|
* }>,
|
||||||
* },
|
* },
|
||||||
* scheduler?: bool|array{ // Scheduler configuration
|
* scheduler?: bool|array{ // Scheduler configuration
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: true
|
||||||
* },
|
* },
|
||||||
* disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true
|
* disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true
|
||||||
* http_client?: bool|array{ // HTTP Client configuration
|
* http_client?: bool|array{ // HTTP Client configuration
|
||||||
|
|||||||
37
backend/migrations/Version20260412094958.php
Normal file
37
backend/migrations/Version20260412094958.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260412094958 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE task_schema (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, task_status VARCHAR(255) NOT NULL, date DATE DEFAULT NULL, `repeat` JSON DEFAULT NULL, start DATE DEFAULT NULL, end DATE DEFAULT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
||||||
|
$this->addSql('ALTER TABLE task ADD schema_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25EA1BEF35 FOREIGN KEY (schema_id) REFERENCES task_schema (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('CREATE INDEX IDX_527EDB25EA1BEF35 ON task (schema_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP TABLE task_schema');
|
||||||
|
$this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25EA1BEF35');
|
||||||
|
$this->addSql('DROP INDEX IDX_527EDB25EA1BEF35 ON task');
|
||||||
|
$this->addSql('ALTER TABLE task DROP schema_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
backend/public/app/haushalt.apk
Normal file
BIN
backend/public/app/haushalt.apk
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"versionCode": 1,
|
"versionCode": 2,
|
||||||
"apkFile": "haushalt.apk"
|
"apkFile": "haushalt.apk"
|
||||||
}
|
}
|
||||||
|
|||||||
29
backend/src/Collection/TaskSchemaCollection.php
Normal file
29
backend/src/Collection/TaskSchemaCollection.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Collection;
|
||||||
|
|
||||||
|
use App\Entity\TaskSchema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements \IteratorAggregate<int, TaskSchema>
|
||||||
|
*/
|
||||||
|
final class TaskSchemaCollection implements \IteratorAggregate
|
||||||
|
{
|
||||||
|
/** @var list<TaskSchema> */
|
||||||
|
private array $schemas = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<TaskSchema> $schemas
|
||||||
|
*/
|
||||||
|
public function __construct(array $schemas = [])
|
||||||
|
{
|
||||||
|
foreach ($schemas as $schema) {
|
||||||
|
$this->schemas[] = $schema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator(): \Iterator
|
||||||
|
{
|
||||||
|
return new \ArrayIterator($this->schemas);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,14 +45,6 @@ class TaskController extends AbstractController
|
|||||||
return $this->json($task, 200, [], ['groups' => ['task:read']]);
|
return $this->json($task, 200, [], ['groups' => ['task:read']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('', methods: ['POST'])]
|
|
||||||
public function create(#[Payload] TaskRequest $dto): JsonResponse
|
|
||||||
{
|
|
||||||
$task = $this->manager->create($dto);
|
|
||||||
|
|
||||||
return $this->json($task, 201, [], ['groups' => ['task:read']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
||||||
public function update(Task $task, #[Payload] TaskRequest $dto): JsonResponse
|
public function update(Task $task, #[Payload] TaskRequest $dto): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
60
backend/src/Controller/Api/TaskSchemaController.php
Normal file
60
backend/src/Controller/Api/TaskSchemaController.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Api;
|
||||||
|
|
||||||
|
use App\DTO\TaskSchemaRequest;
|
||||||
|
use App\Entity\TaskSchema;
|
||||||
|
use App\Repository\TaskSchemaRepository;
|
||||||
|
use App\Service\TaskSchemaManager;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload as Payload;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/task-schemas')]
|
||||||
|
class TaskSchemaController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TaskSchemaRepository $repo,
|
||||||
|
private readonly TaskSchemaManager $manager,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', methods: ['GET'])]
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$schemas = $this->repo->allSchemas();
|
||||||
|
|
||||||
|
return $this->json($schemas, 200, [], ['groups' => ['task_schema:read']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', methods: ['GET'], requirements: ['id' => '\d+'])]
|
||||||
|
public function show(TaskSchema $schema): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->json($schema, 200, [], ['groups' => ['task_schema:read']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', methods: ['POST'])]
|
||||||
|
public function create(#[Payload] TaskSchemaRequest $dto): JsonResponse
|
||||||
|
{
|
||||||
|
$this->manager->create($dto);
|
||||||
|
|
||||||
|
return new JsonResponse(null, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
||||||
|
public function update(TaskSchema $schema, #[Payload] TaskSchemaRequest $dto): JsonResponse
|
||||||
|
{
|
||||||
|
$this->manager->update($schema, $dto);
|
||||||
|
|
||||||
|
return $this->json($schema, 200, [], ['groups' => ['task_schema:read']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
||||||
|
public function delete(TaskSchema $schema): JsonResponse
|
||||||
|
{
|
||||||
|
$this->manager->delete($schema);
|
||||||
|
|
||||||
|
return new JsonResponse(null, 204);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/src/DTO/TaskSchemaRequest.php
Normal file
36
backend/src/DTO/TaskSchemaRequest.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\DTO;
|
||||||
|
|
||||||
|
use App\Enum\TaskSchemaStatus;
|
||||||
|
use App\Enum\TaskStatus;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Context;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
class TaskSchemaRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(max: 255)]
|
||||||
|
public readonly string $name,
|
||||||
|
|
||||||
|
#[Assert\NotNull]
|
||||||
|
public readonly TaskSchemaStatus $status = TaskSchemaStatus::Active,
|
||||||
|
|
||||||
|
#[Assert\NotNull]
|
||||||
|
public readonly TaskStatus $taskStatus = TaskStatus::Active,
|
||||||
|
|
||||||
|
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||||
|
public readonly ?\DateTimeImmutable $date = null,
|
||||||
|
|
||||||
|
public readonly ?array $repeat = null,
|
||||||
|
|
||||||
|
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||||
|
public readonly ?\DateTimeImmutable $start = null,
|
||||||
|
|
||||||
|
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||||
|
public readonly ?\DateTimeImmutable $end = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Enum\TaskStatus;
|
|||||||
use App\Repository\TaskRepository;
|
use App\Repository\TaskRepository;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: TaskRepository::class)]
|
#[ORM\Entity(repositoryClass: TaskRepository::class)]
|
||||||
class Task
|
class Task
|
||||||
@@ -27,6 +28,11 @@ class Task
|
|||||||
#[ORM\Column(enumType: TaskStatus::class)]
|
#[ORM\Column(enumType: TaskStatus::class)]
|
||||||
private TaskStatus $status = TaskStatus::Active;
|
private TaskStatus $status = TaskStatus::Active;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: TaskSchema::class, inversedBy: 'tasks')]
|
||||||
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||||
|
#[Ignore]
|
||||||
|
private ?TaskSchema $schema = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -77,4 +83,16 @@ class Task
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSchema(): ?TaskSchema
|
||||||
|
{
|
||||||
|
return $this->schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSchema(?TaskSchema $schema): self
|
||||||
|
{
|
||||||
|
$this->schema = $schema;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
backend/src/Entity/TaskSchema.php
Normal file
158
backend/src/Entity/TaskSchema.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Enum\TaskSchemaStatus;
|
||||||
|
use App\Enum\TaskStatus;
|
||||||
|
use App\Repository\TaskSchemaRepository;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: TaskSchemaRepository::class)]
|
||||||
|
class TaskSchema
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private string $name = '';
|
||||||
|
|
||||||
|
#[ORM\Column(enumType: TaskSchemaStatus::class)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private TaskSchemaStatus $status = TaskSchemaStatus::Active;
|
||||||
|
|
||||||
|
#[ORM\Column(enumType: TaskStatus::class)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private TaskStatus $taskStatus = TaskStatus::Active;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private ?\DateTimeImmutable $date = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: '`repeat`', type: 'json', nullable: true)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private ?array $repeat = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: '`start`', type: 'date_immutable', nullable: true)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private ?\DateTimeImmutable $start = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: '`end`', type: 'date_immutable', nullable: true)]
|
||||||
|
#[Groups(['task_schema:read'])]
|
||||||
|
private ?\DateTimeImmutable $end = null;
|
||||||
|
|
||||||
|
/** @var Collection<int, Task> */
|
||||||
|
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'schema')]
|
||||||
|
private Collection $tasks;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->tasks = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): TaskSchemaStatus
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(TaskSchemaStatus $status): self
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTaskStatus(): TaskStatus
|
||||||
|
{
|
||||||
|
return $this->taskStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTaskStatus(TaskStatus $taskStatus): self
|
||||||
|
{
|
||||||
|
$this->taskStatus = $taskStatus;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDate(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDate(?\DateTimeImmutable $date): self
|
||||||
|
{
|
||||||
|
$this->date = $date;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRepeat(): ?array
|
||||||
|
{
|
||||||
|
return $this->repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRepeatType(): ?string
|
||||||
|
{
|
||||||
|
return $this->repeat !== null ? array_key_first($this->repeat) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRepeat(?array $repeat): self
|
||||||
|
{
|
||||||
|
$this->repeat = $repeat;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStart(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStart(?\DateTimeImmutable $start): self
|
||||||
|
{
|
||||||
|
$this->start = $start;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnd(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEnd(?\DateTimeImmutable $end): self
|
||||||
|
{
|
||||||
|
$this->end = $end;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, Task> */
|
||||||
|
public function getTasks(): Collection
|
||||||
|
{
|
||||||
|
return $this->tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/Enum/TaskSchemaStatus.php
Normal file
9
backend/src/Enum/TaskSchemaStatus.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum TaskSchemaStatus: string
|
||||||
|
{
|
||||||
|
case Active = 'active';
|
||||||
|
case Inactive = 'inactive';
|
||||||
|
}
|
||||||
7
backend/src/Message/GenerateTasksMessage.php
Normal file
7
backend/src/Message/GenerateTasksMessage.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Message;
|
||||||
|
|
||||||
|
final class GenerateTasksMessage
|
||||||
|
{
|
||||||
|
}
|
||||||
20
backend/src/MessageHandler/GenerateTasksMessageHandler.php
Normal file
20
backend/src/MessageHandler/GenerateTasksMessageHandler.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MessageHandler;
|
||||||
|
|
||||||
|
use App\Message\GenerateTasksMessage;
|
||||||
|
use App\Service\TaskGenerator;
|
||||||
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
|
||||||
|
#[AsMessageHandler]
|
||||||
|
final class GenerateTasksMessageHandler
|
||||||
|
{
|
||||||
|
public function __construct(private TaskGenerator $generator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(GenerateTasksMessage $message): void
|
||||||
|
{
|
||||||
|
$this->generator->generateNewTasks();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/src/Repository/TaskSchemaRepository.php
Normal file
36
backend/src/Repository/TaskSchemaRepository.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Collection\TaskSchemaCollection;
|
||||||
|
use App\Entity\TaskSchema;
|
||||||
|
use App\Enum\TaskSchemaStatus;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<TaskSchema>
|
||||||
|
*/
|
||||||
|
class TaskSchemaRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, TaskSchema::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allSchemas(): TaskSchemaCollection
|
||||||
|
{
|
||||||
|
return new TaskSchemaCollection(parent::findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<TaskSchema> */
|
||||||
|
public function findActiveWithRepeat(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('s')
|
||||||
|
->andWhere('s.status = :status')
|
||||||
|
->andWhere('s.repeat IS NOT NULL')
|
||||||
|
->setParameter('status', TaskSchemaStatus::Active)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
backend/src/Schedule.php
Normal file
27
backend/src/Schedule.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use App\Message\GenerateTasksMessage;
|
||||||
|
use Symfony\Component\Scheduler\Attribute\AsSchedule;
|
||||||
|
use Symfony\Component\Scheduler\RecurringMessage;
|
||||||
|
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
|
||||||
|
use Symfony\Component\Scheduler\ScheduleProviderInterface;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
|
||||||
|
#[AsSchedule]
|
||||||
|
class Schedule implements ScheduleProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CacheInterface $cache,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSchedule(): SymfonySchedule
|
||||||
|
{
|
||||||
|
return (new SymfonySchedule())
|
||||||
|
->stateful($this->cache)
|
||||||
|
->processOnlyLastMissedRun(true)
|
||||||
|
->add(RecurringMessage::cron('0 3 * * *', new GenerateTasksMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
87
backend/src/Service/TaskGenerator.php
Normal file
87
backend/src/Service/TaskGenerator.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Task;
|
||||||
|
use App\Entity\TaskSchema;
|
||||||
|
use App\Enum\TaskStatus;
|
||||||
|
use App\Repository\TaskSchemaRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
class TaskGenerator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private TaskSchemaRepository $schemaRepo,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateNewTasks(): void
|
||||||
|
{
|
||||||
|
$schemas = $this->schemaRepo->findActiveWithRepeat();
|
||||||
|
|
||||||
|
foreach ($schemas as $schema) {
|
||||||
|
$this->removeTasks($schema);
|
||||||
|
$this->generateTasks($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeTasks(TaskSchema $schema): void
|
||||||
|
{
|
||||||
|
foreach ($schema->getTasks() as $task) {
|
||||||
|
if ($task->getStatus() === TaskStatus::Past) continue;
|
||||||
|
$this->em->remove($task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateTasks(TaskSchema $schema): void
|
||||||
|
{
|
||||||
|
$dates = $this->getDates($schema);
|
||||||
|
|
||||||
|
foreach ($dates as $date) {
|
||||||
|
$task = new Task();
|
||||||
|
$task->setName($schema->getName());
|
||||||
|
$task->setDate($date);
|
||||||
|
$task->setStatus($schema->getTaskStatus());
|
||||||
|
$task->setSchema($schema);
|
||||||
|
$this->em->persist($task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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();
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dates[] = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dates;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,19 +13,6 @@ class TaskManager
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(TaskRequest $req): Task
|
|
||||||
{
|
|
||||||
$task = new Task();
|
|
||||||
$task->setName($req->name);
|
|
||||||
$task->setDate($req->date);
|
|
||||||
$task->setStatus($req->status);
|
|
||||||
|
|
||||||
$this->em->persist($task);
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
return $task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Task $task, TaskRequest $req): Task
|
public function update(Task $task, TaskRequest $req): Task
|
||||||
{
|
{
|
||||||
$task->setName($req->name);
|
$task->setName($req->name);
|
||||||
|
|||||||
79
backend/src/Service/TaskSchemaManager.php
Normal file
79
backend/src/Service/TaskSchemaManager.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\DTO\TaskSchemaRequest;
|
||||||
|
use App\Entity\Task;
|
||||||
|
use App\Entity\TaskSchema;
|
||||||
|
use App\Enum\TaskSchemaStatus;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
class TaskSchemaManager
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private TaskGenerator $generator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(TaskSchemaRequest $req): void
|
||||||
|
{
|
||||||
|
if ($req->repeat === null) {
|
||||||
|
$task = new Task();
|
||||||
|
$task->setName($req->name);
|
||||||
|
$task->setDate($req->date);
|
||||||
|
$task->setStatus($req->taskStatus);
|
||||||
|
$this->em->persist($task);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = new TaskSchema();
|
||||||
|
$schema->setName($req->name);
|
||||||
|
$schema->setStatus($req->status);
|
||||||
|
$schema->setTaskStatus($req->taskStatus);
|
||||||
|
$schema->setDate($req->date);
|
||||||
|
$schema->setRepeat($req->repeat);
|
||||||
|
$schema->setStart($req->start);
|
||||||
|
$schema->setEnd($req->end);
|
||||||
|
$this->em->persist($schema);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
if ($schema->getStatus() === TaskSchemaStatus::Inactive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->generator->generateTasks($schema);
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(TaskSchema $schema, TaskSchemaRequest $req): void
|
||||||
|
{
|
||||||
|
$schema->setName($req->name);
|
||||||
|
$schema->setStatus($req->status);
|
||||||
|
$schema->setTaskStatus($req->taskStatus);
|
||||||
|
$schema->setDate($req->date);
|
||||||
|
$schema->setRepeat($req->repeat);
|
||||||
|
$schema->setStart($req->start);
|
||||||
|
$schema->setEnd($req->end);
|
||||||
|
|
||||||
|
if ($schema->getStatus() === TaskSchemaStatus::Inactive) {
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->generator->removeTasks($schema);
|
||||||
|
$this->generator->generateTasks($schema);
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(TaskSchema $schema): void
|
||||||
|
{
|
||||||
|
$this->generator->removeTasks($schema);
|
||||||
|
$this->em->remove($schema);
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -101,6 +101,18 @@
|
|||||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"symfony/messenger": {
|
||||||
|
"version": "7.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.0",
|
||||||
|
"ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/messenger.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/property-info": {
|
"symfony/property-info": {
|
||||||
"version": "7.4",
|
"version": "7.4",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@@ -126,6 +138,18 @@
|
|||||||
"config/routes.yaml"
|
"config/routes.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/scheduler": {
|
||||||
|
"version": "7.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.2",
|
||||||
|
"ref": "caea3c928ee9e1b21288fd76aef36f16ea355515"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/Schedule.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/validator": {
|
"symfony/validator": {
|
||||||
"version": "7.4",
|
"version": "7.4",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
3
backend/vendor/composer/autoload_files.php
vendored
3
backend/vendor/composer/autoload_files.php
vendored
@@ -13,6 +13,7 @@ return array(
|
|||||||
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||||
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
|
||||||
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
|
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
|
||||||
|
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
||||||
|
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
|
||||||
);
|
);
|
||||||
|
|||||||
4
backend/vendor/composer/autoload_psr4.php
vendored
4
backend/vendor/composer/autoload_psr4.php
vendored
@@ -28,11 +28,13 @@ return array(
|
|||||||
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
|
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
|
||||||
'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'),
|
'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'),
|
||||||
'Symfony\\Component\\Serializer\\' => array($vendorDir . '/symfony/serializer'),
|
'Symfony\\Component\\Serializer\\' => array($vendorDir . '/symfony/serializer'),
|
||||||
|
'Symfony\\Component\\Scheduler\\' => array($vendorDir . '/symfony/scheduler'),
|
||||||
'Symfony\\Component\\Runtime\\' => array($vendorDir . '/symfony/runtime'),
|
'Symfony\\Component\\Runtime\\' => array($vendorDir . '/symfony/runtime'),
|
||||||
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
|
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
|
||||||
'Symfony\\Component\\PropertyInfo\\' => array($vendorDir . '/symfony/property-info'),
|
'Symfony\\Component\\PropertyInfo\\' => array($vendorDir . '/symfony/property-info'),
|
||||||
'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'),
|
'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'),
|
||||||
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
|
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
|
||||||
|
'Symfony\\Component\\Messenger\\' => array($vendorDir . '/symfony/messenger'),
|
||||||
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
|
'Symfony\\Component\\HttpKernel\\' => array($vendorDir . '/symfony/http-kernel'),
|
||||||
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
||||||
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
|
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
|
||||||
@@ -43,6 +45,7 @@ return array(
|
|||||||
'Symfony\\Component\\DependencyInjection\\' => array($vendorDir . '/symfony/dependency-injection'),
|
'Symfony\\Component\\DependencyInjection\\' => array($vendorDir . '/symfony/dependency-injection'),
|
||||||
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
|
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
|
||||||
'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
|
'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
|
||||||
|
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
|
||||||
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
|
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
|
||||||
'Symfony\\Bundle\\MakerBundle\\' => array($vendorDir . '/symfony/maker-bundle/src'),
|
'Symfony\\Bundle\\MakerBundle\\' => array($vendorDir . '/symfony/maker-bundle/src'),
|
||||||
'Symfony\\Bundle\\FrameworkBundle\\' => array($vendorDir . '/symfony/framework-bundle'),
|
'Symfony\\Bundle\\FrameworkBundle\\' => array($vendorDir . '/symfony/framework-bundle'),
|
||||||
@@ -50,6 +53,7 @@ return array(
|
|||||||
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
|
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
|
||||||
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
|
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
|
||||||
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
||||||
|
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
|
||||||
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
|
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
|
||||||
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
||||||
'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),
|
'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),
|
||||||
|
|||||||
23
backend/vendor/composer/autoload_static.php
vendored
23
backend/vendor/composer/autoload_static.php
vendored
@@ -14,8 +14,9 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||||
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
|
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
|
||||||
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
|
|
||||||
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
|
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
|
||||||
|
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
|
||||||
|
'2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
|
||||||
);
|
);
|
||||||
|
|
||||||
public static $prefixLengthsPsr4 = array (
|
public static $prefixLengthsPsr4 = array (
|
||||||
@@ -49,11 +50,13 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
'Symfony\\Component\\String\\' => 25,
|
'Symfony\\Component\\String\\' => 25,
|
||||||
'Symfony\\Component\\Stopwatch\\' => 28,
|
'Symfony\\Component\\Stopwatch\\' => 28,
|
||||||
'Symfony\\Component\\Serializer\\' => 29,
|
'Symfony\\Component\\Serializer\\' => 29,
|
||||||
|
'Symfony\\Component\\Scheduler\\' => 28,
|
||||||
'Symfony\\Component\\Runtime\\' => 26,
|
'Symfony\\Component\\Runtime\\' => 26,
|
||||||
'Symfony\\Component\\Routing\\' => 26,
|
'Symfony\\Component\\Routing\\' => 26,
|
||||||
'Symfony\\Component\\PropertyInfo\\' => 31,
|
'Symfony\\Component\\PropertyInfo\\' => 31,
|
||||||
'Symfony\\Component\\PropertyAccess\\' => 33,
|
'Symfony\\Component\\PropertyAccess\\' => 33,
|
||||||
'Symfony\\Component\\Process\\' => 26,
|
'Symfony\\Component\\Process\\' => 26,
|
||||||
|
'Symfony\\Component\\Messenger\\' => 28,
|
||||||
'Symfony\\Component\\HttpKernel\\' => 29,
|
'Symfony\\Component\\HttpKernel\\' => 29,
|
||||||
'Symfony\\Component\\HttpFoundation\\' => 33,
|
'Symfony\\Component\\HttpFoundation\\' => 33,
|
||||||
'Symfony\\Component\\Finder\\' => 25,
|
'Symfony\\Component\\Finder\\' => 25,
|
||||||
@@ -64,6 +67,7 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
'Symfony\\Component\\DependencyInjection\\' => 38,
|
'Symfony\\Component\\DependencyInjection\\' => 38,
|
||||||
'Symfony\\Component\\Console\\' => 26,
|
'Symfony\\Component\\Console\\' => 26,
|
||||||
'Symfony\\Component\\Config\\' => 25,
|
'Symfony\\Component\\Config\\' => 25,
|
||||||
|
'Symfony\\Component\\Clock\\' => 24,
|
||||||
'Symfony\\Component\\Cache\\' => 24,
|
'Symfony\\Component\\Cache\\' => 24,
|
||||||
'Symfony\\Bundle\\MakerBundle\\' => 27,
|
'Symfony\\Bundle\\MakerBundle\\' => 27,
|
||||||
'Symfony\\Bundle\\FrameworkBundle\\' => 31,
|
'Symfony\\Bundle\\FrameworkBundle\\' => 31,
|
||||||
@@ -74,6 +78,7 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
'Psr\\Log\\' => 8,
|
'Psr\\Log\\' => 8,
|
||||||
'Psr\\EventDispatcher\\' => 20,
|
'Psr\\EventDispatcher\\' => 20,
|
||||||
'Psr\\Container\\' => 14,
|
'Psr\\Container\\' => 14,
|
||||||
|
'Psr\\Clock\\' => 10,
|
||||||
'Psr\\Cache\\' => 10,
|
'Psr\\Cache\\' => 10,
|
||||||
'PhpParser\\' => 10,
|
'PhpParser\\' => 10,
|
||||||
'PHPStan\\PhpDocParser\\' => 21,
|
'PHPStan\\PhpDocParser\\' => 21,
|
||||||
@@ -196,6 +201,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/symfony/serializer',
|
0 => __DIR__ . '/..' . '/symfony/serializer',
|
||||||
),
|
),
|
||||||
|
'Symfony\\Component\\Scheduler\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/symfony/scheduler',
|
||||||
|
),
|
||||||
'Symfony\\Component\\Runtime\\' =>
|
'Symfony\\Component\\Runtime\\' =>
|
||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/symfony/runtime',
|
0 => __DIR__ . '/..' . '/symfony/runtime',
|
||||||
@@ -216,6 +225,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/symfony/process',
|
0 => __DIR__ . '/..' . '/symfony/process',
|
||||||
),
|
),
|
||||||
|
'Symfony\\Component\\Messenger\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/symfony/messenger',
|
||||||
|
),
|
||||||
'Symfony\\Component\\HttpKernel\\' =>
|
'Symfony\\Component\\HttpKernel\\' =>
|
||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/symfony/http-kernel',
|
0 => __DIR__ . '/..' . '/symfony/http-kernel',
|
||||||
@@ -256,6 +269,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/symfony/config',
|
0 => __DIR__ . '/..' . '/symfony/config',
|
||||||
),
|
),
|
||||||
|
'Symfony\\Component\\Clock\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/symfony/clock',
|
||||||
|
),
|
||||||
'Symfony\\Component\\Cache\\' =>
|
'Symfony\\Component\\Cache\\' =>
|
||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/symfony/cache',
|
0 => __DIR__ . '/..' . '/symfony/cache',
|
||||||
@@ -284,6 +301,10 @@ class ComposerStaticInit75e7f8d848176580e9902a32f6f14640
|
|||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/psr/container/src',
|
0 => __DIR__ . '/..' . '/psr/container/src',
|
||||||
),
|
),
|
||||||
|
'Psr\\Clock\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/psr/clock/src',
|
||||||
|
),
|
||||||
'Psr\\Cache\\' =>
|
'Psr\\Cache\\' =>
|
||||||
array (
|
array (
|
||||||
0 => __DIR__ . '/..' . '/psr/cache/src',
|
0 => __DIR__ . '/..' . '/psr/cache/src',
|
||||||
|
|||||||
317
backend/vendor/composer/installed.json
vendored
317
backend/vendor/composer/installed.json
vendored
@@ -1568,6 +1568,57 @@
|
|||||||
},
|
},
|
||||||
"install-path": "../psr/cache"
|
"install-path": "../psr/cache"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/clock",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_normalized": "1.0.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/clock.git",
|
||||||
|
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
|
||||||
|
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"time": "2022-11-25T14:36:26+00:00",
|
||||||
|
"type": "library",
|
||||||
|
"installation-source": "dist",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Clock\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for reading the clock.",
|
||||||
|
"homepage": "https://github.com/php-fig/clock",
|
||||||
|
"keywords": [
|
||||||
|
"clock",
|
||||||
|
"now",
|
||||||
|
"psr",
|
||||||
|
"psr-20",
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-fig/clock/issues",
|
||||||
|
"source": "https://github.com/php-fig/clock/tree/1.0.0"
|
||||||
|
},
|
||||||
|
"install-path": "../psr/clock"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/container",
|
"name": "psr/container",
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
@@ -1916,6 +1967,87 @@
|
|||||||
],
|
],
|
||||||
"install-path": "../symfony/cache-contracts"
|
"install-path": "../symfony/cache-contracts"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/clock",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"version_normalized": "7.4.8.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/clock.git",
|
||||||
|
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
|
||||||
|
"reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/clock": "^1.0",
|
||||||
|
"symfony/polyfill-php83": "^1.28"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"psr/clock-implementation": "1.0"
|
||||||
|
},
|
||||||
|
"time": "2026-03-24T13:12:05+00:00",
|
||||||
|
"type": "library",
|
||||||
|
"installation-source": "dist",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"Resources/now.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Clock\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Decouples applications from the system clock",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"clock",
|
||||||
|
"psr20",
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/clock/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"install-path": "../symfony/clock"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/config",
|
"name": "symfony/config",
|
||||||
"version": "v7.4.7",
|
"version": "v7.4.7",
|
||||||
@@ -3395,6 +3527,103 @@
|
|||||||
],
|
],
|
||||||
"install-path": "../symfony/maker-bundle"
|
"install-path": "../symfony/maker-bundle"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/messenger",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"version_normalized": "7.4.8.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/messenger.git",
|
||||||
|
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/messenger/zipball/ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
|
||||||
|
"reference": "ddf5ab29bc0329ece30e16f01c86abb6241e92d8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^1|^2|^3",
|
||||||
|
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/console": "<7.2",
|
||||||
|
"symfony/event-dispatcher": "<6.4",
|
||||||
|
"symfony/event-dispatcher-contracts": "<2.5",
|
||||||
|
"symfony/framework-bundle": "<6.4",
|
||||||
|
"symfony/http-kernel": "<7.3",
|
||||||
|
"symfony/lock": "<7.4",
|
||||||
|
"symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"psr/cache": "^1.0|^2.0|^3.0",
|
||||||
|
"symfony/console": "^7.2|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-kernel": "^7.3|^8.0",
|
||||||
|
"symfony/lock": "^7.4|^8.0",
|
||||||
|
"symfony/process": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/routing": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
|
||||||
|
"symfony/service-contracts": "^2.5|^3",
|
||||||
|
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/validator": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"time": "2026-03-30T12:55:43+00:00",
|
||||||
|
"type": "library",
|
||||||
|
"installation-source": "dist",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Messenger\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Samuel Roze",
|
||||||
|
"email": "samuel.roze@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Helps applications send and receive messages to/from other applications or via message queues",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/messenger/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"install-path": "../symfony/messenger"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-grapheme",
|
"name": "symfony/polyfill-intl-grapheme",
|
||||||
"version": "v1.33.0",
|
"version": "v1.33.0",
|
||||||
@@ -4324,6 +4553,94 @@
|
|||||||
],
|
],
|
||||||
"install-path": "../symfony/runtime"
|
"install-path": "../symfony/runtime"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/scheduler",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"version_normalized": "7.4.8.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/scheduler.git",
|
||||||
|
"reference": "f95e696edaad466db9b087a6480ef936c766c3de"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/scheduler/zipball/f95e696edaad466db9b087a6480ef936c766c3de",
|
||||||
|
"reference": "f95e696edaad466db9b087a6480ef936c766c3de",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/clock": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dragonmantank/cron-expression": "^3.1",
|
||||||
|
"symfony/cache": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/console": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/lock": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/serializer": "^6.4|^7.1|^8.0"
|
||||||
|
},
|
||||||
|
"time": "2026-03-24T13:12:05+00:00",
|
||||||
|
"type": "library",
|
||||||
|
"installation-source": "dist",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Scheduler\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sergey Rabochiy",
|
||||||
|
"email": "upyx.00@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides scheduling through Symfony Messenger",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"cron",
|
||||||
|
"schedule",
|
||||||
|
"scheduler"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/scheduler/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"install-path": "../symfony/scheduler"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/serializer",
|
"name": "symfony/serializer",
|
||||||
"version": "v7.4.7",
|
"version": "v7.4.7",
|
||||||
|
|||||||
46
backend/vendor/composer/installed.php
vendored
46
backend/vendor/composer/installed.php
vendored
@@ -3,7 +3,7 @@
|
|||||||
'name' => 'symfony/skeleton',
|
'name' => 'symfony/skeleton',
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '2f96caaa233f92fb18ffcdc3f13805d0e7e41369',
|
'reference' => '4e81cea8317097bcd2f8ce85decd6b78e5156e8a',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
@@ -196,6 +196,21 @@
|
|||||||
0 => '2.0|3.0',
|
0 => '2.0|3.0',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
'psr/clock' => array(
|
||||||
|
'pretty_version' => '1.0.0',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../psr/clock',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'psr/clock-implementation' => array(
|
||||||
|
'dev_requirement' => false,
|
||||||
|
'provided' => array(
|
||||||
|
0 => '1.0',
|
||||||
|
),
|
||||||
|
),
|
||||||
'psr/container' => array(
|
'psr/container' => array(
|
||||||
'pretty_version' => '2.0.2',
|
'pretty_version' => '2.0.2',
|
||||||
'version' => '2.0.2.0',
|
'version' => '2.0.2.0',
|
||||||
@@ -271,6 +286,15 @@
|
|||||||
0 => '1.1|2.0|3.0',
|
0 => '1.1|2.0|3.0',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
'symfony/clock' => array(
|
||||||
|
'pretty_version' => 'v7.4.8',
|
||||||
|
'version' => '7.4.8.0',
|
||||||
|
'reference' => '674fa3b98e21531dd040e613479f5f6fa8f32111',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../symfony/clock',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
'symfony/config' => array(
|
'symfony/config' => array(
|
||||||
'pretty_version' => 'v7.4.7',
|
'pretty_version' => 'v7.4.7',
|
||||||
'version' => '7.4.7.0',
|
'version' => '7.4.7.0',
|
||||||
@@ -421,6 +445,15 @@
|
|||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
'dev_requirement' => true,
|
'dev_requirement' => true,
|
||||||
),
|
),
|
||||||
|
'symfony/messenger' => array(
|
||||||
|
'pretty_version' => 'v7.4.8',
|
||||||
|
'version' => '7.4.8.0',
|
||||||
|
'reference' => 'ddf5ab29bc0329ece30e16f01c86abb6241e92d8',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../symfony/messenger',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
'symfony/polyfill-ctype' => array(
|
'symfony/polyfill-ctype' => array(
|
||||||
'dev_requirement' => false,
|
'dev_requirement' => false,
|
||||||
'replaced' => array(
|
'replaced' => array(
|
||||||
@@ -568,6 +601,15 @@
|
|||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
'dev_requirement' => false,
|
'dev_requirement' => false,
|
||||||
),
|
),
|
||||||
|
'symfony/scheduler' => array(
|
||||||
|
'pretty_version' => 'v7.4.8',
|
||||||
|
'version' => '7.4.8.0',
|
||||||
|
'reference' => 'f95e696edaad466db9b087a6480ef936c766c3de',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../symfony/scheduler',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
'symfony/serializer' => array(
|
'symfony/serializer' => array(
|
||||||
'pretty_version' => 'v7.4.7',
|
'pretty_version' => 'v7.4.7',
|
||||||
'version' => '7.4.7.0',
|
'version' => '7.4.7.0',
|
||||||
@@ -595,7 +637,7 @@
|
|||||||
'symfony/skeleton' => array(
|
'symfony/skeleton' => array(
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '2f96caaa233f92fb18ffcdc3f13805d0e7e41369',
|
'reference' => '4e81cea8317097bcd2f8ce85decd6b78e5156e8a',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ const props = defineProps({
|
|||||||
<line x1="10" y1="11" x2="10" y2="17" />
|
<line x1="10" y1="11" x2="10" y2="17" />
|
||||||
<line x1="14" y1="11" x2="14" y2="17" />
|
<line x1="14" y1="11" x2="14" y2="17" />
|
||||||
</template>
|
</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'">
|
<template v-else-if="name === 'refresh'">
|
||||||
<polyline points="23 4 23 10 17 10" />
|
<polyline points="23 4 23 10 17 10" />
|
||||||
<polyline points="1 20 1 14 7 14" />
|
<polyline points="1 20 1 14 7 14" />
|
||||||
|
|||||||
@@ -27,16 +27,40 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tasks/create',
|
path: '/schemas',
|
||||||
name: 'tasks-create',
|
name: 'schemas',
|
||||||
component: () => import('../views/TaskCreate.vue'),
|
component: () => import('../views/SchemaAll.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{ label: 'Tasks', to: '/tasks' },
|
{ 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' },
|
{ 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+)',
|
path: '/tasks/:id(\\d+)',
|
||||||
name: 'tasks-edit',
|
name: 'tasks-edit',
|
||||||
|
|||||||
@@ -12,10 +12,17 @@ async function request(path, opts = {}) {
|
|||||||
return res.status === 204 ? null : res.json()
|
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 = {
|
export const taskApi = {
|
||||||
list: (filter) => request(`/tasks${filter ? `?filter=${filter}` : ''}`),
|
list: (filter) => request(`/tasks${filter ? `?filter=${filter}` : ''}`),
|
||||||
get: (id) => request(`/tasks/${id}`),
|
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) }),
|
update: (id, data) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
remove: (id) => request(`/tasks/${id}`, { method: 'DELETE' }),
|
remove: (id) => request(`/tasks/${id}`, { method: 'DELETE' }),
|
||||||
toggle: (id) => request(`/tasks/${id}/toggle`, { method: 'PATCH' }),
|
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()
|
this.availableStatuses = await taskApi.statuses()
|
||||||
},
|
},
|
||||||
|
|
||||||
async create(data) {
|
|
||||||
const task = await taskApi.create(data)
|
|
||||||
this.tasks.push(task)
|
|
||||||
return task
|
|
||||||
},
|
|
||||||
|
|
||||||
async update(id, data) {
|
async update(id, data) {
|
||||||
const updated = await taskApi.update(id, data)
|
const updated = await taskApi.update(id, data)
|
||||||
this.replaceLocal(updated)
|
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>
|
<template>
|
||||||
<div class="task-view">
|
<div class="task-view">
|
||||||
<div class="actions">
|
<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" />
|
<Icon name="plus" />
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" @click="router.push('/tasks/all')" title="Alle Tasks">
|
<button class="icon-btn" @click="router.push('/tasks/all')" title="Alle Tasks">
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="task-all-view">
|
<div class="task-all-view">
|
||||||
<div class="actions">
|
<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" />
|
<Icon name="plus" />
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" @click="store.fetchAll()" title="Neu laden">
|
<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>
|
|
||||||
37
module.md
37
module.md
@@ -91,21 +91,28 @@ Implementierungs-Schritte als Feature-Module - WIE es gebaut wird
|
|||||||
|
|
||||||
# TaskSchema module
|
# TaskSchema module
|
||||||
## Backend
|
## Backend
|
||||||
- TaskSchema - TaskSchema entity
|
- Task - add schema (n:1)
|
||||||
- id, name, status, date, type, repeat, start, end
|
- TaskSchema - id, name, status, taskStatus, date, repeat (json), start, end
|
||||||
- TaskType - Enum for schema type
|
- repeat=null → single, repeat={"daily"/"weekly"/"monthly":...} → repeating
|
||||||
- single (create one task on date or null, delete schema after date or now if null)
|
- TaskSchemaStatus - active, inactive
|
||||||
- repeat (create tasks depending on repeat in start-end range, delete after enddate)
|
- TaskController - remove create route
|
||||||
- TaskRepeat - Enum for schema repeat
|
- TaskManager - remove create
|
||||||
- daily, weekly (array with weekdays), monthly (array with monthdays)
|
- TaskSchemaController - index, show, create, update, delete
|
||||||
- TaskSchemaController - TaskSchema routes
|
- TaskSchemaManager - create (single=task only, repeat=schema+generate), update (remove+generate), delete (remove+schema)
|
||||||
- index, show, create, update, delete
|
- TaskGenerator - generateTasks, removeTasks, generateNewTasks (scheduler)
|
||||||
- TaskSchemaManager - TaskSchema CRUD
|
- Scheduler - daily at 03:00, messenger:consume via DDEV daemon
|
||||||
- create, update, delete
|
- Migration - task_schema table + schema_id FK
|
||||||
- TaskSchemaRepository - TaskSchema queries
|
## Frontend
|
||||||
- TaskGenerator - Create tasks from schema
|
- TaskCreate.vue removed, SchemaCreate/SchemaEdit/SchemaAll added
|
||||||
- generate
|
- 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
|
||||||
|
|
||||||
# Category module
|
# Category module
|
||||||
## Backend
|
## Backend
|
||||||
|
|||||||
Reference in New Issue
Block a user