import type { Opc, OpcNote, OpcArtifact, OpcType, OpcStatus, OpcPriority } from '../types/opc'; const BASE_URL = import.meta.env.VITE_API_URL ?? ''; // ── OPC CRUD ────────────────────────────────────────────────────────────────── export async function getNextNumber(): Promise { const res = await fetch(`${BASE_URL}/api/opc/next-number`); if (!res.ok) throw new Error('Failed to fetch next OPC number'); const data = await res.json(); return data.number as string; } export async function listOpcs(type?: string, status?: string): Promise { const params = new URLSearchParams(); if (type && type !== 'all') params.set('type', type); if (status && status !== 'all') params.set('status', status); const res = await fetch(`${BASE_URL}/api/opc?${params}`); if (!res.ok) throw new Error(`Failed to load OPCs: ${res.statusText}`); // API returns OpcRecord (camelCase from .NET JsonSerializerDefaults.Web) return (await res.json()).map(mapRecord); } export async function getOpc(id: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/${id}`); if (!res.ok) throw new Error(`Failed to load OPC: ${res.statusText}`); return mapRecord(await res.json()); } export async function createOpc(req: { title: string; type: OpcType; priority: OpcPriority; assignee: string; description: string; }): Promise { const res = await fetch(`${BASE_URL}/api/opc`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to create OPC: ${res.statusText}`); return mapRecord(await res.json()); } export async function updateOpc(id: string, req: { title?: string; description?: string; type?: OpcType; status?: OpcStatus; priority?: OpcPriority; assignee?: string; }): Promise { const res = await fetch(`${BASE_URL}/api/opc/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to update OPC: ${res.statusText}`); return mapRecord(await res.json()); } // ── Notes ───────────────────────────────────────────────────────────────────── export async function listNotes(opcId: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/${opcId}/notes`); if (!res.ok) throw new Error(`Failed to load notes: ${res.statusText}`); return (await res.json()).map(mapNote); } export async function addNote(opcId: string, author: string, content: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/${opcId}/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author, content }), }); if (!res.ok) throw new Error(`Failed to add note: ${res.statusText}`); return mapNote(await res.json()); } // ── Artifacts ───────────────────────────────────────────────────────────────── export async function listArtifacts(opcId: string, type?: string): Promise { const params = type ? `?type=${type}` : ''; const res = await fetch(`${BASE_URL}/api/opc/${opcId}/artifacts${params}`); if (!res.ok) throw new Error(`Failed to load artifacts: ${res.statusText}`); return (await res.json()).map(mapArtifact); } export async function createArtifact(opcId: string, req: { artifactType: string; title: string; content: string; }): Promise { const res = await fetch(`${BASE_URL}/api/opc/${opcId}/artifacts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to create artifact: ${res.statusText}`); return mapArtifact(await res.json()); } export async function updateArtifact(artifactId: string, req: { artifactType: string; title: string; content: string; }): Promise { const res = await fetch(`${BASE_URL}/api/opc/artifacts/${artifactId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to update artifact: ${res.statusText}`); return mapArtifact(await res.json()); } export async function deleteArtifact(artifactId: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/artifacts/${artifactId}`, { method: 'DELETE' }); if (!res.ok && res.status !== 404) throw new Error(`Failed to delete artifact: ${res.statusText}`); } // ── Git commit linkage ──────────────────────────────────────────────────────── // Commits are linked by convention: developers include "OPC # XXXX" in their commit message. // The git log endpoint supports ?grep=OPC+%230001 to filter. export interface LinkedCommit { repoKey: string; hash: string; shortHash: string; author: string; date: string; subject: string; files: string[]; } export async function getLinkedCommits(opcNumber: string): Promise { const res = await fetch(`${BASE_URL}/api/git/log?grep=${encodeURIComponent(opcNumber)}&limit=50&repo=all`); if (!res.ok) throw new Error(`Failed to load commits: ${res.statusText}`); return res.json(); // backend now returns { repoKey, hash, shortHash, author, date, subject, files } } // ── Pinned commits ──────────────────────────────────────────────────────────── export interface PinnedCommit { opcId: string; hash: string; shortHash: string; subject: string; author: string; pinnedAt: string; pinnedBy: string; } function mapPinnedCommit(d: Record): PinnedCommit { return { opcId: d.opcId as string, hash: d.hash as string, shortHash: d.shortHash as string, subject: d.subject as string, author: d.author as string, pinnedAt: d.pinnedAt as string, pinnedBy: d.pinnedBy as string, }; } export async function getPinnedCommits(opcId: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/${opcId}/pinned-commits`); if (!res.ok) throw new Error(`Failed to load pinned commits: ${res.statusText}`); return (await res.json()).map(mapPinnedCommit); } export async function pinCommit(opcId: string, hash: string, pinnedBy: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/${opcId}/pinned-commits`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash, pinnedBy }), }); if (!res.ok) { const body = await res.text().catch(() => res.statusText); throw new Error(body || res.statusText); } return mapPinnedCommit(await res.json()); } export async function unpinCommit(opcId: string, hash: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/${opcId}/pinned-commits/${hash}`, { method: 'DELETE' }); if (!res.ok && res.status !== 404) throw new Error(`Failed to unpin commit: ${res.statusText}`); } // ── Branch coverage ─────────────────────────────────────────────────────────── export interface BranchCoverage { branch: string; contains: boolean; tipHash: string; isHead: boolean; } export async function getBranchCoverage(hashes: string[]): Promise { if (hashes.length === 0) return []; const res = await fetch(`${BASE_URL}/api/git/branch-coverage?commits=${hashes.join(',')}`); if (!res.ok) throw new Error(`Failed to get branch coverage: ${res.statusText}`); return res.json(); } // Query branch coverage against a specific repo from the Git:Repos registry. // The backend ignores hashes it cannot resolve, so passing cross-repo hashes is safe. export async function getBranchCoverageForRepo(repoKey: string, hashes: string[]): Promise { if (hashes.length === 0) return []; const res = await fetch( `${BASE_URL}/api/git/branch-coverage?commits=${hashes.join(',')}&repo=${encodeURIComponent(repoKey)}`, ); if (!res.ok) throw new Error(`Failed to get branch coverage for ${repoKey}: ${res.statusText}`); return res.json(); } // ── Commit detail (full diff) ───────────────────────────────────────────────── export interface CommitFile { path: string; oldPath: string; status: string; additions: number; deletions: number; patch: string; } export interface CommitDetail { hash: string; shortHash: string; author: string; email: string; date: string; subject: string; body: string; files: CommitFile[]; } export async function getCommitDetail(hash: string): Promise { const res = await fetch(`${BASE_URL}/api/git/commits/${hash}`); if (!res.ok) throw new Error(`Failed to load commit ${hash}: ${res.statusText}`); return res.json(); } // ── AI assist ───────────────────────────────────────────────────────────────── export async function aiAssist(prompt: string, context?: string): Promise { const res = await fetch(`${BASE_URL}/api/opc/ai-assist`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, context }), }); if (!res.ok) { const err = await res.text(); throw new Error(`AI assist failed: ${err}`); } const data = await res.json(); return data.text as string; } // ── Field mappers (snake_case/PascalCase → camelCase) ───────────────────────── // .NET JsonSerializerDefaults.Web produces camelCase already, so these are // lightweight guards in case of null/missing fields. // eslint-disable-next-line @typescript-eslint/no-explicit-any function mapRecord(r: any): Opc { return { id: r.id, number: r.number, title: r.title, description: r.description ?? '', type: r.type as OpcType, status: r.status as OpcStatus, priority: r.priority as OpcPriority, assignee: r.assignee ?? '', createdAt: r.createdAt, updatedAt: r.updatedAt, notes: [], // loaded separately on drawer open commits: [], // loaded separately on drawer open }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function mapNote(r: any): OpcNote { return { id: r.id, author: r.author, timestamp: r.createdAt, content: r.content, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function mapArtifact(r: any): OpcArtifact { return { id: r.id, opcId: r.opcId, artifactType: r.artifactType, title: r.title, content: r.content, createdAt: r.createdAt, updatedAt: r.updatedAt, }; } // ── Gitea branch integration ─────────────────────────────────────────────────── export interface GiteaBranch { name: string; commitSha: string; protected: boolean; } export async function listGiteaBranches(): Promise { const res = await fetch(`${BASE_URL}/api/gitea/branches`); if (!res.ok) throw new Error(`Failed to load Gitea branches: ${res.statusText}`); return res.json(); } export async function createGiteaBranch( opcNumber: string, opcTitle: string, from = 'master', ): Promise { const res = await fetch(`${BASE_URL}/api/gitea/branches`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ opcNumber, opcTitle, from }), }); if (!res.ok) { const body = await res.text().catch(() => res.statusText); throw new Error(body || res.statusText); } return res.json(); }