@@ -18,7 +18,7 @@ import {
1818 Text
1919} from "@chakra-ui/react" ;
2020import { useParams } from "next/navigation" ;
21- import { useCallback , useEffect , useMemo , useState } from "react" ;
21+ import { useCallback , useEffect , useState } from "react" ;
2222import { Database } from "@/utils/supabase/SupabaseTypes" ;
2323import { Controller , useFieldArray , useForm } from "react-hook-form" ;
2424import { BsPlus , BsTrash , BsX } from "react-icons/bs" ;
@@ -48,6 +48,7 @@ interface ErrorPinRule {
4848
4949interface ErrorPinFormData {
5050 assignment_id : number | null ;
51+ is_class_level : boolean ;
5152 rule_logic : "and" | "or" ;
5253 enabled : boolean ;
5354 rules : ErrorPinRule [ ] ;
@@ -112,14 +113,15 @@ export function ErrorPinModal({
112113 } = useForm < ErrorPinFormData > ( {
113114 defaultValues : {
114115 assignment_id : defaultAssignmentId ?? null ,
116+ is_class_level : false ,
115117 rule_logic : "and" ,
116118 enabled : true ,
117119 rules : [ ]
118120 }
119121 } ) ;
120122
121123 // Set default assignment when modal opens with defaultAssignmentId
122- useMemo ( ( ) => {
124+ useEffect ( ( ) => {
123125 if ( defaultAssignmentId && ! existingPinId ) {
124126 setValue ( "assignment_id" , defaultAssignmentId ) ;
125127 }
@@ -131,6 +133,7 @@ export function ErrorPinModal({
131133 } ) ;
132134
133135 const assignmentId = watch ( "assignment_id" ) ;
136+ const isClassLevel = watch ( "is_class_level" ) ;
134137 const rules = watch ( "rules" ) ;
135138
136139 // Load existing pin data if editing
@@ -186,6 +189,7 @@ export function ErrorPinModal({
186189 if ( existingPin && existingRules ) {
187190 reset ( {
188191 assignment_id : existingPin . assignment_id ,
192+ is_class_level : existingPin . assignment_id === null ,
189193 rule_logic : existingPin . rule_logic as "and" | "or" ,
190194 enabled : existingPin . enabled ,
191195 rules : existingRules . map ( ( r ) => ( {
@@ -221,10 +225,20 @@ export function ErrorPinModal({
221225 } ;
222226
223227 const handlePreview = async ( ) => {
224- if ( ! assignmentId || rules . length === 0 ) {
228+ // For class-level pins, we don't need an assignment_id
229+ // For assignment-level pins, we need an assignment_id
230+ if ( ! isClassLevel && ! assignmentId ) {
231+ toaster . error ( {
232+ title : "Error" ,
233+ description : "Please select an assignment or choose 'All Assignments (Class-Level)'"
234+ } ) ;
235+ return ;
236+ }
237+
238+ if ( rules . length === 0 ) {
225239 toaster . error ( {
226240 title : "Error" ,
227- description : "Please select an assignment and add at least one rule"
241+ description : "Please add at least one rule"
228242 } ) ;
229243 return ;
230244 }
@@ -239,9 +253,10 @@ export function ErrorPinModal({
239253 match_value_max : r . match_value_max ?. trim ( ) || null
240254 } ) ) ;
241255 const { data, error } = await supabase . rpc ( "preview_error_pin_matches" , {
242- p_assignment_id : assignmentId ,
256+ p_assignment_id : isClassLevel ? null : assignmentId ,
243257 p_rules : sanitizedRules as unknown as Json ,
244- p_rule_logic : watch ( "rule_logic" )
258+ p_rule_logic : watch ( "rule_logic" ) ,
259+ p_class_id : isClassLevel ? Number ( course_id ) : null
245260 } ) ;
246261
247262 if ( error ) throw error ;
@@ -264,10 +279,20 @@ export function ErrorPinModal({
264279 } ;
265280
266281 const onSubmit = async ( data : ErrorPinFormData ) => {
267- if ( ! data . assignment_id || data . rules . length === 0 ) {
282+ // For class-level pins, assignment_id should be null
283+ // For assignment-level pins, we need a valid assignment_id
284+ if ( ! data . is_class_level && ! data . assignment_id ) {
285+ toaster . error ( {
286+ title : "Error" ,
287+ description : "Please select an assignment or choose 'All Assignments (Class-Level)'"
288+ } ) ;
289+ return ;
290+ }
291+
292+ if ( data . rules . length === 0 ) {
268293 toaster . error ( {
269294 title : "Error" ,
270- description : "Please select an assignment and add at least one rule"
295+ description : "Please add at least one rule"
271296 } ) ;
272297 return ;
273298 }
@@ -285,7 +310,7 @@ export function ErrorPinModal({
285310 const pinData = {
286311 id : existingPinId || undefined ,
287312 discussion_thread_id,
288- assignment_id : data . assignment_id ,
313+ assignment_id : data . is_class_level ? null : data . assignment_id ,
289314 class_id : Number ( course_id ) ,
290315 created_by : private_profile_id ,
291316 rule_logic : data . rule_logic ,
@@ -337,25 +362,55 @@ export function ErrorPinModal({
337362 < Dialog . Body >
338363 < form onSubmit = { handleSubmit ( onSubmit ) } >
339364 < Stack spaceY = { 4 } >
340- < Field . Root invalid = { ! ! errors . assignment_id } >
341- < Field . Label > Assignment</ Field . Label >
342- < NativeSelect . Root >
343- < NativeSelect . Field
344- { ...register ( "assignment_id" , {
345- required : "Assignment is required" ,
346- valueAsNumber : true
347- } ) }
348- >
349- < option value = "" > Select an assignment</ option >
350- { assignments . map ( ( assignment ) => (
351- < option key = { assignment . id } value = { assignment . id } >
352- { assignment . title }
353- </ option >
354- ) ) }
355- </ NativeSelect . Field >
356- </ NativeSelect . Root >
357- < Field . ErrorText > { errors . assignment_id ?. message } </ Field . ErrorText >
358- </ Field . Root >
365+ < Controller
366+ control = { control }
367+ name = "is_class_level"
368+ render = { ( { field } ) => (
369+ < Field . Root >
370+ < ChakraButton
371+ variant = { field . value ? "solid" : "outline" }
372+ colorPalette = { field . value ? "purple" : "gray" }
373+ onClick = { ( ) => {
374+ field . onChange ( ! field . value ) ;
375+ // Clear assignment selection when switching to class-level
376+ if ( ! field . value ) {
377+ setValue ( "assignment_id" , null ) ;
378+ }
379+ } }
380+ size = "sm"
381+ >
382+ { field . value ? "Class-Level Pin (All Assignments)" : "Assignment-Specific Pin" }
383+ </ ChakraButton >
384+ < Field . HelperText >
385+ { field . value
386+ ? "This pin will match against all submissions in the class, regardless of assignment"
387+ : "This pin will only match against submissions for a specific assignment" }
388+ </ Field . HelperText >
389+ </ Field . Root >
390+ ) }
391+ />
392+
393+ { ! isClassLevel && (
394+ < Field . Root invalid = { ! ! errors . assignment_id } >
395+ < Field . Label > Assignment</ Field . Label >
396+ < NativeSelect . Root >
397+ < NativeSelect . Field
398+ { ...register ( "assignment_id" , {
399+ required : ! isClassLevel ? "Assignment is required" : false ,
400+ valueAsNumber : true
401+ } ) }
402+ >
403+ < option value = "" > Select an assignment</ option >
404+ { assignments . map ( ( assignment ) => (
405+ < option key = { assignment . id } value = { assignment . id } >
406+ { assignment . title }
407+ </ option >
408+ ) ) }
409+ </ NativeSelect . Field >
410+ </ NativeSelect . Root >
411+ < Field . ErrorText > { errors . assignment_id ?. message } </ Field . ErrorText >
412+ </ Field . Root >
413+ ) }
359414
360415 < Controller
361416 control = { control }
@@ -602,7 +657,7 @@ export function ErrorPinModal({
602657 variant = "outline"
603658 onClick = { handlePreview }
604659 loading = { isPreviewing }
605- disabled = { ! assignmentId || rules . length === 0 }
660+ disabled = { ( ! isClassLevel && ! assignmentId ) || rules . length === 0 }
606661 >
607662 Preview Matches
608663 </ ChakraButton >
@@ -614,7 +669,7 @@ export function ErrorPinModal({
614669 colorPalette = "green"
615670 onClick = { handleSubmit ( onSubmit ) }
616671 loading = { isSubmitting }
617- disabled = { ! assignmentId || rules . length === 0 }
672+ disabled = { ( ! isClassLevel && ! assignmentId ) || rules . length === 0 }
618673 >
619674 { existingPinId ? "Update Pin" : "Create Pin" }
620675 </ ChakraButton >
0 commit comments