summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/orders/OrderList.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/orders/OrderList.tsx')
-rw-r--r--makima/frontend/src/components/orders/OrderList.tsx188
1 files changed, 188 insertions, 0 deletions
diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx
new file mode 100644
index 0000000..76ac7a7
--- /dev/null
+++ b/makima/frontend/src/components/orders/OrderList.tsx
@@ -0,0 +1,188 @@
+import { useState, useMemo } from "react";
+import type { Order, OrderStatus, OrderPriority, OrderType } from "../../lib/api";
+
+const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = {
+ open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" },
+ in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" },
+ done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
+ archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
+};
+
+const PRIORITY_COLOR: Record<OrderPriority, string> = {
+ critical: "bg-red-400",
+ high: "bg-orange-400",
+ medium: "bg-yellow-400",
+ low: "bg-[#75aafc]",
+ none: "bg-[#556677]",
+};
+
+const TYPE_BADGE: Record<OrderType, { color: string; label: string }> = {
+ feature: { color: "text-[#75aafc] border-[rgba(117,170,252,0.3)]", label: "FEAT" },
+ bug: { color: "text-red-400 border-red-800", label: "BUG" },
+ spike: { color: "text-yellow-400 border-yellow-800", label: "SPIKE" },
+ chore: { color: "text-[#7788aa] border-[#2a3a5a]", label: "CHORE" },
+ improvement: { color: "text-emerald-400 border-emerald-800", label: "IMPROVE" },
+};
+
+interface OrderListProps {
+ orders: Order[];
+ selectedId: string | null;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+ statusFilter: OrderStatus | undefined;
+ onStatusFilter: (s: OrderStatus | undefined) => void;
+ typeFilter: OrderType | undefined;
+ onTypeFilter: (t: OrderType | undefined) => void;
+}
+
+const STATUS_OPTIONS: (OrderStatus | "all")[] = ["all", "open", "in_progress", "done", "archived"];
+const TYPE_OPTIONS: (OrderType | "all")[] = ["all", "feature", "bug", "spike", "chore", "improvement"];
+
+export function OrderList({
+ orders,
+ selectedId,
+ onSelect,
+ onCreate,
+ statusFilter,
+ onStatusFilter,
+ typeFilter,
+ onTypeFilter,
+}: OrderListProps) {
+ const [search, setSearch] = useState("");
+
+ const filtered = useMemo(() => {
+ if (!search.trim()) return orders;
+ const q = search.toLowerCase();
+ return orders.filter(
+ (o) =>
+ o.title.toLowerCase().includes(q) ||
+ (o.description && o.description.toLowerCase().includes(q)) ||
+ o.labels.some((l) => l.toLowerCase().includes(q)),
+ );
+ }, [orders, search]);
+
+ return (
+ <div className="flex flex-col h-full">
+ {/* Header */}
+ <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Orders
+ </span>
+ <button
+ type="button"
+ onClick={onCreate}
+ className="text-[11px] font-mono text-[#75aafc] hover:text-white bg-transparent border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5 hover:border-[rgba(117,170,252,0.6)] transition-colors"
+ >
+ + New
+ </button>
+ </div>
+
+ {/* Search */}
+ <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.1)]">
+ <input
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ placeholder="Search orders..."
+ className="w-full bg-transparent border-none outline-none text-[11px] font-mono text-white placeholder:text-[#556677]"
+ />
+ </div>
+
+ {/* Filters */}
+ <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.1)] flex flex-col gap-1">
+ <div className="flex items-center gap-1 flex-wrap">
+ <span className="text-[9px] font-mono text-[#556677] uppercase w-[38px] shrink-0">
+ Status
+ </span>
+ {STATUS_OPTIONS.map((s) => (
+ <button
+ key={s}
+ type="button"
+ onClick={() => onStatusFilter(s === "all" ? undefined : s)}
+ className={`text-[9px] font-mono px-1.5 py-0.5 rounded transition-colors ${
+ (s === "all" && !statusFilter) || s === statusFilter
+ ? "text-white bg-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.4)]"
+ : "text-[#556677] hover:text-[#7788aa] border border-transparent"
+ }`}
+ >
+ {s === "all" ? "ALL" : s === "in_progress" ? "WIP" : s.toUpperCase()}
+ </button>
+ ))}
+ </div>
+ <div className="flex items-center gap-1 flex-wrap">
+ <span className="text-[9px] font-mono text-[#556677] uppercase w-[38px] shrink-0">
+ Type
+ </span>
+ {TYPE_OPTIONS.map((t) => (
+ <button
+ key={t}
+ type="button"
+ onClick={() => onTypeFilter(t === "all" ? undefined : t)}
+ className={`text-[9px] font-mono px-1.5 py-0.5 rounded transition-colors ${
+ (t === "all" && !typeFilter) || t === typeFilter
+ ? "text-white bg-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.4)]"
+ : "text-[#556677] hover:text-[#7788aa] border border-transparent"
+ }`}
+ >
+ {t === "all" ? "ALL" : t.toUpperCase()}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ {/* List */}
+ <div className="flex-1 overflow-y-auto">
+ {filtered.length === 0 ? (
+ <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
+ No orders found
+ </div>
+ ) : (
+ filtered.map((o) => {
+ const statusBadge = STATUS_BADGE[o.status] || STATUS_BADGE.open;
+ const typeBadge = TYPE_BADGE[o.orderType] || TYPE_BADGE.feature;
+ const priorityColor = PRIORITY_COLOR[o.priority] || PRIORITY_COLOR.none;
+
+ return (
+ <button
+ key={o.id}
+ type="button"
+ onClick={() => onSelect(o.id)}
+ 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)]" : ""
+ }`}
+ >
+ <div className="flex items-start gap-2 mb-1">
+ {/* Priority dot */}
+ <span
+ className={`w-2 h-2 rounded-full ${priorityColor} shrink-0 mt-[3px]`}
+ title={o.priority}
+ />
+ <span className="text-[12px] font-mono text-white truncate flex-1">
+ {o.title}
+ </span>
+ </div>
+ <div className="flex items-center gap-1.5 pl-4">
+ <span
+ className={`text-[9px] font-mono ${statusBadge.color} border rounded px-1.5 py-0.5`}
+ >
+ {statusBadge.label}
+ </span>
+ <span
+ className={`text-[9px] font-mono ${typeBadge.color} border rounded px-1.5 py-0.5`}
+ >
+ {typeBadge.label}
+ </span>
+ {o.labels.length > 0 && (
+ <span className="text-[9px] font-mono text-[#556677] truncate">
+ {o.labels.slice(0, 2).join(", ")}
+ {o.labels.length > 2 && ` +${o.labels.length - 2}`}
+ </span>
+ )}
+ </div>
+ </button>
+ );
+ })
+ )}
+ </div>
+ </div>
+ );
+}