diff options
Diffstat (limited to 'makima/frontend/src/components/orders')
| -rw-r--r-- | makima/frontend/src/components/orders/OrderContextMenu.tsx | 184 | ||||
| -rw-r--r-- | makima/frontend/src/components/orders/OrderList.tsx | 34 |
2 files changed, 218 insertions, 0 deletions
diff --git a/makima/frontend/src/components/orders/OrderContextMenu.tsx b/makima/frontend/src/components/orders/OrderContextMenu.tsx new file mode 100644 index 0000000..f73ca4f --- /dev/null +++ b/makima/frontend/src/components/orders/OrderContextMenu.tsx @@ -0,0 +1,184 @@ +import { useEffect, useRef } from "react"; +import type { Order, OrderStatus } from "../../lib/api"; + +interface OrderContextMenuProps { + x: number; + y: number; + order: Order; + onClose: () => void; + onChangeStatus: (status: OrderStatus) => void; + onDelete: () => void; + onGoToDirective: () => void; +} + +export function OrderContextMenu({ + x, + y, + order, + onClose, + onChangeStatus, + onDelete, + onGoToDirective, +}: OrderContextMenuProps) { + const menuRef = useRef<HTMLDivElement>(null); + + // Close on click outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + onClose(); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [onClose]); + + // Adjust position if menu would overflow viewport + useEffect(() => { + if (menuRef.current) { + const rect = menuRef.current.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + if (rect.right > viewportWidth) { + menuRef.current.style.left = `${x - rect.width}px`; + } + if (rect.bottom > viewportHeight) { + menuRef.current.style.top = `${y - rect.height}px`; + } + } + }, [x, y]); + + const menuItemClass = + "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2"; + const dividerClass = "border-t border-[rgba(117,170,252,0.2)] my-1"; + + const showOpen = order.status !== "open"; + const showInProgress = order.status !== "in_progress"; + const showUnderReview = order.status !== "under_review"; + const showDone = order.status !== "done"; + const showArchive = order.status !== "archived"; + const showGoToDirective = !!order.directiveId; + + return ( + <div + ref={menuRef} + className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]" + style={{ left: x, top: y }} + > + {/* Header showing order title */} + <div className="px-3 py-1.5 text-[10px] font-mono text-[#555] uppercase border-b border-[rgba(117,170,252,0.2)] truncate max-w-[200px]"> + {order.title} + </div> + + {/* Status actions */} + {showOpen && ( + <button + className={menuItemClass} + onClick={() => { + onChangeStatus("open"); + onClose(); + }} + > + <span className="text-[#75aafc]">○</span> + Mark as Open + </button> + )} + + {showInProgress && ( + <button + className={menuItemClass} + onClick={() => { + onChangeStatus("in_progress"); + onClose(); + }} + > + <span className="text-[#75aafc]">●</span> + Mark as In Progress + </button> + )} + + {showUnderReview && ( + <button + className={menuItemClass} + onClick={() => { + onChangeStatus("under_review"); + onClose(); + }} + > + <span className="text-[#75aafc]">◉</span> + Mark as Under Review + </button> + )} + + {showDone && ( + <button + className={menuItemClass} + onClick={() => { + onChangeStatus("done"); + onClose(); + }} + > + <span className="text-[#75aafc]">✓</span> + Mark as Done + </button> + )} + + {showArchive && ( + <button + className={menuItemClass} + onClick={() => { + onChangeStatus("archived"); + onClose(); + }} + > + <span className="text-[#75aafc]">▣</span> + Archive + </button> + )} + + {/* Directive link */} + {showGoToDirective && ( + <> + <div className={dividerClass} /> + <button + className={menuItemClass} + onClick={() => { + onGoToDirective(); + onClose(); + }} + > + <span className="text-[#75aafc]">▶</span> + Go to Directive + </button> + </> + )} + + <div className={dividerClass} /> + + {/* Delete action */} + <button + className={`${menuItemClass} text-red-400 hover:bg-red-400/10`} + onClick={() => { + onDelete(); + onClose(); + }} + > + <span className="text-red-400">✕</span> + Delete + </button> + </div> + ); +} diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx index 0ebd18d..ec3dcf6 100644 --- a/makima/frontend/src/components/orders/OrderList.tsx +++ b/makima/frontend/src/components/orders/OrderList.tsx @@ -1,5 +1,6 @@ import { useState, useMemo } from "react"; import type { Order, OrderStatus, OrderPriority, OrderType } from "../../lib/api"; +import { OrderContextMenu } from "./OrderContextMenu"; const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = { open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" }, @@ -34,6 +35,9 @@ interface OrderListProps { onStatusFilter: (s: OrderStatus | undefined) => void; typeFilter: OrderType | undefined; onTypeFilter: (t: OrderType | undefined) => void; + onChangeStatus?: (order: Order, status: OrderStatus) => void; + onDelete?: (order: Order) => void; + onGoToDirective?: (order: Order) => void; } const STATUS_OPTIONS: (OrderStatus | "all")[] = ["all", "open", "in_progress", "under_review", "done", "archived"]; @@ -48,8 +52,24 @@ export function OrderList({ onStatusFilter, typeFilter, onTypeFilter, + onChangeStatus, + onDelete, + onGoToDirective, }: OrderListProps) { const [search, setSearch] = useState(""); + const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null); + const [contextMenuOrder, setContextMenuOrder] = useState<Order | null>(null); + + const handleContextMenu = (e: React.MouseEvent, order: Order) => { + e.preventDefault(); + setContextMenuPosition({ x: e.clientX, y: e.clientY }); + setContextMenuOrder(order); + }; + + const closeContextMenu = () => { + setContextMenuPosition(null); + setContextMenuOrder(null); + }; const filtered = useMemo(() => { if (!search.trim()) return orders; @@ -148,6 +168,7 @@ export function OrderList({ key={o.id} type="button" onClick={() => onSelect(o.id)} + onContextMenu={(e) => handleContextMenu(e, o)} className={`w-full text-left px-3 py-2.5 border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] transition-colors ${ selectedId === o.id ? "bg-[rgba(117,170,252,0.1)]" : "" }`} @@ -190,6 +211,19 @@ export function OrderList({ }) )} </div> + + {/* Context Menu */} + {contextMenuPosition && contextMenuOrder && ( + <OrderContextMenu + x={contextMenuPosition.x} + y={contextMenuPosition.y} + order={contextMenuOrder} + onClose={closeContextMenu} + onChangeStatus={(status) => onChangeStatus?.(contextMenuOrder, status)} + onDelete={() => onDelete?.(contextMenuOrder)} + onGoToDirective={() => onGoToDirective?.(contextMenuOrder)} + /> + )} </div> ); } |
