1- import { createDateTimeInput } from "@/components/forms/DateTimeInput"
21import { useFormBuilder } from "@/components/forms/Form"
32import { createSelectInput } from "@/components/forms/SelectInput"
43import {
@@ -9,20 +8,62 @@ 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"
1726
1827export const MembershipWriteFormSchema = MembershipWriteSchema . superRefine ( ( data , ctx ) => {
19- if ( data . end && isBefore ( data . end , data . start ) ) {
28+ if ( data . end !== null && isBefore ( data . end , data . start ) ) {
2029 ctx . addIssue ( {
2130 code : "custom" ,
22- message : "Sluttdato må være etter startdato" ,
31+ message : "Sluttdato må være etter startdato. " ,
2332 path : [ "end" ] ,
2433 } )
2534 }
35+
36+ if ( data . end === null && data . type !== "KNIGHT" ) {
37+ ctx . addIssue ( {
38+ code : "custom" ,
39+ message : "Sluttdato må oppgis for ikke-Ridder-medlemskap." ,
40+ path : [ "end" ] ,
41+ } )
42+ }
43+
44+ if ( data . end !== null && data . type === "KNIGHT" ) {
45+ ctx . addIssue ( {
46+ code : "custom" ,
47+ message : "Riddermedlemskap skal ikke ha sluttdato." ,
48+ path : [ "end" ] ,
49+ } )
50+ }
51+
52+ if ( data . specialization !== null && data . type !== "MASTER_STUDENT" ) {
53+ ctx . addIssue ( {
54+ code : "custom" ,
55+ message : "Spesialisering kan kun oppgis for mastermedlemskap." ,
56+ path : [ "specialization" ] ,
57+ } )
58+ }
59+
60+ if ( data . specialization === null && data . type === "MASTER_STUDENT" ) {
61+ ctx . addIssue ( {
62+ code : "custom" ,
63+ message : "Spesialisering må oppgis for mastermedlemskap." ,
64+ path : [ "specialization" ] ,
65+ } )
66+ }
2667} )
2768
2869type MembershipWriteFormSchema = z . infer < typeof MembershipWriteFormSchema >
@@ -61,8 +102,7 @@ export const useMembershipWriteForm = ({
61102 } ) ) ,
62103 } ) ,
63104 specialization : createSelectInput ( {
64- label : "Spesialisering" ,
65- description : "Masterspesialisering" ,
105+ label : "Masterspesialisering" ,
66106 required : false ,
67107 clearable : true ,
68108 placeholder : "Velg spesialisering" ,
@@ -74,48 +114,164 @@ export const useMembershipWriteForm = ({
74114 } ) ) ,
75115 disabled : false ,
76116 } ) ,
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- } ) ,
117+ semester : ( { state, control } ) => {
118+ const name = "semester"
119+ const label = "Semester"
120+
121+ return (
122+ < Controller
123+ control = { control }
124+ name = { name }
125+ render = { ( { field } ) => {
126+ const oneIndexedValue = field . value !== null ? field . value + 1 : null
127+ const studyGrade = field . value !== null ? getStudyGrade ( field . value ) : null
128+ const even = field . value !== null ? field . value % 2 === 0 : null
129+
130+ return (
131+ < NumberInput
132+ label = { label }
133+ description = {
134+ oneIndexedValue !== null
135+ ? `${ oneIndexedValue } . semester innebærer ${ even ? "våren" : "høsten" } i ${ studyGrade } . årsgang`
136+ : "Velg en verdi"
137+ }
138+ min = { 1 }
139+ max = { 10 }
140+ allowDecimal = { false }
141+ value = { field . value !== null ? field . value + 1 : undefined }
142+ onChange = { ( value ) => {
143+ const zeroIndexedValue = value !== undefined ? Number ( value ) - 1 : null
144+ field . onChange ( zeroIndexedValue )
145+ } }
146+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
147+ />
148+ )
149+ } }
150+ />
151+ )
152+ } ,
153+ start : ( { state, control } ) => {
154+ const name = "start"
155+
156+ return (
157+ < Controller
158+ control = { control }
159+ name = { name }
160+ render = { ( { field } ) => (
161+ < Stack gap = "0.25rem" >
162+ < DatePickerInput
163+ label = "Startdato"
164+ valueFormat = "YYYY-MM-DD"
165+ description = {
166+ field . value
167+ ? isSpringSemester ( field . value )
168+ ? `Vår ${ field . value . getFullYear ( ) } `
169+ : `Høst ${ field . value . getFullYear ( ) } `
170+ : undefined
171+ }
172+ style = { { flexGrow : 1 } }
173+ defaultValue = {
174+ state . defaultValues ?. [ name ] ?? roundToNearestHours ( getCurrentUTC ( ) , { roundingMethod : "ceil" } )
175+ }
176+ value = { field . value }
177+ onChange = { field . onChange }
178+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
179+ required
180+ />
181+ < Group >
182+ < Button
183+ w = "fit-content"
184+ fw = "normal"
185+ color = "gray"
186+ size = "compact-xs"
187+ variant = "subtle"
188+ onClick = { ( ) => field . onChange ( getPreviousSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
189+ leftSection = { < IconArrowLeft size = "0.85rem" /> }
190+ styles = { { section : { marginRight : "0.35rem" } } }
191+ >
192+ Forrige semester
193+ </ Button >
194+ < Button
195+ w = "fit-content"
196+ fw = "normal"
197+ color = "gray"
198+ size = "compact-xs"
199+ variant = "subtle"
200+ onClick = { ( ) => field . onChange ( getNextSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
201+ leftSection = { < IconArrowRight size = "0.85rem" /> }
202+ styles = { { section : { marginRight : "0.35rem" } } }
203+ >
204+ Neste semester
205+ </ Button >
206+ </ Group >
207+ </ Stack >
208+ ) }
209+ />
210+ )
211+ } ,
212+ end : ( { state, control } ) => {
213+ const name = "end"
214+
215+ return (
216+ < Controller
217+ control = { control }
218+ name = { name }
219+ render = { ( { field } ) => (
220+ < Stack gap = "0.25rem" >
221+ < DatePickerInput
222+ label = "Sluttdato"
223+ description = {
224+ field . value
225+ ? isSpringSemester ( field . value )
226+ ? `Vår ${ field . value . getFullYear ( ) } `
227+ : `Høst ${ field . value . getFullYear ( ) } `
228+ : undefined
229+ }
230+ valueFormat = "YYYY-MM-DD"
231+ style = { { flexGrow : 1 } }
232+ defaultValue = {
233+ state . defaultValues ?. [ name ] ?? roundToNearestHours ( getCurrentUTC ( ) , { roundingMethod : "ceil" } )
234+ }
235+ value = { field . value }
236+ onChange = { field . onChange }
237+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
238+ rightSection = {
239+ < ActionIcon w = "fit-content" color = "gray" variant = "subtle" onClick = { ( ) => field . onChange ( null ) } >
240+ < IconX size = "0.85rem" />
241+ </ ActionIcon >
242+ }
243+ />
244+ < Group >
245+ < Button
246+ w = "fit-content"
247+ fw = "normal"
248+ color = "gray"
249+ size = "compact-xs"
250+ variant = "subtle"
251+ onClick = { ( ) => field . onChange ( getPreviousSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
252+ leftSection = { < IconArrowLeft size = "0.85rem" /> }
253+ styles = { { section : { marginRight : "0.35rem" } } }
254+ >
255+ Forrige semester
256+ </ Button >
257+ < Button
258+ w = "fit-content"
259+ fw = "normal"
260+ color = "gray"
261+ size = "compact-xs"
262+ variant = "subtle"
263+ onClick = { ( ) => field . onChange ( getNextSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
264+ leftSection = { < IconArrowRight size = "0.85rem" /> }
265+ styles = { { section : { marginRight : "0.35rem" } } }
266+ >
267+ Neste semester
268+ </ Button >
269+ </ Group >
270+ </ Stack >
271+ ) }
272+ />
273+ )
274+ } ,
119275 } ,
120276 } )
121277}
0 commit comments