@@ -9,9 +9,26 @@ type Props = {
9
9
} > ;
10
10
} ;
11
11
12
+ type ApprovalGroup = {
13
+ id : number ;
14
+ name : string ;
15
+ } ;
16
+
17
+ type GroupMember = {
18
+ userId : string ;
19
+ name : string | null ;
20
+ } ;
21
+
22
+ type PackageMember = {
23
+ userId : string ;
24
+ name : string | null ;
25
+ email : string | null ;
26
+ } ;
27
+
12
28
export default function PackageApprovalConfig ( props : Props ) {
13
29
const params = use ( props . params ) ;
14
30
const [ newGroupName , setNewGroupName ] = useState ( "" ) ;
31
+ const [ selectedGroupId , setSelectedGroupId ] = useState < number | null > ( null ) ;
15
32
const { data : packageDetails } = trpc . packages . getPackageDetails . useQuery ( {
16
33
packageId : parseInt ( params . packageId ) ,
17
34
} ) ;
@@ -22,7 +39,18 @@ export default function PackageApprovalConfig(props: Props) {
22
39
} = trpc . approvals . getPackageApprovalGroups . useQuery ( {
23
40
packageId : parseInt ( params . packageId ) ,
24
41
} ) ;
42
+ const { data : packageMembers } = trpc . packages . getPackageMembers . useQuery ( {
43
+ packageId : parseInt ( params . packageId ) ,
44
+ } ) ;
45
+ const { data : groupMembers , refetch : refetchGroupMembers } =
46
+ trpc . approvals . getGroupMembers . useQuery (
47
+ { groupId : selectedGroupId ! } ,
48
+ { enabled : ! ! selectedGroupId } ,
49
+ ) ;
25
50
const createGroupMutation = trpc . approvals . createApprovalGroup . useMutation ( ) ;
51
+ const addUserMutation = trpc . approvals . addUserToApprovalGroup . useMutation ( ) ;
52
+ const removeUserMutation =
53
+ trpc . approvals . removeUserFromApprovalGroup . useMutation ( ) ;
26
54
27
55
const handleCreateGroup = async ( e : React . FormEvent ) => {
28
56
e . preventDefault ( ) ;
@@ -41,6 +69,36 @@ export default function PackageApprovalConfig(props: Props) {
41
69
}
42
70
} ;
43
71
72
+ const handleAddUser = async ( userId : string ) => {
73
+ if ( ! selectedGroupId ) return ;
74
+ try {
75
+ await addUserMutation . mutateAsync ( {
76
+ groupId : selectedGroupId ,
77
+ userId,
78
+ } ) ;
79
+ void refetch ( ) ;
80
+ } catch ( error ) {
81
+ console . error ( "Error adding user:" , error ) ;
82
+ alert ( "Failed to add user. A user can only be in one approval group." ) ;
83
+ }
84
+ void refetchGroupMembers ( ) ;
85
+ } ;
86
+
87
+ const handleRemoveUser = async ( userId : string ) => {
88
+ if ( ! selectedGroupId ) return ;
89
+ try {
90
+ await removeUserMutation . mutateAsync ( {
91
+ groupId : selectedGroupId ,
92
+ userId,
93
+ } ) ;
94
+ void refetch ( ) ;
95
+ } catch ( error ) {
96
+ console . error ( "Error removing user:" , error ) ;
97
+ alert ( "Failed to remove user. Please try again." ) ;
98
+ }
99
+ void refetchGroupMembers ( ) ;
100
+ } ;
101
+
44
102
if ( isLoading ) return < div > Loading...</ div > ;
45
103
46
104
if ( ! packageDetails ?. isOwner ) {
@@ -55,33 +113,144 @@ export default function PackageApprovalConfig(props: Props) {
55
113
) ;
56
114
}
57
115
116
+ const availableMembers = packageMembers ?. filter (
117
+ ( member : PackageMember ) =>
118
+ ! groupMembers ?. some ( ( gm : GroupMember ) => gm . userId === member . userId ) ,
119
+ ) ;
120
+
58
121
return (
59
122
< div className = "space-y-6" >
60
123
< h1 className = "text-3xl font-bold" > Package Approval Configuration</ h1 >
61
124
< div className = "bg-white rounded-lg shadow-md p-6" >
62
125
< h2 className = "text-xl font-semibold mb-4" > Approval Groups</ h2 >
63
- < ul className = "space-y-2 mb-4" >
64
- { approvalGroups ?. map ( ( group ) => (
65
- < li key = { group . id } className = "bg-gray-50 p-3 rounded-md" >
66
- { group . name }
67
- </ li >
68
- ) ) }
69
- </ ul >
70
- < form onSubmit = { handleCreateGroup } className = "flex space-x-2" >
71
- < input
72
- type = "text"
73
- value = { newGroupName }
74
- onChange = { ( e ) => setNewGroupName ( e . target . value ) }
75
- placeholder = "New group name"
76
- className = "flex-grow px-3 py-2 border border-gray-300 rounded-md"
77
- />
78
- < button
79
- type = "submit"
80
- className = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
81
- >
82
- Create Group
83
- </ button >
84
- </ form >
126
+ < div className = "grid grid-cols-2 gap-6" >
127
+ < div >
128
+ < ul className = "space-y-2 mb-4" >
129
+ { approvalGroups ?. map ( ( group : ApprovalGroup ) => (
130
+ < li
131
+ key = { group . id }
132
+ className = { `bg-gray-50 p-3 rounded-md cursor-pointer flex items-center justify-between hover:bg-gray-100 transition-colors duration-150 ${
133
+ selectedGroupId === group . id ? "ring-2 ring-blue-500" : ""
134
+ } `}
135
+ onClick = { ( ) => setSelectedGroupId ( group . id ) }
136
+ >
137
+ < span > { group . name } </ span >
138
+ < svg
139
+ xmlns = "http://www.w3.org/2000/svg"
140
+ className = "h-5 w-5 text-gray-400"
141
+ viewBox = "0 0 20 20"
142
+ fill = "currentColor"
143
+ >
144
+ < path
145
+ fillRule = "evenodd"
146
+ d = "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
147
+ clipRule = "evenodd"
148
+ />
149
+ </ svg >
150
+ </ li >
151
+ ) ) }
152
+ </ ul >
153
+ < form onSubmit = { handleCreateGroup } className = "flex space-x-2" >
154
+ < input
155
+ type = "text"
156
+ value = { newGroupName }
157
+ onChange = { ( e ) => setNewGroupName ( e . target . value ) }
158
+ placeholder = "New group name"
159
+ className = "flex-grow px-3 py-2 border border-gray-300 rounded-md"
160
+ />
161
+ < button
162
+ type = "submit"
163
+ className = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded inline-flex items-center space-x-1"
164
+ >
165
+ < svg
166
+ xmlns = "http://www.w3.org/2000/svg"
167
+ className = "h-5 w-5"
168
+ viewBox = "0 0 20 20"
169
+ fill = "currentColor"
170
+ >
171
+ < path
172
+ fillRule = "evenodd"
173
+ d = "M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
174
+ clipRule = "evenodd"
175
+ />
176
+ </ svg >
177
+ < span > Add</ span >
178
+ </ button >
179
+ </ form >
180
+ </ div >
181
+
182
+ { selectedGroupId && (
183
+ < div >
184
+ < h3 className = "text-lg font-semibold mb-3" > Group Members</ h3 >
185
+ { groupMembers && groupMembers . length > 0 ? (
186
+ < ul className = "space-y-2 mb-4" >
187
+ { groupMembers . map ( ( member : GroupMember ) => (
188
+ < li
189
+ key = { member . userId }
190
+ className = "flex justify-between items-center bg-gray-50 p-3 rounded-md hover:bg-gray-100 transition-colors duration-150"
191
+ >
192
+ < span > { member . name || member . userId } </ span >
193
+ < button
194
+ onClick = { ( ) => handleRemoveUser ( member . userId ) }
195
+ className = "text-red-500 hover:text-red-700 inline-flex items-center space-x-1"
196
+ >
197
+ < svg
198
+ xmlns = "http://www.w3.org/2000/svg"
199
+ className = "h-5 w-5"
200
+ viewBox = "0 0 20 20"
201
+ fill = "currentColor"
202
+ >
203
+ < path
204
+ fillRule = "evenodd"
205
+ d = "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
206
+ clipRule = "evenodd"
207
+ />
208
+ </ svg >
209
+ < span > Remove</ span >
210
+ </ button >
211
+ </ li >
212
+ ) ) }
213
+ </ ul >
214
+ ) : (
215
+ < p className = "text-gray-500 mb-4" > No members in this group</ p >
216
+ ) }
217
+
218
+ < h3 className = "text-lg font-semibold mb-3" > Available Members</ h3 >
219
+ { availableMembers && availableMembers . length > 0 ? (
220
+ < ul className = "space-y-2" >
221
+ { availableMembers . map ( ( member : PackageMember ) => (
222
+ < li
223
+ key = { member . userId }
224
+ className = "flex justify-between items-center bg-gray-50 p-3 rounded-md hover:bg-gray-100 transition-colors duration-150"
225
+ >
226
+ < span > { member . name || member . userId } </ span >
227
+ < button
228
+ onClick = { ( ) => handleAddUser ( member . userId ) }
229
+ className = "text-blue-500 hover:text-blue-700 inline-flex items-center space-x-1"
230
+ >
231
+ < svg
232
+ xmlns = "http://www.w3.org/2000/svg"
233
+ className = "h-5 w-5"
234
+ viewBox = "0 0 20 20"
235
+ fill = "currentColor"
236
+ >
237
+ < path
238
+ fillRule = "evenodd"
239
+ d = "M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
240
+ clipRule = "evenodd"
241
+ />
242
+ </ svg >
243
+ < span > Add</ span >
244
+ </ button >
245
+ </ li >
246
+ ) ) }
247
+ </ ul >
248
+ ) : (
249
+ < p className = "text-gray-500" > No available members</ p >
250
+ ) }
251
+ </ div >
252
+ ) }
253
+ </ div >
85
254
</ div >
86
255
</ div >
87
256
) ;
0 commit comments