mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat: add approvals workflow and unified member invitation management across projects, workspaces, and videos
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { ApprovalRequest } from '@/components/video-page/types';
|
||||
|
||||
export interface ApprovalCandidate {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
image: string | null;
|
||||
}
|
||||
|
||||
interface UseApprovalsParams {
|
||||
projectId?: string;
|
||||
activeVersionId: string | null;
|
||||
currentUserId: string | null;
|
||||
}
|
||||
|
||||
export function useApprovals({ projectId, activeVersionId, currentUserId }: UseApprovalsParams) {
|
||||
const [requests, setRequests] = useState<ApprovalRequest[]>([]);
|
||||
const [candidates, setCandidates] = useState<ApprovalCandidate[]>([]);
|
||||
const [isLoadingRequests, setIsLoadingRequests] = useState(false);
|
||||
const [isLoadingCandidates, setIsLoadingCandidates] = useState(false);
|
||||
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
|
||||
const [isSubmittingDecision, setIsSubmittingDecision] = useState(false);
|
||||
const [isCancelingRequest, setIsCancelingRequest] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchRequests = useCallback(async () => {
|
||||
if (!activeVersionId) return;
|
||||
setIsLoadingRequests(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/versions/${activeVersionId}/approvals`, { cache: 'no-store' });
|
||||
const payload = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
setError(payload?.error || 'Failed to fetch approval requests');
|
||||
return;
|
||||
}
|
||||
setRequests(payload?.data?.requests || []);
|
||||
} catch {
|
||||
setError('Failed to fetch approval requests');
|
||||
} finally {
|
||||
setIsLoadingRequests(false);
|
||||
}
|
||||
}, [activeVersionId]);
|
||||
|
||||
const fetchCandidates = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
setIsLoadingCandidates(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${projectId}/approval-candidates`, { cache: 'no-store' });
|
||||
const payload = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
setError(payload?.error || 'Failed to fetch approvers');
|
||||
return;
|
||||
}
|
||||
setCandidates(payload?.data?.candidates || []);
|
||||
} catch {
|
||||
setError('Failed to fetch approvers');
|
||||
} finally {
|
||||
setIsLoadingCandidates(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const createRequest = useCallback(async (approverIds: string[], message?: string) => {
|
||||
if (!activeVersionId) return false;
|
||||
setIsSubmittingRequest(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/versions/${activeVersionId}/approvals`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ approverIds, message: message || undefined }),
|
||||
});
|
||||
const payload = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
setError(payload?.error || 'Failed to create approval request');
|
||||
return false;
|
||||
}
|
||||
await fetchRequests();
|
||||
return true;
|
||||
} catch {
|
||||
setError('Failed to create approval request');
|
||||
return false;
|
||||
} finally {
|
||||
setIsSubmittingRequest(false);
|
||||
}
|
||||
}, [activeVersionId, fetchRequests]);
|
||||
|
||||
const submitDecision = useCallback(async (
|
||||
requestId: string,
|
||||
decision: 'APPROVED' | 'REJECTED',
|
||||
note?: string
|
||||
) => {
|
||||
setIsSubmittingDecision(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/approvals/${requestId}/decision`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ decision, note: note || undefined }),
|
||||
});
|
||||
const payload = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
setError(payload?.error || 'Failed to submit approval decision');
|
||||
return false;
|
||||
}
|
||||
await fetchRequests();
|
||||
return true;
|
||||
} catch {
|
||||
setError('Failed to submit approval decision');
|
||||
return false;
|
||||
} finally {
|
||||
setIsSubmittingDecision(false);
|
||||
}
|
||||
}, [fetchRequests]);
|
||||
|
||||
const cancelRequest = useCallback(async (requestId: string) => {
|
||||
setIsCancelingRequest(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/approvals/${requestId}/cancel`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const payload = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
setError(payload?.error || 'Failed to cancel approval request');
|
||||
return false;
|
||||
}
|
||||
await fetchRequests();
|
||||
return true;
|
||||
} catch {
|
||||
setError('Failed to cancel approval request');
|
||||
return false;
|
||||
} finally {
|
||||
setIsCancelingRequest(false);
|
||||
}
|
||||
}, [fetchRequests]);
|
||||
|
||||
const activePendingRequest = useMemo(
|
||||
() => requests.find((request) => request.status === 'PENDING') || null,
|
||||
[requests]
|
||||
);
|
||||
|
||||
const myPendingDecision = useMemo(() => {
|
||||
if (!currentUserId || !activePendingRequest) return null;
|
||||
return activePendingRequest.decisions.find(
|
||||
(decision) => decision.approverId === currentUserId && decision.status === 'PENDING'
|
||||
) || null;
|
||||
}, [activePendingRequest, currentUserId]);
|
||||
|
||||
return {
|
||||
requests,
|
||||
candidates,
|
||||
isLoadingRequests,
|
||||
isLoadingCandidates,
|
||||
isSubmittingRequest,
|
||||
isSubmittingDecision,
|
||||
isCancelingRequest,
|
||||
activePendingRequest,
|
||||
myPendingDecision,
|
||||
error,
|
||||
setError,
|
||||
fetchRequests,
|
||||
fetchCandidates,
|
||||
createRequest,
|
||||
submitDecision,
|
||||
cancelRequest,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user