Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
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
|
| 1047 |
</h2>
|
| 1048 |
-
<div className="bg-surface
|
| 1049 |
{demoBookings.length === 0 ? (
|
| 1050 |
-
<div className="p-xl text-center text-on-surface-variant font-bold">No demo bookings
|
| 1051 |
) : (
|
| 1052 |
<>
|
| 1053 |
-
<
|
| 1054 |
-
<
|
| 1055 |
-
<
|
| 1056 |
-
<
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 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 |
-
|
| 1074 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
};
|