From 0c844a312933e035de2dff8bda769db485d79fe5 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Mar 2026 23:13:13 +0000 Subject: feat: soryu-co/soryu - makima: Filter phase orbs by contract type in PhaseProgressBar --- .../src/components/contracts/ContractDetail.tsx | 1 + .../src/components/contracts/ContractList.tsx | 2 +- .../src/components/contracts/PhaseProgressBar.tsx | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/components/contracts/ContractDetail.tsx b/makima/frontend/src/components/contracts/ContractDetail.tsx index 46b2212..02c129e 100644 --- a/makima/frontend/src/components/contracts/ContractDetail.tsx +++ b/makima/frontend/src/components/contracts/ContractDetail.tsx @@ -195,6 +195,7 @@ export function ContractDetail({
diff --git a/makima/frontend/src/components/contracts/ContractList.tsx b/makima/frontend/src/components/contracts/ContractList.tsx index 4388283..1eee6a3 100644 --- a/makima/frontend/src/components/contracts/ContractList.tsx +++ b/makima/frontend/src/components/contracts/ContractList.tsx @@ -153,7 +153,7 @@ export function ContractList({ )}
- +
{contract.fileCount > 0 && ( {contract.fileCount} files diff --git a/makima/frontend/src/components/contracts/PhaseProgressBar.tsx b/makima/frontend/src/components/contracts/PhaseProgressBar.tsx index 5ee7999..9589db9 100644 --- a/makima/frontend/src/components/contracts/PhaseProgressBar.tsx +++ b/makima/frontend/src/components/contracts/PhaseProgressBar.tsx @@ -1,7 +1,9 @@ -import type { ContractPhase } from "../../lib/api"; +import type { ContractPhase, ContractType } from "../../lib/api"; +import { getValidPhases } from "../../lib/api"; interface PhaseProgressBarProps { currentPhase: ContractPhase; + contractType?: ContractType; onPhaseClick?: (phase: ContractPhase) => void; readonly?: boolean; } @@ -46,14 +48,16 @@ const phaseColors: Record - {phases.map((phase, index) => { + {visiblePhases.map((phase, index) => { const isActive = phase === currentPhase; const isCompleted = index < currentIndex; const colors = phaseColors[phase]; @@ -97,7 +101,7 @@ export function PhaseProgressBar({ {/* Connector line */} - {index < phases.length - 1 && ( + {index < visiblePhases.length - 1 && (
- {phases.map((phase, index) => { + {visiblePhases.map((phase, index) => { const isActive = phase === currentPhase; const isCompleted = index < currentIndex; const colors = phaseColors[phase]; -- cgit v1.2.3 From b8ec238084d9d5b210a67bc8c8cbb9a293facf28 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Mar 2026 23:14:01 +0000 Subject: feat: soryu-co/soryu - makima: Add DOG frontend types, API client, and hooks --- makima/frontend/src/hooks/useDogs.ts | 112 +++++++++++++++++++++++++++++++++++ makima/frontend/src/lib/api.ts | 84 ++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 makima/frontend/src/hooks/useDogs.ts (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/hooks/useDogs.ts b/makima/frontend/src/hooks/useDogs.ts new file mode 100644 index 0000000..819219e --- /dev/null +++ b/makima/frontend/src/hooks/useDogs.ts @@ -0,0 +1,112 @@ +import { useState, useEffect, useCallback } from "react"; +import { + type DirectiveOrderGroup, + type CreateDOGRequest, + type UpdateDOGRequest, + listDogs, + createDog, + getDog, + updateDog as updateDogApi, + deleteDog as deleteDogApi, + pickUpDogOrders as pickUpDogOrdersApi, +} from "../lib/api"; + +export function useDogs(directiveId: string | undefined) { + const [dogs, setDogs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + if (!directiveId) return; + try { + setLoading(true); + setError(null); + const res = await listDogs(directiveId); + setDogs(res.dogs); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load DOGs"); + } finally { + setLoading(false); + } + }, [directiveId]); + + useEffect(() => { + refresh(); + }, [refresh]); + + const create = useCallback(async (req: CreateDOGRequest) => { + if (!directiveId) return null; + const dog = await createDog(directiveId, req); + await refresh(); + return dog; + }, [directiveId, refresh]); + + const update = useCallback(async (dogId: string, req: UpdateDOGRequest) => { + if (!directiveId) return; + await updateDogApi(directiveId, dogId, req); + await refresh(); + }, [directiveId, refresh]); + + const remove = useCallback(async (dogId: string) => { + if (!directiveId) return; + await deleteDogApi(directiveId, dogId); + await refresh(); + }, [directiveId, refresh]); + + const pickUpOrders = useCallback(async (dogId: string) => { + if (!directiveId) return null; + const result = await pickUpDogOrdersApi(directiveId, dogId); + await refresh(); + return result; + }, [directiveId, refresh]); + + return { dogs, loading, error, refresh, create, update, remove, pickUpOrders }; +} + +export function useDog(directiveId: string | undefined, dogId: string | undefined) { + const [dog, setDog] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + if (!directiveId || !dogId) return; + try { + setLoading(true); + setError(null); + const d = await getDog(directiveId, dogId); + setDog(d); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load DOG"); + } finally { + setLoading(false); + } + }, [directiveId, dogId]); + + useEffect(() => { + setDog(null); + setError(null); + setLoading(true); + refresh(); + }, [directiveId, dogId]); // eslint-disable-line react-hooks/exhaustive-deps + + const update = useCallback(async (req: UpdateDOGRequest) => { + if (!directiveId || !dogId) return; + const d = await updateDogApi(directiveId, dogId, req); + setDog(d); + return d; + }, [directiveId, dogId]); + + const remove = useCallback(async () => { + if (!directiveId || !dogId) return; + await deleteDogApi(directiveId, dogId); + }, [directiveId, dogId]); + + const pickUpOrders = useCallback(async () => { + if (!directiveId || !dogId) return null; + const result = await pickUpDogOrdersApi(directiveId, dogId); + await refresh(); + return result; + }, [directiveId, dogId, refresh]); + + return { dog, loading, error, refresh, update, remove, pickUpOrders }; +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 4923c1d..7968583 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3386,6 +3386,85 @@ export async function pickUpOrders(directiveId: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs`); + if (!res.ok) throw new Error(`Failed to list DOGs: ${res.statusText}`); + return res.json(); +} + +export async function createDog(directiveId: string, req: CreateDOGRequest): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req), + }); + if (!res.ok) throw new Error(`Failed to create DOG: ${res.statusText}`); + return res.json(); +} + +export async function getDog(directiveId: string, dogId: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`); + if (!res.ok) throw new Error(`Failed to get DOG: ${res.statusText}`); + return res.json(); +} + +export async function updateDog(directiveId: string, dogId: string, req: UpdateDOGRequest): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req), + }); + if (!res.ok) throw new Error(`Failed to update DOG: ${res.statusText}`); + return res.json(); +} + +export async function deleteDog(directiveId: string, dogId: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error(`Failed to delete DOG: ${res.statusText}`); +} + +export async function pickUpDogOrders(directiveId: string, dogId: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}/pick-up-orders`, { + method: "POST", + }); + if (!res.ok) throw new Error(`Failed to pick up DOG orders: ${res.statusText}`); + return res.json(); +} + // ============================================================================= // Orders API // ============================================================================= @@ -3406,6 +3485,7 @@ export interface Order { directiveId: string | null; directiveStepId: string | null; directiveName: string | null; + dogId: string | null; repositoryUrl: string | null; createdAt: string; updatedAt: string; @@ -3424,6 +3504,7 @@ export interface CreateOrderRequest { orderType?: OrderType; labels?: string[]; directiveId: string; + dogId?: string | null; repositoryUrl?: string | null; } @@ -3436,6 +3517,7 @@ export interface UpdateOrderRequest { labels?: string[]; directiveId?: string | null; directiveStepId?: string | null; + dogId?: string | null; repositoryUrl?: string | null; } @@ -3445,6 +3527,7 @@ export async function listOrders( priority?: OrderPriority, directiveId?: string, search?: string, + dogId?: string, ): Promise { const params = new URLSearchParams(); if (status) params.set("status", status); @@ -3452,6 +3535,7 @@ export async function listOrders( if (priority) params.set("priority", priority); if (directiveId) params.set("directiveId", directiveId); if (search) params.set("search", search); + if (dogId) params.set("dogId", dogId); const qs = params.toString(); const res = await authFetch(`${API_BASE}/api/v1/orders${qs ? `?${qs}` : ""}`); if (!res.ok) throw new Error(`Failed to list orders: ${res.statusText}`); -- cgit v1.2.3 From ae3bc57de7a240c3c8ab15080b405e8ea3e16ccb Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Mar 2026 23:25:40 +0000 Subject: WIP: heartbeat checkpoint --- .../frontend/src/components/directives/DOGList.tsx | 381 +++++++++++++++++++++ .../src/components/directives/DirectiveDetail.tsx | 72 +++- .../frontend/src/components/orders/OrderDetail.tsx | 91 +++++ makima/frontend/src/routes/directives.tsx | 8 + makima/frontend/src/routes/orders.tsx | 3 + 5 files changed, 542 insertions(+), 13 deletions(-) create mode 100644 makima/frontend/src/components/directives/DOGList.tsx (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/components/directives/DOGList.tsx b/makima/frontend/src/components/directives/DOGList.tsx new file mode 100644 index 0000000..de59d7d --- /dev/null +++ b/makima/frontend/src/components/directives/DOGList.tsx @@ -0,0 +1,381 @@ +import { useState } from "react"; +import type { + DirectiveOrderGroup, + DOGStatus, + CreateDOGRequest, +} from "../../lib/api"; + +const DOG_STATUS_BADGE: Record = { + 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; + onUpdateDog: (dogId: string, req: { name?: string; description?: string | null; status?: DOGStatus }) => Promise; + onDeleteDog: (dogId: string) => Promise; + onPickUpOrders: (dogId: string) => Promise; +} + +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 ( +
+ Loading DOGs... +
+ ); + } + + return ( +
+ {/* Header + Create button */} +
+ + Order Groups ({dogs.length}) + + +
+ + {/* Create form */} + {showCreate && ( +
+
+ + 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]" + /> +
+
+ +