summaryrefslogtreecommitdiff
path: root/makima/frontend/src/hooks/useContracts.ts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/hooks/useContracts.ts
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/hooks/useContracts.ts')
-rw-r--r--makima/frontend/src/hooks/useContracts.ts308
1 files changed, 308 insertions, 0 deletions
diff --git a/makima/frontend/src/hooks/useContracts.ts b/makima/frontend/src/hooks/useContracts.ts
new file mode 100644
index 0000000..f803527
--- /dev/null
+++ b/makima/frontend/src/hooks/useContracts.ts
@@ -0,0 +1,308 @@
+import { useState, useCallback, useEffect } from "react";
+import {
+ listContracts,
+ getContract,
+ createContract,
+ updateContract,
+ deleteContract,
+ changeContractPhase,
+ getContractEvents,
+ addRemoteRepository,
+ addLocalRepository,
+ createManagedRepository,
+ deleteContractRepository,
+ setRepositoryPrimary,
+ addTaskToContract,
+ removeTaskFromContract,
+ VersionConflictError,
+ type ContractSummary,
+ type ContractWithRelations,
+ type ContractEvent,
+ type ContractRepository,
+ type ContractPhase,
+ type CreateContractRequest,
+ type UpdateContractRequest,
+ type AddRemoteRepositoryRequest,
+ type AddLocalRepositoryRequest,
+ type CreateManagedRepositoryRequest,
+} from "../lib/api";
+
+export interface ConflictState {
+ hasConflict: boolean;
+ expectedVersion: number;
+ actualVersion: number;
+}
+
+export function useContracts() {
+ const [contracts, setContracts] = useState<ContractSummary[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+ const [conflict, setConflict] = useState<ConflictState | null>(null);
+
+ const fetchContracts = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await listContracts();
+ setContracts(response.contracts);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to fetch contracts");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const fetchContract = useCallback(
+ async (id: string): Promise<ContractWithRelations | null> => {
+ setError(null);
+ try {
+ return await getContract(id);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to fetch contract");
+ return null;
+ }
+ },
+ []
+ );
+
+ const saveContract = useCallback(
+ async (data: CreateContractRequest): Promise<ContractSummary | null> => {
+ setError(null);
+ try {
+ const contract = await createContract(data);
+ await fetchContracts(); // Refresh list
+ return contract;
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to save contract");
+ return null;
+ }
+ },
+ [fetchContracts]
+ );
+
+ const editContract = useCallback(
+ async (
+ id: string,
+ data: UpdateContractRequest
+ ): Promise<ContractSummary | null> => {
+ setError(null);
+ setConflict(null);
+ try {
+ const contract = await updateContract(id, data);
+ await fetchContracts(); // Refresh list
+ return contract;
+ } catch (e) {
+ if (e instanceof VersionConflictError) {
+ setConflict({
+ hasConflict: true,
+ expectedVersion: e.expectedVersion,
+ actualVersion: e.actualVersion,
+ });
+ return null;
+ }
+ setError(e instanceof Error ? e.message : "Failed to update contract");
+ return null;
+ }
+ },
+ [fetchContracts]
+ );
+
+ const clearConflict = useCallback(() => {
+ setConflict(null);
+ }, []);
+
+ const removeContract = useCallback(
+ async (id: string): Promise<boolean> => {
+ setError(null);
+ try {
+ await deleteContract(id);
+ await fetchContracts(); // Refresh list
+ return true;
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to delete contract");
+ return false;
+ }
+ },
+ [fetchContracts]
+ );
+
+ const changePhase = useCallback(
+ async (
+ id: string,
+ phase: ContractPhase
+ ): Promise<ContractSummary | null> => {
+ setError(null);
+ try {
+ const contract = await changeContractPhase(id, phase);
+ await fetchContracts(); // Refresh list
+ return contract;
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to change phase");
+ return null;
+ }
+ },
+ [fetchContracts]
+ );
+
+ const fetchEvents = useCallback(
+ async (id: string): Promise<ContractEvent[]> => {
+ setError(null);
+ try {
+ return await getContractEvents(id);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to fetch events");
+ return [];
+ }
+ },
+ []
+ );
+
+ // Repository management
+ const addRemoteRepo = useCallback(
+ async (
+ contractId: string,
+ data: AddRemoteRepositoryRequest
+ ): Promise<ContractRepository | null> => {
+ setError(null);
+ try {
+ return await addRemoteRepository(contractId, data);
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to add remote repository"
+ );
+ return null;
+ }
+ },
+ []
+ );
+
+ const addLocalRepo = useCallback(
+ async (
+ contractId: string,
+ data: AddLocalRepositoryRequest
+ ): Promise<ContractRepository | null> => {
+ setError(null);
+ try {
+ return await addLocalRepository(contractId, data);
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to add local repository"
+ );
+ return null;
+ }
+ },
+ []
+ );
+
+ const createManagedRepo = useCallback(
+ async (
+ contractId: string,
+ data: CreateManagedRepositoryRequest
+ ): Promise<ContractRepository | null> => {
+ setError(null);
+ try {
+ return await createManagedRepository(contractId, data);
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to create managed repository"
+ );
+ return null;
+ }
+ },
+ []
+ );
+
+ const removeRepo = useCallback(
+ async (contractId: string, repoId: string): Promise<boolean> => {
+ setError(null);
+ try {
+ await deleteContractRepository(contractId, repoId);
+ return true;
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to delete repository"
+ );
+ return false;
+ }
+ },
+ []
+ );
+
+ const setRepoPrimary = useCallback(
+ async (contractId: string, repoId: string): Promise<boolean> => {
+ setError(null);
+ try {
+ await setRepositoryPrimary(contractId, repoId);
+ return true;
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to set repository as primary"
+ );
+ return false;
+ }
+ },
+ []
+ );
+
+ // Task association
+ const addTask = useCallback(
+ async (contractId: string, taskId: string): Promise<boolean> => {
+ setError(null);
+ try {
+ await addTaskToContract(contractId, taskId);
+ return true;
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to add task to contract"
+ );
+ return false;
+ }
+ },
+ []
+ );
+
+ const removeTask = useCallback(
+ async (contractId: string, taskId: string): Promise<boolean> => {
+ setError(null);
+ try {
+ await removeTaskFromContract(contractId, taskId);
+ return true;
+ } catch (e) {
+ setError(
+ e instanceof Error ? e.message : "Failed to remove task from contract"
+ );
+ return false;
+ }
+ },
+ []
+ );
+
+ // Initial fetch
+ useEffect(() => {
+ fetchContracts();
+ }, [fetchContracts]);
+
+ return {
+ contracts,
+ loading,
+ error,
+ conflict,
+ clearConflict,
+ fetchContracts,
+ fetchContract,
+ saveContract,
+ editContract,
+ removeContract,
+ changePhase,
+ fetchEvents,
+ // Repository management
+ addRemoteRepo,
+ addLocalRepo,
+ createManagedRepo,
+ removeRepo,
+ setRepoPrimary,
+ // Task association
+ addTask,
+ removeTask,
+ };
+}