Skip to content

Commit 12d8d27

Browse files
committed
feat: add payment verification functionality to TeamsTable and ApiService
1 parent d3272ec commit 12d8d27

File tree

3 files changed

+227
-1
lines changed

3 files changed

+227
-1
lines changed

src/components/admin/teamdetails.tsx

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useState } from "react";
2+
import { AnimatePresence, motion } from "framer-motion";
23
import { ApiService } from "@/lib/api";
34
import type { Team } from "@/lib/types";
45

@@ -8,6 +9,12 @@ export default function TeamsTable() {
89
const [page, setPage] = useState(0);
910
const [loading, setLoading] = useState(true);
1011
const [error, setError] = useState<string | null>(null);
12+
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null);
13+
const [panelOpen, setPanelOpen] = useState(false);
14+
const [panelError, setPanelError] = useState<string | null>(null);
15+
const [saving, setSaving] = useState(false);
16+
const [hasChanges, setHasChanges] = useState(false);
17+
const [pendingPaymentStatus, setPendingPaymentStatus] = useState<boolean | null>(null);
1118

1219
const rowsPerPage = 10; // Number of rows per page
1320

@@ -66,6 +73,209 @@ export default function TeamsTable() {
6673
text-sm sm:text-base text-gray-800 dark:text-gray-100"
6774
/>
6875
</div>
76+
{/* Slide-in Side Panel */}
77+
<AnimatePresence>
78+
{panelOpen && (
79+
<motion.div
80+
className="fixed inset-0 z-50 flex items-center justify-end"
81+
initial={{ opacity: 0 }}
82+
animate={{ opacity: 1 }}
83+
exit={{ opacity: 0 }}
84+
>
85+
{/* Background overlay */}
86+
<div
87+
className="absolute inset-0 bg-black/30"
88+
onClick={() => {
89+
setPanelOpen(false);
90+
setSelectedTeam(null);
91+
setPanelError(null);
92+
setHasChanges(false);
93+
setPendingPaymentStatus(null);
94+
}}
95+
/>
96+
97+
{/* Panel */}
98+
<motion.div
99+
className="relative bg-white dark:bg-gray-900 shadow-xl w-full sm:w-[500px] max-h-screen overflow-auto p-6 border-l border-gray-200 dark:border-gray-700"
100+
initial={{ x: '100%' }}
101+
animate={{ x: 0 }}
102+
exit={{ x: '100%' }}
103+
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
104+
>
105+
<div className="flex items-center justify-between mb-4">
106+
<h2 className="text-lg font-bold text-gray-800 dark:text-white">Team Details</h2>
107+
<button
108+
onClick={() => {
109+
setPanelOpen(false);
110+
setSelectedTeam(null);
111+
setPanelError(null);
112+
setHasChanges(false);
113+
setPendingPaymentStatus(null);
114+
}}
115+
className="text-gray-600 dark:text-gray-300 hover:text-gray-900"
116+
>
117+
Close
118+
</button>
119+
</div>
120+
121+
{panelError ? (
122+
<div className="text-sm text-red-500">{panelError}</div>
123+
) : selectedTeam ? (
124+
<div className="space-y-4">
125+
{/* Team Basic Info */}
126+
<div className="bg-orange-50 dark:bg-orange-900/20 p-4 rounded-lg">
127+
<h3 className="font-semibold text-orange-700 dark:text-orange-300 mb-3">Team Information</h3>
128+
<div className="grid grid-cols-2 gap-3 text-sm">
129+
<div>
130+
<span className="font-medium text-gray-600 dark:text-gray-400">Team ID:</span>
131+
<div className="text-gray-800 dark:text-gray-200">{selectedTeam.teamId}</div>
132+
</div>
133+
<div>
134+
<span className="font-medium text-gray-600 dark:text-gray-400">SCC ID:</span>
135+
<div className="text-gray-800 dark:text-gray-200">{selectedTeam.scc_id}</div>
136+
</div>
137+
<div className="col-span-2">
138+
<span className="font-medium text-gray-600 dark:text-gray-400">Team Name:</span>
139+
<div className="text-gray-800 dark:text-gray-200 font-medium">{selectedTeam.title}</div>
140+
</div>
141+
</div>
142+
</div>
143+
144+
{/* Team Members */}
145+
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-lg">
146+
<h3 className="font-semibold text-gray-700 dark:text-gray-300 mb-3">
147+
Team Members ({selectedTeam.members.length})
148+
</h3>
149+
<div className="space-y-4">
150+
{selectedTeam.members.map((member, index) => (
151+
<div key={member.id} className="bg-white dark:bg-gray-700 p-3 rounded-lg border border-gray-200 dark:border-gray-600">
152+
<div className="flex items-start justify-between mb-2">
153+
<h4 className="font-medium text-gray-800 dark:text-gray-200">
154+
{member.name}
155+
{index === 0 && <span className="ml-2 text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 px-2 py-1 rounded-full">Leader</span>}
156+
</h4>
157+
<span className="text-xs bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 px-2 py-1 rounded">
158+
Year {member.year_of_study}
159+
</span>
160+
</div>
161+
162+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs text-gray-600 dark:text-gray-400">
163+
<div>
164+
<span className="font-medium">Email:</span>
165+
<div className="text-gray-800 dark:text-gray-300 break-all">{member.email}</div>
166+
</div>
167+
<div>
168+
<span className="font-medium">Phone:</span>
169+
<div className="text-gray-800 dark:text-gray-300">{member.phone_number}</div>
170+
</div>
171+
<div>
172+
<span className="font-medium">Department:</span>
173+
<div className="text-gray-800 dark:text-gray-300">{member.department}</div>
174+
</div>
175+
<div>
176+
<span className="font-medium">T-Shirt:</span>
177+
<div className="text-gray-800 dark:text-gray-300">{member.tShirtSize}</div>
178+
</div>
179+
<div className="sm:col-span-2">
180+
<span className="font-medium">College:</span>
181+
<div className="text-gray-800 dark:text-gray-300">{member.college_name}</div>
182+
</div>
183+
{member.location && (
184+
<div>
185+
<span className="font-medium">Location:</span>
186+
<div className="text-gray-800 dark:text-gray-300">{member.location}</div>
187+
</div>
188+
)}
189+
<div>
190+
<span className="font-medium">Attendance:</span>
191+
<div className="text-gray-800 dark:text-gray-300">{member.attendance}</div>
192+
</div>
193+
</div>
194+
</div>
195+
))}
196+
</div>
197+
</div>
198+
199+
{/* Payment Verification Section */}
200+
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
201+
<h3 className="font-semibold text-blue-700 dark:text-blue-300 mb-3">Payment Status</h3>
202+
<div className="mb-4">
203+
<label className="flex items-center gap-3">
204+
<input
205+
type="checkbox"
206+
checked={pendingPaymentStatus ?? false}
207+
onChange={(e) => {
208+
const newValue = e.target.checked;
209+
setPendingPaymentStatus(newValue);
210+
setHasChanges(newValue !== (selectedTeam?.paymentVerified ?? false));
211+
}}
212+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
213+
/>
214+
<span className="text-gray-700 dark:text-gray-300">Payment Verified</span>
215+
<span className={`px-2 py-1 text-xs rounded-full ${
216+
pendingPaymentStatus
217+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
218+
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
219+
}`}>
220+
{pendingPaymentStatus ? 'Verified' : 'Pending'}
221+
</span>
222+
</label>
223+
</div>
224+
225+
{/* Save Button */}
226+
{hasChanges && (
227+
<div className="flex gap-2">
228+
<button
229+
onClick={async () => {
230+
if (!selectedTeam || pendingPaymentStatus === null) return;
231+
232+
try {
233+
setSaving(true);
234+
setPanelError(null);
235+
await ApiService.admin.verifyTeamPayment(selectedTeam.teamId, pendingPaymentStatus);
236+
237+
// Update local UI
238+
const updatedTeam = { ...selectedTeam, paymentVerified: pendingPaymentStatus };
239+
setSelectedTeam(updatedTeam);
240+
setTeams((prev) => prev.map((t) =>
241+
t.teamId === selectedTeam.teamId
242+
? { ...t, paymentVerified: pendingPaymentStatus }
243+
: t
244+
));
245+
setHasChanges(false);
246+
} catch (err) {
247+
console.error('Failed to update payment status', err);
248+
setPanelError('Failed to update payment status');
249+
} finally {
250+
setSaving(false);
251+
}
252+
}}
253+
disabled={saving}
254+
className="px-4 py-2 bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white text-sm rounded-lg transition"
255+
>
256+
{saving ? 'Saving...' : 'Save Changes'}
257+
</button>
258+
<button
259+
onClick={() => {
260+
setPendingPaymentStatus(selectedTeam?.paymentVerified ?? false);
261+
setHasChanges(false);
262+
}}
263+
disabled={saving}
264+
className="px-4 py-2 bg-gray-500 hover:bg-gray-600 disabled:bg-gray-400 text-white text-sm rounded-lg transition"
265+
>
266+
Cancel
267+
</button>
268+
</div>
269+
)}
270+
</div>
271+
</div>
272+
) : (
273+
<div className="text-sm text-gray-500">No team selected</div>
274+
)}
275+
</motion.div>
276+
</motion.div>
277+
)}
278+
</AnimatePresence>
69279

70280
{/* Main Content */}
71281
<div className="max-w-5xl mx-auto">
@@ -92,7 +302,14 @@ export default function TeamsTable() {
92302
paginatedTeams.map((team) => (
93303
<tr
94304
key={team.teamId}
95-
className="border-b border-gray-200 dark:border-gray-700 hover:bg-orange-50 dark:hover:bg-orange-600/20 transition"
305+
className="border-b border-gray-200 dark:border-gray-700 hover:bg-orange-50 dark:hover:bg-orange-600/20 transition cursor-pointer"
306+
onClick={() => {
307+
setSelectedTeam(team);
308+
setPanelOpen(true);
309+
setPanelError(null);
310+
setHasChanges(false);
311+
setPendingPaymentStatus(team.paymentVerified ?? false);
312+
}}
96313
>
97314
<td className="px-4 py-3 text-gray-800 dark:text-white">{team.teamId}</td>
98315
<td className="px-4 py-3 text-gray-800 dark:text-gray-100">{team.scc_id}</td>

src/lib/api/service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export class ApiService {
6868
const response = await apiClient.get(`/admin/teams/${id}`);
6969
return response.data;
7070
},
71+
// Verify/unverify team payment
72+
verifyTeamPayment: async (teamId: number, verified: boolean): Promise<{ message: string; team: Team }> => {
73+
const response = await apiClient.patch(`/admin/teams/${teamId}/verify-payment`, { verified });
74+
return response.data;
75+
},
7176

7277
getLeaderboard: async (): Promise<LeaderboardEntry[]> => {
7378
const response = await apiClient.get('/admin/leaderboards');

src/lib/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface Team {
3333
problem_statement?: ProblemStatement;
3434
team_members?: Member[]; // For backward compatibility
3535
members: Member[]; // Backend returns this field name in getAllTeams
36+
paymentVerified?: boolean; // Payment status, optional for compatibility
3637
tasks?: Task[];
3738
}
3839

@@ -44,7 +45,10 @@ export interface Member {
4445
department?: string;
4546
college_name: string;
4647
year_of_study?: number;
48+
location?: string;
4749
attendance: number;
50+
tShirtSize?: string;
51+
teamId?: number;
4852
}
4953

5054
export interface ProblemStatement {

0 commit comments

Comments
 (0)