Skip to content

Commit

Permalink
Admin dashboard for managing orders (#627)
Browse files Browse the repository at this point in the history
  • Loading branch information
Winston-Hsiao authored Nov 19, 2024
1 parent f6fc6ec commit 14cea28
Show file tree
Hide file tree
Showing 16 changed files with 704 additions and 52 deletions.
6 changes: 6 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Navbar from "@/components/nav/Navbar";
import APIKeys from "@/components/pages/APIKeys";
import About from "@/components/pages/About";
import Account from "@/components/pages/Account";
import AdminDashboard from "@/components/pages/AdminDashboard";
import Browse from "@/components/pages/Browse";
import CreateSell from "@/components/pages/CreateSell";
import CreateShare from "@/components/pages/CreateShare";
Expand Down Expand Up @@ -163,6 +164,11 @@ const App = () => {
element={<LinkRobot />}
/>

<Route
path={ROUTES.ADMIN.path}
element={<AdminDashboard />}
/>

{/* Not found */}
<Route
path={ROUTES.NOT_FOUND.path}
Expand Down
104 changes: 104 additions & 0 deletions frontend/src/components/admin/AdminManageOrder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useState } from "react";

import AdminProcessPreorderModal from "@/components/modals/AdminProcessPreorderModal";
import AdminUpdateStatusModal from "@/components/modals/AdminUpdateStatusModal";
import CancelOrderModal from "@/components/modals/CancelOrderModal";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { OrderWithProduct } from "@/lib/types/orders";
import { canModifyOrder } from "@/lib/utils/orders";

interface AdminManageOrderProps {
order: OrderWithProduct;
onOrderUpdate?: (updatedOrder: OrderWithProduct) => void;
}

const AdminManageOrder: React.FC<AdminManageOrderProps> = ({
order,
onOrderUpdate,
}) => {
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
const [isPreorderModalOpen, setIsPreorderModalOpen] = useState(false);
const [isCancelOrderModalOpen, setIsCancelOrderModalOpen] = useState(false);

const handleOrderUpdate = (updatedOrder: OrderWithProduct) => {
if (onOrderUpdate) {
onOrderUpdate(updatedOrder);
}
};

const showPreorderOption =
order.order.preorder_deposit_amount &&
order.order.status !== "awaiting_final_payment" &&
order.order.status !== "cancelled" &&
order.order.status !== "refunded";

return (
<div className="mb-4 p-2 bg-primary/10 rounded-md">
<div className="flex justify-between items-center">
<span className="text-primary font-semibold">Admin Controls</span>
<DropdownMenu>
<DropdownMenuTrigger className="text-primary underline hover:text-primary/80 hover:underline-offset-2">
Manage Order
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
disabled={!canModifyOrder(order)}
onSelect={() => setIsStatusModalOpen(true)}
className="cursor-pointer"
>
Change order status
</DropdownMenuItem>
{showPreorderOption && (
<>
<div className="border-t border-gray-200 my-1"></div>
<DropdownMenuItem
disabled={!canModifyOrder(order)}
onSelect={() => setIsPreorderModalOpen(true)}
className="cursor-pointer"
>
Process pre-order
</DropdownMenuItem>
</>
)}
<div className="border-t border-gray-200 my-1"></div>
<DropdownMenuItem
disabled={!canModifyOrder(order)}
onSelect={() => setIsCancelOrderModalOpen(true)}
className="cursor-pointer"
>
Cancel order
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

<AdminUpdateStatusModal
isOpen={isStatusModalOpen}
onOpenChange={setIsStatusModalOpen}
order={order}
onOrderUpdate={handleOrderUpdate}
/>

<AdminProcessPreorderModal
isOpen={isPreorderModalOpen}
onOpenChange={setIsPreorderModalOpen}
order={order}
onOrderUpdate={handleOrderUpdate}
/>

<CancelOrderModal
isOpen={isCancelOrderModalOpen}
onOpenChange={setIsCancelOrderModalOpen}
order={order}
onOrderUpdate={handleOrderUpdate}
/>
</div>
);
};

export default AdminManageOrder;
106 changes: 106 additions & 0 deletions frontend/src/components/modals/AdminProcessPreorderModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState } from "react";

import Modal from "@/components/ui/Modal";
import { Button } from "@/components/ui/button";
import { useAlertQueue } from "@/hooks/useAlertQueue";
import { useAuthentication } from "@/hooks/useAuth";
import type { OrderWithProduct } from "@/lib/types/orders";
import { formatPrice } from "@/lib/utils/formatNumber";

interface AdminProcessPreorderModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
order: OrderWithProduct;
onOrderUpdate: (updatedOrder: OrderWithProduct) => void;
}

const AdminProcessPreorderModal: React.FC<AdminProcessPreorderModalProps> = ({
isOpen,
onOpenChange,
order,
onOrderUpdate,
}) => {
const { api } = useAuthentication();
const { addAlert, addErrorAlert } = useAlertQueue();
const [isProcessing, setIsProcessing] = useState(false);

const handleProcessPreorder = async () => {
setIsProcessing(true);
try {
const { data, error } = await api.client.POST(
"/stripe/process/preorder/{order_id}",
{
params: {
path: { order_id: order.order.id },
},
},
);

if (error) {
throw error;
}

onOrderUpdate({
...order,
order: {
...order.order,
status: "awaiting_final_payment",
final_payment_checkout_session_id: data.checkout_session.id,
},
});

addAlert("Pre-order processed successfully", "success");
onOpenChange(false);
} catch (error) {
console.error(error);
addErrorAlert("Failed to process pre-order");
} finally {
setIsProcessing(false);
}
};

return (
<Modal isOpen={isOpen} onClose={() => onOpenChange(false)} size="xl">
<div className="p-6 text-gray-1">
<h2 className="text-xl font-semibold mb-4">Process Pre-order</h2>

<div className="space-y-4">
<p className="text-gray-6">
Are you sure you want to process this pre-order? This will:
</p>
<ul className="list-disc list-inside text-sm text-gray-8 space-y-1">
<li>Mark the order as ready for final payment</li>
<li>
Request customer to pay remaining balance of{" "}
{formatPrice(
order.order.price_amount -
(order.order.preorder_deposit_amount || 0),
)}
</li>
<li>Notify the customer via email</li>
</ul>

<div className="flex justify-end gap-2">
<Button
type="button"
variant="default"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button
type="submit"
variant="outline"
onClick={handleProcessPreorder}
disabled={isProcessing}
>
{isProcessing ? "Processing..." : "Process Pre-order"}
</Button>
</div>
</div>
</div>
</Modal>
);
};

export default AdminProcessPreorderModal;
109 changes: 109 additions & 0 deletions frontend/src/components/modals/AdminUpdateStatusModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useState } from "react";

import Modal from "@/components/ui/Modal";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useAlertQueue } from "@/hooks/useAlertQueue";
import { useAuthentication } from "@/hooks/useAuth";
import type { OrderWithProduct } from "@/lib/types/orders";
import { OrderStatus, orderStatuses } from "@/lib/types/orders";
import { normalizeStatus } from "@/lib/utils/formatString";

interface AdminUpdateStatusModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
order: OrderWithProduct;
onOrderUpdate: (updatedOrder: OrderWithProduct) => void;
}

const AdminUpdateStatusModal: React.FC<AdminUpdateStatusModalProps> = ({
isOpen,
onOpenChange,
order,
onOrderUpdate,
}) => {
const { api } = useAuthentication();
const { addAlert, addErrorAlert } = useAlertQueue();
const [selectedStatus, setSelectedStatus] = useState(order.order.status);
const [isUpdating, setIsUpdating] = useState(false);

const handleUpdateStatus = async () => {
setIsUpdating(true);
try {
const { data, error } = await api.client.PUT(
"/orders/admin/status/{order_id}",
{
params: {
path: { order_id: order.order.id },
},
body: {
status: selectedStatus,
},
},
);

if (error) {
throw error;
}

onOrderUpdate({
order: data,
product: order.product,
});

addAlert("Order status updated successfully", "success");
onOpenChange(false);
} catch (error) {
console.error(error);
addErrorAlert("Failed to update order status");
} finally {
setIsUpdating(false);
}
};

return (
<Modal isOpen={isOpen} onClose={() => onOpenChange(false)}>
<div className="p-6 text-gray-1">
<h2 className="text-xl font-semibold mb-4">Update Order Status</h2>

<div className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="status">Select New Status</Label>
<select
id="status"
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value as OrderStatus)}
className="bg-gray-2 border-gray-3 text-gray-12 rounded-md p-2"
>
{orderStatuses.map((status) => (
<option key={status} value={status}>
{normalizeStatus(status)}
</option>
))}
</select>
</div>

<div className="flex justify-end gap-2">
<Button
type="button"
variant="default"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button
type="submit"
variant="outline"
onClick={handleUpdateStatus}
disabled={isUpdating || selectedStatus === order.order.status}
>
{isUpdating ? "Updating..." : "Update Status"}
</Button>
</div>
</div>
</div>
</Modal>
);
};

export default AdminUpdateStatusModal;
10 changes: 8 additions & 2 deletions frontend/src/components/nav/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FaRobot, FaTerminal } from "react-icons/fa";
import { FaRegFileLines } from "react-icons/fa6";
import { FaChartBar, FaRegFileLines } from "react-icons/fa6";

import ROUTES from "@/lib/types/routes";

Expand Down Expand Up @@ -34,6 +34,12 @@ const TERMINAL_NAV_ITEM: BaseNavItem = {
icon: <FaTerminal />,
};

const ADMIN_DASHBOARD_NAV_ITEM: BaseNavItem = {
name: "Admin Dashboard",
path: ROUTES.ADMIN.path,
icon: <FaChartBar />,
};

export const AUTHENTICATED_NAV_ITEMS: BaseNavItem[] = [];

export const getNavItems = (
Expand All @@ -43,7 +49,7 @@ export const getNavItems = (
let navItems = [...DEFAULT_NAV_ITEMS];

if (isAdmin) {
navItems = [TERMINAL_NAV_ITEM, ...navItems];
navItems = [ADMIN_DASHBOARD_NAV_ITEM, TERMINAL_NAV_ITEM, ...navItems];
}

if (isAuthenticated) {
Expand Down
Loading

0 comments on commit 14cea28

Please sign in to comment.