1+ <script lang =" ts" >
2+ import { onMount } from ' svelte' ;
3+
4+ type EditRequest = {
5+ requestId: number ;
6+ userId: number ;
7+ projectId: number ;
8+ requestType: string ;
9+ currentData: any ;
10+ requestedData: any ;
11+ status: ' pending' | ' approved' | ' rejected' ;
12+ reason: string | null ;
13+ reviewedBy: number | null ;
14+ reviewedAt: string | null ;
15+ createdAt: string ;
16+ updatedAt: string ;
17+ user: {
18+ userId: number ;
19+ firstName: string | null ;
20+ lastName: string | null ;
21+ email: string ;
22+ };
23+ project: {
24+ projectId: number ;
25+ projectTitle: string ;
26+ projectType: string ;
27+ };
28+ };
29+
30+ let { apiUrl, initialEditRequests = [] }: { apiUrl: string ; initialEditRequests? : EditRequest [] } = $props ();
31+
32+ let editRequests = $state <EditRequest []>(initialEditRequests );
33+ let loading = $state (false );
34+ let statusFilter = $state <' all' | ' pending' | ' approved' | ' rejected' >(' pending' );
35+ let editRequestBusy = $state <Record <number , boolean >>({});
36+ let editRequestErrors = $state <Record <number , string >>({});
37+ let editRequestSuccess = $state <Record <number , string >>({});
38+
39+ let filteredRequests = $derived (
40+ editRequests .filter (req => {
41+ if (statusFilter !== ' all' && req .status !== statusFilter ) {
42+ return false ;
43+ }
44+ return true ;
45+ })
46+ );
47+
48+ async function loadEditRequests() {
49+ loading = true ;
50+ try {
51+ const response = await fetch (` ${apiUrl }/api/admin/edit-requests ` , {
52+ credentials: ' include' ,
53+ });
54+
55+ if (! response .ok ) {
56+ throw new Error (' Failed to load edit requests' );
57+ }
58+
59+ editRequests = await response .json ();
60+ } catch (err ) {
61+ console .error (' Error loading edit requests:' , err );
62+ } finally {
63+ loading = false ;
64+ }
65+ }
66+
67+ async function approveEditRequest(requestId : number ) {
68+ editRequestBusy = { ... editRequestBusy , [requestId ]: true };
69+ editRequestErrors = { ... editRequestErrors , [requestId ]: ' ' };
70+ editRequestSuccess = { ... editRequestSuccess , [requestId ]: ' ' };
71+
72+ try {
73+ const response = await fetch (` ${apiUrl }/api/admin/edit-requests/${requestId }/approve ` , {
74+ method: ' PUT' ,
75+ credentials: ' include' ,
76+ });
77+
78+ if (! response .ok ) {
79+ const { message } = await response .json ().catch (() => ({ message: ' Failed to approve request' }));
80+ editRequestErrors = { ... editRequestErrors , [requestId ]: message ?? ' Failed to approve request' };
81+ return ;
82+ }
83+
84+ editRequestSuccess = { ... editRequestSuccess , [requestId ]: ' Request approved successfully' };
85+ await loadEditRequests ();
86+ } catch (err ) {
87+ editRequestErrors = {
88+ ... editRequestErrors ,
89+ [requestId ]: err instanceof Error ? err .message : ' Failed to approve request' ,
90+ };
91+ } finally {
92+ editRequestBusy = { ... editRequestBusy , [requestId ]: false };
93+ }
94+ }
95+
96+ async function rejectEditRequest(requestId : number ) {
97+ const reason = prompt (' Provide a reason for rejecting this request:' );
98+ if (! reason ) return ;
99+
100+ editRequestBusy = { ... editRequestBusy , [requestId ]: true };
101+ editRequestErrors = { ... editRequestErrors , [requestId ]: ' ' };
102+ editRequestSuccess = { ... editRequestSuccess , [requestId ]: ' ' };
103+
104+ try {
105+ const response = await fetch (` ${apiUrl }/api/admin/edit-requests/${requestId }/reject ` , {
106+ method: ' PUT' ,
107+ headers: { ' Content-Type' : ' application/json' },
108+ credentials: ' include' ,
109+ body: JSON .stringify ({ reason }),
110+ });
111+
112+ if (! response .ok ) {
113+ const { message } = await response .json ().catch (() => ({ message: ' Failed to reject request' }));
114+ editRequestErrors = { ... editRequestErrors , [requestId ]: message ?? ' Failed to reject request' };
115+ return ;
116+ }
117+
118+ editRequestSuccess = { ... editRequestSuccess , [requestId ]: ' Request rejected successfully' };
119+ await loadEditRequests ();
120+ } catch (err ) {
121+ editRequestErrors = {
122+ ... editRequestErrors ,
123+ [requestId ]: err instanceof Error ? err .message : ' Failed to reject request' ,
124+ };
125+ } finally {
126+ editRequestBusy = { ... editRequestBusy , [requestId ]: false };
127+ }
128+ }
129+
130+ function formatDate(value : string ) {
131+ return new Date (value ).toLocaleString ();
132+ }
133+
134+ function fullName(user : { firstName: string | null ; lastName: string | null }) {
135+ const first = user .firstName ?? ' ' ;
136+ const last = user .lastName ?? ' ' ;
137+ const name = ` ${first } ${last } ` .trim ();
138+ return name || ' Unknown' ;
139+ }
140+
141+ function renderValue(value : any ): string {
142+ if (value === null || value === undefined ) return ' —' ;
143+ if (Array .isArray (value )) return value .join (' , ' );
144+ if (typeof value === ' object' ) return JSON .stringify (value , null , 2 );
145+ return String (value );
146+ }
147+
148+ function getChangedFields(currentData : any , requestedData : any ): string [] {
149+ const fields = new Set ([... Object .keys (currentData || {}), ... Object .keys (requestedData || {})]);
150+ return Array .from (fields ).filter (key => {
151+ const current = currentData ?.[key ];
152+ const requested = requestedData ?.[key ];
153+ return JSON .stringify (current ) !== JSON .stringify (requested );
154+ });
155+ }
156+ </script >
157+
158+ <section class =" space-y-4" >
159+ <div class =" flex flex-col gap-4 md:flex-row md:items-center md:justify-between" >
160+ <h2 class =" text-2xl font-semibold" >Edit Requests</h2 >
161+ <button
162+ class =" px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg border border-gray-700 transition-colors"
163+ onclick ={loadEditRequests }
164+ disabled ={loading }
165+ >
166+ {loading ? ' Loading...' : ' Refresh' }
167+ </button >
168+ </div >
169+
170+ <div class =" rounded-2xl border border-gray-700 bg-gray-900/70 backdrop-blur p-6 space-y-6" >
171+ <div class =" flex gap-2" >
172+ <button
173+ class ={` px-4 py-2 rounded-lg border transition-colors ${statusFilter === ' all' ? ' bg-purple-600 border-purple-400' : ' bg-gray-800 border-gray-700 hover:bg-gray-700' } ` }
174+ onclick ={() => (statusFilter = ' all' )}
175+ >
176+ All
177+ </button >
178+ <button
179+ class ={` px-4 py-2 rounded-lg border transition-colors ${statusFilter === ' pending' ? ' bg-purple-600 border-purple-400' : ' bg-gray-800 border-gray-700 hover:bg-gray-700' } ` }
180+ onclick ={() => (statusFilter = ' pending' )}
181+ >
182+ Pending
183+ </button >
184+ <button
185+ class ={` px-4 py-2 rounded-lg border transition-colors ${statusFilter === ' approved' ? ' bg-purple-600 border-purple-400' : ' bg-gray-800 border-gray-700 hover:bg-gray-700' } ` }
186+ onclick ={() => (statusFilter = ' approved' )}
187+ >
188+ Approved
189+ </button >
190+ <button
191+ class ={` px-4 py-2 rounded-lg border transition-colors ${statusFilter === ' rejected' ? ' bg-purple-600 border-purple-400' : ' bg-gray-800 border-gray-700 hover:bg-gray-700' } ` }
192+ onclick ={() => (statusFilter = ' rejected' )}
193+ >
194+ Rejected
195+ </button >
196+ </div >
197+
198+ {#if filteredRequests .length === 0 }
199+ <div class =" text-center py-12 text-gray-400" >
200+ No {statusFilter !== ' all' ? statusFilter : ' ' } edit requests found
201+ </div >
202+ {:else }
203+ <div class =" space-y-4" >
204+ {#each filteredRequests as request (request .requestId )}
205+ {@const changedFields = getChangedFields (request .currentData , request .requestedData )}
206+ <div class =" rounded-xl border border-gray-700 bg-gray-800/50 p-6 space-y-4" >
207+ <div class =" flex justify-between items-start" >
208+ <div >
209+ <h3 class ="text-xl font-semibold text-white" >{request .project .projectTitle }</h3 >
210+ <p class =" text-sm text-gray-400" >
211+ {fullName (request .user )} ({request .user .email })
212+ </p >
213+ <p class =" text-xs text-gray-500 mt-1" >
214+ Requested: {formatDate (request .createdAt )}
215+ </p >
216+ </div >
217+ <div class =" flex flex-col items-end gap-2" >
218+ <span
219+ class ={` px-3 py-1 rounded-full text-xs font-semibold ${
220+ request .status === ' pending'
221+ ? ' bg-yellow-500/20 text-yellow-400 border border-yellow-500/30'
222+ : request .status === ' approved'
223+ ? ' bg-green-500/20 text-green-400 border border-green-500/30'
224+ : ' bg-red-500/20 text-red-400 border border-red-500/30'
225+ } ` }
226+ >
227+ {request .status .toUpperCase ()}
228+ </span >
229+ <span class ="text-xs text-gray-500" >ID: {request .requestId }</span >
230+ </div >
231+ </div >
232+
233+ {#if request .reason }
234+ <div class =" bg-gray-900/50 rounded-lg p-4 border border-gray-700" >
235+ <p class =" text-sm text-gray-400 font-semibold mb-1" >Reason for Changes:</p >
236+ <p class ="text-white whitespace-pre-wrap" >{request .reason }</p >
237+ </div >
238+ {/if }
239+
240+ <div class =" space-y-3" >
241+ <h4 class =" text-sm font-semibold text-gray-400 uppercase tracking-wide" >Requested Changes:</h4 >
242+ {#each changedFields as field }
243+ <div class =" bg-gray-900/50 rounded-lg p-4 border border-gray-700" >
244+ <p class ="text-sm text-gray-400 font-semibold mb-2" >{field }</p >
245+ <div class =" grid grid-cols-1 md:grid-cols-2 gap-4" >
246+ <div >
247+ <p class =" text-xs text-gray-500 mb-1" >Current:</p >
248+ <pre class ="text-sm text-red-400 bg-red-900/20 p-2 rounded border border-red-500/30 overflow-x-auto" >{renderValue (request .currentData ?.[field ])}</pre >
249+ </div >
250+ <div >
251+ <p class =" text-xs text-gray-500 mb-1" >Requested:</p >
252+ <pre class ="text-sm text-green-400 bg-green-900/20 p-2 rounded border border-green-500/30 overflow-x-auto" >{renderValue (request .requestedData ?.[field ])}</pre >
253+ </div >
254+ </div >
255+ </div >
256+ {/each }
257+ </div >
258+
259+ {#if editRequestErrors [request .requestId ]}
260+ <div class =" bg-red-500/10 border border-red-500/30 rounded-lg p-3 text-sm text-red-400" >
261+ {editRequestErrors [request .requestId ]}
262+ </div >
263+ {/if }
264+
265+ {#if editRequestSuccess [request .requestId ]}
266+ <div class =" bg-green-500/10 border border-green-500/30 rounded-lg p-3 text-sm text-green-400" >
267+ {editRequestSuccess [request .requestId ]}
268+ </div >
269+ {/if }
270+
271+ {#if request .status === ' pending' }
272+ <div class =" flex gap-3" >
273+ <button
274+ class =" flex-1 px-4 py-2 bg-green-600 hover:bg-green-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-semibold"
275+ onclick ={() => approveEditRequest (request .requestId )}
276+ disabled ={editRequestBusy [request .requestId ]}
277+ >
278+ {editRequestBusy [request .requestId ] ? ' Approving...' : ' Approve' }
279+ </button >
280+ <button
281+ class =" flex-1 px-4 py-2 bg-red-600 hover:bg-red-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-semibold"
282+ onclick ={() => rejectEditRequest (request .requestId )}
283+ disabled ={editRequestBusy [request .requestId ]}
284+ >
285+ {editRequestBusy [request .requestId ] ? ' Rejecting...' : ' Reject' }
286+ </button >
287+ </div >
288+ {:else if request .reviewedAt }
289+ <p class =" text-sm text-gray-400" >
290+ Reviewed: {formatDate (request .reviewedAt )}
291+ </p >
292+ {/if }
293+ </div >
294+ {/each }
295+ </div >
296+ {/if }
297+ </div >
298+ </section >
0 commit comments