summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/directives/DOGList.tsx
blob: de59d7d39cc53d7c14f1777214cb31814af9a62c (plain) (tree)




























































































































































































































































































































































































                                                                                                                                                                            
import { useState } from "react";
import type {
  DirectiveOrderGroup,
  DOGStatus,
  CreateDOGRequest,
} from "../../lib/api";

const DOG_STATUS_BADGE: Record<DOGStatus, { 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 DOG_STATUS_OPTIONS: DOGStatus[] = ["open", "in_progress", "done", "archived"];

interface DOGListProps {
  dogs: DirectiveOrderGroup[];
  loading: boolean;
  onCreateDog: (req: CreateDOGRequest) => Promise<DirectiveOrderGroup | null>;
  onUpdateDog: (dogId: string, req: { name?: string; description?: string | null; status?: DOGStatus }) => Promise<void>;
  onDeleteDog: (dogId: string) => Promise<void>;
  onPickUpOrders: (dogId: string) => Promise<any>;
}

export function DOGList({
  dogs,
  loading,
  onCreateDog,
  onUpdateDog,
  onDeleteDog,
  onPickUpOrders,
}: DOGListProps) {
  const [showCreate, setShowCreate] = useState(false);
  const [newName, setNewName] = useState("");
  const [newDesc, setNewDesc] = useState("");
  const [creating, setCreating] = useState(false);

  const handleCreate = async () => {
    if (!newName.trim()) return;
    setCreating(true);
    try {
      await onCreateDog({
        name: newName.trim(),
        description: newDesc.trim() || null,
      });
      setNewName("");
      setNewDesc("");
      setShowCreate(false);
    } catch (e) {
      console.error("Failed to create DOG:", e);
    } finally {
      setCreating(false);
    }
  };

  if (loading) {
    return (
      <div className="flex items-center justify-center py-8">
        <span className="text-[10px] font-mono text-[#556677]">Loading DOGs...</span>
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-3">
      {/* Header + Create button */}
      <div className="flex items-center justify-between">
        <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
          Order Groups ({dogs.length})
        </span>
        <button
          type="button"
          onClick={() => setShowCreate(!showCreate)}
          className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5"
        >
          {showCreate ? "Cancel" : "+ New DOG"}
        </button>
      </div>

      {/* Create form */}
      {showCreate && (
        <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] rounded p-3 flex flex-col gap-2">
          <div>
            <label className="text-[9px] font-mono text-[#7788aa] uppercase tracking-wide block mb-1">
              Name *
            </label>
            <input
              value={newName}
              onChange={(e) => setNewName(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter" && newName.trim()) handleCreate();
                if (e.key === "Escape") setShowCreate(false);
              }}
              placeholder="Group name..."
              autoFocus
              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 placeholder:text-[#445566]"
            />
          </div>
          <div>
            <label className="text-[9px] font-mono text-[#7788aa] uppercase tracking-wide block mb-1">
              Description
            </label>
            <textarea
              value={newDesc}
              onChange={(e) => setNewDesc(e.target.value)}
              placeholder="Optional description..."
              rows={2}
              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 placeholder:text-[#445566]"
            />
          </div>
          <div className="flex gap-1.5">
            <button
              type="button"
              onClick={handleCreate}
              disabled={!newName.trim() || creating}
              className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5 disabled:opacity-50"
            >
              {creating ? "Creating..." : "Create"}
            </button>
            <button
              type="button"
              onClick={() => setShowCreate(false)}
              className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
            >
              Cancel
            </button>
          </div>
        </div>
      )}

      {/* DOG list */}
      {dogs.length === 0 ? (
        <div className="flex items-center justify-center py-6">
          <span className="text-[10px] font-mono text-[#556677] italic">
            No order groups yet
          </span>
        </div>
      ) : (
        <div className="flex flex-col gap-2">
          {dogs.map((dog) => (
            <DOGCard
              key={dog.id}
              dog={dog}
              onUpdate={onUpdateDog}
              onDelete={onDeleteDog}
              onPickUpOrders={onPickUpOrders}
            />
          ))}
        </div>
      )}
    </div>
  );
}

function DOGCard({
  dog,
  onUpdate,
  onDelete,
  onPickUpOrders,
}: {
  dog: DirectiveOrderGroup;
  onUpdate: (dogId: string, req: { name?: string; description?: string | null; status?: DOGStatus }) => Promise<void>;
  onDelete: (dogId: string) => Promise<void>;
  onPickUpOrders: (dogId: string) => Promise<any>;
}) {
  const [editingName, setEditingName] = useState(false);
  const [nameText, setNameText] = useState(dog.name);
  const [editingDesc, setEditingDesc] = useState(false);
  const [descText, setDescText] = useState(dog.description || "");
  const [pickingUp, setPickingUp] = useState(false);
  const [pickUpResult, setPickUpResult] = useState<string | null>(null);
  const [deleting, setDeleting] = useState(false);

  const badge = DOG_STATUS_BADGE[dog.status] || DOG_STATUS_BADGE.open;

  const handleNameSave = async () => {
    if (nameText.trim() && nameText !== dog.name) {
      await onUpdate(dog.id, { name: nameText.trim() });
    }
    setEditingName(false);
  };

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

  const handleStatusChange = async (status: DOGStatus) => {
    await onUpdate(dog.id, { status });
  };

  const handlePickUpOrders = async () => {
    setPickingUp(true);
    setPickUpResult(null);
    try {
      const result = await onPickUpOrders(dog.id);
      if (result) {
        setPickUpResult(result.message);
        setTimeout(() => setPickUpResult(null), 5000);
      }
    } catch (e) {
      setPickUpResult(e instanceof Error ? e.message : "Failed to plan orders");
      setTimeout(() => setPickUpResult(null), 5000);
    } finally {
      setPickingUp(false);
    }
  };

  const handleDelete = async () => {
    if (!window.confirm(`Delete DOG "${dog.name}"?`)) return;
    setDeleting(true);
    try {
      await onDelete(dog.id);
    } catch (e) {
      console.error("Failed to delete DOG:", e);
      setDeleting(false);
    }
  };

  return (
    <div className="border border-[rgba(117,170,252,0.15)] bg-[#0a1525] rounded">
      {/* Name + Status */}
      <div className="px-3 py-2 border-b border-[rgba(117,170,252,0.08)]">
        <div className="flex items-center justify-between mb-1.5">
          {editingName ? (
            <div className="flex-1 flex items-center gap-2 pr-2">
              <input
                value={nameText}
                onChange={(e) => setNameText(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === "Enter") handleNameSave();
                  if (e.key === "Escape") setEditingName(false);
                }}
                autoFocus
                className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-0.5 text-[12px] font-mono text-white"
              />
              <button
                type="button"
                onClick={handleNameSave}
                className="text-[9px] font-mono text-emerald-400 hover:text-emerald-300"
              >
                [save]
              </button>
              <button
                type="button"
                onClick={() => setEditingName(false)}
                className="text-[9px] font-mono text-[#556677] hover:text-white"
              >
                [cancel]
              </button>
            </div>
          ) : (
            <span
              className="text-[12px] font-mono text-white font-medium cursor-pointer hover:text-[#9bc3ff] truncate pr-2"
              onClick={() => {
                setNameText(dog.name);
                setEditingName(true);
              }}
            >
              {dog.name}
            </span>
          )}
          <span className={`text-[9px] font-mono ${badge.color} border rounded px-1.5 py-0.5 shrink-0`}>
            {badge.label}
          </span>
        </div>

        {/* Status selector */}
        <div className="flex gap-1 flex-wrap">
          {DOG_STATUS_OPTIONS.map((s) => {
            const sBadge = DOG_STATUS_BADGE[s];
            return (
              <button
                key={s}
                type="button"
                onClick={() => handleStatusChange(s)}
                className={`text-[9px] font-mono border rounded px-1.5 py-0.5 transition-colors ${
                  s === dog.status
                    ? `${sBadge.color} bg-[rgba(117,170,252,0.1)]`
                    : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
                }`}
              >
                {sBadge.label}
              </button>
            );
          })}
        </div>
      </div>

      {/* Description */}
      <div className="px-3 py-2 border-b border-[rgba(117,170,252,0.08)]">
        <div className="flex items-center justify-between mb-1">
          <span className="text-[9px] font-mono text-[#7788aa] uppercase tracking-wide">
            Description
          </span>
          {!editingDesc && (
            <button
              type="button"
              onClick={() => {
                setDescText(dog.description || "");
                setEditingDesc(true);
              }}
              className="text-[8px] font-mono text-[#556677] hover:text-[#75aafc]"
            >
              [edit]
            </button>
          )}
        </div>
        {editingDesc ? (
          <div className="flex flex-col gap-1">
            <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 text-[10px] font-mono text-white resize-y min-h-[40px]"
              rows={2}
              autoFocus
            />
            <div className="flex gap-1">
              <button
                type="button"
                onClick={handleDescSave}
                className="text-[9px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-1.5 py-0.5"
              >
                Save
              </button>
              <button
                type="button"
                onClick={() => setEditingDesc(false)}
                className="text-[9px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-1.5 py-0.5"
              >
                Cancel
              </button>
            </div>
          </div>
        ) : (
          <p className="text-[10px] font-mono text-[#c0d0e0] whitespace-pre-wrap">
            {dog.description || <span className="text-[#556677] italic">No description</span>}
          </p>
        )}
      </div>

      {/* Actions */}
      <div className="px-3 py-2 flex items-center gap-2">
        <button
          type="button"
          onClick={handlePickUpOrders}
          disabled={pickingUp}
          className="text-[10px] font-mono text-[#c084fc] hover:text-[#d8b4fe] border border-[rgba(192,132,252,0.3)] rounded px-2 py-0.5 disabled:opacity-50"
        >
          {pickingUp ? "Planning..." : "Plan Orders"}
        </button>
        <button
          type="button"
          onClick={handleDelete}
          disabled={deleting}
          className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-0.5 ml-auto disabled:opacity-50"
        >
          {deleting ? "Deleting..." : "Delete"}
        </button>
      </div>

      {/* Pick-up result */}
      {pickUpResult && (
        <div className="mx-3 mb-2 px-2 py-1.5 bg-[#1a1030] border border-[rgba(192,132,252,0.2)] rounded">
          <span className="text-[10px] font-mono text-[#c084fc]">{pickUpResult}</span>
        </div>
      )}

      {/* Metadata */}
      <div className="px-3 py-1.5 border-t border-[rgba(117,170,252,0.05)]">
        <span className="text-[8px] font-mono text-[#445566]">
          Created {new Date(dog.createdAt).toLocaleDateString()}
        </span>
      </div>
    </div>
  );
}