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" ) {
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" ) {
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,159 @@ 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+ start : ( { state, control } ) => {
118+ const name = "start"
119+
120+ return (
121+ < Controller
122+ control = { control }
123+ name = { name }
124+ render = { ( { field } ) => (
125+ < Stack gap = "0.25rem" >
126+ < DatePickerInput
127+ label = "Startdato"
128+ valueFormat = "YYYY-MM-DD"
129+ description = {
130+ field . value
131+ ? isSpringSemester ( field . value )
132+ ? `Vår ${ field . value . getFullYear ( ) } `
133+ : `Høst ${ field . value . getFullYear ( ) } `
134+ : undefined
135+ }
136+ style = { { flexGrow : 1 } }
137+ defaultValue = {
138+ state . defaultValues ?. [ name ] ?? roundToNearestHours ( getCurrentUTC ( ) , { roundingMethod : "ceil" } )
139+ }
140+ value = { field . value }
141+ onChange = { field . onChange }
142+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
143+ required
144+ />
145+ < Group >
146+ < Button
147+ w = "fit-content"
148+ fw = "normal"
149+ color = "gray"
150+ size = "compact-xs"
151+ variant = "subtle"
152+ onClick = { ( ) => field . onChange ( getPreviousSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
153+ leftSection = { < IconArrowLeft size = "0.85rem" /> }
154+ styles = { { section : { marginRight : "0.35rem" } } }
155+ >
156+ Forrige semester
157+ </ Button >
158+ < Button
159+ w = "fit-content"
160+ fw = "normal"
161+ color = "gray"
162+ size = "compact-xs"
163+ variant = "subtle"
164+ onClick = { ( ) => field . onChange ( getNextSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
165+ leftSection = { < IconArrowRight size = "0.85rem" /> }
166+ styles = { { section : { marginRight : "0.35rem" } } }
167+ >
168+ Neste semester
169+ </ Button >
170+ </ Group >
171+ </ Stack >
172+ ) }
173+ />
174+ )
175+ } ,
176+ end : ( { state, control } ) => {
177+ const name = "end"
178+
179+ return (
180+ < Controller
181+ control = { control }
182+ name = { name }
183+ render = { ( { field } ) => (
184+ < Stack gap = "0.25rem" >
185+ < DatePickerInput
186+ label = "Sluttdato"
187+ description = {
188+ field . value
189+ ? isSpringSemester ( field . value )
190+ ? `Vår ${ field . value . getFullYear ( ) } `
191+ : `Høst ${ field . value . getFullYear ( ) } `
192+ : undefined
193+ }
194+ valueFormat = "YYYY-MM-DD"
195+ style = { { flexGrow : 1 } }
196+ defaultValue = {
197+ state . defaultValues ?. [ name ] ?? roundToNearestHours ( getCurrentUTC ( ) , { roundingMethod : "ceil" } )
198+ }
199+ value = { field . value }
200+ onChange = { field . onChange }
201+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
202+ rightSection = {
203+ < ActionIcon w = "fit-content" color = "gray" variant = "subtle" onClick = { ( ) => field . onChange ( null ) } >
204+ < IconX size = "0.85rem" />
205+ </ ActionIcon >
206+ }
207+ />
208+ < Group >
209+ < Button
210+ w = "fit-content"
211+ fw = "normal"
212+ color = "gray"
213+ size = "compact-xs"
214+ variant = "subtle"
215+ onClick = { ( ) => field . onChange ( getPreviousSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
216+ leftSection = { < IconArrowLeft size = "0.85rem" /> }
217+ styles = { { section : { marginRight : "0.35rem" } } }
218+ >
219+ Forrige semester
220+ </ Button >
221+ < Button
222+ w = "fit-content"
223+ fw = "normal"
224+ color = "gray"
225+ size = "compact-xs"
226+ variant = "subtle"
227+ onClick = { ( ) => field . onChange ( getNextSemesterStart ( field . value ?? getCurrentUTC ( ) ) ) }
228+ leftSection = { < IconArrowRight size = "0.85rem" /> }
229+ styles = { { section : { marginRight : "0.35rem" } } }
230+ >
231+ Neste semester
232+ </ Button >
233+ </ Group >
234+ </ Stack >
235+ ) }
236+ />
237+ )
238+ } ,
239+ semester : ( { state, control } ) => {
240+ const name = "semester"
241+ const label = "Semester"
242+
243+ return (
244+ < Controller
245+ control = { control }
246+ name = { name }
247+ render = { ( { field } ) => {
248+ const oneIndexedValue = field . value !== null ? field . value + 1 : null
249+ const studyGrade = field . value !== null ? getStudyGrade ( field . value ) : null
250+
251+ return (
252+ < NumberInput
253+ label = { label }
254+ description = { `${ oneIndexedValue } . semester innebærer ${ studyGrade } . årsgang` }
255+ min = { 1 }
256+ max = { 10 }
257+ allowDecimal = { false }
258+ value = { field . value !== null ? field . value + 1 : undefined }
259+ onChange = { ( value ) => {
260+ const zeroIndexedValue = value !== undefined ? Number ( value ) - 1 : null
261+ field . onChange ( zeroIndexedValue )
262+ } }
263+ error = { state . errors [ name ] && < ErrorMessage errors = { state . errors } name = { name } /> }
264+ />
265+ )
266+ } }
267+ />
268+ )
269+ } ,
119270 } ,
120271 } )
121272}
0 commit comments