1- import { createDateTimeInput } from "@/components/forms/DateTimeInput"
21import { useFormBuilder } from "@/components/forms/Form"
32import { createSelectInput } from "@/components/forms/SelectInput"
43import {
@@ -9,20 +8,100 @@ import {
98 getMembershipTypeName ,
109 getSpecializationName ,
1110} from "@dotkomonline/types"
12- import { getNextSemesterStart , getCurrentSemesterStart } from "@dotkomonline/utils"
13- import { isBefore } from "date-fns"
11+ import {
12+ getCurrentSemesterStart ,
13+ getNextSemesterStart ,
14+ getStudyGrade ,
15+ getCurrentUTC ,
16+ getPreviousSemesterStart ,
17+ isSpringSemester ,
18+ } from "@dotkomonline/utils"
19+ import { isBefore , roundToNearestHours } from "date-fns"
1420import type { z } from "zod"
15- import { createNumberInput } from "@/components/forms/NumberInput"
16- import { Code , Stack , Text } from "@mantine/core"
21+ import { ActionIcon , Button , Group , NumberInput , Stack } from "@mantine/core"
22+ import { Controller } from "react-hook-form"
23+ import { ErrorMessage } from "@hookform/error-message"
24+ import { DatePickerInput } from "@mantine/dates"
25+ import { IconArrowLeft , IconArrowRight , IconX } from "@tabler/icons-react"
26+
27+ const BACHELOR_SEMESTERS = 6
28+ const MASTER_SEMESTER_OFFSET = BACHELOR_SEMESTERS
29+ const MASTER_SEMESTERS = 4
1730
1831export const MembershipWriteFormSchema = MembershipWriteSchema . superRefine ( ( data , ctx ) => {
19- if ( data . end && isBefore ( data . end , data . start ) ) {
32+ if ( data . end !== null && isBefore ( data . end , data . start ) ) {
33+ ctx . addIssue ( {
34+ code : "custom" ,
35+ message : "Sluttdato må være etter startdato." ,
36+ path : [ "end" ] ,
37+ } )
38+ }
39+
40+ if ( data . end === null && data . type !== "KNIGHT" ) {
41+ ctx . addIssue ( {
42+ code : "custom" ,
43+ message : "Sluttdato må oppgis for ikke-Ridder-medlemskap." ,
44+ path : [ "end" ] ,
45+ } )
46+ }
47+
48+ if ( data . end !== null && data . type === "KNIGHT" ) {
2049 ctx . addIssue ( {
2150 code : "custom" ,
22- message : "Sluttdato må være etter startdato " ,
51+ message : "Riddermedlemskap skal ikke ha sluttdato. " ,
2352 path : [ "end" ] ,
2453 } )
2554 }
55+
56+ if ( data . type === "MASTER_STUDENT" ) {
57+ if ( data . specialization === null ) {
58+ ctx . addIssue ( {
59+ code : "custom" ,
60+ message : "Spesialisering må oppgis for mastermedlemskap." ,
61+ path : [ "specialization" ] ,
62+ } )
63+ }
64+
65+ if (
66+ data . semester === null ||
67+ data . semester < MASTER_SEMESTER_OFFSET ||
68+ data . semester >= MASTER_SEMESTER_OFFSET + MASTER_SEMESTERS
69+ ) {
70+ ctx . addIssue ( {
71+ code : "custom" ,
72+ message : `Semester må være oppgitt og minst ${ MASTER_SEMESTER_OFFSET + 1 } og maks ${ MASTER_SEMESTER_OFFSET + MASTER_SEMESTERS } for mastermedlemskap.` ,
73+ path : [ "semester" ] ,
74+ } )
75+ }
76+ }
77+
78+ if ( data . specialization !== null && data . type !== "MASTER_STUDENT" ) {
79+ ctx . addIssue ( {
80+ code : "custom" ,
81+ message : "Spesialisering kan kun oppgis for mastermedlemskap." ,
82+ path : [ "specialization" ] ,
83+ } )
84+ }
85+
86+ if ( data . type === "BACHELOR_STUDENT" ) {
87+ if ( data . semester === null || data . semester < 0 || data . semester >= BACHELOR_SEMESTERS ) {
88+ ctx . addIssue ( {
89+ code : "custom" ,
90+ message : `Semester må være oppgitt og minst 1 og maks ${ BACHELOR_SEMESTERS } for bachelormedlemskap.` ,
91+ path : [ "semester" ] ,
92+ } )
93+ }
94+ }
95+
96+ if ( data . type === "SOCIAL_MEMBER" ) {
97+ if ( data . semester === null || data . semester < 0 || data . semester >= BACHELOR_SEMESTERS + MASTER_SEMESTERS ) {
98+ ctx . addIssue ( {
99+ code : "custom" ,
100+ message : `Semester må være oppgitt og minst 1 og maks ${ BACHELOR_SEMESTERS + MASTER_SEMESTERS } for sosialmedlemskap.` ,
101+ path : [ "semester" ] ,
102+ } )
103+ }
104+ }
26105} )
27106
28107type MembershipWriteFormSchema = z . infer < typeof MembershipWriteFormSchema >
@@ -61,8 +140,7 @@ export const useMembershipWriteForm = ({
61140 } ) ) ,
62141 } ) ,
63142 specialization : createSelectInput ( {
64- label : "Spesialisering" ,
65- description : "Masterspesialisering" ,
143+ label : "Masterspesialisering" ,
66144 required : false ,
67145 clearable : true ,
68146 placeholder : "Velg spesialisering" ,
@@ -74,48 +152,179 @@ export const useMembershipWriteForm = ({
74152 } ) ) ,
75153 disabled : false ,
76154 } ) ,
77- start : createDateTimeInput ( {
78- label : "Startdato" ,
79- required : true ,
80- } ) ,
81- end : createDateTimeInput ( {
82- label : "Sluttdato" ,
83- required : true ,
84- } ) ,
85- semester : createNumberInput ( {
86- label : "Semester" ,
87- description : (
88- < Stack gap = "xs" >
89- < Text size = "xs" c = "dimmed" >
90- Hvilket semester medlemskapet innebærer. 0-indeksert.
91- </ Text >
92- < Stack gap = "0.25rem" >
93- < Text size = "xs" c = "dimmed" >
94- < Code > 0</ Code > → 1. semester (1. årstrinn)
95- </ Text >
96- < Text size = "xs" c = "dimmed" >
97- < Code > 1</ Code > → 2. semester (1. årstrinn)
98- </ Text >
99- < Text size = "xs" c = "dimmed" >
100- < Code > 2</ Code > → 3. semester (2. årstrinn)
101- </ Text >
102- < Text size = "xs" c = "dimmed" >
103- ...
104- </ Text >
105- < Text size = "xs" c = "dimmed" >
106- < Code > 8</ Code > → 9. semester (5. årstrinn)
107- </ Text >
108- < Text size = "xs" c = "dimmed" >
109- < Code > 9</ Code > → 10. semester (5. årstrinn)
110- </ Text >
111- </ Stack >
112- </ Stack >
113- ) ,
114- required : false ,
115- min : 0 ,
116- max : 9 ,
117- allowDecimal : false ,
118- } ) ,
155+ semester : ( { state, control } ) => {
156+ const name = "semester"
157+ const label = "Semester"
158+
159+ return (
160+ < Controller
161+ control = { control }
162+ name = { name }
163+ render = { ( { field } ) => {
164+ const zeroIndexedValue = field . value !== null ? field . value : null
165+ const oneIndexedValue = zeroIndexedValue !== null ? zeroIndexedValue + 1 : null
166+ const studyGrade = zeroIndexedValue !== null ? getStudyGrade ( zeroIndexedValue ) : null
167+ const isAutumnSemester = zeroIndexedValue !== null ? zeroIndexedValue % 2 === 0 : null
168+
169+ return (
170+ < Stack gap = "0.25rem" >
171+ < NumberInput
172+ label = { label }
173+ description = {
174+ oneIndexedValue !== null && isAutumnSemester !== null && studyGrade !== null
175+ ? `${ oneIndexedValue } . semester innebærer ${ isAutumnSemester ? "høsten" : "våren" } i ${ studyGrade } . årsgang`
176+ : "Ingen semesterverdi"
177+ }
178+ min = { 1 }
179+ max = { 10 }
180+ allowDecimal = { false }
181+ value = { field . value !== null ? field . value + 1 : undefined }
182+ onChange = { ( value ) => {
183+ const zeroIndexedValue = value !== undefined ? Number ( value ) - 1 : null
184+ field . onChange ( zeroIndexedValue )
185+ } }
186+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
187+ />
188+ < Button
189+ w = "fit-content"
190+ fw = "normal"
191+ color = "gray"
192+ size = "compact-xs"
193+ variant = "subtle"
194+ onClick = { ( ) => field . onChange ( null ) }
195+ leftSection = { < IconX size = "0.85rem" /> }
196+ styles = { { section : { marginRight : "0.35rem" } } }
197+ >
198+ Fjern verdi
199+ </ Button >
200+ </ Stack >
201+ )
202+ } }
203+ />
204+ )
205+ } ,
206+ start : ( { state, control } ) => {
207+ const name = "start"
208+
209+ return (
210+ < Controller
211+ control = { control }
212+ name = { name }
213+ render = { ( { field } ) => (
214+ < Stack gap = "0.25rem" >
215+ < DatePickerInput
216+ label = "Startdato"
217+ valueFormat = "YYYY-MM-DD"
218+ description = {
219+ field . value
220+ ? isSpringSemester ( field . value )
221+ ? `Vår ${ field . value . getFullYear ( ) } `
222+ : `Høst ${ field . value . getFullYear ( ) } `
223+ : undefined
224+ }
225+ style = { { flexGrow : 1 } }
226+ defaultValue = {
227+ state . defaultValues ?. [ name ] ?? roundToNearestHours ( getCurrentUTC ( ) , { roundingMethod : "ceil" } )
228+ }
229+ value = { field . value }
230+ onChange = { field . onChange }
231+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
232+ required
233+ />
234+ < Group >
235+ < Button
236+ w = "fit-content"
237+ fw = "normal"
238+ color = "gray"
239+ size = "compact-xs"
240+ variant = "subtle"
241+ onClick = { ( ) => field . onChange ( getPreviousSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
242+ leftSection = { < IconArrowLeft size = "0.85rem" /> }
243+ styles = { { section : { marginRight : "0.35rem" } } }
244+ >
245+ Forrige semester
246+ </ Button >
247+ < Button
248+ w = "fit-content"
249+ fw = "normal"
250+ color = "gray"
251+ size = "compact-xs"
252+ variant = "subtle"
253+ onClick = { ( ) => field . onChange ( getNextSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
254+ leftSection = { < IconArrowRight size = "0.85rem" /> }
255+ styles = { { section : { marginRight : "0.35rem" } } }
256+ >
257+ Neste semester
258+ </ Button >
259+ </ Group >
260+ </ Stack >
261+ ) }
262+ />
263+ )
264+ } ,
265+ end : ( { state, control } ) => {
266+ const name = "end"
267+
268+ return (
269+ < Controller
270+ control = { control }
271+ name = { name }
272+ render = { ( { field } ) => (
273+ < Stack gap = "0.25rem" >
274+ < DatePickerInput
275+ label = "Sluttdato"
276+ description = {
277+ field . value
278+ ? isSpringSemester ( field . value )
279+ ? `Vår ${ field . value . getFullYear ( ) } `
280+ : `Høst ${ field . value . getFullYear ( ) } `
281+ : undefined
282+ }
283+ valueFormat = "YYYY-MM-DD"
284+ style = { { flexGrow : 1 } }
285+ defaultValue = {
286+ state . defaultValues ?. [ name ] ?? roundToNearestHours ( getCurrentUTC ( ) , { roundingMethod : "ceil" } )
287+ }
288+ value = { field . value }
289+ onChange = { field . onChange }
290+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
291+ rightSection = {
292+ < ActionIcon w = "fit-content" color = "gray" variant = "subtle" onClick = { ( ) => field . onChange ( null ) } >
293+ < IconX size = "0.85rem" />
294+ </ ActionIcon >
295+ }
296+ />
297+ < Group >
298+ < Button
299+ w = "fit-content"
300+ fw = "normal"
301+ color = "gray"
302+ size = "compact-xs"
303+ variant = "subtle"
304+ onClick = { ( ) => field . onChange ( getPreviousSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
305+ leftSection = { < IconArrowLeft size = "0.85rem" /> }
306+ styles = { { section : { marginRight : "0.35rem" } } }
307+ >
308+ Forrige semester
309+ </ Button >
310+ < Button
311+ w = "fit-content"
312+ fw = "normal"
313+ color = "gray"
314+ size = "compact-xs"
315+ variant = "subtle"
316+ onClick = { ( ) => field . onChange ( getNextSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
317+ leftSection = { < IconArrowRight size = "0.85rem" /> }
318+ styles = { { section : { marginRight : "0.35rem" } } }
319+ >
320+ Neste semester
321+ </ Button >
322+ </ Group >
323+ </ Stack >
324+ ) }
325+ />
326+ )
327+ } ,
119328 } ,
120329 } )
121330}
0 commit comments