- Add GET /files/{storage_key:path} endpoint to serve stored files
- Add getFile() method to apiService to fetch files from backend
- Update convertProofToFrontend() to preserve fileStorageKey
- Update handleRetryAnalysis() to fetch file from backend when not in memory
- Update handleDownload() to download original file instead of thumbnail
After page refresh, the retry button now fetches the original file from
backend storage using the fileStorageKey, allowing failed proofs to be
reprocessed. The Download Asset button also now downloads the original
uploaded file rather than the preview thumbnail.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
404 lines
13 KiB
TypeScript
Executable file
404 lines
13 KiB
TypeScript
Executable file
import { IPublicClientApplication } from '@azure/msal-browser';
|
|
import { getAccessToken } from './authService';
|
|
import type { AgentReview, FlaggedItem, ResolvedItem, ErrorItem } from '../types';
|
|
|
|
const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000';
|
|
|
|
// Types for API responses
|
|
export interface CampaignResponse {
|
|
id: string;
|
|
name: string;
|
|
workfront_id: string | null;
|
|
client_lead: string | null;
|
|
agency_lead: string | null;
|
|
brand_guidelines: string | null;
|
|
status: string;
|
|
agency: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
proofs: number;
|
|
}
|
|
|
|
export interface ProofVersionResponse {
|
|
id: string;
|
|
version: number;
|
|
file_storage_key: string | null;
|
|
thumbnail_url: string | null;
|
|
agent_review: AgentReview | null;
|
|
overall_status: string | null;
|
|
workfront_id: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ProofResponse {
|
|
id: string;
|
|
proof_name: string;
|
|
channel: string | null;
|
|
sub_channel: string | null;
|
|
proof_type: string | null;
|
|
workfront_id: string | null;
|
|
created_at: string;
|
|
versions: ProofVersionResponse[];
|
|
}
|
|
|
|
export interface AnalyticsResponse {
|
|
total_reviews: number;
|
|
passed: number;
|
|
failed: number;
|
|
errors: number;
|
|
legal_review: number;
|
|
}
|
|
|
|
export interface FlaggedItemResponse {
|
|
id: string;
|
|
proof_version_id: string;
|
|
agent_flagged: string;
|
|
comments: string | null;
|
|
submitter_name: string | null;
|
|
submitter_agency: string | null;
|
|
campaign_name: string | null;
|
|
proof_name: string | null;
|
|
version: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ResolvedItemResponse {
|
|
id: string;
|
|
proof_version_id: string;
|
|
agent: string;
|
|
issue: string | null;
|
|
resolution: string | null;
|
|
submitter_name: string | null;
|
|
submitter_agency: string | null;
|
|
campaign_name: string | null;
|
|
proof_name: string | null;
|
|
version: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ErrorItemResponse {
|
|
id: string;
|
|
proof_version_id: string;
|
|
error_summary: string | null;
|
|
campaign_name: string | null;
|
|
proof_name: string | null;
|
|
version: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
class ApiService {
|
|
private msalInstance: IPublicClientApplication | null = null;
|
|
|
|
setMsalInstance(instance: IPublicClientApplication) {
|
|
this.msalInstance = instance;
|
|
}
|
|
|
|
private async getHeaders(): Promise<HeadersInit> {
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (this.msalInstance) {
|
|
const token = await getAccessToken(this.msalInstance);
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
private async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
const headers = await this.getHeaders();
|
|
const response = await fetch(`${API_URL}/api${endpoint}`, {
|
|
...options,
|
|
headers: {
|
|
...headers,
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
// Handle 204 No Content
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Campaign endpoints
|
|
async getCampaigns(): Promise<CampaignResponse[]> {
|
|
return this.fetch<CampaignResponse[]>('/campaigns');
|
|
}
|
|
|
|
async getCampaign(id: string): Promise<CampaignResponse> {
|
|
return this.fetch<CampaignResponse>(`/campaigns/${id}`);
|
|
}
|
|
|
|
async createCampaign(data: {
|
|
name: string;
|
|
workfront_id?: string;
|
|
client_lead?: string;
|
|
agency_lead?: string;
|
|
brand_guidelines?: string;
|
|
}): Promise<CampaignResponse> {
|
|
return this.fetch<CampaignResponse>('/campaigns', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateCampaign(id: string, data: {
|
|
name?: string;
|
|
workfront_id?: string;
|
|
client_lead?: string;
|
|
agency_lead?: string;
|
|
brand_guidelines?: string;
|
|
status?: string;
|
|
}): Promise<CampaignResponse> {
|
|
return this.fetch<CampaignResponse>(`/campaigns/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteCampaign(id: string): Promise<void> {
|
|
return this.fetch<void>(`/campaigns/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// Proof endpoints
|
|
async getProofs(campaignId: string): Promise<ProofResponse[]> {
|
|
return this.fetch<ProofResponse[]>(`/campaigns/${campaignId}/proofs`);
|
|
}
|
|
|
|
async getProof(id: string): Promise<ProofResponse> {
|
|
return this.fetch<ProofResponse>(`/proofs/${id}`);
|
|
}
|
|
|
|
async getFile(storageKey: string): Promise<File> {
|
|
const headers = await this.getHeaders();
|
|
const response = await fetch(`${API_URL}/api/files/${encodeURIComponent(storageKey)}`, { headers });
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch file: HTTP ${response.status}`);
|
|
}
|
|
const blob = await response.blob();
|
|
const filename = storageKey.split('/').pop() || 'proof';
|
|
return new File([blob], filename, { type: blob.type });
|
|
}
|
|
|
|
async deleteProof(id: string): Promise<void> {
|
|
return this.fetch<void>(`/proofs/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// Audit endpoints
|
|
async flagProofVersion(proofId: string, version: number, data: {
|
|
agent_flagged: string;
|
|
comments?: string;
|
|
}): Promise<FlaggedItemResponse> {
|
|
return this.fetch<FlaggedItemResponse>(`/proofs/${proofId}/versions/${version}/flag`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
proof_version_id: '', // Will be set by backend
|
|
...data,
|
|
}),
|
|
});
|
|
}
|
|
|
|
async resolveProofVersion(proofId: string, version: number, data: {
|
|
agent: string;
|
|
issue?: string;
|
|
resolution?: string;
|
|
}): Promise<ResolvedItemResponse> {
|
|
return this.fetch<ResolvedItemResponse>(`/proofs/${proofId}/versions/${version}/resolve`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
proof_version_id: '', // Will be set by backend
|
|
...data,
|
|
}),
|
|
});
|
|
}
|
|
|
|
async getFlaggedItems(): Promise<FlaggedItemResponse[]> {
|
|
return this.fetch<FlaggedItemResponse[]>('/audit/flagged');
|
|
}
|
|
|
|
async getResolvedItems(): Promise<ResolvedItemResponse[]> {
|
|
return this.fetch<ResolvedItemResponse[]>('/audit/resolved');
|
|
}
|
|
|
|
async getErrorItems(): Promise<ErrorItemResponse[]> {
|
|
return this.fetch<ErrorItemResponse[]>('/audit/errors');
|
|
}
|
|
|
|
// Analytics endpoint
|
|
async getAnalytics(): Promise<AnalyticsResponse> {
|
|
return this.fetch<AnalyticsResponse>('/analytics');
|
|
}
|
|
|
|
// Helper to convert API response to frontend format
|
|
convertCampaignToFrontend(campaign: CampaignResponse) {
|
|
return {
|
|
name: campaign.name,
|
|
workfrontId: campaign.workfront_id || '',
|
|
clientLead: campaign.client_lead || '',
|
|
agency: campaign.agency || '',
|
|
agencyLead: campaign.agency_lead || '',
|
|
proofs: campaign.proofs,
|
|
status: campaign.status as 'In Progress' | 'Completed',
|
|
lastModified: campaign.updated_at,
|
|
brandGuidelines: campaign.brand_guidelines || 'Barclays',
|
|
_id: campaign.id,
|
|
};
|
|
}
|
|
|
|
convertProofToFrontend(proof: ProofResponse) {
|
|
const latestVersion = proof.versions[0];
|
|
return {
|
|
proofName: proof.proof_name,
|
|
channel: proof.channel || '',
|
|
subChannel: proof.sub_channel || '',
|
|
proofType: proof.proof_type || '',
|
|
status: latestVersion?.overall_status === 'Analysis Error' ? 'error' : 'completed' as 'completed' | 'analyzing' | 'error' | 'loading',
|
|
overallStatus: latestVersion?.overall_status as any,
|
|
versions: proof.versions.map(v => ({
|
|
version: v.version,
|
|
timestamp: v.created_at.split('T')[0],
|
|
workfrontId: v.workfront_id || '',
|
|
proofPreviewUrl: v.thumbnail_url || '',
|
|
feedback: v.agent_review || {} as AgentReview,
|
|
overallStatus: v.overall_status as any,
|
|
fileStorageKey: v.file_storage_key || '',
|
|
})),
|
|
_id: proof.id,
|
|
fileStorageKey: latestVersion?.file_storage_key || '',
|
|
};
|
|
}
|
|
|
|
convertFlaggedItemToFrontend(item: FlaggedItemResponse): FlaggedItem {
|
|
return {
|
|
id: item.id,
|
|
campaignName: item.campaign_name || '',
|
|
proofName: item.proof_name || '',
|
|
version: item.version || 1,
|
|
submitter: item.submitter_name || '',
|
|
submitAgency: item.submitter_agency || '',
|
|
agentFlagged: item.agent_flagged,
|
|
comments: item.comments || '',
|
|
timestamp: item.created_at,
|
|
};
|
|
}
|
|
|
|
convertResolvedItemToFrontend(item: ResolvedItemResponse): ResolvedItem {
|
|
return {
|
|
id: item.id,
|
|
campaignName: item.campaign_name || '',
|
|
proofName: item.proof_name || '',
|
|
version: item.version || 1,
|
|
submitter: item.submitter_name || '',
|
|
submitAgency: item.submitter_agency || '',
|
|
agent: item.agent,
|
|
issue: item.issue || '',
|
|
resolution: item.resolution || '',
|
|
timestamp: item.created_at,
|
|
};
|
|
}
|
|
|
|
convertErrorItemToFrontend(item: ErrorItemResponse): ErrorItem {
|
|
return {
|
|
id: item.id,
|
|
campaignName: item.campaign_name || '',
|
|
proofName: item.proof_name || '',
|
|
version: item.version || 1,
|
|
submitter: '',
|
|
submitAgency: '',
|
|
errorSummary: item.error_summary || '',
|
|
timestamp: item.created_at,
|
|
};
|
|
}
|
|
|
|
// Dropdown options endpoints
|
|
async getDropdownOptions(): Promise<DropdownOptionsResponse> {
|
|
const response = await this.fetch<DropdownOptionsResponse>('/dropdown-options');
|
|
// Debug logging
|
|
console.log('[DEBUG API Response] Raw dropdown options:', JSON.stringify(response, null, 2));
|
|
console.log('[DEBUG API Response] Social.Meta proof types:', response.channels?.Social?.Meta);
|
|
return response;
|
|
}
|
|
|
|
async addChannel(name: string): Promise<void> {
|
|
await this.fetch<void>(`/dropdown-options/channels?name=${encodeURIComponent(name)}`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async addSubChannel(channel: string, name: string): Promise<void> {
|
|
await this.fetch<void>(`/dropdown-options/channels/${encodeURIComponent(channel)}/sub-channels?name=${encodeURIComponent(name)}`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async addProofType(channel: string, subChannel: string, name: string): Promise<void> {
|
|
await this.fetch<void>(`/dropdown-options/channels/${encodeURIComponent(channel)}/sub-channels/${encodeURIComponent(subChannel)}/proof-types?name=${encodeURIComponent(name)}`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async deleteChannel(channel: string): Promise<void> {
|
|
await this.fetch<void>(`/dropdown-options/channels/${encodeURIComponent(channel)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async deleteSubChannel(channel: string, subChannel: string): Promise<void> {
|
|
await this.fetch<void>(`/dropdown-options/channels/${encodeURIComponent(channel)}/sub-channels/${encodeURIComponent(subChannel)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async deleteProofType(channel: string, subChannel: string, proofType: string): Promise<void> {
|
|
await this.fetch<void>(`/dropdown-options/channels/${encodeURIComponent(channel)}/sub-channels/${encodeURIComponent(subChannel)}/proof-types/${encodeURIComponent(proofType)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// Agency endpoints
|
|
async getAgencies(): Promise<AgencyResponse[]> {
|
|
return this.fetch<AgencyResponse[]>('/agencies');
|
|
}
|
|
|
|
// Support email endpoint
|
|
async sendSupportEmail(data: {
|
|
message: string;
|
|
subject: string;
|
|
user_name?: string;
|
|
user_email?: string;
|
|
}): Promise<{ success: boolean; message: string }> {
|
|
return this.fetch('/support/email', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
}
|
|
|
|
export interface DropdownOptionsResponse {
|
|
campaigns: string[];
|
|
channels: Record<string, Record<string, string[]>>;
|
|
brand_guidelines: string[];
|
|
}
|
|
|
|
export interface AgencyResponse {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
export const apiService = new ApiService();
|
|
export default apiService;
|