-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Admin dashboard for managing orders (#627)
- Loading branch information
1 parent
f6fc6ec
commit 14cea28
Showing
16 changed files
with
704 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
106
frontend/src/components/modals/AdminProcessPreorderModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
109
frontend/src/components/modals/AdminUpdateStatusModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.