Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin dashboard for managing orders #627

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading