Phase 7: Final cleanup - Remove deprecated booking/appointment code
Deleted deprecated files: - backend/routes/booking.py - frontend/app/admin/appointments/, booking/, appointments/, sum/, audit/ - frontend/app/utils/appointment.ts - frontend/e2e/booking.spec.ts, appointments.spec.ts Updated references: - exchange/page.tsx: Use /api/exchange/slots instead of /api/booking/slots - useRequireAuth.ts: Redirect to /admin/trades and /exchange - profile.tsx, invites.tsx: Update fallback redirect - E2E tests: Update all /audit references to /admin/trades - profile.test.tsx: Update admin redirect test
This commit is contained in:
parent
9e8d0af435
commit
bbd9fae763
16 changed files with 29 additions and 2103 deletions
|
|
@ -1,319 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Permission } from "../../auth-context";
|
||||
import { api } from "../../api";
|
||||
import { Header } from "../../components/Header";
|
||||
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
||||
import { components } from "../../generated/api";
|
||||
import { formatDateTime } from "../../utils/date";
|
||||
import { getStatusDisplay } from "../../utils/appointment";
|
||||
import { sharedStyles } from "../../styles/shared";
|
||||
|
||||
type AppointmentResponse = components["schemas"]["AppointmentResponse"];
|
||||
type PaginatedAppointments = components["schemas"]["PaginatedResponse_AppointmentResponse_"];
|
||||
|
||||
const pageStyles: Record<string, React.CSSProperties> = {
|
||||
main: {
|
||||
minHeight: "100vh",
|
||||
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
loader: {
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: "2rem",
|
||||
maxWidth: "900px",
|
||||
margin: "0 auto",
|
||||
width: "100%",
|
||||
},
|
||||
pageTitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1.75rem",
|
||||
fontWeight: 600,
|
||||
color: "#fff",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
pageSubtitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.9rem",
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
filterRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
filterLabel: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
filterSelect: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.5rem 1rem",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: "6px",
|
||||
color: "#fff",
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
appointmentList: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.75rem",
|
||||
},
|
||||
appointmentCard: {
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: "1.25rem",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
appointmentCardPast: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
appointmentHeader: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
gap: "1rem",
|
||||
},
|
||||
appointmentTime: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 500,
|
||||
color: "#fff",
|
||||
marginBottom: "0.25rem",
|
||||
},
|
||||
appointmentUser: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.875rem",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
marginBottom: "0.25rem",
|
||||
},
|
||||
appointmentNote: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.875rem",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
fontStyle: "italic",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
statusBadge: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.25rem 0.75rem",
|
||||
borderRadius: "9999px",
|
||||
display: "inline-block",
|
||||
},
|
||||
buttonGroup: {
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
cancelButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.35rem 0.75rem",
|
||||
fontSize: "0.75rem",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: "6px",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
confirmButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.35rem 0.75rem",
|
||||
fontSize: "0.75rem",
|
||||
background: "rgba(239, 68, 68, 0.2)",
|
||||
border: "1px solid rgba(239, 68, 68, 0.3)",
|
||||
borderRadius: "6px",
|
||||
color: "#f87171",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
emptyState: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
textAlign: "center",
|
||||
padding: "3rem",
|
||||
},
|
||||
};
|
||||
|
||||
const styles = { ...sharedStyles, ...pageStyles };
|
||||
|
||||
export default function AdminAppointmentsPage() {
|
||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||
requiredPermission: Permission.VIEW_ALL_APPOINTMENTS,
|
||||
fallbackRedirect: "/",
|
||||
});
|
||||
|
||||
const [appointments, setAppointments] = useState<AppointmentResponse[]>([]);
|
||||
const [isLoadingAppointments, setIsLoadingAppointments] = useState(true);
|
||||
const [cancellingId, setCancellingId] = useState<number | null>(null);
|
||||
const [confirmCancelId, setConfirmCancelId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
|
||||
const fetchAppointments = useCallback(async () => {
|
||||
try {
|
||||
// Fetch with large per_page to get all appointments for now
|
||||
const data = await api.get<PaginatedAppointments>("/api/admin/appointments?per_page=100");
|
||||
setAppointments(data.records);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch appointments:", err);
|
||||
setError("Failed to load appointments");
|
||||
} finally {
|
||||
setIsLoadingAppointments(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && isAuthorized) {
|
||||
fetchAppointments();
|
||||
}
|
||||
}, [user, isAuthorized, fetchAppointments]);
|
||||
|
||||
const handleCancel = async (appointmentId: number) => {
|
||||
setCancellingId(appointmentId);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await api.post<AppointmentResponse>(`/api/admin/appointments/${appointmentId}/cancel`, {});
|
||||
await fetchAppointments();
|
||||
setConfirmCancelId(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to cancel appointment");
|
||||
} finally {
|
||||
setCancellingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main style={styles.main}>
|
||||
<div style={styles.loader}>Loading...</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredAppointments = appointments.filter((apt) => {
|
||||
if (statusFilter === "all") return true;
|
||||
return apt.status === statusFilter;
|
||||
});
|
||||
|
||||
const bookedCount = appointments.filter((a) => a.status === "booked").length;
|
||||
|
||||
return (
|
||||
<main style={styles.main}>
|
||||
<Header currentPage="admin-appointments" />
|
||||
<div style={styles.content}>
|
||||
<h1 style={styles.pageTitle}>All Appointments</h1>
|
||||
<p style={styles.pageSubtitle}>View and manage all user appointments</p>
|
||||
|
||||
{error && <div style={styles.errorBanner}>{error}</div>}
|
||||
|
||||
{/* Status Filter */}
|
||||
<div style={styles.filterRow}>
|
||||
<span style={styles.filterLabel}>Filter:</span>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
style={styles.filterSelect}
|
||||
>
|
||||
<option value="all">All ({appointments.length})</option>
|
||||
<option value="booked">Booked ({bookedCount})</option>
|
||||
<option value="cancelled_by_user">Cancelled by User</option>
|
||||
<option value="cancelled_by_admin">Cancelled by Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{isLoadingAppointments ? (
|
||||
<div style={styles.emptyState}>Loading appointments...</div>
|
||||
) : appointments.length === 0 ? (
|
||||
<div style={styles.emptyState}>No appointments yet.</div>
|
||||
) : filteredAppointments.length === 0 ? (
|
||||
<div style={styles.emptyState}>No appointments match the filter.</div>
|
||||
) : (
|
||||
<div style={styles.appointmentList}>
|
||||
{filteredAppointments.map((apt) => {
|
||||
const status = getStatusDisplay(apt.status);
|
||||
const isPast = new Date(apt.slot_start) <= new Date();
|
||||
return (
|
||||
<div
|
||||
key={apt.id}
|
||||
style={{
|
||||
...styles.appointmentCard,
|
||||
...(isPast ? styles.appointmentCardPast : {}),
|
||||
}}
|
||||
>
|
||||
<div style={styles.appointmentHeader}>
|
||||
<div>
|
||||
<div style={styles.appointmentTime}>{formatDateTime(apt.slot_start)}</div>
|
||||
<div style={styles.appointmentUser}>{apt.user_email}</div>
|
||||
{apt.note && <div style={styles.appointmentNote}>"{apt.note}"</div>}
|
||||
<span
|
||||
style={{
|
||||
...styles.statusBadge,
|
||||
background: status.bgColor,
|
||||
color: status.textColor,
|
||||
}}
|
||||
>
|
||||
{status.text}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{apt.status === "booked" && (
|
||||
<div style={styles.buttonGroup}>
|
||||
{confirmCancelId === apt.id ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleCancel(apt.id)}
|
||||
disabled={cancellingId === apt.id}
|
||||
style={styles.confirmButton}
|
||||
>
|
||||
{cancellingId === apt.id ? "..." : "Confirm"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmCancelId(null)}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setConfirmCancelId(apt.id)}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue