Skip to content

Commit fdfddde

Browse files
committed
feat/qg-264: на страницу списка клиентов добавлена информация о количестве и даты последней записи журнала
1 parent ffed9fc commit fdfddde

23 files changed

Lines changed: 384 additions & 111 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
testFixturesApi("org.springframework.boot:spring-boot-testcontainers")
5555
testFixturesApi(testLibs.kotest.assertions)
5656
testFixturesApi(testLibs.kotest.runner)
57+
testFixturesApi(testLibs.kotest.datatest)
5758
testFixturesApi(testLibs.jsoup)
5859
testFixturesApi(testLibs.datafaker)
5960
testFixturesApi(testLibs.greenmail)

app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ class ErgoRepository<T : Any, ID : Any>(
196196
pageRequest: Pageable,
197197
fetch: Iterable<KProperty1<T, *>> = emptySet(),
198198
): Page<T> {
199+
return findPage(query, paramMap, pageRequest, rowMapper, fetch)
200+
}
201+
202+
fun <V : Any> findPage(
203+
query: String,
204+
paramMap: Map<String, Any?>,
205+
pageRequest: Pageable,
206+
rowMapper: RowMapper<V>,
207+
fetch: Iterable<KProperty1<V, *>> = emptySet(),
208+
): Page<V> {
199209
val page = namedParameterJdbcOperations.queryForPage(query, paramMap, pageRequest, rowMapper)
200210
return page.mapContent { jdbcAggregateTemplate.hydrate(it, FetchSpec(fetch)) }
201211
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package pro.azhidkov.platform.ui
2+
3+
import java.time.Period
4+
5+
6+
object PeriodFormatter {
7+
8+
fun formatPeriodInGenitiveCase(period: Period): String {
9+
val normalizedPeriod = period.normalized()
10+
return when {
11+
normalizedPeriod.years > 0 -> RussianPeriodDeclensions.YEAR.formatInGenitiveCase(normalizedPeriod.years)
12+
normalizedPeriod.months > 0 -> RussianPeriodDeclensions.MONTH.formatInGenitiveCase(normalizedPeriod.months)
13+
normalizedPeriod.days >= 0 -> RussianPeriodDeclensions.DAY.formatInGenitiveCase(normalizedPeriod.days)
14+
else -> error("Must never happen")
15+
}
16+
}
17+
18+
}
19+
20+
private enum class RussianPeriodDeclensions(
21+
private val singular: String,
22+
private val few: String,
23+
private val many: String
24+
) {
25+
26+
YEAR("года", "лет", "лет"),
27+
MONTH("месяца", "месяцев", "месяцев"),
28+
DAY("дня", "дней", "дней");
29+
30+
fun formatInGenitiveCase(count: Int): String {
31+
val mod100 = count % 100
32+
val mod10 = count % 10
33+
34+
return when {
35+
count == 1 -> singular
36+
mod100 in 11..14 -> "$count $many"
37+
mod10 in 2..4 -> "$count $few"
38+
else -> "$count $many"
39+
}
40+
}
41+
42+
}

app/src/main/kotlin/pro/qyoga/app/therapist/clients/ClientsListPageModel.kt

Lines changed: 0 additions & 20 deletions
This file was deleted.

app/src/main/kotlin/pro/qyoga/app/therapist/clients/ClientsListPageController.kt renamed to app/src/main/kotlin/pro/qyoga/app/therapist/clients/list/ClientsListPageController.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1-
package pro.qyoga.app.therapist.clients
1+
package pro.qyoga.app.therapist.clients.list
22

33
import org.springframework.data.domain.Pageable
44
import org.springframework.data.web.PageableDefault
55
import org.springframework.security.core.annotation.AuthenticationPrincipal
66
import org.springframework.stereotype.Controller
7-
import org.springframework.web.bind.annotation.*
7+
import org.springframework.web.bind.annotation.DeleteMapping
8+
import org.springframework.web.bind.annotation.GetMapping
9+
import org.springframework.web.bind.annotation.PathVariable
10+
import org.springframework.web.bind.annotation.ResponseBody
811
import pro.azhidkov.platform.spring.sdj.withSortBy
912
import pro.qyoga.core.clients.cards.ClientsRepo
1013
import pro.qyoga.core.clients.cards.ClientsRepo.Companion.descendingTouchTime
1114
import pro.qyoga.core.clients.cards.dtos.ClientSearchDto
12-
import pro.qyoga.core.clients.cards.findTherapistClientsPageBySearchForm
1315
import pro.qyoga.core.users.auth.dtos.QyogaUserDetails
16+
import java.time.LocalDate
1417
import java.util.*
1518

1619
@Controller
1720
class ClientsListPageController(
1821
private val clientsRepo: ClientsRepo
1922
) {
2023

21-
@GetMapping
22-
@RequestMapping(PATH)
24+
@GetMapping(PATH)
2325
fun getClients(
2426
@AuthenticationPrincipal principal: QyogaUserDetails,
2527
@PageableDefault(value = 10, page = 0) pageRequest: Pageable,
@@ -31,7 +33,7 @@ class ClientsListPageController(
3133
searchDto,
3234
pageRequest.withSortBy(descendingTouchTime)
3335
)
34-
return ClientsListPageModel(clients, searchDto)
36+
return ClientsListPageModel(clients, LocalDate.now(), searchDto)
3537
}
3638

3739
@GetMapping(SEARCH_PATH)
@@ -46,7 +48,7 @@ class ClientsListPageController(
4648
searchDto,
4749
pageRequest.withSortBy(descendingTouchTime)
4850
)
49-
return ClientsListPageModel(clients, searchDto, "clients")
51+
return ClientsListPageModel(clients, LocalDate.now(), searchDto, "clients")
5052
}
5153

5254
@DeleteMapping(DELETE_PATH)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package pro.qyoga.app.therapist.clients.list
2+
3+
import org.springframework.data.domain.Page
4+
import org.springframework.web.servlet.ModelAndView
5+
import pro.azhidkov.platform.spring.mvc.viewId
6+
import pro.azhidkov.platform.ui.PeriodFormatter
7+
import pro.qyoga.core.clients.cards.dtos.ClientSearchDto
8+
import pro.qyoga.core.clients.cards.model.HasPersonName
9+
import pro.qyoga.core.users.therapists.TherapistRef
10+
import pro.qyoga.l10n.russianDayOfMonthFormat
11+
import java.time.Instant
12+
import java.time.LocalDate
13+
import java.time.Period
14+
import java.util.*
15+
16+
17+
data class ClientListItemView(
18+
val id: UUID,
19+
override val firstName: String,
20+
override val lastName: String,
21+
override val middleName: String?,
22+
val journalEntriesCount: Int,
23+
private val lastJournalEntryDate: LocalDate?,
24+
val therapistRef: TherapistRef,
25+
val createdAt: Instant
26+
) : HasPersonName {
27+
28+
fun lastJournalEntryDateLabel(now: LocalDate): String = when (lastJournalEntryDate) {
29+
null ->
30+
NO_LAST_JOURNAL_ENTRY_DATE
31+
32+
in ((now - Period.ofMonths(1))..now) ->
33+
lastJournalEntryDate.format(russianDayOfMonthFormat)
34+
35+
else ->
36+
"Более ${PeriodFormatter.formatPeriodInGenitiveCase(Period.between(lastJournalEntryDate, now))} назад"
37+
}
38+
39+
companion object {
40+
const val NO_LAST_JOURNAL_ENTRY_DATE = ""
41+
}
42+
43+
}
44+
45+
data class ClientsListPageModel(
46+
val clients: Page<ClientListItemView>,
47+
val today: LocalDate,
48+
val searchDto: ClientSearchDto,
49+
val fragment: String? = null
50+
) : ModelAndView(
51+
viewId("therapist/clients/clients-list", fragment), mapOf(
52+
"clients" to clients,
53+
"today" to today,
54+
"searchDto" to searchDto,
55+
"pageNumbers" to 1..clients.totalPages
56+
)
57+
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package pro.qyoga.app.therapist.clients.list
2+
3+
import org.intellij.lang.annotations.Language
4+
import org.springframework.core.convert.converter.Converter
5+
import org.springframework.core.convert.support.DefaultConversionService
6+
import org.springframework.data.domain.Page
7+
import org.springframework.data.domain.Pageable
8+
import org.springframework.data.jdbc.core.mapping.AggregateReference
9+
import org.springframework.jdbc.core.DataClassRowMapper
10+
import pro.qyoga.core.clients.cards.ClientsRepo
11+
import pro.qyoga.core.clients.cards.dtos.ClientSearchDto
12+
import pro.qyoga.core.users.therapists.Therapist
13+
import java.util.*
14+
15+
fun ClientsRepo.findTherapistClientsPageBySearchForm(
16+
therapistId: UUID,
17+
clientSearchDto: ClientSearchDto,
18+
pageRequest: Pageable
19+
): Page<ClientListItemView> {
20+
@Language("PostgreSQL") val query = """
21+
SELECT c.*,
22+
le.date last_journal_entry_date,
23+
GREATEST(c.created_at, c.modified_at, le.created_at, le.last_modified_at) touch_time,
24+
(SELECT COUNT(*) FROM journal_entries je WHERE je.client_ref = c.id) journal_entries_count
25+
FROM clients c
26+
LEFT JOIN client_last_journal_entries le ON le.client_ref = c.id
27+
WHERE c.therapist_ref = :therapistRef
28+
AND c.first_name ILIKE '%' || :firstName || '%'
29+
AND c.last_name ILIKE '%' || :lastName || '%'
30+
AND c.phone_number ILIKE '%' || :phoneNumber || '%'
31+
"""
32+
33+
val paramMap = mapOf(
34+
"therapistRef" to therapistId,
35+
"firstName" to (clientSearchDto.firstName ?: ""),
36+
"lastName" to (clientSearchDto.lastName ?: ""),
37+
"phoneNumber" to (clientSearchDto.digitsOnlyPhoneNumber ?: "")
38+
)
39+
val clientListItemViewRowMapper = DataClassRowMapper(ClientListItemView::class.java).apply {
40+
conversionService = DefaultConversionService().apply {
41+
@Suppress("ObjectLiteralToLambda") val converter =
42+
object : Converter<UUID, AggregateReference<Therapist, UUID>> {
43+
override fun convert(source: UUID): AggregateReference<Therapist, UUID>? {
44+
return AggregateReference.to(source)
45+
}
46+
}
47+
addConverter(converter)
48+
}
49+
}
50+
return findPage(query, paramMap, pageRequest, clientListItemViewRowMapper)
51+
}

app/src/main/kotlin/pro/qyoga/app/therapist/root/TherapistMainPageController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package pro.qyoga.app.therapist.root
22

33
import org.springframework.stereotype.Controller
44
import org.springframework.web.bind.annotation.GetMapping
5-
import pro.qyoga.app.therapist.clients.ClientsListPageController
5+
import pro.qyoga.app.therapist.clients.list.ClientsListPageController
66

77

88
@Controller

app/src/main/kotlin/pro/qyoga/core/clients/cards/ClientsRepo.kt

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package pro.qyoga.core.clients.cards
22

3-
import org.intellij.lang.annotations.Language
4-
import org.springframework.data.domain.*
3+
import org.springframework.data.domain.PageRequest
4+
import org.springframework.data.domain.Pageable
5+
import org.springframework.data.domain.Slice
6+
import org.springframework.data.domain.Sort
57
import org.springframework.data.jdbc.core.JdbcAggregateOperations
68
import org.springframework.data.jdbc.core.convert.JdbcConverter
79
import org.springframework.data.relational.core.mapping.RelationalMappingContext
@@ -11,7 +13,6 @@ import pro.azhidkov.platform.spring.sdj.ergo.ErgoRepository
1113
import pro.azhidkov.platform.spring.sdj.query.BuildMode
1214
import pro.azhidkov.platform.spring.sdj.query.query
1315
import pro.azhidkov.platform.spring.sdj.sortBy
14-
import pro.qyoga.core.clients.cards.dtos.ClientSearchDto
1516
import pro.qyoga.core.clients.cards.errors.DuplicatedPhoneException
1617
import pro.qyoga.core.clients.cards.model.Client
1718
import pro.qyoga.core.clients.cards.model.PhoneNumber
@@ -49,31 +50,6 @@ class ClientsRepo(
4950

5051
}
5152

52-
fun ClientsRepo.findTherapistClientsPageBySearchForm(
53-
therapistId: UUID,
54-
clientSearchDto: ClientSearchDto,
55-
pageRequest: Pageable
56-
): Page<Client> {
57-
@Language("PostgreSQL") val query = """
58-
SELECT c.*,
59-
GREATEST(c.created_at, c.modified_at, le.created_at, le.last_modified_at) touch_time
60-
FROM clients c
61-
LEFT JOIN client_last_journal_entries le ON le.client_ref = c.id
62-
WHERE c.therapist_ref = :therapistRef
63-
AND c.first_name ILIKE '%' || :firstName || '%'
64-
AND c.last_name ILIKE '%' || :lastName || '%'
65-
AND c.phone_number ILIKE '%' || :phoneNumber || '%'
66-
"""
67-
68-
val paramMap = mapOf(
69-
"therapistRef" to therapistId,
70-
"firstName" to (clientSearchDto.firstName ?: ""),
71-
"lastName" to (clientSearchDto.lastName ?: ""),
72-
"phoneNumber" to (clientSearchDto.digitsOnlyPhoneNumber ?: "")
73-
)
74-
return findPage(query, paramMap, pageRequest)
75-
}
76-
7753
fun ClientsRepo.findTherapistClientsSliceBySearchKey(
7854
therapistId: UUID,
7955
searchKey: String,

app/src/main/kotlin/pro/qyoga/core/clients/cards/dtos/ClientCardDto.kt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import com.fasterxml.jackson.annotation.JsonFormat
44
import org.springframework.format.annotation.DateTimeFormat
55
import pro.qyoga.core.clients.cards.model.DistributionSource
66
import pro.qyoga.core.clients.cards.model.DistributionSourceType
7+
import pro.qyoga.core.clients.cards.model.HasPersonName
78
import pro.qyoga.l10n.RUSSIAN_DATE_FORMAT_PATTERN
89
import java.time.LocalDate
910

1011
data class ClientCardDto(
11-
val firstName: String,
12-
val lastName: String,
13-
val middleName: String?,
12+
override val firstName: String,
13+
override val lastName: String,
14+
override val middleName: String?,
1415
@DateTimeFormat(pattern = RUSSIAN_DATE_FORMAT_PATTERN)
1516
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = RUSSIAN_DATE_FORMAT_PATTERN)
1617
val birthDate: LocalDate?,
@@ -22,12 +23,8 @@ data class ClientCardDto(
2223
val distributionSourceType: DistributionSourceType?,
2324
val distributionSourceComment: String?,
2425
val version: Long
25-
) {
26+
) : HasPersonName {
2627

2728
val distributionSource = distributionSourceType?.let { DistributionSource(it, distributionSourceComment) }
2829

29-
fun fullName() = listOf(lastName, firstName, middleName)
30-
.filter { it?.isNotBlank() ?: false }
31-
.joinToString(" ")
32-
3330
}

0 commit comments

Comments
 (0)