Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/SuperAdminPanel.jsx
Browse files
frontend/src/pages/SuperAdminPanel.jsx
CHANGED
|
@@ -129,6 +129,59 @@ const BillingTierCard = ({ tier, onSave }) => {
|
|
| 129 |
);
|
| 130 |
};
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
const SuperAdminPanel = () => {
|
| 133 |
const { user, refreshAccessToken, loading: authLoading } = useAuth();
|
| 134 |
|
|
@@ -330,7 +383,9 @@ const SuperAdminPanel = () => {
|
|
| 330 |
bookingId: null,
|
| 331 |
email: '',
|
| 332 |
meetingDate: '',
|
|
|
|
| 333 |
meetingTime: '',
|
|
|
|
| 334 |
status: 'rescheduled'
|
| 335 |
});
|
| 336 |
|
|
@@ -822,13 +877,22 @@ const SuperAdminPanel = () => {
|
|
| 822 |
};
|
| 823 |
|
| 824 |
const handleOpenReschedule = (booking) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
setRescheduleModal({
|
| 826 |
isOpen: true,
|
| 827 |
bookingId: booking.id,
|
| 828 |
email: booking.email,
|
| 829 |
-
meetingDate:
|
| 830 |
-
|
| 831 |
-
|
|
|
|
|
|
|
| 832 |
});
|
| 833 |
};
|
| 834 |
|
|
@@ -842,14 +906,14 @@ const SuperAdminPanel = () => {
|
|
| 842 |
'Content-Type': 'application/json'
|
| 843 |
},
|
| 844 |
body: JSON.stringify({
|
| 845 |
-
meeting_date: rescheduleModal.meetingDate,
|
| 846 |
-
meeting_time: rescheduleModal.meetingTime,
|
| 847 |
status: rescheduleModal.status
|
| 848 |
})
|
| 849 |
});
|
| 850 |
if (res.ok) {
|
| 851 |
toast.success("Demo booking rescheduled successfully!");
|
| 852 |
-
setRescheduleModal({ isOpen: false, bookingId: null, email: '', meetingDate: '', meetingTime: '', status: 'rescheduled' });
|
| 853 |
fetchStats();
|
| 854 |
} else {
|
| 855 |
toast.error("Failed to reschedule demo booking");
|
|
@@ -1598,79 +1662,141 @@ const SuperAdminPanel = () => {
|
|
| 1598 |
)}
|
| 1599 |
</CustomModal>
|
| 1600 |
|
| 1601 |
-
{/* Reschedule Demo Call Modal */}
|
| 1602 |
-
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
| 1607 |
-
|
| 1608 |
-
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
</div>
|
| 1615 |
-
<
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
|
| 1621 |
-
|
| 1622 |
-
|
| 1623 |
-
|
| 1624 |
-
|
| 1625 |
-
|
| 1626 |
-
|
| 1627 |
-
|
| 1628 |
-
|
| 1629 |
-
|
| 1630 |
-
|
| 1631 |
-
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
|
| 1635 |
-
|
| 1636 |
-
|
| 1637 |
-
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
|
| 1641 |
-
|
| 1642 |
-
|
| 1643 |
-
|
| 1644 |
-
|
| 1645 |
-
|
| 1646 |
-
|
| 1647 |
-
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
-
|
| 1651 |
-
|
| 1652 |
-
|
| 1653 |
-
|
| 1654 |
-
|
| 1655 |
-
|
| 1656 |
-
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
-
|
| 1660 |
-
|
| 1661 |
-
|
| 1662 |
-
|
| 1663 |
-
|
| 1664 |
-
|
| 1665 |
-
|
| 1666 |
-
|
| 1667 |
-
|
| 1668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1669 |
</div>
|
| 1670 |
-
</
|
| 1671 |
</div>
|
| 1672 |
-
|
| 1673 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1674 |
</div>
|
| 1675 |
);
|
| 1676 |
};
|
|
|
|
| 129 |
);
|
| 130 |
};
|
| 131 |
|
| 132 |
+
// Helper functions for Reschedule Modal Date & Time
|
| 133 |
+
const parseToISODate = (dateStr) => {
|
| 134 |
+
if (!dateStr) return new Date().toISOString().split('T')[0];
|
| 135 |
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
|
| 136 |
+
const parsed = new Date(dateStr);
|
| 137 |
+
if (!isNaN(parsed.getTime())) {
|
| 138 |
+
const yyyy = parsed.getFullYear();
|
| 139 |
+
const mm = String(parsed.getMonth() + 1).padStart(2, '0');
|
| 140 |
+
const dd = String(parsed.getDate()).padStart(2, '0');
|
| 141 |
+
return `${yyyy}-${mm}-${dd}`;
|
| 142 |
+
}
|
| 143 |
+
return new Date().toISOString().split('T')[0];
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const formatToReadableDate = (isoStr) => {
|
| 147 |
+
if (!isoStr) return '';
|
| 148 |
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(isoStr)) return isoStr;
|
| 149 |
+
const [yyyy, mm, dd] = isoStr.split('-');
|
| 150 |
+
const parsed = new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10));
|
| 151 |
+
if (!isNaN(parsed.getTime())) {
|
| 152 |
+
return parsed.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
| 153 |
+
}
|
| 154 |
+
return isoStr;
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
const parseToISOTime = (timeStr) => {
|
| 158 |
+
if (!timeStr) return '09:00';
|
| 159 |
+
if (/^\d{2}:\d{2}$/.test(timeStr)) return timeStr;
|
| 160 |
+
const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)?/i);
|
| 161 |
+
if (match) {
|
| 162 |
+
let hours = parseInt(match[1], 10);
|
| 163 |
+
const minutes = match[2];
|
| 164 |
+
const ampm = match[3] ? match[3].toUpperCase() : null;
|
| 165 |
+
if (ampm === 'PM' && hours < 12) hours += 12;
|
| 166 |
+
if (ampm === 'AM' && hours === 12) hours = 0;
|
| 167 |
+
return `${String(hours).padStart(2, '0')}:${minutes}`;
|
| 168 |
+
}
|
| 169 |
+
return '09:00';
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
const formatTo12HrTime = (isoTime) => {
|
| 173 |
+
if (!isoTime) return '';
|
| 174 |
+
if (/AM|PM/i.test(isoTime)) return isoTime;
|
| 175 |
+
const [hStr, mStr] = isoTime.split(':');
|
| 176 |
+
if (hStr !== undefined && mStr !== undefined) {
|
| 177 |
+
let h = parseInt(hStr, 10);
|
| 178 |
+
const ampm = h >= 12 ? 'PM' : 'AM';
|
| 179 |
+
h = h % 12 || 12;
|
| 180 |
+
return `${String(h).padStart(2, '0')}:${mStr} ${ampm}`;
|
| 181 |
+
}
|
| 182 |
+
return isoTime;
|
| 183 |
+
};
|
| 184 |
+
|
| 185 |
const SuperAdminPanel = () => {
|
| 186 |
const { user, refreshAccessToken, loading: authLoading } = useAuth();
|
| 187 |
|
|
|
|
| 383 |
bookingId: null,
|
| 384 |
email: '',
|
| 385 |
meetingDate: '',
|
| 386 |
+
isoDate: '',
|
| 387 |
meetingTime: '',
|
| 388 |
+
isoTime: '',
|
| 389 |
status: 'rescheduled'
|
| 390 |
});
|
| 391 |
|
|
|
|
| 877 |
};
|
| 878 |
|
| 879 |
const handleOpenReschedule = (booking) => {
|
| 880 |
+
const rawDate = booking.meeting_date || '';
|
| 881 |
+
const rawTime = booking.meeting_time || '';
|
| 882 |
+
const isoDate = parseToISODate(rawDate);
|
| 883 |
+
const isoTime = parseToISOTime(rawTime);
|
| 884 |
+
const formattedDate = formatToReadableDate(isoDate);
|
| 885 |
+
const formattedTime = formatTo12HrTime(isoTime);
|
| 886 |
+
|
| 887 |
setRescheduleModal({
|
| 888 |
isOpen: true,
|
| 889 |
bookingId: booking.id,
|
| 890 |
email: booking.email,
|
| 891 |
+
meetingDate: formattedDate,
|
| 892 |
+
isoDate: isoDate,
|
| 893 |
+
meetingTime: formattedTime,
|
| 894 |
+
isoTime: isoTime,
|
| 895 |
+
status: booking.status === 'pending' || !booking.status ? 'rescheduled' : booking.status
|
| 896 |
});
|
| 897 |
};
|
| 898 |
|
|
|
|
| 906 |
'Content-Type': 'application/json'
|
| 907 |
},
|
| 908 |
body: JSON.stringify({
|
| 909 |
+
meeting_date: rescheduleModal.meetingDate || formatToReadableDate(rescheduleModal.isoDate),
|
| 910 |
+
meeting_time: rescheduleModal.meetingTime || formatTo12HrTime(rescheduleModal.isoTime),
|
| 911 |
status: rescheduleModal.status
|
| 912 |
})
|
| 913 |
});
|
| 914 |
if (res.ok) {
|
| 915 |
toast.success("Demo booking rescheduled successfully!");
|
| 916 |
+
setRescheduleModal({ isOpen: false, bookingId: null, email: '', meetingDate: '', isoDate: '', meetingTime: '', isoTime: '', status: 'rescheduled' });
|
| 917 |
fetchStats();
|
| 918 |
} else {
|
| 919 |
toast.error("Failed to reschedule demo booking");
|
|
|
|
| 1662 |
)}
|
| 1663 |
</CustomModal>
|
| 1664 |
|
| 1665 |
+
{/* Reschedule Demo Call Modal using CustomModal */}
|
| 1666 |
+
<CustomModal
|
| 1667 |
+
isOpen={rescheduleModal.isOpen}
|
| 1668 |
+
onClose={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })}
|
| 1669 |
+
title="Reschedule Demo Call"
|
| 1670 |
+
description="Select a new meeting date, time slot, and update booking status."
|
| 1671 |
+
maxWidth="max-w-lg"
|
| 1672 |
+
>
|
| 1673 |
+
<form onSubmit={handleSaveReschedule} className="flex flex-col gap-4 text-left">
|
| 1674 |
+
<div>
|
| 1675 |
+
<label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider block mb-1">Lead Email</label>
|
| 1676 |
+
<input
|
| 1677 |
+
type="text"
|
| 1678 |
+
disabled
|
| 1679 |
+
value={rescheduleModal.email}
|
| 1680 |
+
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"
|
| 1681 |
+
/>
|
| 1682 |
+
</div>
|
| 1683 |
+
|
| 1684 |
+
<div>
|
| 1685 |
+
<div className="flex justify-between items-center mb-1">
|
| 1686 |
+
<label className="text-xs font-bold text-on-surface block">New Meeting Date</label>
|
| 1687 |
+
{rescheduleModal.meetingDate && (
|
| 1688 |
+
<span className="text-[11px] font-bold text-primary bg-primary/10 px-2 py-0.5 rounded border border-primary/20 flex items-center gap-1">
|
| 1689 |
+
<span className="material-symbols-outlined text-[13px]">calendar_today</span>
|
| 1690 |
+
{rescheduleModal.meetingDate}
|
| 1691 |
+
</span>
|
| 1692 |
+
)}
|
| 1693 |
</div>
|
| 1694 |
+
<input
|
| 1695 |
+
type="date"
|
| 1696 |
+
required
|
| 1697 |
+
value={rescheduleModal.isoDate}
|
| 1698 |
+
onChange={(e) => {
|
| 1699 |
+
const newIso = e.target.value;
|
| 1700 |
+
setRescheduleModal(prev => ({
|
| 1701 |
+
...prev,
|
| 1702 |
+
isoDate: newIso,
|
| 1703 |
+
meetingDate: formatToReadableDate(newIso)
|
| 1704 |
+
}));
|
| 1705 |
+
}}
|
| 1706 |
+
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"
|
| 1707 |
+
/>
|
| 1708 |
+
</div>
|
| 1709 |
+
|
| 1710 |
+
<div>
|
| 1711 |
+
<div className="flex justify-between items-center mb-1">
|
| 1712 |
+
<label className="text-xs font-bold text-on-surface block">New Meeting Time</label>
|
| 1713 |
+
{rescheduleModal.meetingTime && (
|
| 1714 |
+
<span className="text-[11px] font-bold text-primary bg-primary/10 px-2 py-0.5 rounded border border-primary/20 flex items-center gap-1">
|
| 1715 |
+
<span className="material-symbols-outlined text-[13px]">schedule</span>
|
| 1716 |
+
{rescheduleModal.meetingTime}
|
| 1717 |
+
</span>
|
| 1718 |
+
)}
|
| 1719 |
+
</div>
|
| 1720 |
+
|
| 1721 |
+
{/* Time Picker */}
|
| 1722 |
+
<input
|
| 1723 |
+
type="time"
|
| 1724 |
+
required
|
| 1725 |
+
value={rescheduleModal.isoTime}
|
| 1726 |
+
onChange={(e) => {
|
| 1727 |
+
const newIso = e.target.value;
|
| 1728 |
+
setRescheduleModal(prev => ({
|
| 1729 |
+
...prev,
|
| 1730 |
+
isoTime: newIso,
|
| 1731 |
+
meetingTime: formatTo12HrTime(newIso)
|
| 1732 |
+
}));
|
| 1733 |
+
}}
|
| 1734 |
+
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 mb-2"
|
| 1735 |
+
/>
|
| 1736 |
+
|
| 1737 |
+
{/* Quick Time Slots */}
|
| 1738 |
+
<div className="flex flex-col gap-1">
|
| 1739 |
+
<span className="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider">Quick Select Time Slot:</span>
|
| 1740 |
+
<div className="grid grid-cols-4 gap-1.5">
|
| 1741 |
+
{['09:00 AM', '10:00 AM', '11:00 AM', '02:00 PM', '04:00 PM', '05:00 PM', '06:00 PM', '09:30 PM'].map((slot) => {
|
| 1742 |
+
const isSelected = rescheduleModal.meetingTime === slot;
|
| 1743 |
+
return (
|
| 1744 |
+
<button
|
| 1745 |
+
key={slot}
|
| 1746 |
+
type="button"
|
| 1747 |
+
onClick={() => {
|
| 1748 |
+
setRescheduleModal(prev => ({
|
| 1749 |
+
...prev,
|
| 1750 |
+
meetingTime: slot,
|
| 1751 |
+
isoTime: parseToISOTime(slot)
|
| 1752 |
+
}));
|
| 1753 |
+
}}
|
| 1754 |
+
className={`py-1.5 px-1 text-center text-[10.5px] font-bold border rounded-lg transition-all cursor-pointer ${
|
| 1755 |
+
isSelected
|
| 1756 |
+
? 'bg-primary text-white border-primary shadow-xs'
|
| 1757 |
+
: 'bg-surface-container-low border-outline-variant/60 text-on-surface-variant hover:bg-surface-container-high'
|
| 1758 |
+
}`}
|
| 1759 |
+
>
|
| 1760 |
+
{slot}
|
| 1761 |
+
</button>
|
| 1762 |
+
);
|
| 1763 |
+
})}
|
| 1764 |
</div>
|
| 1765 |
+
</div>
|
| 1766 |
</div>
|
| 1767 |
+
|
| 1768 |
+
<div>
|
| 1769 |
+
<label className="text-xs font-bold text-on-surface block mb-1">Update Booking Status</label>
|
| 1770 |
+
<select
|
| 1771 |
+
value={rescheduleModal.status}
|
| 1772 |
+
onChange={(e) => setRescheduleModal({ ...rescheduleModal, status: e.target.value })}
|
| 1773 |
+
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"
|
| 1774 |
+
>
|
| 1775 |
+
<option value="rescheduled">Rescheduled</option>
|
| 1776 |
+
<option value="pending">Pending</option>
|
| 1777 |
+
<option value="completed">Completed / Conducted</option>
|
| 1778 |
+
<option value="cancelled">Cancelled / Not Conducted</option>
|
| 1779 |
+
</select>
|
| 1780 |
+
</div>
|
| 1781 |
+
|
| 1782 |
+
<div className="flex justify-end gap-2 pt-3 border-t border-outline-variant">
|
| 1783 |
+
<button
|
| 1784 |
+
type="button"
|
| 1785 |
+
onClick={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })}
|
| 1786 |
+
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"
|
| 1787 |
+
>
|
| 1788 |
+
Cancel
|
| 1789 |
+
</button>
|
| 1790 |
+
<button
|
| 1791 |
+
type="submit"
|
| 1792 |
+
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"
|
| 1793 |
+
>
|
| 1794 |
+
<span className="material-symbols-outlined text-[16px]">save</span>
|
| 1795 |
+
Save & Update
|
| 1796 |
+
</button>
|
| 1797 |
+
</div>
|
| 1798 |
+
</form>
|
| 1799 |
+
</CustomModal>
|
| 1800 |
</div>
|
| 1801 |
);
|
| 1802 |
};
|