larxius commited on
Commit
922a767
·
verified ·
1 Parent(s): ff7b66f

Update frontend/src/pages/SupportEngineerPanel.jsx

Browse files
frontend/src/pages/SupportEngineerPanel.jsx CHANGED
@@ -23,12 +23,54 @@ import {
23
  Server,
24
  Database,
25
  HardDrive,
26
- Eye,
27
- Download,
28
- Receipt
 
 
 
29
  } from 'lucide-react';
30
  import { CustomModal } from '../components/CustomModal';
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  const TablePagination = ({ currentPage, totalEntries, pageSize, onPageChange, onPageSizeChange }) => {
33
  const totalPages = Math.ceil(totalEntries / pageSize) || 1;
34
  const validCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
@@ -206,6 +248,112 @@ export const SupportEngineerPanel = () => {
206
  const closePrompt = () => { setPromptModal({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null }); setPromptValues({}); };
207
  const closeConfirm = () => setConfirmModal({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  const handlePromptChange = (key, value) => setPromptValues(prev => ({ ...prev, [key]: value }));
210
 
211
  const fetchStats = async () => {
@@ -1043,35 +1191,99 @@ export const SupportEngineerPanel = () => {
1043
  {activeTab === 'bookings' && (
1044
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1045
  <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md">
1046
- <Calendar className="w-5 h-5 text-primary mr-2" /> Demo Schedule Bookings
1047
  </h2>
1048
- <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto">
1049
  {demoBookings.length === 0 ? (
1050
- <div className="p-xl text-center text-on-surface-variant font-bold">No demo bookings yet.</div>
1051
  ) : (
1052
  <>
1053
- <table className="w-full text-left border-collapse">
1054
- <thead className="bg-surface border-b border-outline-variant text-on-surface-variant">
1055
- <tr>
1056
- <th className="px-md py-sm font-bold text-[12px] uppercase">Client Email</th>
1057
- <th className="px-md py-sm font-bold text-[12px] uppercase">Meeting Date & Time</th>
1058
- <th className="px-md py-sm font-bold text-[12px] uppercase">Status</th>
1059
- </tr>
1060
- </thead>
1061
- <tbody className="divide-y divide-outline-variant">
1062
- {demoBookings.slice((bookingPage - 1) * bookingPageSize, bookingPage * bookingPageSize).map((b) => (
1063
- <tr key={b.id} className="hover:bg-surface-container transition-colors">
1064
- <td className="px-md py-sm font-bold text-on-surface text-[13.5px]">{b.user_email}</td>
1065
- <td className="px-md py-sm text-on-surface-variant text-[13px]">{b.meeting_date} at {b.meeting_time}</td>
1066
- <td className="px-md py-sm">
1067
- <span className={`px-2 py-0.5 rounded text-[11px] font-bold uppercase ${b.status === 'confirmed' ? 'bg-green-500/10 text-green-600' : 'bg-blue-500/10 text-blue-600'}`}>
1068
- {b.status}
1069
- </span>
1070
- </td>
1071
  </tr>
1072
- ))}
1073
- </tbody>
1074
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1075
  <TablePagination
1076
  currentPage={bookingPage}
1077
  totalEntries={demoBookings.length}
@@ -1274,6 +1486,99 @@ export const SupportEngineerPanel = () => {
1274
  }
1275
  />
1276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1277
  </div>
1278
  );
1279
  };
 
23
  Server,
24
  Database,
25
  HardDrive,
26
+ Eye,
27
+ Download,
28
+ Receipt,
29
+ Trash2,
30
+ CheckCircle,
31
+ XCircle
32
  } from 'lucide-react';
33
  import { CustomModal } from '../components/CustomModal';
34
 
35
+ const parseToISODate = (str) => {
36
+ if (!str) return '';
37
+ const d = new Date(str);
38
+ if (!isNaN(d.getTime())) return d.toISOString().split('T')[0];
39
+ return '';
40
+ };
41
+
42
+ const parseToISOTime = (str) => {
43
+ if (!str) return '';
44
+ const match = str.match(/(\d+):(\d+)\s*(AM|PM)?/i);
45
+ if (match) {
46
+ let hours = parseInt(match[1], 10);
47
+ const minutes = match[2];
48
+ const ampm = match[3];
49
+ if (ampm) {
50
+ if (ampm.toUpperCase() === 'PM' && hours < 12) hours += 12;
51
+ if (ampm.toUpperCase() === 'AM' && hours === 12) hours = 0;
52
+ }
53
+ return `${String(hours).padStart(2, '0')}:${minutes}`;
54
+ }
55
+ return '';
56
+ };
57
+
58
+ const formatToReadableDate = (isoDate) => {
59
+ if (!isoDate) return '';
60
+ const [y, m, d] = isoDate.split('-');
61
+ const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
62
+ return `${months[parseInt(m, 10) - 1]} ${parseInt(d, 10)}, ${y}`;
63
+ };
64
+
65
+ const formatTo12HrTime = (isoTime) => {
66
+ if (!isoTime) return '';
67
+ const [h, m] = isoTime.split(':');
68
+ let hours = parseInt(h, 10);
69
+ const ampm = hours >= 12 ? 'PM' : 'AM';
70
+ hours = hours % 12 || 12;
71
+ return `${String(hours).padStart(2, '0')}:${m} ${ampm}`;
72
+ };
73
+
74
  const TablePagination = ({ currentPage, totalEntries, pageSize, onPageChange, onPageSizeChange }) => {
75
  const totalPages = Math.ceil(totalEntries / pageSize) || 1;
76
  const validCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
 
248
  const closePrompt = () => { setPromptModal({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null }); setPromptValues({}); };
249
  const closeConfirm = () => setConfirmModal({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
250
 
251
+ const [rescheduleModal, setRescheduleModal] = useState({ isOpen: false, bookingId: null, email: '', meetingDate: '', isoDate: '', meetingTime: '', isoTime: '', status: 'rescheduled' });
252
+
253
+ const handleUpdateBookingStatus = async (bookingId, status) => {
254
+ try {
255
+ const res = await authFetch(`/api/demo/bookings/${bookingId}`, {
256
+ method: 'PUT',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify({ status })
259
+ });
260
+ if (res.ok) {
261
+ toast.success(`Booking status updated to ${status}`);
262
+ fetchStats();
263
+ } else {
264
+ toast.error("Failed to update booking status");
265
+ }
266
+ } catch (err) {
267
+ toast.error("Network error updating booking");
268
+ }
269
+ };
270
+
271
+ const handleCancelBooking = (booking) => {
272
+ setConfirmModal({
273
+ isOpen: true,
274
+ title: 'Cancel Demo Booking',
275
+ desc: `Are you sure you want to cancel the demo call booking for ${booking.email || booking.user_email || 'this lead'}?`,
276
+ type: 'error',
277
+ onConfirm: async () => {
278
+ await handleUpdateBookingStatus(booking.id, 'cancelled');
279
+ closeConfirm();
280
+ }
281
+ });
282
+ };
283
+
284
+ const handleOpenReschedule = (booking) => {
285
+ const rawDate = booking.meeting_date || '';
286
+ const rawTime = booking.meeting_time || '';
287
+ const isoDate = parseToISODate(rawDate);
288
+ const isoTime = parseToISOTime(rawTime);
289
+ const formattedDate = formatToReadableDate(isoDate);
290
+ const formattedTime = formatTo12HrTime(isoTime);
291
+
292
+ setRescheduleModal({
293
+ isOpen: true,
294
+ bookingId: booking.id,
295
+ email: booking.email || booking.user_email,
296
+ meetingDate: formattedDate,
297
+ isoDate: isoDate,
298
+ meetingTime: formattedTime,
299
+ isoTime: isoTime,
300
+ status: booking.status === 'pending' || !booking.status ? 'rescheduled' : booking.status
301
+ });
302
+ };
303
+
304
+ const handleSaveReschedule = async (e) => {
305
+ e.preventDefault();
306
+ try {
307
+ const res = await authFetch(`/api/demo/bookings/${rescheduleModal.bookingId}`, {
308
+ method: 'PUT',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({
311
+ meeting_date: rescheduleModal.meetingDate || formatToReadableDate(rescheduleModal.isoDate),
312
+ meeting_time: rescheduleModal.meetingTime || formatTo12HrTime(rescheduleModal.isoTime),
313
+ status: rescheduleModal.status
314
+ })
315
+ });
316
+ if (res.ok) {
317
+ toast.success("Demo booking rescheduled successfully!");
318
+ setRescheduleModal({ isOpen: false, bookingId: null, email: '', meetingDate: '', isoDate: '', meetingTime: '', isoTime: '', status: 'rescheduled' });
319
+ fetchStats();
320
+ } else {
321
+ toast.error("Failed to reschedule demo booking");
322
+ }
323
+ } catch (err) {
324
+ toast.error("Error rescheduling booking");
325
+ }
326
+ };
327
+
328
+ const handleDeleteBooking = (booking) => {
329
+ const bookingId = typeof booking === 'object' ? booking.id : booking;
330
+ const email = typeof booking === 'object' && (booking.email || booking.user_email) ? (booking.email || booking.user_email) : '';
331
+ setConfirmModal({
332
+ isOpen: true,
333
+ title: 'Delete Demo Lead',
334
+ desc: email
335
+ ? `Are you sure you want to permanently delete the demo lead for ${email}?`
336
+ : 'Are you sure you want to permanently delete this demo booking lead?',
337
+ type: 'error',
338
+ onConfirm: async () => {
339
+ try {
340
+ const res = await authFetch(`/api/demo/bookings/${bookingId}`, {
341
+ method: 'DELETE'
342
+ });
343
+ if (res.ok) {
344
+ toast.success("Demo booking lead deleted successfully!");
345
+ fetchStats();
346
+ } else {
347
+ toast.error("Failed to delete demo lead");
348
+ }
349
+ } catch (err) {
350
+ toast.error("Error deleting lead");
351
+ }
352
+ closeConfirm();
353
+ }
354
+ });
355
+ };
356
+
357
  const handlePromptChange = (key, value) => setPromptValues(prev => ({ ...prev, [key]: value }));
358
 
359
  const fetchStats = async () => {
 
1191
  {activeTab === 'bookings' && (
1192
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1193
  <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md">
1194
+ <Calendar className="w-5 h-5 text-primary mr-2" /> Demo Bookings & Leads
1195
  </h2>
1196
+ <div className="bg-surface border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
1197
  {demoBookings.length === 0 ? (
1198
+ <div className="p-xl text-center text-on-surface-variant font-bold">No demo bookings found.</div>
1199
  ) : (
1200
  <>
1201
+ <div className="overflow-x-auto">
1202
+ <table className="w-full text-left border-collapse">
1203
+ <thead className="bg-surface-container-lowest border-b border-outline-variant select-none">
1204
+ <tr>
1205
+ <th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Email</th>
1206
+ <th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Size</th>
1207
+ <th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Date & Time</th>
1208
+ <th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Status</th>
1209
+ <th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Actions</th>
 
 
 
 
 
 
 
 
 
1210
  </tr>
1211
+ </thead>
1212
+ <tbody className="divide-y divide-outline-variant">
1213
+ {demoBookings.slice((bookingPage - 1) * bookingPageSize, bookingPage * bookingPageSize).map(b => {
1214
+ const statusStyle =
1215
+ b.status === 'completed'
1216
+ ? 'bg-green-500/10 text-green-600 border-green-500/30'
1217
+ : b.status === 'cancelled'
1218
+ ? 'bg-red-500/10 text-red-600 border-red-500/30'
1219
+ : b.status === 'rescheduled'
1220
+ ? 'bg-blue-500/10 text-blue-600 border-blue-500/30'
1221
+ : 'bg-orange-500/10 text-orange-600 border-orange-500/30';
1222
+
1223
+ const statusLabel =
1224
+ b.status === 'completed' ? 'Completed' :
1225
+ b.status === 'cancelled' ? 'Cancelled' :
1226
+ b.status === 'rescheduled' ? 'Rescheduled' : 'Pending';
1227
+
1228
+ const displayEmail = b.email || b.user_email;
1229
+
1230
+ return (
1231
+ <tr key={b.id} className="hover:bg-surface-container-lowest transition-colors">
1232
+ <td className="px-md py-sm font-bold text-on-surface text-[13px]">{displayEmail}</td>
1233
+ <td className="px-md py-sm text-on-surface-variant text-[13px]">{b.company_size ? b.company_size.replace('Company Size: ', '') : 'N/A'}</td>
1234
+ <td className="px-md py-sm text-on-surface-variant text-[13px]">
1235
+ {b.meeting_date} <br/>
1236
+ <span className="text-[11px] font-bold text-primary">{b.meeting_time}</span>
1237
+ </td>
1238
+ <td className="px-md py-sm">
1239
+ <span className={`px-2.5 py-1 rounded-full text-[11px] font-bold uppercase tracking-wider border ${statusStyle}`}>
1240
+ {statusLabel}
1241
+ </span>
1242
+ </td>
1243
+ <td className="px-md py-sm">
1244
+ <div className="flex items-center gap-1.5 flex-wrap">
1245
+ {b.status !== 'completed' && (
1246
+ <button
1247
+ onClick={() => handleUpdateBookingStatus(b.id, 'completed')}
1248
+ className="bg-primary text-white border-0 py-1.5 px-3 rounded font-bold cursor-pointer text-[11px] hover:brightness-110 transition-all flex items-center gap-1 shadow-xs"
1249
+ title="Mark Demo as Completed"
1250
+ >
1251
+ Mark Complete
1252
+ </button>
1253
+ )}
1254
+ {b.status !== 'cancelled' && (
1255
+ <button
1256
+ onClick={() => handleCancelBooking(b)}
1257
+ className="bg-red-500/10 text-red-600 hover:bg-red-600 hover:text-white border border-red-500/30 py-1 px-2.5 rounded font-bold cursor-pointer text-[11px] transition-all flex items-center gap-1"
1258
+ title="Mark as Cancelled"
1259
+ >
1260
+ <X className="w-3.5 h-3.5" />
1261
+ Cancel
1262
+ </button>
1263
+ )}
1264
+ <button
1265
+ onClick={() => handleOpenReschedule(b)}
1266
+ className="bg-blue-500/10 text-blue-600 hover:bg-blue-600 hover:text-white border border-blue-500/30 py-1 px-2.5 rounded font-bold cursor-pointer text-[11px] transition-all flex items-center gap-1"
1267
+ title="Reschedule Date & Time"
1268
+ >
1269
+ <Edit className="w-3.5 h-3.5" />
1270
+ Reschedule
1271
+ </button>
1272
+ <button
1273
+ onClick={() => handleDeleteBooking(b)}
1274
+ className="text-on-surface-variant hover:text-red-600 border-0 bg-transparent p-1 cursor-pointer transition-colors"
1275
+ title="Delete Lead"
1276
+ >
1277
+ <Trash2 className="w-4 h-4" />
1278
+ </button>
1279
+ </div>
1280
+ </td>
1281
+ </tr>
1282
+ );
1283
+ })}
1284
+ </tbody>
1285
+ </table>
1286
+ </div>
1287
  <TablePagination
1288
  currentPage={bookingPage}
1289
  totalEntries={demoBookings.length}
 
1486
  }
1487
  />
1488
 
1489
+ {/* Reschedule Demo Call Modal using CustomModal */}
1490
+ <CustomModal
1491
+ isOpen={rescheduleModal.isOpen}
1492
+ onClose={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })}
1493
+ title="Reschedule Demo Call"
1494
+ description="Select a new meeting date, time slot, and update booking status."
1495
+ maxWidth="max-w-lg"
1496
+ >
1497
+ <form onSubmit={handleSaveReschedule} className="flex flex-col gap-4 text-left">
1498
+ <div>
1499
+ <label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider block mb-1">Lead Email</label>
1500
+ <input
1501
+ type="text"
1502
+ disabled
1503
+ value={rescheduleModal.email}
1504
+ className="w-full bg-surface-container border border-outline-variant/60 rounded-lg p-2.5 text-xs text-on-surface-variant font-bold cursor-not-allowed"
1505
+ />
1506
+ </div>
1507
+
1508
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
1509
+ <div>
1510
+ <label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider block mb-1">Meeting Date</label>
1511
+ <input
1512
+ type="date"
1513
+ value={rescheduleModal.isoDate}
1514
+ onChange={(e) => {
1515
+ const val = e.target.value;
1516
+ setRescheduleModal({
1517
+ ...rescheduleModal,
1518
+ isoDate: val,
1519
+ meetingDate: formatToReadableDate(val)
1520
+ });
1521
+ }}
1522
+ className="w-full border border-outline-variant rounded-lg p-2 text-xs text-on-surface bg-surface-container-lowest focus:border-primary outline-none cursor-pointer font-medium"
1523
+ />
1524
+ <span className="text-[11px] text-primary font-bold mt-1 block">
1525
+ {rescheduleModal.meetingDate || 'No date selected'}
1526
+ </span>
1527
+ </div>
1528
+
1529
+ <div>
1530
+ <label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider block mb-1">Meeting Time</label>
1531
+ <input
1532
+ type="time"
1533
+ value={rescheduleModal.isoTime}
1534
+ onChange={(e) => {
1535
+ const val = e.target.value;
1536
+ setRescheduleModal({
1537
+ ...rescheduleModal,
1538
+ isoTime: val,
1539
+ meetingTime: formatTo12HrTime(val)
1540
+ });
1541
+ }}
1542
+ className="w-full border border-outline-variant rounded-lg p-2 text-xs text-on-surface bg-surface-container-lowest focus:border-primary outline-none cursor-pointer font-medium"
1543
+ />
1544
+ <span className="text-[11px] text-primary font-bold mt-1 block">
1545
+ {rescheduleModal.meetingTime || 'No time selected'}
1546
+ </span>
1547
+ </div>
1548
+ </div>
1549
+
1550
+ <div>
1551
+ <label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider block mb-1">Booking Status</label>
1552
+ <select
1553
+ value={rescheduleModal.status}
1554
+ onChange={(e) => setRescheduleModal({ ...rescheduleModal, status: e.target.value })}
1555
+ className="w-full border border-outline-variant rounded-lg p-2.5 text-xs text-on-surface bg-surface-container-lowest focus:border-primary outline-none cursor-pointer"
1556
+ >
1557
+ <option value="rescheduled">Rescheduled</option>
1558
+ <option value="pending">Pending</option>
1559
+ <option value="completed">Completed</option>
1560
+ <option value="cancelled">Cancelled</option>
1561
+ </select>
1562
+ </div>
1563
+
1564
+ <div className="flex justify-end gap-2 pt-3 border-t border-outline-variant">
1565
+ <button
1566
+ type="button"
1567
+ onClick={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })}
1568
+ className="px-4 py-2 rounded-lg text-xs font-bold text-on-surface-variant hover:bg-surface-container cursor-pointer bg-transparent border border-outline-variant"
1569
+ >
1570
+ Cancel
1571
+ </button>
1572
+ <button
1573
+ type="submit"
1574
+ className="px-4 py-2 rounded-lg text-xs font-bold text-white bg-primary hover:brightness-110 shadow-sm cursor-pointer border-0 flex items-center gap-1"
1575
+ >
1576
+ Save & Update
1577
+ </button>
1578
+ </div>
1579
+ </form>
1580
+ </CustomModal>
1581
+
1582
  </div>
1583
  );
1584
  };