mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
130 lines
3.4 KiB
TypeScript
130 lines
3.4 KiB
TypeScript
/** Shared SH-116 resumable media upload contract (dispatcher + vendor portal). */
|
|
|
|
export const MEDIA_UPLOAD_CHUNK_SIZE_BYTES = 5_242_880;
|
|
|
|
export const MEDIA_UPLOAD_SCANNING_STATE = "Scanning" as const;
|
|
|
|
export type UploadSurface = "WorkOrderTable" | "CompletionMedia" | "VendorPortal";
|
|
|
|
export type UploadCategory = "Before" | "After" | "Extra" | "Aveta";
|
|
|
|
export type MediaKind = "Photo" | "Video";
|
|
|
|
export type MediaUploadState =
|
|
"Created" | "Uploading" | "Scanning" | "Completed" | "Rejected" | "Canceled" | "Expired";
|
|
|
|
export interface MediaUploadLimits {
|
|
photoMaxBytes: number;
|
|
videoMaxBytes: number;
|
|
videoMaxSeconds: number;
|
|
maxPhotos: number;
|
|
maxVideos: number;
|
|
}
|
|
|
|
export interface MediaUploadRemaining {
|
|
photos: number;
|
|
videos: number;
|
|
}
|
|
|
|
export interface MediaUploadPolicy {
|
|
workOrderType: string;
|
|
canUpload: boolean;
|
|
limits: MediaUploadLimits;
|
|
remaining: MediaUploadRemaining;
|
|
}
|
|
|
|
export interface MediaUploadCreateDto {
|
|
idempotencyKey: string;
|
|
fileName: string;
|
|
contentType: string;
|
|
sizeBytes: number;
|
|
category?: UploadCategory;
|
|
surface: UploadSurface;
|
|
}
|
|
|
|
export interface MediaUploadSessionDto {
|
|
uploadId: string;
|
|
state: MediaUploadState;
|
|
chunkSizeBytes: number;
|
|
sizeBytes: number;
|
|
receivedChunks: number[];
|
|
expiresAt: string;
|
|
attachment?: { id: string | number };
|
|
}
|
|
|
|
export interface MediaChunkAckDto {
|
|
index: number;
|
|
sha256: string;
|
|
sizeBytes: number;
|
|
receivedChunks: number[];
|
|
}
|
|
|
|
export const MEDIA_UPLOAD_TERMINAL_STATES: ReadonlySet<MediaUploadState> = new Set([
|
|
"Completed",
|
|
"Rejected",
|
|
"Canceled",
|
|
"Expired",
|
|
]);
|
|
|
|
export function isTerminalMediaUploadState(state: MediaUploadState): boolean {
|
|
return MEDIA_UPLOAD_TERMINAL_STATES.has(state);
|
|
}
|
|
|
|
export function isUploadCategory(value: string | undefined): value is UploadCategory {
|
|
return value === "Before" || value === "After" || value === "Extra" || value === "Aveta";
|
|
}
|
|
|
|
/** Safe, stable user-facing messages. Never render raw server payloads. */
|
|
export function safeMediaUploadErrorMessage(error: Error): string {
|
|
if (error instanceof MediaUploadValidationError) {
|
|
return error.message;
|
|
}
|
|
if (error instanceof MediaUploadRejectedError) {
|
|
return "The file was rejected by the security scan and was not saved.";
|
|
}
|
|
if (error instanceof MediaUploadExpiredError) {
|
|
return "The upload session expired. Please try again.";
|
|
}
|
|
if (error instanceof MediaUploadScanTimeoutError) {
|
|
return "The upload is still processing. Check back shortly to see the result.";
|
|
}
|
|
if (error.name === "AbortError") {
|
|
return "Upload canceled.";
|
|
}
|
|
return "Upload failed. Check your connection and try again.";
|
|
}
|
|
|
|
export class MediaUploadValidationError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "MediaUploadValidationError";
|
|
}
|
|
}
|
|
|
|
export class MediaUploadRejectedError extends Error {
|
|
constructor() {
|
|
super("The file was rejected by the security scan.");
|
|
this.name = "MediaUploadRejectedError";
|
|
}
|
|
}
|
|
|
|
export class MediaUploadExpiredError extends Error {
|
|
constructor() {
|
|
super("The upload session expired.");
|
|
this.name = "MediaUploadExpiredError";
|
|
}
|
|
}
|
|
|
|
export class MediaUploadCanceledError extends Error {
|
|
constructor() {
|
|
super("The upload session was canceled.");
|
|
this.name = "MediaUploadCanceledError";
|
|
}
|
|
}
|
|
|
|
export class MediaUploadScanTimeoutError extends Error {
|
|
constructor() {
|
|
super("The upload is still processing.");
|
|
this.name = "MediaUploadScanTimeoutError";
|
|
}
|
|
}
|