diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt index 8ea3866..8a63766 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt @@ -17,7 +17,7 @@ import java.math.BigDecimal import java.time.Instant import java.time.LocalDateTime import java.time.OffsetDateTime -import java.time.ZoneOffset +import java.time.ZoneId import java.util.UUID @Service @@ -291,7 +291,7 @@ class LabService( val v = value?.takeIf { it.isNotBlank() } ?: return null return runCatching { Instant.parse(v) } .recoverCatching { OffsetDateTime.parse(v).toInstant() } - .recoverCatching { LocalDateTime.parse(v).atOffset(ZoneOffset.UTC).toInstant() } + .recoverCatching { LocalDateTime.parse(v).atZone(ZoneId.of("Europe/Moscow")).toInstant() } .getOrNull() } } \ No newline at end of file diff --git a/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt b/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt index b3a2bb1..93ea2dd 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/DateUtils.kt @@ -1,6 +1,7 @@ package ru.ysu.lms.web import java.time.Instant +import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -8,14 +9,33 @@ import java.time.format.DateTimeFormatter object DateUtils { private val MSK = ZoneId.of("Europe/Moscow") private val FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + private val INPUT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") + /** ISO instant string -> "02.11.2026 23:59" в Москве. */ fun formatMsk(value: String?): String? = - value?.let { + toZoned(value)?.format(FORMAT) + + /** ISO instant string -> "2026-11-02T02:59" для (в московском времени). */ + fun localInput(value: String?): String? = + toZoned(value)?.format(INPUT_FORMAT) ?: "" + + /** Значение datetime-local (локальное московское) -> ISO instant строка, понятная API. */ + fun toUtcIso(localValue: String?): String? = + localValue?.takeIf { it.isNotBlank() }?.let { s -> try { - val zdt = ZonedDateTime.ofInstant(Instant.parse(it), MSK) - zdt.format(FORMAT) + LocalDateTime.parse(s, INPUT_FORMAT) + .atZone(MSK) + .toInstant() + .toString() } catch (_: Exception) { - it + null } } + + private fun toZoned(value: String?): ZonedDateTime? = + try { + value?.let { ZonedDateTime.ofInstant(Instant.parse(it), MSK) } + } catch (_: Exception) { + null + } } \ No newline at end of file diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt index f72ffd0..da75689 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt @@ -285,8 +285,8 @@ class AdminController( model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd)) model.addAttribute("deadlineMsk", ru.ysu.lms.web.DateUtils.formatMsk(lab.deadlineAt)) model.addAttribute("defenseMsk", ru.ysu.lms.web.DateUtils.formatMsk(lab.defenseAt)) - model.addAttribute("deadlineLocal", lab.deadlineAt?.take(16) ?: "") - model.addAttribute("defenseLocal", lab.defenseAt?.take(16) ?: "") + model.addAttribute("deadlineLocal", ru.ysu.lms.web.DateUtils.localInput(lab.deadlineAt)) + model.addAttribute("defenseLocal", ru.ysu.lms.web.DateUtils.localInput(lab.defenseAt)) return "admin/lab" } @@ -336,8 +336,8 @@ class AdminController( LabCreatePayload( number = number, title = title.trim(), - deadlineAt = deadlineAt?.takeIf { it.isNotBlank() }, - defenseAt = defenseAt?.takeIf { it.isNotBlank() }, + deadlineAt = ru.ysu.lms.web.DateUtils.toUtcIso(deadlineAt), + defenseAt = ru.ysu.lms.web.DateUtils.toUtcIso(defenseAt), pointsMax = java.math.BigDecimal(pointsMax), status = status, descriptionMd = descriptionMd, @@ -425,8 +425,8 @@ class AdminController( val lab = api.labDetail(token(session), id) // datetime-local требует формат yyyy-MM-dd'T'HH:mm model.addAttribute("lab", lab) - model.addAttribute("deadlineLocal", lab.deadlineAt?.take(16) ?: "") - model.addAttribute("defenseLocal", lab.defenseAt?.take(16) ?: "") + model.addAttribute("deadlineLocal", ru.ysu.lms.web.DateUtils.localInput(lab.deadlineAt)) + model.addAttribute("defenseLocal", ru.ysu.lms.web.DateUtils.localInput(lab.defenseAt)) return "admin/lab-edit" } @@ -444,7 +444,7 @@ class AdminController( ): String = try { api.updateLab( token(session), id, - LabUpdatePayload(title, deadlineAt?.takeIf { it.isNotBlank() }, defenseAt?.takeIf { it.isNotBlank() }, pointsMax, status, descriptionMd), + LabUpdatePayload(title, ru.ysu.lms.web.DateUtils.toUtcIso(deadlineAt), ru.ysu.lms.web.DateUtils.toUtcIso(defenseAt), pointsMax, status, descriptionMd), ) redirect.addFlashAttribute("notice", "Лабораторная сохранена") "redirect:/admin/labs/$id" diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index 946cefc..29ae525 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -165,6 +165,8 @@ tr:last-child td { border-bottom: none; } .inline-form .input-sm { width: auto; min-width: 10rem; } .input-sm { padding: 0.25rem 0.5rem; font-size: 0.8125rem; } .input-score { width: 4.5rem; } +.inline-form .input-sm { width: auto; min-width: 0; } +.inline-form .input-score, input[type="number"].input-score { width: 4.5rem; min-width: 4.5rem; flex: 0 0 auto; } .filter-select { width: auto; } .hidden { display: none !important; } diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html index 6527211..b34beea 100644 --- a/web/src/main/resources/templates/admin/group.html +++ b/web/src/main/resources/templates/admin/group.html @@ -106,7 +106,6 @@ -
Логин: —
Пароль: —