1+ "use client" ;
2+
3+ import { useQuery , useMutation , useQueryClient } from "@tanstack/react-query" ;
4+ import { CampaignUpdate , createCampaignRole , getCampaign , getCampaignRoles , updateCampaign , getCampaignAttachments , uploadAttachments , deleteCampaignAttachment , CampaignRole , RoleDetails } from "@/models/campaign" ;
5+ import { getRatingCategories , createCategory , updateCategory , deleteCategory , RatingCategory } from "@/models/rating" ;
6+ import { Button } from "@/components/ui/button" ;
7+ import { Copy , Pencil , Trash , Share , BookOpenCheck , Check , Plus , FormIcon , CircleCheck , Upload , X , FileText , BarChart , ArrowLeft } from "lucide-react" ;
8+ import { ButtonGroup } from "@/components/ui/button-group" ;
9+ import { cn } from "@/lib/utils" ;
10+ import {
11+ Table ,
12+ TableBody ,
13+ TableCell ,
14+ TableHead ,
15+ TableHeader ,
16+ TableRow ,
17+ } from "@/components/ui/table"
18+ import Link from "next/link" ;
19+ import { getOrganisationUserRole } from "@/models/organisation" ;
20+ import { useState } from "react" ;
21+ import { Input } from "@/components/ui/input" ;
22+ import { Label } from "@/components/ui/label" ;
23+ import { Textarea } from "@/components/ui/textarea" ;
24+ import SlugInput from "@/components/slug-input" ;
25+ import { DatePicker } from "@/components/ui/date-picker" ;
26+ import { snowflakeGenerator } from "@/lib" ;
27+ import { deleteRole , RoleUpdate , updateRole } from "@/models/role" ;
28+
29+ export default function CampaignSettings ( { campaignId, orgId, dict } : { campaignId : string , orgId : string , dict : any } ) {
30+ const queryClient = useQueryClient ( ) ;
31+
32+ const { data : campaign } = useQuery ( {
33+ queryKey : [ `${ campaignId } -campaign-details` ] ,
34+ queryFn : ( ) => getCampaign ( campaignId ) ,
35+ } ) ;
36+
37+ const { data : roles } = useQuery ( {
38+ queryKey : [ `${ campaignId } -campaign-roles` ] ,
39+ queryFn : ( ) => getCampaignRoles ( campaignId ) ,
40+ } ) ;
41+
42+ const { data : ratingCategories } = useQuery ( {
43+ queryKey : [ `${ campaignId } -rating-categories` ] ,
44+ queryFn : ( ) => getRatingCategories ( campaignId ) ,
45+ } ) ;
46+
47+ const [ campaignName , setCampaignName ] = useState ( campaign ?. name ?? "" ) ;
48+ const [ campaignSlug , setCampaignSlug ] = useState ( campaign ?. campaign_slug ?? "" ) ;
49+ const [ campaignDescription , setCampaignDescription ] = useState ( campaign ?. description ?? "" ) ;
50+ const [ campaignStartsAt , setCampaignStartsAt ] = useState ( new Date ( campaign ?. starts_at ?? "" ) . toISOString ( ) ) ;
51+ const [ campaignEndsAt , setCampaignEndsAt ] = useState ( new Date ( campaign ?. ends_at ?? "" ) . toISOString ( ) ) ;
52+
53+ const { mutateAsync : mutateUpdateCampaignDetails } = useMutation ( {
54+ mutationFn : ( data : CampaignUpdate ) => updateCampaign ( campaignId , data ) ,
55+ onSuccess : ( ) => {
56+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-details` ] } ) ;
57+ } ,
58+ onError : ( ) => {
59+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-details` ] } ) ;
60+ } ,
61+ } )
62+
63+ const { mutateAsync : mutateAddRole } = useMutation ( {
64+ mutationFn : ( data : RoleDetails ) => createCampaignRole ( campaignId , data ) ,
65+ onSuccess : ( ) => {
66+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-roles` ] } ) ;
67+ } ,
68+ onError : ( ) => {
69+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-roles` ] } ) ;
70+ } ,
71+ } )
72+
73+ const { mutateAsync : mutateUpdateRole } = useMutation ( {
74+ mutationFn : ( { roleId, data } : { roleId : string , data : RoleUpdate } ) => updateRole ( roleId , data ) ,
75+ onSuccess : ( ) => {
76+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-roles` ] } ) ;
77+ } ,
78+ onError : ( ) => {
79+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-roles` ] } ) ;
80+ } ,
81+ } ) ;
82+
83+ const { mutateAsync : mutateDeleteRole } = useMutation ( {
84+ mutationFn : ( roleId : string ) => deleteRole ( roleId ) ,
85+ onSuccess : ( ) => {
86+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-roles` ] } ) ;
87+ } ,
88+ onError : ( ) => {
89+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -campaign-roles` ] } ) ;
90+ } ,
91+ } ) ;
92+
93+ const { mutateAsync : mutateAddRatingCategory } = useMutation ( {
94+ mutationFn : ( name : string ) => createCategory ( name , campaignId ) ,
95+ onSuccess : ( ) => {
96+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -rating-categories` ] } ) ;
97+ } ,
98+ onError : ( ) => {
99+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -rating-categories` ] } ) ;
100+ } ,
101+ } )
102+
103+ const { mutateAsync : mutateUpdateRatingCategory } = useMutation ( {
104+ mutationFn : ( { name, categoryId } : { name : string , categoryId : string } ) => updateCategory ( name , campaignId , categoryId ) ,
105+ onSuccess : ( ) => {
106+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -rating-categories` ] } ) ;
107+ } ,
108+ onError : ( ) => {
109+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -rating-categories` ] } ) ;
110+ } ,
111+ } ) ;
112+
113+ const { mutateAsync : mutateDeleteRatingCategory } = useMutation ( {
114+ mutationFn : ( categoryId : string ) => deleteCategory ( campaignId , categoryId ) ,
115+ onSuccess : ( ) => {
116+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -rating-categories` ] } ) ;
117+ } ,
118+ onError : ( ) => {
119+ queryClient . invalidateQueries ( { queryKey : [ `${ campaignId } -rating-categories` ] } ) ;
120+ } ,
121+ } ) ;
122+
123+ const handleCampaignDetailsUpdate = ( overrides ?: Partial < CampaignUpdate > ) => {
124+ mutateUpdateCampaignDetails ( {
125+ name : campaignName ,
126+ slug : campaignSlug ,
127+ description : campaignDescription ,
128+ starts_at : campaignStartsAt ,
129+ ends_at : campaignEndsAt ,
130+ ...overrides ,
131+ } ) ;
132+ }
133+
134+
135+ return (
136+ < div className = "flex flex-col gap-10 max-w-2xl" >
137+ < div >
138+ < Link href = { `/dashboard/organisation/${ orgId } /campaigns/${ campaignId } ` } >
139+ < div className = "flex items-center gap-1" >
140+ < ArrowLeft className = "w-4 h-4" />
141+ { dict . common . back }
142+ </ div >
143+ </ Link >
144+ < h1 className = "text-2xl font-bold" > { dict . dashboard . campaigns . settings . campaign_settings } </ h1 >
145+ < p className = "text-lg font-medium" > { campaign ?. name } </ p >
146+ </ div >
147+
148+ { /* General Settings */ }
149+ < div className = "flex flex-col gap-5" >
150+ < h2 className = "text-lg font-semibold" > { dict . dashboard . campaigns . settings . general_settings } </ h2 >
151+ < div className = "flex flex-col gap-1" >
152+ < Label htmlFor = "campaign-name" > { dict . common . name } </ Label >
153+ < Input value = { campaignName } onChange = { ( e ) => setCampaignName ( e . target . value ) } onBlur = { ( ) => handleCampaignDetailsUpdate ( ) } />
154+ </ div >
155+
156+ < div className = "flex flex-col gap-1" >
157+ < Label htmlFor = "campaign-slug" > { dict . common . slug } </ Label >
158+ < SlugInput orgId = { orgId } name = { campaignName } value = { campaignSlug } currentSlug = { campaign ?. campaign_slug } onChange = { ( value ) => setCampaignSlug ( value ) } onBlur = { handleCampaignDetailsUpdate } dict = { dict } />
159+ </ div >
160+
161+ < div className = "flex flex-col gap-1" >
162+ < Label htmlFor = "campaign-description" > { dict . common . description } </ Label >
163+ < Textarea className = "min-h-[300px]" value = { campaignDescription } onChange = { ( e ) => setCampaignDescription ( e . target . value ) } onBlur = { ( ) => handleCampaignDetailsUpdate ( ) } />
164+ </ div >
165+
166+ < div className = "flex flex-col gap-1" >
167+ < DatePicker label = { dict . common . starts_at } value = { campaignStartsAt } onChange = { ( value ) => { setCampaignStartsAt ( value ) ; handleCampaignDetailsUpdate ( { starts_at : value } ) } } />
168+ </ div >
169+
170+ < div className = "flex flex-col gap-1" >
171+ < DatePicker label = { dict . common . ends_at } value = { campaignEndsAt } onChange = { ( value ) => { setCampaignEndsAt ( value ) ; handleCampaignDetailsUpdate ( { ends_at : value } ) } } />
172+ </ div >
173+ </ div >
174+
175+ { /* Role Settings */ }
176+ < div className = "flex flex-col gap-5" >
177+ < div className = "flex items-center justify-between" >
178+ < h2 className = "text-lg font-semibold" > { dict . dashboard . campaigns . settings . role_settings } </ h2 >
179+ < Button
180+ variant = "outline"
181+ size = "sm"
182+ onClick = { ( ) => {
183+ const newRoleId = snowflakeGenerator . generate ( ) . toString ( ) ;
184+ const newRole : RoleDetails = {
185+ id : newRoleId ,
186+ campaign_id : campaignId ,
187+ name : `New Role ${ newRoleId } ` ,
188+ description : "" ,
189+ min_available : 1 ,
190+ max_available : 2 ,
191+ finalised : false ,
192+ } ;
193+ mutateAddRole ( newRole ) ;
194+ } }
195+ >
196+ < Plus className = "w-4 h-4 mr-1" />
197+ { dict . common . add }
198+ </ Button >
199+ </ div >
200+ { roles ?. map ( ( r ) =>
201+ < RoleCard key = { r . id } role = { r } updateRole = { mutateUpdateRole } deleteRole = { mutateDeleteRole } dict = { dict } />
202+ ) }
203+ </ div >
204+
205+ { /* Rating Settings */ }
206+ < div className = "flex flex-col gap-2" >
207+ < div className = "flex items-center justify-between" >
208+ < h2 className = "text-lg font-semibold" > { dict . dashboard . campaigns . settings . rating_category_settings } </ h2 >
209+ < Button
210+ variant = "outline"
211+ size = "sm"
212+ onClick = { ( ) => {
213+ const newCategoryId = snowflakeGenerator . generate ( ) . toString ( ) ;
214+ mutateAddRatingCategory ( `New Category ${ newCategoryId } ` ) ;
215+ } }
216+ >
217+ < Plus className = "w-4 h-4 mr-1" />
218+ { dict . common . add }
219+ </ Button >
220+ </ div >
221+ { ratingCategories ?. map ( ( r ) =>
222+ < RatingCategoryCard key = { r . id } category = { r } updateCategory = { mutateUpdateRatingCategory } deleteCategory = { mutateDeleteRatingCategory } />
223+ ) }
224+ </ div >
225+
226+ { /* Attachment Settings */ }
227+ < div className = "flex flex-col gap-2" >
228+ < h2 className = "text-lg font-semibold" > { dict . common . attachments } </ h2 >
229+ < p > **In development**</ p >
230+ </ div >
231+ </ div >
232+ ) ;
233+ }
234+
235+ function RoleCard ( { role, updateRole, deleteRole, dict } : { role : RoleDetails , updateRole : ( { roleId, data } : { roleId : string , data : RoleUpdate } ) => void , deleteRole : ( roleId : string ) => void , dict : any } ) {
236+ const [ name , setName ] = useState ( role . name ) ;
237+ const [ minAvailable , setMinAvailable ] = useState ( role . min_available ) ;
238+ const [ maxAvailable , setMaxAvailable ] = useState ( role . max_available ) ;
239+ const [ description , setDescription ] = useState ( role . description ?? "" ) ;
240+
241+ const handleUpdate = ( ) => {
242+ updateRole ( {
243+ roleId : role . id ,
244+ data : {
245+ name : name ,
246+ min_available : minAvailable ,
247+ max_available : maxAvailable ,
248+ description : description ,
249+ finalised : true ,
250+ } ,
251+ } ) ;
252+ }
253+
254+ return (
255+ < div className = "border p-2 rounded-md" >
256+ < div className = "flex gap-1 items-center" >
257+ < div className = "flex-1" >
258+ < Label className = "text-xs text-muted-foreground" > { dict . common . name } </ Label >
259+ < Input value = { name } onChange = { ( e ) => setName ( e . target . value ) } onBlur = { handleUpdate } />
260+ </ div >
261+ < div className = "w-24" >
262+ < Label className = "text-xs text-muted-foreground" > { dict . common . min } </ Label >
263+ < Input type = "number" value = { minAvailable } onChange = { ( e ) => setMinAvailable ( parseInt ( e . target . value ) || 0 ) } onBlur = { handleUpdate } />
264+ </ div >
265+ < div className = "w-24" >
266+ < Label className = "text-xs text-muted-foreground" > { dict . common . max } </ Label >
267+ < Input type = "number" value = { maxAvailable } onChange = { ( e ) => setMaxAvailable ( parseInt ( e . target . value ) || 0 ) } onBlur = { handleUpdate } />
268+ </ div >
269+ < Button variant = "ghost" size = "icon" className = "mt-4" onClick = { ( ) => deleteRole ( role . id ) } >
270+ < Trash className = "w-4 h-4 text-destructive" />
271+ </ Button >
272+ </ div >
273+ < div className = "mt-2" >
274+ < Label className = "text-xs text-muted-foreground" > { dict . common . description } </ Label >
275+ < Textarea value = { description } onChange = { ( e ) => setDescription ( e . target . value ) } onBlur = { handleUpdate } />
276+ </ div >
277+ </ div >
278+ )
279+ }
280+
281+ function RatingCategoryCard ( { category, updateCategory, deleteCategory } : { category : RatingCategory , updateCategory : ( { name, categoryId } : { name : string , categoryId : string } ) => void , deleteCategory : ( categoryId : string ) => void } ) {
282+ const [ name , setName ] = useState ( category . name ) ;
283+
284+ const handleUpdate = ( ) => {
285+ updateCategory ( {
286+ name : name ,
287+ categoryId : category . id ,
288+ } ) ;
289+ }
290+
291+ return (
292+ < div >
293+ < div className = "flex gap-1 items-center" >
294+ < div className = "flex-1" >
295+ < Input value = { name } onChange = { ( e ) => setName ( e . target . value ) } onBlur = { handleUpdate } />
296+ </ div >
297+ < Button variant = "ghost" size = "icon" onClick = { ( ) => deleteCategory ( category . id ) } >
298+ < Trash className = "w-4 h-4 text-destructive" />
299+ </ Button >
300+ </ div >
301+ </ div >
302+ )
303+ }
0 commit comments