Skip to content

Commit 996f345

Browse files
committed
feat/qg-261: добавлено сохранение локальных черновиков на странице создания/редактирования записи журнала
1 parent a8d4074 commit 996f345

17 files changed

Lines changed: 189 additions & 101 deletions

File tree

app/src/main/kotlin/pro/qyoga/app/therapist/clients/journal/edit_entry/edit/EditJournalEntryPageModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.springframework.web.servlet.ModelAndView
44
import pro.azhidkov.platform.spring.mvc.viewId
55
import pro.qyoga.app.therapist.clients.journal.edit_entry.shared.JOURNAL_ENTRY_VIEW_NAME
66
import pro.qyoga.core.clients.cards.model.ClientRef
7+
import pro.qyoga.core.clients.journals.dtos.EditJournalEntryRq
78
import pro.qyoga.core.clients.journals.model.JournalEntry
89

910

@@ -16,7 +17,8 @@ data class EditJournalEntryPageModel(
1617
) : ModelAndView(
1718
viewId(JOURNAL_ENTRY_VIEW_NAME, fragment), mapOf(
1819
"client" to clientRef,
19-
"entry" to entry,
20+
"entryId" to entry.id,
21+
"entry" to EditJournalEntryRq(entry),
2022
"duplicatedDate" to duplicatedDate,
2123
"formAction" to formAction
2224
)

app/src/main/kotlin/pro/qyoga/app/therapist/clients/journal/list/GetJournalPageOp.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import org.springframework.stereotype.Component
66
import pro.qyoga.core.clients.cards.ClientsRepo
77
import pro.qyoga.core.clients.cards.model.Client
88
import pro.qyoga.core.clients.journals.JournalEntriesRepo
9-
import pro.qyoga.core.clients.journals.dtos.JournalPageRequest
9+
import pro.qyoga.core.clients.journals.dtos.JournalPageRq
1010
import pro.qyoga.core.clients.journals.model.JournalEntry
1111

1212
sealed interface GetJournalPageResult {
@@ -18,13 +18,13 @@ sealed interface GetJournalPageResult {
1818
class GetJournalPageOp(
1919
private val clientsRepo: ClientsRepo,
2020
private val journalEntriesRepo: JournalEntriesRepo
21-
) : (JournalPageRequest) -> GetJournalPageResult {
21+
) : (JournalPageRq) -> GetJournalPageResult {
2222

23-
override fun invoke(journalPageRequest: JournalPageRequest): GetJournalPageResult {
24-
val client = clientsRepo.findByIdOrNull(journalPageRequest.clientId)
23+
override fun invoke(journalPageRq: JournalPageRq): GetJournalPageResult {
24+
val client = clientsRepo.findByIdOrNull(journalPageRq.clientId)
2525
?: return GetJournalPageResult.ClientNotFound
2626

27-
val journal = journalEntriesRepo.getJournalPage(journalPageRequest)
27+
val journal = journalEntriesRepo.getJournalPage(journalPageRq)
2828

2929
return GetJournalPageResult.Success(client, journal)
3030
}

app/src/main/kotlin/pro/qyoga/app/therapist/clients/journal/list/JournalPageController.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import org.springframework.web.servlet.ModelAndView
88
import pro.qyoga.app.platform.notFound
99
import pro.qyoga.app.therapist.clients.ClientPageTab
1010
import pro.qyoga.app.therapist.clients.clientPageModel
11-
import pro.qyoga.core.clients.journals.dtos.JournalPageRequest
11+
import pro.qyoga.core.clients.journals.dtos.JournalPageRq
1212
import pro.qyoga.core.clients.journals.model.JournalEntry
1313
import java.util.*
1414

@@ -24,7 +24,7 @@ class JournalPageController(
2424
fun handleGetJournalPage(
2525
@PathVariable clientId: UUID
2626
): ModelAndView {
27-
val firstPage = JournalPageRequest.firstPage(clientId, fetch = listOf(JournalEntry::therapeuticTask))
27+
val firstPage = JournalPageRq.firstPage(clientId, fetch = listOf(JournalEntry::therapeuticTask))
2828
return when (val result = getJournalPage(firstPage)) {
2929
is GetJournalPageResult.ClientNotFound ->
3030
notFound

app/src/main/kotlin/pro/qyoga/core/clients/journals/JournalEntriesRepo.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Repository
1111
import org.springframework.transaction.annotation.Transactional
1212
import pro.azhidkov.platform.spring.sdj.ergo.ErgoRepository
1313
import pro.azhidkov.platform.spring.sdj.sortBy
14-
import pro.qyoga.core.clients.journals.dtos.JournalPageRequest
14+
import pro.qyoga.core.clients.journals.dtos.JournalPageRq
1515
import pro.qyoga.core.clients.journals.errors.DuplicatedDate
1616
import pro.qyoga.core.clients.journals.model.JournalEntry
1717
import java.util.*
@@ -39,13 +39,13 @@ class JournalEntriesRepo(
3939
}
4040
}
4141

42-
fun getJournalPage(journalPageRequest: JournalPageRequest): Page<JournalEntry> {
42+
fun getJournalPage(journalPageRq: JournalPageRq): Page<JournalEntry> {
4343
return findPage(
44-
pageRequest = PageRequest.of(0, journalPageRequest.pageSize, sortBy(JournalEntry::date).descending()),
45-
fetch = journalPageRequest.fetch,
44+
pageRequest = PageRequest.of(0, journalPageRq.pageSize, sortBy(JournalEntry::date).descending()),
45+
fetch = journalPageRq.fetch,
4646
) {
47-
JournalEntry::clientRef isEqual AggregateReference.to(journalPageRequest.clientId)
48-
JournalEntry::date isLessThanIfNotNull journalPageRequest.date
47+
JournalEntry::clientRef isEqual AggregateReference.to(journalPageRq.clientId)
48+
JournalEntry::date isLessThanIfNotNull journalPageRq.date
4949
}
5050
}
5151

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
package pro.qyoga.core.clients.journals.dtos
22

3+
import com.fasterxml.jackson.annotation.JsonFormat
34
import org.springframework.format.annotation.DateTimeFormat
5+
import pro.azhidkov.platform.spring.sdj.ergo.hydration.resolveOrThrow
6+
import pro.qyoga.core.clients.journals.model.JournalEntry
47
import pro.qyoga.l10n.RUSSIAN_DATE_FORMAT_PATTERN
58
import java.time.LocalDate
69

710

811
data class EditJournalEntryRq(
912
@DateTimeFormat(pattern = RUSSIAN_DATE_FORMAT_PATTERN)
13+
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = RUSSIAN_DATE_FORMAT_PATTERN)
1014
val date: LocalDate,
1115
val therapeuticTaskName: String,
12-
val journalEntryText: String
13-
)
16+
val journalEntryText: String,
17+
val version: Long
18+
) {
19+
20+
constructor(journalEntry: JournalEntry) : this(
21+
journalEntry.date,
22+
journalEntry.therapeuticTask.resolveOrThrow().name,
23+
journalEntry.entryText,
24+
journalEntry.version
25+
)
26+
27+
}

app/src/main/kotlin/pro/qyoga/core/clients/journals/dtos/JournalPageRequest.kt renamed to app/src/main/kotlin/pro/qyoga/core/clients/journals/dtos/JournalPageRq.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import java.util.*
66
import kotlin.reflect.KProperty1
77

88

9-
data class JournalPageRequest(
9+
data class JournalPageRq(
1010
val clientId: UUID,
1111
val date: LocalDate? = null,
1212
val pageSize: Int = 10,
@@ -16,10 +16,10 @@ data class JournalPageRequest(
1616
companion object {
1717

1818
fun firstPage(clientId: UUID, fetch: Iterable<KProperty1<JournalEntry, *>> = emptySet()) =
19-
JournalPageRequest(clientId, fetch = fetch)
19+
JournalPageRq(clientId, fetch = fetch)
2020

2121
fun wholeJournal(clientId: UUID, fetch: Iterable<KProperty1<JournalEntry, *>> = emptySet()) =
22-
JournalPageRequest(clientId, null, Int.MAX_VALUE, fetch)
22+
JournalPageRq(clientId, null, Int.MAX_VALUE, fetch)
2323

2424
}
2525

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function shallowEqual(object1, object2) {
2+
const keys1 = Object.keys(object1);
3+
const keys2 = Object.keys(object2);
4+
5+
if (keys1.length !== keys2.length) {
6+
return false;
7+
}
8+
9+
for (let key of keys1) {
10+
if ((object1[key] instanceof Object && !shallowEqual(object1[key], object2[key])) ||
11+
(!(object1[key] instanceof Object) && object1[key] !== object2[key])) {
12+
return false;
13+
}
14+
}
15+
16+
return true;
17+
}

app/src/main/resources/templates/therapist/clients/client-card-fragment.html

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
<script src="/js/form-drafts.js"></script>
2+
13
<script th:inline="javascript">
24
let serverState = /*[[${client}]]*/ null;
3-
let clientId = /*[[${id}]]*/ null;
5+
let clientId = /*[[${clientId}]]*/ null;
46

57
let localStateKey = `qyoga.clientCardForm.${clientId || "new"}`;
68

@@ -33,24 +35,6 @@
3335
localStorage.removeItem(localStateKey);
3436
}
3537

36-
function shallowEqual(object1, object2) {
37-
const keys1 = Object.keys(object1);
38-
const keys2 = Object.keys(object2);
39-
40-
if (keys1.length !== keys2.length) {
41-
return false;
42-
}
43-
44-
for (let key of keys1) {
45-
if ((object1[key] instanceof Object && !shallowEqual(object1[key], object2[key])) ||
46-
(!(object1[key] instanceof Object) && object1[key] !== object2[key])) {
47-
return false;
48-
}
49-
}
50-
51-
return true;
52-
}
53-
5438
function isChanged(form, key) {
5539
return serverState != null && serverState[key] !== form[key];
5640
}
@@ -186,7 +170,7 @@
186170
<input class="form-control w-100" id="address"
187171
name="address" placeholder=""
188172
th:value="${client?.address ?: ''}" type="text" value="">
189-
<label for="birthDate">Адрес (город, улица, дом, квартира)</label>
173+
<label for="address">Адрес (город, улица, дом, квартира)</label>
190174
</div>
191175
</div>
192176
</div>

app/src/main/resources/templates/therapist/clients/client-edit.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<div th:replace="~{fragments/header.html}"></div>
55
<title th:text="${client?.fullName() ?: 'ФИО'}"></title>
66
<link href="/styles/therapist/clients/style.css" rel="stylesheet">
7+
<script defer src="/js/form-drafts.js"></script>
78
<style>
89
.pill {
910
padding: 0.5rem 0;

app/src/main/resources/templates/therapist/clients/journal-entry.html

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,54 @@
1+
2+
<script th:inline="javascript">
3+
let serverState = /*[[${entry}]]*/ null;
4+
let clientId = /*[[${client.id}]]*/ null;
5+
let entryId = /*[[${entryId}]]*/ null;
6+
let entryDate = /*[[${#temporals.format((entry?.date ?: entryDate), T(pro.qyoga.l10n.DateFormatsKt).RUSSIAN_DATE_FORMAT_PATTERN)}]]*/ null;
7+
8+
let localStateKey = `qyoga.journalEntryForm.${clientId}.${entryId || "new"}`;
9+
10+
let localState = localStorage.getItem(localStateKey) ? JSON.parse(localStorage.getItem(localStateKey)) : null;
11+
12+
let formData;
13+
if (serverState == null) {
14+
formData = localState || {version: 0, date: entryDate};
15+
} else if (serverState.version === localState?.version) {
16+
formData = localState
17+
} else {
18+
formData = {...serverState}
19+
}
20+
21+
function entryData() {
22+
let hasUnsavedEdits = serverState != null && localState != null && !shallowEqual(serverState, localState);
23+
let newCardWasntSaved = serverState == null && localState != null;
24+
return {
25+
form: formData,
26+
serverState: serverState,
27+
hasUnsavedChanges: hasUnsavedEdits || newCardWasntSaved
28+
}
29+
}
30+
31+
function saveLocalState(formData) {
32+
localStorage.setItem(localStateKey, JSON.stringify(formData));
33+
}
34+
35+
function resetLocalState() {
36+
localStorage.removeItem(localStateKey);
37+
}
38+
39+
function isChanged(form, key) {
40+
return serverState != null && serverState[key] !== form[key];
41+
}
42+
43+
window.addEventListener("htmx:afterSettle", () => {
44+
document.querySelectorAll(".form-control").forEach(it => {
45+
it.setAttribute("x-model", "form." + it.name);
46+
it.setAttribute(":class", "isChanged(form, '" + it.name + "') ? 'border-warning' : ''");
47+
});
48+
});
49+
50+
</script>
51+
152
<div id="createJournalEntryTabContent">
253
<style>
354
/* For medium screens and larger */
@@ -20,7 +71,23 @@
2071

2172
<form hx-swap="outerHtml" id="journalEntryFrom"
2273
th:fragment="journalEntryFrom"
23-
th:hx-post="${formAction}">
74+
th:hx-post="${formAction}"
75+
x-data="entryData()"
76+
x-init="$watch('form', (form) => saveLocalState(form))">
77+
78+
<input
79+
name="version"
80+
th:value="${entry?.version ?: 0}"
81+
type="hidden"
82+
>
83+
84+
<div class="alert alert-warning w-100"
85+
role="alert"
86+
x-show="hasUnsavedChanges">
87+
<i class="fa-solid fa-triangle-exclamation"></i>
88+
В карточке восстановлены несохранённые данные прошлой сессии
89+
</div>
90+
2491
<div class="row">
2592
<div class="mb-3 col-12 col-md-2">
2693
<label class="form-label" for="date-input">Дата</label>
@@ -45,7 +112,7 @@
45112
hx-target="#therapeuticTasks"
46113
hx-trigger="input[inputType != 'insertReplacementText' && target.value.length > 2] delay:0.3s"
47114
list="therapeuticTasks"
48-
th:value="${entry?.therapeuticTask?.entity?.name ?: ''}"
115+
th:value="${entry?.therapeuticTaskName ?: ''}"
49116
type="text"
50117
>
51118
<datalist id="therapeuticTasks"></datalist>
@@ -55,19 +122,25 @@
55122
<label class="form-label" for="text-input">Запись</label>
56123
<textarea class="form-control full-height"
57124
id="text-input" name="journalEntryText" required rows="12"
58-
th:text="${entry?.entryText ?: ''}"
125+
th:text="${entry?.journalEntryText ?: ''}"
59126
></textarea>
60127
</div>
61128

62129
<div class="row g-2 justify-content-end">
63130
<div class="col-6 col-sm-auto text-center">
64-
<a class="btn btn-outline-danger" style="min-width: 110px;"
131+
<a @click="resetLocalState()"
132+
class="btn btn-outline-danger"
133+
style="min-width: 110px;"
65134
th:href="@{/therapist/clients/{id}/journal(id=${client.id})}">
66135
Отмена
67136
</a>
68137
</div>
69138
<div class="col-6 col-sm-auto text-center">
70-
<button class="btn btn-outline-success" name="confirmButton" style="min-width: 110px;">
139+
<button
140+
@click="resetLocalState()"
141+
class="btn btn-outline-success"
142+
name="confirmButton"
143+
style="min-width: 110px;">
71144
Сохранить
72145
</button>
73146
</div>

0 commit comments

Comments
 (0)