generated from coulomb/repo-seed
Add document lifecycle tracking with order/production status
- Extended PDFStatus with full lifecycle: pending → uploading → in_basket → ordering → in_production → sent/canceled - Added shipDocument() and getDocumentStatus() API methods - Grouped UI sections: Ready to Upload, In Basket, In Production, Completed - Order button for documents in basket to place production order - Refresh button to check current status from Binect server - Display price and recipient address for uploaded documents - Status icons and color-coded indicators for each state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,16 +9,29 @@ import { DetectedPDF } from './pdf-detector';
|
||||
|
||||
const STORAGE_KEY = 'pdfQueue';
|
||||
const MAX_ENTRIES = 50;
|
||||
const UPLOADED_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const SENT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const FAILED_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
export type PDFStatus = 'pending' | 'uploading' | 'uploaded' | 'failed';
|
||||
export type PDFStatus =
|
||||
| 'pending' // Not yet uploaded
|
||||
| 'uploading' // Upload in progress
|
||||
| 'failed' // Upload failed
|
||||
| 'in_basket' // Uploaded, SHIPPABLE, awaiting order
|
||||
| 'ordering' // Order in progress
|
||||
| 'in_production' // PRODUCTION_QUEUE or PRINTING
|
||||
| 'sent' // SENT - terminal
|
||||
| 'canceled'; // CANCELED - terminal
|
||||
|
||||
export interface PDFQueueEntry extends DetectedPDF {
|
||||
status: PDFStatus;
|
||||
uploadedAt?: number;
|
||||
binectDocumentId?: number;
|
||||
binectStatus?: number; // DocumentStatus code from Binect (1-7)
|
||||
binectStatusText?: string; // Human-readable status from Binect
|
||||
price?: number; // Price in euro cents
|
||||
recipientAddress?: string; // Extracted recipient address
|
||||
errorMessage?: string;
|
||||
uploadedAt?: number;
|
||||
orderedAt?: number;
|
||||
}
|
||||
|
||||
interface PDFQueueState {
|
||||
@@ -57,7 +70,9 @@ export async function addPDF(pdf: DetectedPDF): Promise<PDFQueueEntry | null> {
|
||||
// Check for duplicate by URL
|
||||
const existing = state.entries.find(e => e.url === pdf.url);
|
||||
if (existing) {
|
||||
if (existing.status === 'uploaded') {
|
||||
// Skip if already uploaded (in basket, production, or completed)
|
||||
const uploadedStatuses: PDFStatus[] = ['in_basket', 'ordering', 'in_production', 'sent', 'canceled'];
|
||||
if (uploadedStatuses.includes(existing.status)) {
|
||||
console.log('[PDF Queue] PDF already uploaded, skipping:', pdf.filename);
|
||||
return null;
|
||||
}
|
||||
@@ -82,13 +97,25 @@ export async function addPDF(pdf: DetectedPDF): Promise<PDFQueueEntry | null> {
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for status updates
|
||||
*/
|
||||
export interface PDFStatusMeta {
|
||||
binectDocumentId?: number;
|
||||
binectStatus?: number;
|
||||
binectStatusText?: string;
|
||||
price?: number;
|
||||
recipientAddress?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of a PDF in the queue
|
||||
*/
|
||||
export async function updatePDFStatus(
|
||||
id: string,
|
||||
status: PDFStatus,
|
||||
meta?: { binectDocumentId?: number; errorMessage?: string }
|
||||
meta?: PDFStatusMeta
|
||||
): Promise<void> {
|
||||
const state = await loadQueue();
|
||||
const entry = state.entries.find(e => e.id === id);
|
||||
@@ -100,15 +127,32 @@ export async function updatePDFStatus(
|
||||
|
||||
entry.status = status;
|
||||
|
||||
if (status === 'uploaded') {
|
||||
entry.uploadedAt = Date.now();
|
||||
if (meta?.binectDocumentId) {
|
||||
entry.binectDocumentId = meta.binectDocumentId;
|
||||
}
|
||||
// Update Binect-specific fields
|
||||
if (meta?.binectDocumentId !== undefined) {
|
||||
entry.binectDocumentId = meta.binectDocumentId;
|
||||
}
|
||||
if (meta?.binectStatus !== undefined) {
|
||||
entry.binectStatus = meta.binectStatus;
|
||||
}
|
||||
if (meta?.binectStatusText !== undefined) {
|
||||
entry.binectStatusText = meta.binectStatusText;
|
||||
}
|
||||
if (meta?.price !== undefined) {
|
||||
entry.price = meta.price;
|
||||
}
|
||||
if (meta?.recipientAddress !== undefined) {
|
||||
entry.recipientAddress = meta.recipientAddress;
|
||||
}
|
||||
if (meta?.errorMessage !== undefined) {
|
||||
entry.errorMessage = meta.errorMessage;
|
||||
}
|
||||
|
||||
if (status === 'failed' && meta?.errorMessage) {
|
||||
entry.errorMessage = meta.errorMessage;
|
||||
// Set timestamps based on status
|
||||
if (status === 'in_basket' && !entry.uploadedAt) {
|
||||
entry.uploadedAt = Date.now();
|
||||
}
|
||||
if (status === 'in_production' && !entry.orderedAt) {
|
||||
entry.orderedAt = Date.now();
|
||||
}
|
||||
|
||||
await saveQueue(state);
|
||||
@@ -133,19 +177,45 @@ export async function removePDF(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending and failed PDFs (for display in popup)
|
||||
* Get all PDFs for display in popup (all non-terminal statuses + recent terminal)
|
||||
*/
|
||||
export async function getPendingPDFs(): Promise<PDFQueueEntry[]> {
|
||||
export async function getAllPDFs(): Promise<PDFQueueEntry[]> {
|
||||
const state = await loadQueue();
|
||||
return state.entries.filter(e => e.status === 'pending' || e.status === 'failed');
|
||||
return state.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of pending PDFs (for badge)
|
||||
* Get PDFs that need user action (pending, failed, in_basket)
|
||||
*/
|
||||
export async function getActionablePDFs(): Promise<PDFQueueEntry[]> {
|
||||
const state = await loadQueue();
|
||||
return state.entries.filter(e =>
|
||||
e.status === 'pending' ||
|
||||
e.status === 'failed' ||
|
||||
e.status === 'in_basket'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of PDFs needing action (for badge)
|
||||
*/
|
||||
export async function getActionableCount(): Promise<number> {
|
||||
const actionable = await getActionablePDFs();
|
||||
return actionable.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy: Get pending PDFs (for backward compatibility)
|
||||
*/
|
||||
export async function getPendingPDFs(): Promise<PDFQueueEntry[]> {
|
||||
return getActionablePDFs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy: Get pending count (for backward compatibility)
|
||||
*/
|
||||
export async function getPendingCount(): Promise<number> {
|
||||
const pending = await getPendingPDFs();
|
||||
return pending.length;
|
||||
return getActionableCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,15 +227,21 @@ export async function cleanupOldEntries(): Promise<void> {
|
||||
const initialCount = state.entries.length;
|
||||
|
||||
state.entries = state.entries.filter(entry => {
|
||||
// Always keep pending entries
|
||||
if (entry.status === 'pending' || entry.status === 'uploading') {
|
||||
// Always keep active entries
|
||||
if (
|
||||
entry.status === 'pending' ||
|
||||
entry.status === 'uploading' ||
|
||||
entry.status === 'in_basket' ||
|
||||
entry.status === 'ordering' ||
|
||||
entry.status === 'in_production'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove uploaded entries older than 7 days
|
||||
if (entry.status === 'uploaded' && entry.uploadedAt) {
|
||||
const age = now - entry.uploadedAt;
|
||||
if (age > UPLOADED_MAX_AGE_MS) {
|
||||
// Remove sent/canceled entries older than 7 days
|
||||
if (entry.status === 'sent' || entry.status === 'canceled') {
|
||||
const age = now - (entry.orderedAt || entry.uploadedAt || entry.timestamp);
|
||||
if (age > SENT_MAX_AGE_MS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -188,36 +264,30 @@ export async function cleanupOldEntries(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce maximum entries by removing oldest uploaded/failed entries
|
||||
* Enforce maximum entries by removing oldest terminal entries
|
||||
*/
|
||||
async function enforceMaxEntries(state: PDFQueueState): Promise<void> {
|
||||
const terminalStatuses: PDFStatus[] = ['sent', 'canceled', 'failed'];
|
||||
|
||||
while (state.entries.length > MAX_ENTRIES) {
|
||||
// Find oldest uploaded entry
|
||||
let removeIndex = -1;
|
||||
let oldestTime = Infinity;
|
||||
|
||||
// Find oldest terminal entry (sent, canceled, failed)
|
||||
for (let i = state.entries.length - 1; i >= 0; i--) {
|
||||
const entry = state.entries[i];
|
||||
if (entry.status === 'uploaded' && entry.uploadedAt && entry.uploadedAt < oldestTime) {
|
||||
oldestTime = entry.uploadedAt;
|
||||
removeIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// If no uploaded entries, find oldest failed
|
||||
if (removeIndex === -1) {
|
||||
for (let i = state.entries.length - 1; i >= 0; i--) {
|
||||
const entry = state.entries[i];
|
||||
if (entry.status === 'failed' && entry.timestamp < oldestTime) {
|
||||
oldestTime = entry.timestamp;
|
||||
if (terminalStatuses.includes(entry.status)) {
|
||||
const entryTime = entry.orderedAt || entry.uploadedAt || entry.timestamp;
|
||||
if (entryTime < oldestTime) {
|
||||
oldestTime = entryTime;
|
||||
removeIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still nothing, we can't remove any more (all pending)
|
||||
// If no terminal entries, we can't remove any more
|
||||
if (removeIndex === -1) {
|
||||
console.warn('[PDF Queue] Max entries reached, but all are pending');
|
||||
console.warn('[PDF Queue] Max entries reached, but all are active');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user