mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-05-31 08:43:37 +00:00
feat: add dashboard success-rate tracking and submission stages
This commit is contained in:
@@ -8,6 +8,7 @@ from app.schemas import (
|
||||
AiSuggestionRequest,
|
||||
SubmissionCreateRequest,
|
||||
SubmissionResponse,
|
||||
SubmissionStatusUpdateRequest,
|
||||
SuggestionResponse,
|
||||
SuggestionUpdateRequest,
|
||||
)
|
||||
@@ -16,8 +17,10 @@ from app.services.submissions import (
|
||||
get_submission,
|
||||
list_submissions,
|
||||
request_ai_suggestions,
|
||||
update_submission_status,
|
||||
update_suggestion,
|
||||
)
|
||||
from app.models import SubmissionStatus
|
||||
from dlib.auth import AuthenticatedUser
|
||||
|
||||
|
||||
@@ -40,7 +43,9 @@ async def get_submission_endpoint(
|
||||
session: AsyncSession = Depends(get_db),
|
||||
user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
submission = await get_submission(session, owner_id=user.sub, submission_id=submission_id)
|
||||
submission = await get_submission(
|
||||
session, owner_id=user.sub, submission_id=submission_id
|
||||
)
|
||||
if not submission:
|
||||
raise HTTPException(status_code=404, detail="Submission not found")
|
||||
return SubmissionResponse.model_validate(submission)
|
||||
@@ -66,6 +71,24 @@ async def create_submission_endpoint(
|
||||
return SubmissionResponse.model_validate(submission)
|
||||
|
||||
|
||||
@router.patch("/{submission_id}/status", response_model=SubmissionResponse)
|
||||
async def update_submission_status_endpoint(
|
||||
submission_id: str,
|
||||
payload: SubmissionStatusUpdateRequest,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
submission = await update_submission_status(
|
||||
session,
|
||||
owner_id=user.sub,
|
||||
submission_id=submission_id,
|
||||
status=SubmissionStatus(payload.status),
|
||||
)
|
||||
if not submission:
|
||||
raise HTTPException(status_code=404, detail="Submission not found")
|
||||
return SubmissionResponse.model_validate(submission)
|
||||
|
||||
|
||||
@router.post("/{submission_id}/ai", response_model=list[SuggestionResponse])
|
||||
async def request_submissions_ai(
|
||||
submission_id: str,
|
||||
@@ -85,7 +108,9 @@ async def request_submissions_ai(
|
||||
return [SuggestionResponse.model_validate(item) for item in suggestions]
|
||||
|
||||
|
||||
@router.patch("/{submission_id}/suggestions/{suggestion_id}", response_model=SuggestionResponse)
|
||||
@router.patch(
|
||||
"/{submission_id}/suggestions/{suggestion_id}", response_model=SuggestionResponse
|
||||
)
|
||||
async def update_suggestion_endpoint(
|
||||
submission_id: str,
|
||||
suggestion_id: str,
|
||||
|
||||
@@ -11,6 +11,7 @@ from .cv import (
|
||||
PublishRequest,
|
||||
SubmissionCreateRequest,
|
||||
SubmissionResponse,
|
||||
SubmissionStatusUpdateRequest,
|
||||
SuggestionResponse,
|
||||
SuggestionUpdateRequest,
|
||||
VersionResponse,
|
||||
@@ -25,6 +26,7 @@ __all__ = [
|
||||
"PatchApplyRequest",
|
||||
"SubmissionCreateRequest",
|
||||
"SubmissionResponse",
|
||||
"SubmissionStatusUpdateRequest",
|
||||
"AiSuggestionRequest",
|
||||
"SuggestionResponse",
|
||||
"SuggestionUpdateRequest",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -90,6 +90,16 @@ class SubmissionCreateRequest(BaseModel):
|
||||
job_description: str | None = None
|
||||
|
||||
|
||||
class SubmissionStatusUpdateRequest(BaseModel):
|
||||
status: Literal[
|
||||
"draft",
|
||||
"tailoring",
|
||||
"pending_review",
|
||||
"published",
|
||||
"archived",
|
||||
]
|
||||
|
||||
|
||||
class SuggestionResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -133,6 +133,22 @@ async def update_suggestion(
|
||||
return suggestion
|
||||
|
||||
|
||||
async def update_submission_status(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
owner_id: str,
|
||||
submission_id: str,
|
||||
status: SubmissionStatus,
|
||||
) -> Submission | None:
|
||||
submission = await _get_submission_for_owner(session, owner_id, submission_id)
|
||||
if not submission:
|
||||
return None
|
||||
submission.status = status
|
||||
await session.commit()
|
||||
await session.refresh(submission)
|
||||
return submission
|
||||
|
||||
|
||||
async def _get_version_for_owner(
|
||||
session: AsyncSession, owner_id: str, version_id: str
|
||||
) -> CvVersion | None:
|
||||
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
fetchDocuments, fetchSubmissions, fetchPublicAssetAnalytics, getPublicPdfUrl,
|
||||
publishVersion, PublicAsset, PublicAssetAnalytics,
|
||||
requestAiSuggestions,
|
||||
Submission, StructuredBlock, Suggestion, updateSuggestion, uploadDocument, Version,
|
||||
Submission,
|
||||
SubmissionStatus,
|
||||
StructuredBlock,
|
||||
Suggestion,
|
||||
updateSubmissionStatus,
|
||||
updateSuggestion,
|
||||
uploadDocument,
|
||||
Version,
|
||||
} from '@/libs/api';
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -20,12 +27,32 @@ function fmt(iso: string) {
|
||||
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
function statusBadge(status: string) {
|
||||
const SUBMISSION_STATUS_LABELS: Record<SubmissionStatus, string> = {
|
||||
draft: 'Draft',
|
||||
tailoring: 'Tailoring',
|
||||
pending_review: 'Passed screening',
|
||||
published: 'Submitted',
|
||||
archived: 'Closed',
|
||||
};
|
||||
|
||||
const SUBMISSION_STATUS_OPTIONS: Array<{ value: SubmissionStatus; label: string }> = [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'tailoring', label: 'Tailoring' },
|
||||
{ value: 'published', label: 'Submitted' },
|
||||
{ value: 'pending_review', label: 'Passed screening' },
|
||||
{ value: 'archived', label: 'Closed' },
|
||||
];
|
||||
|
||||
function isSubmittedStatus(status: SubmissionStatus) {
|
||||
return status === 'published' || status === 'pending_review' || status === 'archived';
|
||||
}
|
||||
|
||||
function statusBadge(status: SubmissionStatus) {
|
||||
const cls = ({
|
||||
draft: 'badge-draft', tailoring: 'badge-submitted', pending_review: 'badge-interviewing',
|
||||
published: 'badge-public', archived: 'badge-closed',
|
||||
} as Record<string, string>)[status] ?? 'badge-draft';
|
||||
return <span className={`badge ${cls}`}>{status.replace('_', ' ')}</span>;
|
||||
} as Record<SubmissionStatus, string>)[status] ?? 'badge-draft';
|
||||
return <span className={`badge ${cls}`}>{SUBMISSION_STATUS_LABELS[status] ?? status.replace('_', ' ')}</span>;
|
||||
}
|
||||
|
||||
// ── modals ────────────────────────────────────────────────────────────────────
|
||||
@@ -304,20 +331,27 @@ function ContentTab({
|
||||
// ── submissions tab ───────────────────────────────────────────────────────────
|
||||
|
||||
function SubmissionsTab({
|
||||
submissions, loading, versionId,
|
||||
onNewSubmission, onRefresh,
|
||||
submissions, loading,
|
||||
onNewSubmission, onRefresh, onStatusChange,
|
||||
}: {
|
||||
submissions: Submission[];
|
||||
loading: boolean;
|
||||
versionId: string;
|
||||
onNewSubmission: () => void;
|
||||
onRefresh: () => void;
|
||||
onStatusChange: (submissionId: string, status: SubmissionStatus) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const [aiLoading, setAiLoading] = useState<string | null>(null);
|
||||
const [statusLoading, setStatusLoading] = useState<string | null>(null);
|
||||
const [aiJd, setAiJd] = useState<Record<string, string>>({});
|
||||
const [suggestions, setSuggestions] = useState<Record<string, Suggestion[]>>({});
|
||||
|
||||
const submittedCount = submissions.filter(s => isSubmittedStatus(s.status)).length;
|
||||
const passedScreeningCount = submissions.filter(s => s.status === 'pending_review').length;
|
||||
const successRate = submittedCount > 0
|
||||
? Math.round((passedScreeningCount / submittedCount) * 100)
|
||||
: 0;
|
||||
|
||||
const loadAi = async (s: Submission) => {
|
||||
const jd = aiJd[s.id] ?? s.job_description ?? '';
|
||||
if (!jd.trim()) return;
|
||||
@@ -340,6 +374,19 @@ function SubmissionsTab({
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const changeStatus = async (sub: Submission, nextStatus: SubmissionStatus) => {
|
||||
if (sub.status === nextStatus) return;
|
||||
setStatusLoading(sub.id);
|
||||
try {
|
||||
const updated = await updateSubmissionStatus(sub.id, nextStatus);
|
||||
onStatusChange(updated.id, updated.status);
|
||||
} catch {
|
||||
// ignore for now
|
||||
} finally {
|
||||
setStatusLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ padding: '20px 0', color: 'var(--text-faint)', fontSize: 13 }}>Loading…</div>;
|
||||
|
||||
return (
|
||||
@@ -349,6 +396,21 @@ function SubmissionsTab({
|
||||
<button className="btn btn-ghost" style={{ fontSize: 12 }} onClick={onNewSubmission}>+ New submission</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: '#fff' }}>
|
||||
<div className="label" style={{ marginBottom: 4 }}>Submitted</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{submittedCount}</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: '#fff' }}>
|
||||
<div className="label" style={{ marginBottom: 4 }}>Passed screening</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{passedScreeningCount}</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: '#fff' }}>
|
||||
<div className="label" style={{ marginBottom: 4 }}>Success rate</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{successRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submissions.length === 0 && (
|
||||
<div style={{ padding: '20px 0', color: 'var(--text-faint)', fontSize: 13 }}>
|
||||
No submissions yet. Create one to track a job application and get AI tailoring suggestions.
|
||||
@@ -389,6 +451,20 @@ function SubmissionsTab({
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="label" style={{ marginBottom: 6 }}>Application stage</div>
|
||||
<select
|
||||
value={s.status}
|
||||
onChange={e => changeStatus(s, e.target.value as SubmissionStatus)}
|
||||
disabled={statusLoading === s.id}
|
||||
style={{ maxWidth: 280 }}
|
||||
>
|
||||
{SUBMISSION_STATUS_OPTIONS.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* AI tailoring */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="label" style={{ marginBottom: 6 }}>AI tailoring</div>
|
||||
@@ -485,6 +561,7 @@ export default function Dashboard() {
|
||||
const [recentlyPublishedSlug, setRecentlyPublishedSlug] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<Tab>('content');
|
||||
const [submissions, setSubmissions] = useState<Submission[]>([]);
|
||||
const [allSubmissions, setAllSubmissions] = useState<Submission[]>([]);
|
||||
const [subsLoading, setSubsLoading] = useState(false);
|
||||
const [pendingEdits, setPendingEdits] = useState<Map<string, { old_value: string; new_value: string }>>(new Map());
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
@@ -493,9 +570,10 @@ export default function Dashboard() {
|
||||
const [applyError, setApplyError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocuments()
|
||||
.then(d => {
|
||||
Promise.all([fetchDocuments(), fetchSubmissions().catch(() => [])])
|
||||
.then(([d, allSubs]) => {
|
||||
setDocs(d);
|
||||
setAllSubmissions(allSubs);
|
||||
if (d.length) { setSelectedDocId(d[0].id); setSelectedVersionId(d[0].root_version_id ?? null); }
|
||||
})
|
||||
.catch(() => setError('Failed to load documents. Make sure the backend is running.'))
|
||||
@@ -521,6 +599,13 @@ export default function Dashboard() {
|
||||
|
||||
const selectedDoc = docs.find(d => d.id === selectedDocId) ?? null;
|
||||
const selectedVersion = selectedDoc?.versions.find(v => v.id === selectedVersionId) ?? null;
|
||||
const selectedDocVersionIds = new Set((selectedDoc?.versions ?? []).map(v => v.id));
|
||||
const selectedDocSubmissions = allSubmissions.filter(s => selectedDocVersionIds.has(s.version_id));
|
||||
const selectedDocSubmittedCount = selectedDocSubmissions.filter(s => isSubmittedStatus(s.status)).length;
|
||||
const selectedDocPassedScreeningCount = selectedDocSubmissions.filter(s => s.status === 'pending_review').length;
|
||||
const selectedDocSuccessRate = selectedDocSubmittedCount > 0
|
||||
? Math.round((selectedDocPassedScreeningCount / selectedDocSubmittedCount) * 100)
|
||||
: 0;
|
||||
const publishedAssets = selectedVersion?.public_assets ?? [];
|
||||
const sortedPublishedAssets = [...publishedAssets].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
const publicBaseUrl = (process.env.NEXT_PUBLIC_BASE_URL ?? '').replace(/\/$/, '');
|
||||
@@ -543,6 +628,7 @@ export default function Dashboard() {
|
||||
const refreshDocs = async () => {
|
||||
const fresh = await fetchDocuments().catch(() => docs);
|
||||
setDocs(fresh);
|
||||
refreshAllSubs();
|
||||
return fresh;
|
||||
};
|
||||
|
||||
@@ -551,6 +637,10 @@ export default function Dashboard() {
|
||||
fetchSubmissions(selectedVersionId).then(setSubmissions).catch(() => { });
|
||||
};
|
||||
|
||||
function refreshAllSubs() {
|
||||
fetchSubmissions().then(setAllSubmissions).catch(() => { });
|
||||
}
|
||||
|
||||
const onUploadDone = (doc: Document) => {
|
||||
setDocs(prev => [doc, ...prev.filter(d => d.id !== doc.id)]);
|
||||
setSelectedDocId(doc.id);
|
||||
@@ -569,10 +659,16 @@ export default function Dashboard() {
|
||||
|
||||
const onSubmissionDone = (s: Submission) => {
|
||||
setSubmissions(prev => [s, ...prev]);
|
||||
setAllSubmissions(prev => [s, ...prev]);
|
||||
setModal(null);
|
||||
setActiveTab('submissions');
|
||||
};
|
||||
|
||||
const handleSubmissionStatusChange = (submissionId: string, status: SubmissionStatus) => {
|
||||
setSubmissions(prev => prev.map(s => (s.id === submissionId ? { ...s, status } : s)));
|
||||
setAllSubmissions(prev => prev.map(s => (s.id === submissionId ? { ...s, status } : s)));
|
||||
};
|
||||
|
||||
const stageEdit = (path: string, old_value: string, new_value: string) => {
|
||||
setPendingEdits(prev => new Map(prev).set(path, { old_value, new_value }));
|
||||
};
|
||||
@@ -610,7 +706,6 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
const handleDeleteVersion = async (versionId: string) => {
|
||||
const version = selectedDoc?.versions.find(v => v.id === versionId);
|
||||
const hasChildren = selectedDoc?.versions.some(v => v.parent_version_id === versionId);
|
||||
const msg = hasChildren
|
||||
? 'Delete this branch and all its sub-branches? This cannot be undone.'
|
||||
@@ -767,6 +862,50 @@ export default function Dashboard() {
|
||||
|
||||
{selectedVersion && (
|
||||
<>
|
||||
{selectedDoc && (
|
||||
<div style={{ padding: '16px 20px 0', flexShrink: 0 }}>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 8, background: '#fff', padding: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div className="label" style={{ marginBottom: 3 }}>Dashboard overview</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{selectedDoc.title}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-faint)' }}>{selectedDoc.versions.length} version{selectedDoc.versions.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: 8, marginBottom: 10 }}>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: 'var(--surface)' }}>
|
||||
<div className="label" style={{ marginBottom: 3 }}>Submissions</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{selectedDocSubmissions.length}</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: 'var(--surface)' }}>
|
||||
<div className="label" style={{ marginBottom: 3 }}>Submitted</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{selectedDocSubmittedCount}</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: 'var(--surface)' }}>
|
||||
<div className="label" style={{ marginBottom: 3 }}>Passed screening</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{selectedDocPassedScreeningCount}</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '8px 10px', background: 'var(--surface)' }}>
|
||||
<div className="label" style={{ marginBottom: 3 }}>Success rate</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>{selectedDocSuccessRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', maxHeight: 220, overflow: 'auto' }}>
|
||||
<div className="label" style={{ padding: '8px 10px 4px' }}>Full branch tree</div>
|
||||
<CVTree
|
||||
versions={selectedDoc.versions}
|
||||
selectedVersionId={selectedVersionId}
|
||||
onSelect={selectVersion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* version header */}
|
||||
<div style={{ padding: '16px 20px 0', flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12, gap: 12 }}>
|
||||
@@ -934,9 +1073,9 @@ export default function Dashboard() {
|
||||
<SubmissionsTab
|
||||
submissions={submissions}
|
||||
loading={subsLoading}
|
||||
versionId={selectedVersionId!}
|
||||
onNewSubmission={() => setModal('submission')}
|
||||
onRefresh={refreshSubs}
|
||||
onStatusChange={handleSubmissionStatusChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,6 @@ function Node({ node, depth, selectedId, onSelect, onDelete, colorIndex = 0 }: {
|
||||
const v = node.version;
|
||||
const isRoot = !v.parent_version_id;
|
||||
const isSelected = v.id === selectedId;
|
||||
const isLeaf = node.children.length === 0;
|
||||
const dotColor = DOT_COLORS[colorIndex % DOT_COLORS.length];
|
||||
|
||||
return (
|
||||
@@ -147,4 +146,3 @@ export default function CVTree({ versions, selectedVersionId, onSelect, onDelete
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,13 @@ export type Suggestion = {
|
||||
metadata_json?: { keywords?: string[]; confidence?: number } | null;
|
||||
};
|
||||
|
||||
export type SubmissionStatus =
|
||||
| 'draft'
|
||||
| 'tailoring'
|
||||
| 'pending_review'
|
||||
| 'published'
|
||||
| 'archived';
|
||||
|
||||
export type Submission = {
|
||||
id: string;
|
||||
version_id: string;
|
||||
@@ -58,7 +65,7 @@ export type Submission = {
|
||||
role_title: string;
|
||||
job_url?: string | null;
|
||||
job_description?: string | null;
|
||||
status: string;
|
||||
status: SubmissionStatus;
|
||||
suggestions: Suggestion[];
|
||||
created_at: string;
|
||||
};
|
||||
@@ -159,8 +166,10 @@ export async function createSubmission(
|
||||
});
|
||||
}
|
||||
|
||||
export const fetchSubmissions = (versionId: string): Promise<Submission[]> =>
|
||||
req<Submission[]>(`/api/v1/submissions?version_id=${versionId}`);
|
||||
export const fetchSubmissions = (versionId?: string): Promise<Submission[]> => {
|
||||
const query = versionId ? `?version_id=${encodeURIComponent(versionId)}` : '';
|
||||
return req<Submission[]>(`/api/v1/submissions${query}`);
|
||||
};
|
||||
|
||||
export const fetchSubmission = (id: string): Promise<Submission> =>
|
||||
req<Submission>(`/api/v1/submissions/${id}`);
|
||||
@@ -189,6 +198,17 @@ export async function updateSuggestion(
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSubmissionStatus(
|
||||
submissionId: string,
|
||||
status: SubmissionStatus,
|
||||
): Promise<Submission> {
|
||||
return req<Submission>(`/api/v1/submissions/${submissionId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function publishVersion(
|
||||
versionId?: string | null,
|
||||
submissionId?: string | null,
|
||||
|
||||
Reference in New Issue
Block a user