1+ "use client"
2+
3+ import { useState } from "react"
4+ import { useRouter , useSearchParams } from "next/navigation"
5+ import { zodResolver } from "@hookform/resolvers/zod"
6+ import { useForm } from "react-hook-form"
7+ import * as z from "zod"
8+ import { format } from "date-fns"
9+ import { CalendarIcon } from "lucide-react"
10+ import { cn } from "@/lib/utils"
11+ import { variableCategories } from "@/app/dfda/lib/variableCategories"
12+ import { GlobalVariable } from "@/types/models/GlobalVariable"
13+ import { UserVariable } from "@/types/models/UserVariable"
14+
15+ import { Button } from "@/components/ui/button"
16+ import { Calendar } from "@/components/ui/calendar"
17+ import {
18+ Form ,
19+ FormControl ,
20+ FormField ,
21+ FormItem ,
22+ FormLabel ,
23+ FormMessage ,
24+ FormDescription ,
25+ } from "@/components/ui/form"
26+ import {
27+ Select ,
28+ SelectContent ,
29+ SelectItem ,
30+ SelectTrigger ,
31+ SelectValue ,
32+ } from "@/components/ui/select"
33+ import { Input } from "@/components/ui/input"
34+ import {
35+ Popover ,
36+ PopoverContent ,
37+ PopoverTrigger ,
38+ } from "@/components/ui/popover"
39+ import { Textarea } from "@/components/ui/textarea"
40+ import { toast } from "@/components/ui/use-toast"
41+ import { createMeasurement } from "./measurementActions"
42+ import { Valence , ratingButtons } from "@/lib/constants/ratings"
43+
44+ const measurementFormSchema = z . object ( {
45+ variableName : z . string ( ) . min ( 1 , "Variable name is required" ) ,
46+ variableCategoryName : z . string ( ) . min ( 1 , "Category is required" ) ,
47+ value : z . number ( ) . or ( z . string ( ) ) . optional ( ) ,
48+ unitAbbreviatedName : z . string ( ) . min ( 1 , "Unit is required" ) ,
49+ note : z . string ( ) . optional ( ) ,
50+ startAt : z . date ( ) ,
51+ } )
52+
53+ type MeasurementFormValues = z . infer < typeof measurementFormSchema >
54+
55+ interface MeasurementFormProps {
56+ variable ?: GlobalVariable | UserVariable
57+ }
58+
59+ export function MeasurementForm ( { variable } : MeasurementFormProps ) {
60+ const router = useRouter ( )
61+ const searchParams = useSearchParams ( )
62+ const [ loading , setLoading ] = useState ( false )
63+
64+ const defaultValues : Partial < MeasurementFormValues > = {
65+ startAt : new Date ( ) ,
66+ ...( variable && {
67+ variableName : variable . name ,
68+ variableCategoryName : variable . variableCategoryName ,
69+ unitAbbreviatedName : variable . unitAbbreviatedName ,
70+ value : variable . mostCommonValue ,
71+ } )
72+ }
73+
74+ const form = useForm < MeasurementFormValues > ( {
75+ resolver : zodResolver ( measurementFormSchema ) ,
76+ defaultValues,
77+ } )
78+
79+ const handleFaceButtonClick = ( numericValue : number ) => {
80+ form . setValue ( "value" , numericValue , { shouldValidate : true } )
81+ }
82+
83+ async function onSubmit ( data : MeasurementFormValues ) {
84+ try {
85+ setLoading ( true )
86+
87+ const result = await createMeasurement ( {
88+ ...data ,
89+ variableId : variable ?. variableId ,
90+ } )
91+
92+ if ( ! result . success ) throw new Error ( result . error )
93+
94+ toast ( {
95+ description : `Recorded ${ data . value } ${ data . unitAbbreviatedName } for ${ data . variableName } on ${ format ( data . startAt , "PPP" ) } ` ,
96+ } )
97+
98+ router . push ( "/dfda/safe/measurements" )
99+ router . refresh ( )
100+ } catch ( error ) {
101+ toast ( {
102+ title : "Error" ,
103+ description : error instanceof Error ? error . message : "Failed to save measurement" ,
104+ variant : "destructive" ,
105+ } )
106+ } finally {
107+ setLoading ( false )
108+ }
109+ }
110+
111+ // Determine if we should show rating buttons
112+ const showRatingButtons = variable ?. unitAbbreviatedName === "/5" && variable ?. valence
113+ const buttons = showRatingButtons ? ratingButtons [ variable . valence as Valence ] : null
114+
115+ // Check if we have an existing variable
116+ const isExistingVariable = Boolean ( variable ?. variableId )
117+
118+ return (
119+ < Form { ...form } >
120+ < form onSubmit = { form . handleSubmit ( onSubmit ) } className = "space-y-8" >
121+ { ! isExistingVariable && (
122+ < >
123+ < FormField
124+ control = { form . control }
125+ name = "variableName"
126+ render = { ( { field } ) => (
127+ < FormItem >
128+ < FormLabel className = "text-black font-bold" > Variable Name</ FormLabel >
129+ < FormControl >
130+ < Input
131+ placeholder = "Enter variable name"
132+ { ...field }
133+ className = "border-2 border-black bg-white text-black placeholder:text-gray-500 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
134+ />
135+ </ FormControl >
136+ < FormMessage className = "text-red-500" />
137+ </ FormItem >
138+ ) }
139+ />
140+
141+ < FormField
142+ control = { form . control }
143+ name = "variableCategoryName"
144+ render = { ( { field } ) => (
145+ < FormItem >
146+ < FormLabel className = "text-black font-bold" > Category</ FormLabel >
147+ < Select onValueChange = { field . onChange } defaultValue = { field . value } >
148+ < FormControl >
149+ < SelectTrigger className = "border-2 border-black bg-white text-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]" >
150+ < SelectValue placeholder = "Select category" />
151+ </ SelectTrigger >
152+ </ FormControl >
153+ < SelectContent className = "border-2 border-black bg-white" >
154+ { variableCategories . map ( ( category ) => (
155+ < SelectItem
156+ key = { category . name }
157+ value = { category . name }
158+ className = "hover:bg-gray-100"
159+ >
160+ { category . name }
161+ </ SelectItem >
162+ ) ) }
163+ </ SelectContent >
164+ </ Select >
165+ < FormMessage className = "text-red-500" />
166+ </ FormItem >
167+ ) }
168+ />
169+ </ >
170+ ) }
171+
172+ < FormField
173+ control = { form . control }
174+ name = "value"
175+ render = { ( { field } ) => (
176+ < FormItem >
177+ < FormLabel className = "text-black font-bold" > Value</ FormLabel >
178+ { buttons ? (
179+ < div className = "flex w-full justify-around items-center" >
180+ { buttons . map ( ( option ) => (
181+ < img
182+ key = { option . numericValue }
183+ src = { option . src }
184+ title = { option . title }
185+ className = { `cursor-pointer ${
186+ form . watch ( "value" ) === option . numericValue
187+ ? "brightness-110 scale-110 drop-shadow-[0_0_8px_rgba(59,130,246,0.5)]"
188+ : "brightness-90 hover:brightness-100 scale-75 hover:scale-90"
189+ } w-auto max-w-[20%] transition-all duration-200 rounded-full`}
190+ onClick = { ( ) => handleFaceButtonClick ( option . numericValue ) }
191+ alt = { `Rating ${ option . numericValue } ` }
192+ />
193+ ) ) }
194+ </ div >
195+ ) : (
196+ < FormControl >
197+ < div className = "flex items-center gap-2" >
198+ < Input
199+ type = "number"
200+ placeholder = "Enter value"
201+ min = { variable ?. minimumAllowedValue }
202+ max = { variable ?. maximumAllowedValue }
203+ { ...field }
204+ onChange = { e => field . onChange ( e . target . valueAsNumber ) }
205+ className = "border-2 border-black bg-white text-black placeholder:text-gray-500 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
206+ />
207+ < span className = "text-sm font-bold text-black" >
208+ { variable ?. unitAbbreviatedName }
209+ </ span >
210+ </ div >
211+ </ FormControl >
212+ ) }
213+ < FormMessage className = "text-red-500" />
214+ </ FormItem >
215+ ) }
216+ />
217+
218+ < FormField
219+ control = { form . control }
220+ name = "startAt"
221+ render = { ( { field } ) => (
222+ < FormItem className = "flex flex-col" >
223+ < FormLabel className = "text-black font-bold" > Date & Time </ FormLabel >
224+ < Popover >
225+ < PopoverTrigger asChild >
226+ < FormControl >
227+ < Button
228+ variant = { "outline" }
229+ className = { cn (
230+ "w-full pl-3 text-left font-normal border-2 border-black bg-white text-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all" ,
231+ ! field . value && "text-gray-500"
232+ ) }
233+ >
234+ { field . value ? (
235+ format ( field . value , "PPP HH:mm" )
236+ ) : (
237+ < span > Pick a date</ span >
238+ ) }
239+ < CalendarIcon className = "ml-auto h-4 w-4 opacity-50" />
240+ </ Button >
241+ </ FormControl >
242+ </ PopoverTrigger >
243+ < PopoverContent className = "border-2 border-black bg-white p-0" align = "start" >
244+ < Calendar
245+ mode = "single"
246+ selected = { field . value }
247+ onSelect = { field . onChange }
248+ disabled = { ( date ) =>
249+ date > new Date ( ) || date < new Date ( "1900-01-01" )
250+ }
251+ initialFocus
252+ className = "bg-white"
253+ />
254+ < div className = "p-3 border-t-2 border-black" >
255+ < Input
256+ type = "time"
257+ value = { format ( field . value || new Date ( ) , "HH:mm" ) }
258+ onChange = { ( e ) => {
259+ const [ hours , minutes ] = e . target . value . split ( ":" )
260+ const newDate = new Date ( field . value || new Date ( ) )
261+ newDate . setHours ( parseInt ( hours ) )
262+ newDate . setMinutes ( parseInt ( minutes ) )
263+ field . onChange ( newDate )
264+ } }
265+ className = "border-2 border-black bg-white text-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
266+ />
267+ </ div >
268+ </ PopoverContent >
269+ </ Popover >
270+ < FormDescription className = "text-gray-600" > When this measurement was taken</ FormDescription >
271+ < FormMessage className = "text-red-500" />
272+ </ FormItem >
273+ ) }
274+ />
275+
276+ < FormField
277+ control = { form . control }
278+ name = "note"
279+ render = { ( { field } ) => (
280+ < FormItem >
281+ < FormLabel className = "text-black font-bold" > Note</ FormLabel >
282+ < FormControl >
283+ < Textarea
284+ placeholder = "Add any additional notes..."
285+ className = "resize-none border-2 border-black bg-white text-black placeholder:text-gray-500 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
286+ { ...field }
287+ />
288+ </ FormControl >
289+ < FormMessage className = "text-red-500" />
290+ </ FormItem >
291+ ) }
292+ />
293+
294+ < div className = "flex gap-4" >
295+ < Button
296+ type = "button"
297+ variant = "outline"
298+ onClick = { ( ) => router . back ( ) }
299+ className = "border-2 border-black bg-white text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-[4px] hover:translate-y-[4px] transition-all"
300+ >
301+ Cancel
302+ </ Button >
303+ < Button
304+ type = "submit"
305+ disabled = { loading }
306+ className = "border-2 border-black bg-blue-500 text-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-[4px] hover:translate-y-[4px] transition-all disabled:opacity-50"
307+ >
308+ { loading ? "Saving..." : "Record Measurement" }
309+ </ Button >
310+ </ div >
311+ </ form >
312+ </ Form >
313+ )
314+ }
0 commit comments