Skip to content

Commit e787f65

Browse files
committed
fix(questions): show during checkin answers, validation display for question inputs
1 parent aa5929c commit e787f65

File tree

10 files changed

+576
-55
lines changed

10 files changed

+576
-55
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package eu.pretix.desktop.app.ui
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material.icons.Icons
8+
import androidx.compose.material.icons.filled.Delete
9+
import androidx.compose.material3.Button
10+
import androidx.compose.material3.Icon
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.text.font.FontWeight
17+
import androidx.compose.ui.unit.dp
18+
import eu.pretix.scan.tickets.presentation.QuestionImagePreview
19+
import org.jetbrains.compose.resources.stringResource
20+
import pretixscan.composeapp.generated.resources.Res
21+
import pretixscan.composeapp.generated.resources.delete_photo
22+
import pretixscan.composeapp.generated.resources.question_input_invalid
23+
import pretixscan.composeapp.generated.resources.question_input_required
24+
import pretixscan.composeapp.generated.resources.take_a_photo
25+
26+
@Composable
27+
fun FiledFileUpload(
28+
label: String? = null,
29+
required: Boolean = false,
30+
validation: FieldValidationState?,
31+
selectedFilePath: String?,
32+
onSelectFile: () -> Unit = {},
33+
onDeleteFile: () -> Unit = {},
34+
) {
35+
Column(
36+
horizontalAlignment = Alignment.Start
37+
) {
38+
if (label != null) {
39+
RequiredTextLabel(label = label, required = required, fontWeight = FontWeight.SemiBold)
40+
}
41+
Row {
42+
Column(
43+
modifier = Modifier.weight(2f)
44+
.padding(end = 16.dp)
45+
) {
46+
Button(onClick = onSelectFile) {
47+
Text(stringResource(Res.string.take_a_photo))
48+
}
49+
if (selectedFilePath != null) {
50+
Button(
51+
onClick = onDeleteFile) {
52+
Icon(
53+
Icons.Filled.Delete,
54+
contentDescription = stringResource(Res.string.delete_photo)
55+
)
56+
Text(stringResource(Res.string.delete_photo))
57+
}
58+
}
59+
}
60+
61+
if (selectedFilePath != null) {
62+
Box(modifier = Modifier.weight(1f)) {
63+
QuestionImagePreview(filePath = selectedFilePath)
64+
}
65+
}
66+
}
67+
68+
if (validation != null) {
69+
when (validation) {
70+
FieldValidationState.INVALID -> {
71+
Text(
72+
stringResource(Res.string.question_input_invalid),
73+
color = Color.Red,
74+
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp)
75+
)
76+
}
77+
78+
FieldValidationState.MISSING -> {
79+
Text(
80+
stringResource(Res.string.question_input_required),
81+
color = Color.Red,
82+
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp)
83+
)
84+
}
85+
}
86+
}
87+
}
88+
}

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/scan/tickets/presentation/CountryReadableName.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ import com.vanniktech.locale.Country
33

44

55
fun Country.readableName(): String {
6-
return name.lowercase().replace(Regex("""(?:^|_+)(\p{L})""")) { (if (it.value.startsWith("_")) " " else "") + it.groupValues[1].uppercase() } // LIKE_THIS -> "Like This"
6+
if (name.length <= 4 && name.all { it.isUpperCase() || it == '_' }) {
7+
return name
8+
}
9+
return name.lowercase().replace(Regex("""(?:^|_+)(\p{L})""")) { (if (it.value.startsWith("_")) " " else "") + it.groupValues[1].uppercase() }
710
}

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/scan/tickets/presentation/QuestionPhoneNumber.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fun QuestionPhoneNumber(
2121
var country by remember { mutableStateOf(calculateDefaultCountry(selectedValue)) }
2222
country.callingCodes.first()
2323

24-
LaunchedEffect(Unit, selectedValue) {
24+
LaunchedEffect(Unit) {
2525
country = calculateDefaultCountry(selectedValue)
2626
}
2727

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/scan/tickets/presentation/QuestionsDialogView.kt

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -210,46 +210,14 @@ fun QuestionsDialogView(
210210
}
211211

212212
QuestionType.F -> {
213-
Column(
214-
horizontalAlignment = Alignment.Start
215-
) {
216-
RequiredTextLabel(
217-
label = field.label,
218-
required = field.required,
219-
fontWeight = FontWeight.SemiBold
220-
)
221-
Row {
222-
Column(
223-
modifier = Modifier.weight(2f)
224-
.padding(end = 16.dp)
225-
) {
226-
Button(
227-
onClick = {
228-
viewModel.showModal(field)
229-
}) {
230-
Text(stringResource(Res.string.take_a_photo))
231-
}
232-
if (field.value != null) {
233-
Button(
234-
onClick = {
235-
viewModel.updateAnswer(field.id, null)
236-
}) {
237-
Icon(
238-
Icons.Filled.Delete,
239-
contentDescription = stringResource(Res.string.delete_photo)
240-
)
241-
Text(stringResource(Res.string.delete_photo))
242-
}
243-
}
244-
}
245-
246-
if (field.value != null) {
247-
Box(modifier = Modifier.weight(1f)) {
248-
QuestionImagePreview(filePath = field.value!!)
249-
}
250-
}
251-
}
252-
}
213+
FiledFileUpload(
214+
label = field.label,
215+
required = field.required,
216+
validation = field.validation,
217+
selectedFilePath = field.value,
218+
onSelectFile = { viewModel.showModal(field) },
219+
onDeleteFile = { viewModel.updateAnswer(field.id, null) }
220+
)
253221
}
254222

255223
QuestionType.D -> {

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/scan/tickets/presentation/QuestionsDialogViewModel.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
103103
it.question,
104104
startingAnswerValue(it, data.answers[it]),
105105
it.type,
106-
true
106+
it.required
107107
)
108108
}
109109

@@ -113,7 +113,7 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
113113
it.question,
114114
startingAnswerValue(it, data.answers[it]),
115115
it.type,
116-
true,
116+
it.required,
117117
keyValueOptions = it.options?.sortedBy { option -> option.position }
118118
?.map { option ->
119119
SelectableValue(
@@ -131,7 +131,7 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
131131
it.question,
132132
startingAnswerValue(it, data.answers[it]),
133133
it.type,
134-
true,
134+
it.required,
135135
keyValueOptions = it.options?.sortedBy { option -> option.position }
136136
?.map { option ->
137137
SelectableValue(
@@ -151,7 +151,7 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
151151
it.question,
152152
startingAnswerValue(it, data.answers[it]),
153153
it.type,
154-
true,
154+
it.required,
155155
dateConfig = DateConfig(minDate = it.valid_date_min, maxDate = it.valid_date_max)
156156
)
157157
}
@@ -162,7 +162,7 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
162162
it.question,
163163
startingAnswerValue(it, data.answers[it]),
164164
it.type,
165-
true
165+
it.required
166166
)
167167
}
168168

@@ -172,7 +172,7 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
172172
it.question,
173173
startingAnswerValue(it, data.answers[it]),
174174
it.type,
175-
true,
175+
it.required,
176176
keyValueOptions = Country.entries.map { country ->
177177
SelectableValue(
178178
country.code,
@@ -267,7 +267,11 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
267267
val answer = it.value
268268
val country = it.uiExtra
269269
if (answer.isNullOrBlank()) {
270-
it.copy(validation = FieldValidationState.MISSING)
270+
if (it.required) {
271+
it.copy(validation = FieldValidationState.MISSING)
272+
} else {
273+
it.copy(validation = null)
274+
}
271275
} else {
272276
val parsed = phoneValidator.parse(answer, country)
273277
if (parsed == null) {
@@ -278,8 +282,20 @@ class QuestionsDialogViewModel(private val config: DataStoreConfigStore) : ViewM
278282
}
279283
}
280284

285+
QuestionType.M -> {
286+
if (it.required && it.values.isNullOrEmpty()) {
287+
it.copy(validation = FieldValidationState.MISSING)
288+
} else {
289+
it.copy(validation = null)
290+
}
291+
}
292+
281293
else -> {
282-
it
294+
if (it.required && it.value.isNullOrBlank()) {
295+
it.copy(validation = FieldValidationState.MISSING)
296+
} else {
297+
it.copy(validation = null)
298+
}
283299
}
284300
}
285301
}

pretixscan/composeApp/src/commonMain/kotlin/eu/pretix/scan/tickets/presentation/TicketSuccess.kt

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.Alignment
1212
import androidx.compose.ui.Modifier
1313
import androidx.compose.ui.layout.ContentScale
14+
import androidx.compose.ui.text.font.FontWeight
1415
import androidx.compose.ui.unit.dp
16+
import androidx.compose.ui.unit.sp
1517
import eu.pretix.desktop.app.ui.CustomColor
1618
import eu.pretix.desktop.app.ui.asColor
1719
import eu.pretix.scan.tickets.data.ResultStateData
@@ -89,15 +91,62 @@ fun TicketSuccess(
8991
}
9092
}
9193

92-
Row(
94+
Column(
9395
modifier = Modifier
9496
.fillMaxWidth()
9597
.background(CustomColor.White.asColor())
96-
.padding(16.dp),
97-
horizontalArrangement = Arrangement.End,
98-
verticalAlignment = Alignment.CenterVertically
98+
.padding(16.dp)
9999
) {
100-
Text(data.orderCodeAndPositionId ?: "", style = MaterialTheme.typography.bodyLarge)
100+
Row(
101+
modifier = Modifier.fillMaxWidth(),
102+
horizontalArrangement = Arrangement.SpaceBetween,
103+
verticalAlignment = Alignment.Top
104+
) {
105+
if (data.attendeeName != null) {
106+
Text(
107+
text = data.attendeeName,
108+
style = MaterialTheme.typography.bodyMedium,
109+
fontWeight = FontWeight.Bold,
110+
modifier = Modifier.weight(1f)
111+
)
112+
}
113+
114+
if (data.orderCodeAndPositionId != null) {
115+
Text(
116+
text = data.orderCodeAndPositionId,
117+
style = MaterialTheme.typography.bodyMedium
118+
)
119+
}
120+
}
121+
122+
if (data.seat != null || data.questionAndAnswers != null || data.checkInTexts != null) {
123+
Spacer(modifier = Modifier.height(4.dp))
124+
125+
Column {
126+
if (data.seat != null) {
127+
Text(
128+
text = data.seat,
129+
style = MaterialTheme.typography.bodySmall
130+
)
131+
}
132+
133+
if (data.questionAndAnswers != null) {
134+
Text(
135+
text = data.questionAndAnswers,
136+
style = MaterialTheme.typography.bodySmall,
137+
lineHeight = 18.sp
138+
)
139+
}
140+
141+
if (data.checkInTexts != null) {
142+
Text(
143+
text = data.checkInTexts,
144+
style = MaterialTheme.typography.bodySmall,
145+
lineHeight = 18.sp
146+
)
147+
}
148+
}
149+
}
101150
}
102151

103152
// Countdown progress indicator

pretixscan/composeApp/src/desktopTest/kotlin/eu/pretix/desktop/scan/tickets/data/PhoneValidatorTest.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,66 @@ class PhoneValidatorTest {
3838
assertEquals("+493012345678", parsed.number)
3939
assertEquals("+49 30 1234.5678", parsed.rawValue)
4040
}
41+
42+
@Test
43+
fun null_region_code_with_international_format() = runTest {
44+
val sut = PhoneValidator()
45+
val phone = "+32474123456"
46+
assertEquals(true, sut.isValid(phone, null))
47+
val parsed = sut.parse(phone, null)
48+
assertNotNull(parsed)
49+
assertEquals("+32474123456", parsed.number)
50+
}
51+
52+
@Test
53+
fun empty_region_code_with_international_format() = runTest {
54+
val sut = PhoneValidator()
55+
val phone = "+32474123456"
56+
assertEquals(true, sut.isValid(phone, ""))
57+
val parsed = sut.parse(phone, "")
58+
assertNotNull(parsed)
59+
assertEquals("+32474123456", parsed.number)
60+
}
61+
62+
@Test
63+
fun formats_US_number_correctly() = runTest {
64+
val sut = PhoneValidator()
65+
val phone = "2125551234"
66+
val region = "US"
67+
assertEquals(true, sut.isValid(phone, region))
68+
val parsed = sut.parse(phone, region)
69+
assertNotNull(parsed)
70+
assertEquals("+12125551234", parsed.number)
71+
}
72+
73+
@Test
74+
fun formats_UK_number_correctly() = runTest {
75+
val sut = PhoneValidator()
76+
val phone = "02071234567"
77+
val region = "GB"
78+
assertEquals(true, sut.isValid(phone, region))
79+
val parsed = sut.parse(phone, region)
80+
assertNotNull(parsed)
81+
assertEquals("+442071234567", parsed.number)
82+
}
83+
84+
@Test
85+
fun preserves_correct_country_code_in_E164_format() = runTest {
86+
val sut = PhoneValidator()
87+
val phone = "0474 12 34 56"
88+
val region = "BE"
89+
val parsed = sut.parse(phone, region)
90+
assertNotNull(parsed)
91+
assertEquals("+32474123456", parsed.number)
92+
}
93+
94+
@Test
95+
fun does_not_change_country_code_when_region_specified() = runTest {
96+
val sut = PhoneValidator()
97+
val phone = "0474 12 34 56"
98+
val wrongRegion = "US"
99+
val parsed = sut.parse(phone, wrongRegion)
100+
assertNotNull(parsed)
101+
assertEquals("+10474123456", parsed.number)
102+
}
41103
}

0 commit comments

Comments
 (0)