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







                                 
                      




                                                                                    
                                                                                                       



















                                                                                         
                                                                                                  



                                 
                              


                                                          
                                       





                             
       


                  









                                                                        
                                                             
                                                                













































                                                                                                          










































































                                                                                                                                        


                                                                                                                          






                                                                                                











                                                                                                 






















































































































































































































                                                                                                                                                              





















                                                                                                                                                          
                                   


































































                                                                                                                                                                                      



                    










































































                                                                                                                                                                                      

                                                           

                           

                                                                                                                                                 
             
                                       
                     













                                                                                               
import { useState } from "react";
import type {
  Order,
  OrderStatus,
  OrderPriority,
  OrderType,
  UpdateOrderRequest,
  DirectiveSummary,
  DirectiveOrderGroup,
} 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" },
  under_review: { color: "bg-purple-400/20 text-purple-400 border-purple-800", label: "UNDER REVIEW" },
  done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
  archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};

const PRIORITY_OPTIONS: { value: OrderPriority; color: string; label: string }[] = [
  { value: "critical", color: "text-red-400 border-red-800", label: "Critical" },
  { value: "high", color: "text-orange-400 border-orange-800", label: "High" },
  { value: "medium", color: "text-yellow-400 border-yellow-800", label: "Medium" },
  { value: "low", color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "Low" },
  { value: "none", color: "text-[#556677] border-[#2a3a5a]", label: "None" },
];

const TYPE_OPTIONS: { value: OrderType; color: string; label: string }[] = [
  { value: "feature", color: "text-[#75aafc]", label: "Feature" },
  { value: "bug", color: "text-red-400", label: "Bug" },
  { value: "spike", color: "text-yellow-400", label: "Spike" },
  { value: "chore", color: "text-[#7788aa]", label: "Chore" },
  { value: "improvement", color: "text-emerald-400", label: "Improvement" },
];

const STATUS_OPTIONS: OrderStatus[] = ["open", "in_progress", "under_review", "done", "archived"];

interface OrderDetailProps {
  order: Order;
  directives: DirectiveSummary[];
  dogs: DirectiveOrderGroup[];
  onUpdate: (req: UpdateOrderRequest) => Promise<void>;
  onDelete: () => void;
  onLinkDirective: (directiveId: string) => Promise<void>;
  onConvertToStep: () => Promise<void>;
  onRefresh: () => void;
}

export function OrderDetail({
  order,
  directives,
  dogs,
  onUpdate,
  onDelete,
  onLinkDirective,
  onConvertToStep,
  onRefresh,
}: OrderDetailProps) {
  const [editingTitle, setEditingTitle] = useState(false);
  const [titleText, setTitleText] = useState(order.title);
  const [editingDesc, setEditingDesc] = useState(false);
  const [descText, setDescText] = useState(order.description || "");
  const [editingLabels, setEditingLabels] = useState(false);
  const [labelsText, setLabelsText] = useState(order.labels.join(", "));
  const [showLinkDirective, setShowLinkDirective] = useState(false);
  const [directiveSearch, setDirectiveSearch] = useState("");
  const [showDogSelector, setShowDogSelector] = useState(false);

  const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open;
  const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4];
  const currentType = TYPE_OPTIONS.find((t) => t.value === order.orderType) || TYPE_OPTIONS[0];

  const handleTitleSave = async () => {
    if (titleText.trim() && titleText !== order.title) {
      await onUpdate({ title: titleText.trim() });
    }
    setEditingTitle(false);
  };

  const handleDescSave = async () => {
    const newDesc = descText.trim() || null;
    if (newDesc !== order.description) {
      await onUpdate({ description: newDesc });
    }
    setEditingDesc(false);
  };

  const handleLabelsSave = async () => {
    const newLabels = labelsText
      .split(",")
      .map((l) => l.trim())
      .filter((l) => l.length > 0);
    await onUpdate({ labels: newLabels });
    setEditingLabels(false);
  };

  const handleStatusChange = async (status: OrderStatus) => {
    await onUpdate({ status });
  };

  const handlePriorityChange = async (priority: OrderPriority) => {
    await onUpdate({ priority });
  };

  const handleTypeChange = async (orderType: OrderType) => {
    await onUpdate({ orderType });
  };

  const handleLinkDirective = async (directiveId: string) => {
    await onLinkDirective(directiveId);
    setShowLinkDirective(false);
  };


  return (
    <div className="flex flex-col h-full overflow-y-auto">
      {/* Header */}
      <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
        <div className="flex items-center justify-between mb-2">
          {editingTitle ? (
            <div className="flex-1 flex items-center gap-2 pr-2">
              <input
                value={titleText}
                onChange={(e) => setTitleText(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === "Enter") handleTitleSave();
                  if (e.key === "Escape") setEditingTitle(false);
                }}
                autoFocus
                className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[14px] font-mono text-white"
              />
              <button
                type="button"
                onClick={handleTitleSave}
                className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300"
              >
                [save]
              </button>
              <button
                type="button"
                onClick={() => setEditingTitle(false)}
                className="text-[10px] font-mono text-[#556677] hover:text-white"
              >
                [cancel]
              </button>
            </div>
          ) : (
            <h2
              className="text-[14px] font-mono text-white font-medium truncate pr-2 cursor-pointer hover:text-[#9bc3ff]"
              onClick={() => {
                setTitleText(order.title);
                setEditingTitle(true);
              }}
            >
              {order.title}
            </h2>
          )}
          <div className="flex items-center gap-2 shrink-0">
            <span
              className={`text-[10px] font-mono ${badge.color} border rounded px-2 py-0.5`}
            >
              {badge.label}
            </span>
            <button
              type="button"
              onClick={onRefresh}
              className="text-[10px] font-mono text-[#7788aa] hover:text-white"
              title="Refresh"
            >
              [refresh]
            </button>
          </div>
        </div>

        {/* Type + Priority inline */}
        <div className="flex items-center gap-3 mb-2">
          <span className={`text-[10px] font-mono ${currentType.color}`}>
            {currentType.label}
          </span>
          <span className="text-[10px] font-mono text-[#2a3a5a]">/</span>
          <span className={`text-[10px] font-mono ${currentPriority.color} border rounded px-1.5 py-0.5`}>
            {currentPriority.label}
          </span>
        </div>

        {/* Linked entities */}
        {order.directiveId && (
          <div className="text-[10px] font-mono text-[#556677] mb-1 truncate">
            Directive: <a href={`/directives/${order.directiveId}`} className="text-[#75aafc] hover:text-white underline">
              {order.directiveName || order.directiveId.slice(0, 8) + "..."}
            </a>
          </div>
        )}
        {order.directiveStepId && (
          <div className="text-[10px] font-mono text-[#556677] mb-1 truncate">
            Step: <span className="text-[#7788aa]">{order.directiveStepId.slice(0, 8)}...</span>
          </div>
        )}
        {order.directiveId && (
          <div className="text-[10px] font-mono text-[#556677] mb-1">
            DOG:{" "}
            {order.dogId ? (
              <span className="text-[#75aafc]">
                {dogs.find((d) => d.id === order.dogId)?.name || order.dogId.slice(0, 8) + "..."}
              </span>
            ) : (
              <span className="text-[#445566] italic">None</span>
            )}
          </div>
        )}

        {/* Controls */}
        <div className="flex flex-wrap gap-2 mt-2">
          <button
            type="button"
            onClick={onDelete}
            className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1 ml-auto"
          >
            Delete
          </button>
        </div>
      </div>

      {/* Status selector */}
      <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
        <div className="flex items-center justify-between mb-1.5">
          <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
            Status
          </span>
        </div>
        <div className="flex gap-1.5 flex-wrap">
          {STATUS_OPTIONS.map((s) => {
            const sBadge = STATUS_BADGE[s];
            return (
              <button
                key={s}
                type="button"
                onClick={() => handleStatusChange(s)}
                className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
                  s === order.status
                    ? `${sBadge.color} bg-[rgba(117,170,252,0.1)]`
                    : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
                }`}
              >
                {sBadge.label}
              </button>
            );
          })}
        </div>
      </div>

      {/* Priority selector */}
      <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
        <div className="flex items-center justify-between mb-1.5">
          <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
            Priority
          </span>
        </div>
        <div className="flex gap-1.5 flex-wrap">
          {PRIORITY_OPTIONS.map((p) => (
            <button
              key={p.value}
              type="button"
              onClick={() => handlePriorityChange(p.value)}
              className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
                p.value === order.priority
                  ? `${p.color} bg-[rgba(117,170,252,0.1)]`
                  : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
              }`}
            >
              {p.label}
            </button>
          ))}
        </div>
      </div>

      {/* Type selector */}
      <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
        <div className="flex items-center justify-between mb-1.5">
          <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
            Type
          </span>
        </div>
        <div className="flex gap-1.5 flex-wrap">
          {TYPE_OPTIONS.map((t) => (
            <button
              key={t.value}
              type="button"
              onClick={() => handleTypeChange(t.value)}
              className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
                t.value === order.orderType
                  ? `${t.color} border-current bg-[rgba(117,170,252,0.1)]`
                  : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
              }`}
            >
              {t.label}
            </button>
          ))}
        </div>
      </div>

      {/* Description */}
      <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
        <div className="flex items-center justify-between mb-1">
          <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
            Description
          </span>
          {!editingDesc && (
            <button
              type="button"
              onClick={() => {
                setDescText(order.description || "");
                setEditingDesc(true);
              }}
              className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
            >
              [edit]
            </button>
          )}
        </div>
        {editingDesc ? (
          <div className="flex flex-col gap-1.5">
            <textarea
              value={descText}
              onChange={(e) => setDescText(e.target.value)}
              className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white resize-y min-h-[80px]"
              rows={4}
              autoFocus
            />
            <div className="flex gap-1.5">
              <button
                type="button"
                onClick={handleDescSave}
                className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
              >
                Save
              </button>
              <button
                type="button"
                onClick={() => setEditingDesc(false)}
                className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
              >
                Cancel
              </button>
            </div>
          </div>
        ) : (
          <p className="text-[11px] font-mono text-[#c0d0e0] whitespace-pre-wrap">
            {order.description || <span className="text-[#556677] italic">No description</span>}
          </p>
        )}
      </div>

      {/* Labels */}
      <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
        <div className="flex items-center justify-between mb-1">
          <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
            Labels
          </span>
          {!editingLabels && (
            <button
              type="button"
              onClick={() => {
                setLabelsText(order.labels.join(", "));
                setEditingLabels(true);
              }}
              className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
            >
              [edit]
            </button>
          )}
        </div>
        {editingLabels ? (
          <div className="flex flex-col gap-1.5">
            <input
              value={labelsText}
              onChange={(e) => setLabelsText(e.target.value)}
              placeholder="label1, label2, ..."
              className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white"
              autoFocus
            />
            <div className="flex gap-1.5">
              <button
                type="button"
                onClick={handleLabelsSave}
                className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
              >
                Save
              </button>
              <button
                type="button"
                onClick={() => setEditingLabels(false)}
                className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
              >
                Cancel
              </button>
            </div>
          </div>
        ) : (
          <div className="flex gap-1 flex-wrap">
            {order.labels.length > 0 ? (
              order.labels.map((l) => (
                <span
                  key={l}
                  className="text-[10px] font-mono text-[#9bc3ff] bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5"
                >
                  {l}
                </span>
              ))
            ) : (
              <span className="text-[10px] font-mono text-[#556677] italic">No labels</span>
            )}
          </div>
        )}
      </div>

      {/* Actions */}
      <div className="px-4 py-3 flex-1">
        <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
          Actions
        </span>

        <div className="flex flex-col gap-2">
          {/* Link to Directive */}
          <div>
            <div className="flex items-center gap-1.5">
              <button
                type="button"
                onClick={() => {
                  setShowLinkDirective(!showLinkDirective);
                  setDirectiveSearch("");
                }}
                className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 flex-1 text-left"
              >
                {order.directiveId ? "Change Directive" : "Link to Directive"}
              </button>
              {order.directiveId && (
                <button
                  type="button"
                  onClick={() => onUpdate({ directiveId: null, directiveStepId: null })}
                  className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1"
                  title="Unlink directive"
                >
                  Unlink
                </button>
              )}
            </div>
            {showLinkDirective && (
              <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] rounded">
                <div className="px-2 py-1.5 border-b border-[rgba(117,170,252,0.1)]">
                  <input
                    type="text"
                    value={directiveSearch}
                    onChange={(e) => setDirectiveSearch(e.target.value)}
                    placeholder="Search directives..."
                    autoFocus
                    className="w-full bg-transparent border-none outline-none text-[10px] font-mono text-[#75aafc] placeholder-[#556677]"
                  />
                </div>
                <div className="max-h-32 overflow-y-auto">
                  {directives.length === 0 ? (
                    <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
                      No directives available
                    </div>
                  ) : (
                    (() => {
                      const filtered = directives.filter((d) =>
                        d.title.toLowerCase().includes(directiveSearch.toLowerCase())
                      );
                      if (filtered.length === 0) {
                        return (
                          <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
                            No matching directives
                          </div>
                        );
                      }
                      return filtered.map((d) => {
                        const isLinked = d.id === order.directiveId;
                        const statusColors: Record<string, string> = {
                          draft: "text-[#556677] border-[#2a3a5a]",
                          active: "text-emerald-400 border-emerald-800",
                          idle: "text-[#7788aa] border-[#2a3a5a]",
                          paused: "text-yellow-400 border-yellow-800",
                          archived: "text-[#556677] border-[#2a3a5a]",
                        };
                        const sColor = statusColors[d.status] || statusColors.draft;
                        return (
                          <button
                            key={d.id}
                            type="button"
                            onClick={() => handleLinkDirective(d.id)}
                            className={`w-full text-left px-3 py-1.5 text-[10px] font-mono hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0 ${
                              isLinked ? "bg-[rgba(117,170,252,0.08)] text-white" : "text-[#9bc3ff]"
                            }`}
                          >
                            <div className="flex items-center gap-1.5">
                              <span className={`shrink-0 text-[8px] font-mono ${sColor} border rounded px-1 py-0.5 uppercase`}>
                                {d.status}
                              </span>
                              <span className="truncate">{d.title}</span>
                              {isLinked && (
                                <span className="shrink-0 text-[8px] text-emerald-400">●</span>
                              )}
                            </div>
                            {d.repositoryUrl && (
                              <div className="text-[8px] text-[#556677] truncate mt-0.5">
                                {d.repositoryUrl}
                              </div>
                            )}
                          </button>
                        );
                      });
                    })()
                  )}
                </div>
              </div>
            )}
          </div>

          {/* Assign to DOG */}
          {order.directiveId && (
            <div>
              <div className="flex items-center gap-1.5">
                <button
                  type="button"
                  onClick={() => setShowDogSelector(!showDogSelector)}
                  className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 flex-1 text-left"
                >
                  {order.dogId ? "Change DOG" : "Assign to DOG"}
                </button>
                {order.dogId && (
                  <button
                    type="button"
                    onClick={() => onUpdate({ dogId: null })}
                    className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1"
                    title="Remove DOG assignment"
                  >
                    Unlink
                  </button>
                )}
              </div>
              {showDogSelector && (
                <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] rounded">
                  <div className="max-h-32 overflow-y-auto">
                    {dogs.length === 0 ? (
                      <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
                        No DOGs available for this directive
                      </div>
                    ) : (
                      dogs.map((d) => {
                        const isAssigned = d.id === order.dogId;
                        const statusColors: Record<string, string> = {
                          open: "text-[#75aafc] border-[rgba(117,170,252,0.4)]",
                          in_progress: "text-yellow-400 border-yellow-800",
                          done: "text-emerald-400 border-emerald-800",
                          archived: "text-[#556677] border-[#2a3a5a]",
                        };
                        const sColor = statusColors[d.status] || statusColors.open;
                        return (
                          <button
                            key={d.id}
                            type="button"
                            onClick={async () => {
                              await onUpdate({ dogId: d.id });
                              setShowDogSelector(false);
                            }}
                            className={`w-full text-left px-3 py-1.5 text-[10px] font-mono hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0 ${
                              isAssigned ? "bg-[rgba(117,170,252,0.08)] text-white" : "text-[#9bc3ff]"
                            }`}
                          >
                            <div className="flex items-center gap-1.5">
                              <span className={`shrink-0 text-[8px] font-mono ${sColor} border rounded px-1 py-0.5 uppercase`}>
                                {d.status}
                              </span>
                              <span className="truncate">{d.name}</span>
                              {isAssigned && (
                                <span className="shrink-0 text-[8px] text-emerald-400">current</span>
                              )}
                            </div>
                            {d.description && (
                              <div className="text-[8px] text-[#556677] truncate mt-0.5">
                                {d.description}
                              </div>
                            )}
                          </button>
                        );
                      })
                    )}
                  </div>
                </div>
              )}
            </div>
          )}

          {/* Convert to Directive Step */}
          {!order.directiveStepId && order.directiveId && (
            <button
              type="button"
              onClick={() => onConvertToStep()}
              className="text-[10px] font-mono text-yellow-400 hover:text-yellow-300 border border-yellow-800 rounded px-2 py-1 w-full text-left"
            >
              Convert to Directive Step
            </button>
          )}
        </div>
      </div>

      {/* Metadata */}
      <div className="px-4 py-2 border-t border-[rgba(117,170,252,0.1)]">
        <div className="flex items-center justify-between text-[9px] font-mono text-[#556677]">
          <span>Created {new Date(order.createdAt).toLocaleDateString()}</span>
          <span>Updated {new Date(order.updatedAt).toLocaleDateString()}</span>
        </div>
      </div>
    </div>
  );
}