summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/orders/OrderList.tsx
blob: ec3dcf61dc27c587da6f6917ebe8c9c3d87d0c0f (plain) (tree)
1
2
3
4
5
6
7
8

                                                                                  
                                                      



                                                                                    
                                                                                




























                                                                                     


                                                               

 
                                                                                                                   










                                                                                                       


                  

                                           












                                                                                                        







                                                                     

                                                                       













































                                                                                                                                                                                                           
                                                                                                                     








































                                                                                                  
                                                              











                                                                                                                                                       




                                                                                           
























                                                                                                        












                                                                                 


          
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" },
  in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" },
  under_review: { color: "text-purple-400 border-purple-800", label: "REVIEW" },
  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;
  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"];
const TYPE_OPTIONS: (OrderType | "all")[] = ["all", "feature", "bug", "spike", "chore", "improvement"];

export function OrderList({
  orders,
  selectedId,
  onSelect,
  onCreate,
  statusFilter,
  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;
    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)) ||
        (o.directiveName && o.directiveName.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 === "under_review" ? "REVIEW" : 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)}
                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)]" : ""
                }`}
              >
                <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}
                    {o.directiveName && (
                      <span className="text-[9px] font-mono text-[#556677] truncate block">
                        {o.directiveName}
                      </span>
                    )}
                  </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>

      {/* 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>
  );
}