feat: add dashboard success-rate tracking and submission stages

This commit is contained in:
2026-04-04 15:16:12 +02:00
parent 07fbfbbd85
commit 5facc4b7a5
7 changed files with 229 additions and 19 deletions

View File

@@ -8,6 +8,7 @@ from app.schemas import (
AiSuggestionRequest, AiSuggestionRequest,
SubmissionCreateRequest, SubmissionCreateRequest,
SubmissionResponse, SubmissionResponse,
SubmissionStatusUpdateRequest,
SuggestionResponse, SuggestionResponse,
SuggestionUpdateRequest, SuggestionUpdateRequest,
) )
@@ -16,8 +17,10 @@ from app.services.submissions import (
get_submission, get_submission,
list_submissions, list_submissions,
request_ai_suggestions, request_ai_suggestions,
update_submission_status,
update_suggestion, update_suggestion,
) )
from app.models import SubmissionStatus
from dlib.auth import AuthenticatedUser from dlib.auth import AuthenticatedUser
@@ -40,7 +43,9 @@ async def get_submission_endpoint(
session: AsyncSession = Depends(get_db), session: AsyncSession = Depends(get_db),
user: AuthenticatedUser = Depends(get_current_user), 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: if not submission:
raise HTTPException(status_code=404, detail="Submission not found") raise HTTPException(status_code=404, detail="Submission not found")
return SubmissionResponse.model_validate(submission) return SubmissionResponse.model_validate(submission)
@@ -66,6 +71,24 @@ async def create_submission_endpoint(
return SubmissionResponse.model_validate(submission) 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]) @router.post("/{submission_id}/ai", response_model=list[SuggestionResponse])
async def request_submissions_ai( async def request_submissions_ai(
submission_id: str, submission_id: str,
@@ -85,7 +108,9 @@ async def request_submissions_ai(
return [SuggestionResponse.model_validate(item) for item in suggestions] 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( async def update_suggestion_endpoint(
submission_id: str, submission_id: str,
suggestion_id: str, suggestion_id: str,

View File

@@ -11,6 +11,7 @@ from .cv import (
PublishRequest, PublishRequest,
SubmissionCreateRequest, SubmissionCreateRequest,
SubmissionResponse, SubmissionResponse,
SubmissionStatusUpdateRequest,
SuggestionResponse, SuggestionResponse,
SuggestionUpdateRequest, SuggestionUpdateRequest,
VersionResponse, VersionResponse,
@@ -25,6 +26,7 @@ __all__ = [
"PatchApplyRequest", "PatchApplyRequest",
"SubmissionCreateRequest", "SubmissionCreateRequest",
"SubmissionResponse", "SubmissionResponse",
"SubmissionStatusUpdateRequest",
"AiSuggestionRequest", "AiSuggestionRequest",
"SuggestionResponse", "SuggestionResponse",
"SuggestionUpdateRequest", "SuggestionUpdateRequest",

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@@ -90,6 +90,16 @@ class SubmissionCreateRequest(BaseModel):
job_description: str | None = None job_description: str | None = None
class SubmissionStatusUpdateRequest(BaseModel):
status: Literal[
"draft",
"tailoring",
"pending_review",
"published",
"archived",
]
class SuggestionResponse(BaseModel): class SuggestionResponse(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)

View File

@@ -133,6 +133,22 @@ async def update_suggestion(
return 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( async def _get_version_for_owner(
session: AsyncSession, owner_id: str, version_id: str session: AsyncSession, owner_id: str, version_id: str
) -> CvVersion | None: ) -> CvVersion | None:

View File

@@ -11,7 +11,14 @@ import {
fetchDocuments, fetchSubmissions, fetchPublicAssetAnalytics, getPublicPdfUrl, fetchDocuments, fetchSubmissions, fetchPublicAssetAnalytics, getPublicPdfUrl,
publishVersion, PublicAsset, PublicAssetAnalytics, publishVersion, PublicAsset, PublicAssetAnalytics,
requestAiSuggestions, requestAiSuggestions,
Submission, StructuredBlock, Suggestion, updateSuggestion, uploadDocument, Version, Submission,
SubmissionStatus,
StructuredBlock,
Suggestion,
updateSubmissionStatus,
updateSuggestion,
uploadDocument,
Version,
} from '@/libs/api'; } from '@/libs/api';
// ── helpers ─────────────────────────────────────────────────────────────────── // ── helpers ───────────────────────────────────────────────────────────────────
@@ -20,12 +27,32 @@ function fmt(iso: string) {
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); 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 = ({ const cls = ({
draft: 'badge-draft', tailoring: 'badge-submitted', pending_review: 'badge-interviewing', draft: 'badge-draft', tailoring: 'badge-submitted', pending_review: 'badge-interviewing',
published: 'badge-public', archived: 'badge-closed', published: 'badge-public', archived: 'badge-closed',
} as Record<string, string>)[status] ?? 'badge-draft'; } as Record<SubmissionStatus, string>)[status] ?? 'badge-draft';
return <span className={`badge ${cls}`}>{status.replace('_', ' ')}</span>; return <span className={`badge ${cls}`}>{SUBMISSION_STATUS_LABELS[status] ?? status.replace('_', ' ')}</span>;
} }
// ── modals ──────────────────────────────────────────────────────────────────── // ── modals ────────────────────────────────────────────────────────────────────
@@ -304,20 +331,27 @@ function ContentTab({
// ── submissions tab ─────────────────────────────────────────────────────────── // ── submissions tab ───────────────────────────────────────────────────────────
function SubmissionsTab({ function SubmissionsTab({
submissions, loading, versionId, submissions, loading,
onNewSubmission, onRefresh, onNewSubmission, onRefresh, onStatusChange,
}: { }: {
submissions: Submission[]; submissions: Submission[];
loading: boolean; loading: boolean;
versionId: string;
onNewSubmission: () => void; onNewSubmission: () => void;
onRefresh: () => void; onRefresh: () => void;
onStatusChange: (submissionId: string, status: SubmissionStatus) => void;
}) { }) {
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
const [aiLoading, setAiLoading] = 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 [aiJd, setAiJd] = useState<Record<string, string>>({});
const [suggestions, setSuggestions] = useState<Record<string, Suggestion[]>>({}); 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 loadAi = async (s: Submission) => {
const jd = aiJd[s.id] ?? s.job_description ?? ''; const jd = aiJd[s.id] ?? s.job_description ?? '';
if (!jd.trim()) return; if (!jd.trim()) return;
@@ -340,6 +374,19 @@ function SubmissionsTab({
} catch { /* ignore */ } } 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>; if (loading) return <div style={{ padding: '20px 0', color: 'var(--text-faint)', fontSize: 13 }}>Loading</div>;
return ( return (
@@ -349,6 +396,21 @@ function SubmissionsTab({
<button className="btn btn-ghost" style={{ fontSize: 12 }} onClick={onNewSubmission}>+ New submission</button> <button className="btn btn-ghost" style={{ fontSize: 12 }} onClick={onNewSubmission}>+ New submission</button>
</div> </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 && ( {submissions.length === 0 && (
<div style={{ padding: '20px 0', color: 'var(--text-faint)', fontSize: 13 }}> <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. No submissions yet. Create one to track a job application and get AI tailoring suggestions.
@@ -389,6 +451,20 @@ function SubmissionsTab({
</a> </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 */} {/* AI tailoring */}
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<div className="label" style={{ marginBottom: 6 }}>AI tailoring</div> <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 [recentlyPublishedSlug, setRecentlyPublishedSlug] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<Tab>('content'); const [activeTab, setActiveTab] = useState<Tab>('content');
const [submissions, setSubmissions] = useState<Submission[]>([]); const [submissions, setSubmissions] = useState<Submission[]>([]);
const [allSubmissions, setAllSubmissions] = useState<Submission[]>([]);
const [subsLoading, setSubsLoading] = useState(false); const [subsLoading, setSubsLoading] = useState(false);
const [pendingEdits, setPendingEdits] = useState<Map<string, { old_value: string; new_value: string }>>(new Map()); const [pendingEdits, setPendingEdits] = useState<Map<string, { old_value: string; new_value: string }>>(new Map());
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -493,9 +570,10 @@ export default function Dashboard() {
const [applyError, setApplyError] = useState(''); const [applyError, setApplyError] = useState('');
useEffect(() => { useEffect(() => {
fetchDocuments() Promise.all([fetchDocuments(), fetchSubmissions().catch(() => [])])
.then(d => { .then(([d, allSubs]) => {
setDocs(d); setDocs(d);
setAllSubmissions(allSubs);
if (d.length) { setSelectedDocId(d[0].id); setSelectedVersionId(d[0].root_version_id ?? null); } 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.')) .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 selectedDoc = docs.find(d => d.id === selectedDocId) ?? null;
const selectedVersion = selectedDoc?.versions.find(v => v.id === selectedVersionId) ?? 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 publishedAssets = selectedVersion?.public_assets ?? [];
const sortedPublishedAssets = [...publishedAssets].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); 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(/\/$/, ''); const publicBaseUrl = (process.env.NEXT_PUBLIC_BASE_URL ?? '').replace(/\/$/, '');
@@ -543,6 +628,7 @@ export default function Dashboard() {
const refreshDocs = async () => { const refreshDocs = async () => {
const fresh = await fetchDocuments().catch(() => docs); const fresh = await fetchDocuments().catch(() => docs);
setDocs(fresh); setDocs(fresh);
refreshAllSubs();
return fresh; return fresh;
}; };
@@ -551,6 +637,10 @@ export default function Dashboard() {
fetchSubmissions(selectedVersionId).then(setSubmissions).catch(() => { }); fetchSubmissions(selectedVersionId).then(setSubmissions).catch(() => { });
}; };
function refreshAllSubs() {
fetchSubmissions().then(setAllSubmissions).catch(() => { });
}
const onUploadDone = (doc: Document) => { const onUploadDone = (doc: Document) => {
setDocs(prev => [doc, ...prev.filter(d => d.id !== doc.id)]); setDocs(prev => [doc, ...prev.filter(d => d.id !== doc.id)]);
setSelectedDocId(doc.id); setSelectedDocId(doc.id);
@@ -569,10 +659,16 @@ export default function Dashboard() {
const onSubmissionDone = (s: Submission) => { const onSubmissionDone = (s: Submission) => {
setSubmissions(prev => [s, ...prev]); setSubmissions(prev => [s, ...prev]);
setAllSubmissions(prev => [s, ...prev]);
setModal(null); setModal(null);
setActiveTab('submissions'); 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) => { const stageEdit = (path: string, old_value: string, new_value: string) => {
setPendingEdits(prev => new Map(prev).set(path, { old_value, new_value })); 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 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 hasChildren = selectedDoc?.versions.some(v => v.parent_version_id === versionId);
const msg = hasChildren const msg = hasChildren
? 'Delete this branch and all its sub-branches? This cannot be undone.' ? 'Delete this branch and all its sub-branches? This cannot be undone.'
@@ -767,6 +862,50 @@ export default function Dashboard() {
{selectedVersion && ( {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 */} {/* version header */}
<div style={{ padding: '16px 20px 0', flexShrink: 0 }}> <div style={{ padding: '16px 20px 0', flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12, gap: 12 }}> <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12, gap: 12 }}>
@@ -934,9 +1073,9 @@ export default function Dashboard() {
<SubmissionsTab <SubmissionsTab
submissions={submissions} submissions={submissions}
loading={subsLoading} loading={subsLoading}
versionId={selectedVersionId!}
onNewSubmission={() => setModal('submission')} onNewSubmission={() => setModal('submission')}
onRefresh={refreshSubs} onRefresh={refreshSubs}
onStatusChange={handleSubmissionStatusChange}
/> />
)} )}
</div> </div>

View File

@@ -28,7 +28,6 @@ function Node({ node, depth, selectedId, onSelect, onDelete, colorIndex = 0 }: {
const v = node.version; const v = node.version;
const isRoot = !v.parent_version_id; const isRoot = !v.parent_version_id;
const isSelected = v.id === selectedId; const isSelected = v.id === selectedId;
const isLeaf = node.children.length === 0;
const dotColor = DOT_COLORS[colorIndex % DOT_COLORS.length]; const dotColor = DOT_COLORS[colorIndex % DOT_COLORS.length];
return ( return (
@@ -147,4 +146,3 @@ export default function CVTree({ versions, selectedVersionId, onSelect, onDelete
</div> </div>
); );
} }

View File

@@ -51,6 +51,13 @@ export type Suggestion = {
metadata_json?: { keywords?: string[]; confidence?: number } | null; metadata_json?: { keywords?: string[]; confidence?: number } | null;
}; };
export type SubmissionStatus =
| 'draft'
| 'tailoring'
| 'pending_review'
| 'published'
| 'archived';
export type Submission = { export type Submission = {
id: string; id: string;
version_id: string; version_id: string;
@@ -58,7 +65,7 @@ export type Submission = {
role_title: string; role_title: string;
job_url?: string | null; job_url?: string | null;
job_description?: string | null; job_description?: string | null;
status: string; status: SubmissionStatus;
suggestions: Suggestion[]; suggestions: Suggestion[];
created_at: string; created_at: string;
}; };
@@ -159,8 +166,10 @@ export async function createSubmission(
}); });
} }
export const fetchSubmissions = (versionId: string): Promise<Submission[]> => export const fetchSubmissions = (versionId?: string): Promise<Submission[]> => {
req<Submission[]>(`/api/v1/submissions?version_id=${versionId}`); const query = versionId ? `?version_id=${encodeURIComponent(versionId)}` : '';
return req<Submission[]>(`/api/v1/submissions${query}`);
};
export const fetchSubmission = (id: string): Promise<Submission> => export const fetchSubmission = (id: string): Promise<Submission> =>
req<Submission>(`/api/v1/submissions/${id}`); 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( export async function publishVersion(
versionId?: string | null, versionId?: string | null,
submissionId?: string | null, submissionId?: string | null,