- Add persistent Docker volume for file storage to fix 404 download errors - Set FILE_STORAGE_PATH env var in Dockerfile and docker-compose.yml - Increase thumbnail generation limit from 500KB to 10MB for images - Remove encodeURIComponent from file download URL to prevent path encoding 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/${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;
|