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:
@@ -7,13 +7,16 @@ import { startPDFDetection, DetectedPDF } from '../utils/pdf-detector';
|
|||||||
import { loadCredentials } from '../utils/storage';
|
import { loadCredentials } from '../utils/storage';
|
||||||
import {
|
import {
|
||||||
addPDF,
|
addPDF,
|
||||||
getPendingCount,
|
getActionableCount,
|
||||||
|
getAllPDFs,
|
||||||
getPendingPDFs,
|
getPendingPDFs,
|
||||||
updatePDFStatus,
|
updatePDFStatus,
|
||||||
removePDF,
|
removePDF,
|
||||||
cleanupOldEntries,
|
cleanupOldEntries,
|
||||||
PDFStatus
|
PDFStatus,
|
||||||
|
PDFStatusMeta
|
||||||
} from '../utils/pdf-queue';
|
} from '../utils/pdf-queue';
|
||||||
|
import { shipDocument, getDocumentStatus } from '../utils/binect-api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize extension on install
|
* Initialize extension on install
|
||||||
@@ -78,10 +81,10 @@ async function checkAndDeleteExpiredCredentials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update badge with pending PDF count
|
* Update badge with actionable PDF count
|
||||||
*/
|
*/
|
||||||
async function updateBadge() {
|
async function updateBadge() {
|
||||||
const count = await getPendingCount();
|
const count = await getActionableCount();
|
||||||
const text = count > 0 ? count.toString() : '•';
|
const text = count > 0 ? count.toString() : '•';
|
||||||
chrome.action.setBadgeText({ text });
|
chrome.action.setBadgeText({ text });
|
||||||
chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue
|
chrome.action.setBadgeBackgroundColor({ color: '#4A90E2' }); // Binect Blue
|
||||||
@@ -117,6 +120,16 @@ startPDFDetection(async (pdf: DetectedPDF) => {
|
|||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
console.log('[Service Worker] Message received:', request.action);
|
console.log('[Service Worker] Message received:', request.action);
|
||||||
|
|
||||||
|
// Get all PDFs (including completed ones for display)
|
||||||
|
if (request.action === 'getAllPDFs') {
|
||||||
|
getAllPDFs().then(entries => {
|
||||||
|
console.log('[Service Worker] Returning all PDFs:', entries.length, 'entries');
|
||||||
|
sendResponse({ entries });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy: Get only actionable PDFs
|
||||||
if (request.action === 'getPDFQueue') {
|
if (request.action === 'getPDFQueue') {
|
||||||
getPendingPDFs().then(entries => {
|
getPendingPDFs().then(entries => {
|
||||||
console.log('[Service Worker] Returning PDF queue:', entries.length, 'entries');
|
console.log('[Service Worker] Returning PDF queue:', entries.length, 'entries');
|
||||||
@@ -126,7 +139,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request.action === 'updatePDFStatus') {
|
if (request.action === 'updatePDFStatus') {
|
||||||
const { id, status, meta } = request as { id: string; status: PDFStatus; meta?: object };
|
const { id, status, meta } = request as { id: string; status: PDFStatus; meta?: PDFStatusMeta };
|
||||||
updatePDFStatus(id, status, meta).then(() => {
|
updatePDFStatus(id, status, meta).then(() => {
|
||||||
return updateBadge();
|
return updateBadge();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -144,9 +157,50 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ship a document (place order for production)
|
||||||
|
if (request.action === 'shipDocument') {
|
||||||
|
const { documentId, username, password } = request as {
|
||||||
|
documentId: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
shipDocument(documentId, username, password)
|
||||||
|
.then(result => {
|
||||||
|
sendResponse({ success: true, ...result });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to ship document'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get document status from Binect
|
||||||
|
if (request.action === 'getDocumentStatus') {
|
||||||
|
const { documentId, username, password } = request as {
|
||||||
|
documentId: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
getDocumentStatus(documentId, username, password)
|
||||||
|
.then(result => {
|
||||||
|
sendResponse({ success: true, ...result });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to get status'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Legacy handlers for backward compatibility
|
// Legacy handlers for backward compatibility
|
||||||
if (request.action === 'getLastPDF') {
|
if (request.action === 'getLastPDF') {
|
||||||
// Return the first pending PDF for backward compatibility
|
|
||||||
getPendingPDFs().then(entries => {
|
getPendingPDFs().then(entries => {
|
||||||
const pdf = entries.length > 0 ? entries[0] : null;
|
const pdf = entries.length > 0 ? entries[0] : null;
|
||||||
sendResponse({ pdf });
|
sendResponse({ pdf });
|
||||||
|
|||||||
@@ -311,16 +311,34 @@ body {
|
|||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status-specific item styles */
|
||||||
.pdf-list-item.uploading {
|
.pdf-list-item.uploading {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-list-item.uploaded {
|
.pdf-list-item.in-basket {
|
||||||
|
background: rgba(74, 144, 226, 0.1);
|
||||||
|
border-left: 3px solid var(--binect-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-list-item.in-production {
|
||||||
|
background: rgba(0, 188, 212, 0.1);
|
||||||
|
border-left: 3px solid var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-list-item.sent {
|
||||||
background: rgba(76, 175, 80, 0.1);
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
border-left: 3px solid var(--signal-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-list-item.canceled {
|
||||||
|
background: rgba(153, 153, 153, 0.1);
|
||||||
|
border-left: 3px solid var(--text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-list-item.failed {
|
.pdf-list-item.failed {
|
||||||
background: rgba(229, 57, 53, 0.1);
|
background: rgba(229, 57, 53, 0.1);
|
||||||
|
border-left: 3px solid var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-item-icon {
|
.pdf-item-icon {
|
||||||
@@ -361,6 +379,66 @@ body {
|
|||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-item-status.in-basket {
|
||||||
|
color: var(--binect-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-item-status.in-production {
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-item-status.sent {
|
||||||
|
color: var(--signal-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-item-status.canceled {
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price display */
|
||||||
|
.pdf-price {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--binect-blue-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recipient address */
|
||||||
|
.pdf-item-recipient {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section headers */
|
||||||
|
.pdf-section {
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-section-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
padding-bottom: var(--spacing-xs);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-section-completed {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-section-completed .pdf-section-header {
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-item-actions {
|
.pdf-item-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -405,6 +483,52 @@ body {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Order button */
|
||||||
|
.btn-order-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
min-height: auto;
|
||||||
|
background: var(--signal-green);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-order-item:hover {
|
||||||
|
background: #43a047;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-order-item:disabled {
|
||||||
|
background: var(--border-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Refresh button */
|
||||||
|
.btn-refresh-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
min-height: auto;
|
||||||
|
background: var(--light-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh-item:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove button */
|
||||||
|
.btn-remove {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
/* Status Messages */
|
/* Status Messages */
|
||||||
.status-message {
|
.status-message {
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
import './popup.css';
|
import './popup.css';
|
||||||
import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage';
|
import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage';
|
||||||
import { uploadPDF, testConnection, BinectAPIError } from '../utils/binect-api';
|
import { uploadPDF, testConnection, BinectAPIError, Document } from '../utils/binect-api';
|
||||||
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
|
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
|
||||||
import { addTrackingEntry } from '../tracking/tracker';
|
import { addTrackingEntry } from '../tracking/tracker';
|
||||||
import { PDFQueueEntry, PDFStatus } from '../utils/pdf-queue';
|
import { PDFQueueEntry, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue';
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const authView = document.getElementById('authView')!;
|
const authView = document.getElementById('authView')!;
|
||||||
@@ -152,8 +152,8 @@ async function handleLogin(e: Event) {
|
|||||||
async function loadPDFQueue() {
|
async function loadPDFQueue() {
|
||||||
console.log('[Popup] Loading PDF queue...');
|
console.log('[Popup] Loading PDF queue...');
|
||||||
|
|
||||||
// Get queue from background script
|
// Get all PDFs from background script (including completed ones)
|
||||||
const response = await chrome.runtime.sendMessage({ action: 'getPDFQueue' });
|
const response = await chrome.runtime.sendMessage({ action: 'getAllPDFs' });
|
||||||
pdfQueue = response?.entries || [];
|
pdfQueue = response?.entries || [];
|
||||||
console.log('[Popup] Got', pdfQueue.length, 'entries from background');
|
console.log('[Popup] Got', pdfQueue.length, 'entries from background');
|
||||||
|
|
||||||
@@ -238,13 +238,19 @@ async function checkRecentDownloads(): Promise<DetectedPDF[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the PDF list
|
* Render the PDF list with grouped sections
|
||||||
*/
|
*/
|
||||||
function renderPDFList() {
|
function renderPDFList() {
|
||||||
// Filter to only pending and failed PDFs
|
// Group PDFs by status category
|
||||||
const pending = pdfQueue.filter(p => p.status === 'pending' || p.status === 'failed');
|
const pendingUpload = pdfQueue.filter(p => p.status === 'pending' || p.status === 'uploading' || p.status === 'failed');
|
||||||
|
const inBasket = pdfQueue.filter(p => p.status === 'in_basket' || p.status === 'ordering');
|
||||||
|
const inProduction = pdfQueue.filter(p => p.status === 'in_production');
|
||||||
|
const completed = pdfQueue.filter(p => p.status === 'sent' || p.status === 'canceled');
|
||||||
|
|
||||||
if (pending.length === 0) {
|
// Count actionable items
|
||||||
|
const actionableCount = pendingUpload.length + inBasket.length;
|
||||||
|
|
||||||
|
if (pdfQueue.length === 0) {
|
||||||
showNoPDF();
|
showNoPDF();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -253,27 +259,117 @@ function renderPDFList() {
|
|||||||
pdfListView.style.display = 'block';
|
pdfListView.style.display = 'block';
|
||||||
|
|
||||||
// Update count
|
// Update count
|
||||||
pdfCount.textContent = `${pending.length} PDF${pending.length > 1 ? 's' : ''} ready`;
|
if (actionableCount > 0) {
|
||||||
|
pdfCount.textContent = `${actionableCount} PDF${actionableCount > 1 ? 's' : ''} need attention`;
|
||||||
|
} else if (inProduction.length > 0) {
|
||||||
|
pdfCount.textContent = `${inProduction.length} in production`;
|
||||||
|
} else {
|
||||||
|
pdfCount.textContent = `${completed.length} completed`;
|
||||||
|
}
|
||||||
|
|
||||||
// Render list items
|
// Build HTML for each section
|
||||||
pdfList.innerHTML = pending.map(pdf => `
|
let html = '';
|
||||||
<div class="pdf-list-item ${pdf.status}" data-id="${escapeHtml(pdf.id)}">
|
|
||||||
<div class="pdf-item-icon">📄</div>
|
if (pendingUpload.length > 0) {
|
||||||
<div class="pdf-item-details">
|
html += `<div class="pdf-section">
|
||||||
<div class="pdf-item-filename" title="${escapeHtml(pdf.filename)}">${escapeHtml(pdf.filename)}</div>
|
<div class="pdf-section-header">Ready to Upload</div>
|
||||||
<div class="pdf-item-meta">${formatFileSize(pdf.size)} · ${escapeHtml(pdf.sourceDomain)}</div>
|
${pendingUpload.map(pdf => renderPDFItem(pdf, 'pending')).join('')}
|
||||||
<div class="pdf-item-status ${pdf.status === 'failed' ? 'error' : ''}">${getStatusText(pdf)}</div>
|
</div>`;
|
||||||
</div>
|
}
|
||||||
<div class="pdf-item-actions">
|
|
||||||
|
if (inBasket.length > 0) {
|
||||||
|
html += `<div class="pdf-section">
|
||||||
|
<div class="pdf-section-header">In Basket</div>
|
||||||
|
${inBasket.map(pdf => renderPDFItem(pdf, 'basket')).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inProduction.length > 0) {
|
||||||
|
html += `<div class="pdf-section">
|
||||||
|
<div class="pdf-section-header">In Production</div>
|
||||||
|
${inProduction.map(pdf => renderPDFItem(pdf, 'production')).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completed.length > 0) {
|
||||||
|
// Show only last 5 completed items
|
||||||
|
const recentCompleted = completed.slice(0, 5);
|
||||||
|
html += `<div class="pdf-section pdf-section-completed">
|
||||||
|
<div class="pdf-section-header">Recently Completed</div>
|
||||||
|
${recentCompleted.map(pdf => renderPDFItem(pdf, 'completed')).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfList.innerHTML = html;
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
setupPDFListEventListeners();
|
||||||
|
hideStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single PDF item
|
||||||
|
*/
|
||||||
|
function renderPDFItem(pdf: PDFQueueEntry, section: 'pending' | 'basket' | 'production' | 'completed'): string {
|
||||||
|
const statusClass = getStatusClass(pdf.status);
|
||||||
|
const statusText = getStatusText(pdf);
|
||||||
|
const priceText = pdf.price ? `${(pdf.price / 100).toFixed(2)} €` : '';
|
||||||
|
|
||||||
|
let actionsHtml = '';
|
||||||
|
|
||||||
|
switch (section) {
|
||||||
|
case 'pending':
|
||||||
|
actionsHtml = `
|
||||||
<button class="btn-send-item" data-id="${escapeHtml(pdf.id)}" ${pdf.status === 'uploading' ? 'disabled' : ''}>
|
<button class="btn-send-item" data-id="${escapeHtml(pdf.id)}" ${pdf.status === 'uploading' ? 'disabled' : ''}>
|
||||||
${pdf.status === 'uploading' ? 'Sending...' : (pdf.status === 'failed' ? 'Retry' : 'Send')}
|
${pdf.status === 'uploading' ? 'Uploading...' : (pdf.status === 'failed' ? 'Retry' : 'Upload')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-dismiss" data-id="${escapeHtml(pdf.id)}">Dismiss</button>
|
<button class="btn-dismiss" data-id="${escapeHtml(pdf.id)}">Dismiss</button>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'basket':
|
||||||
|
actionsHtml = `
|
||||||
|
<button class="btn-order-item" data-id="${escapeHtml(pdf.id)}" ${pdf.status === 'ordering' ? 'disabled' : ''}>
|
||||||
|
${pdf.status === 'ordering' ? 'Ordering...' : 'Order'}
|
||||||
|
</button>
|
||||||
|
<button class="btn-dismiss" data-id="${escapeHtml(pdf.id)}">Cancel</button>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'production':
|
||||||
|
actionsHtml = `
|
||||||
|
<button class="btn-refresh-item" data-id="${escapeHtml(pdf.id)}">Refresh</button>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
actionsHtml = `
|
||||||
|
<button class="btn-dismiss btn-remove" data-id="${escapeHtml(pdf.id)}">Remove</button>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="pdf-list-item ${statusClass}" data-id="${escapeHtml(pdf.id)}">
|
||||||
|
<div class="pdf-item-icon">${getStatusIcon(pdf.status)}</div>
|
||||||
|
<div class="pdf-item-details">
|
||||||
|
<div class="pdf-item-filename" title="${escapeHtml(pdf.filename)}">${escapeHtml(pdf.filename)}</div>
|
||||||
|
<div class="pdf-item-meta">
|
||||||
|
${formatFileSize(pdf.size)} · ${escapeHtml(pdf.sourceDomain)}
|
||||||
|
${priceText ? ` · <span class="pdf-price">${priceText}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
${pdf.recipientAddress ? `<div class="pdf-item-recipient">${escapeHtml(pdf.recipientAddress.split('\n')[0])}</div>` : ''}
|
||||||
|
<div class="pdf-item-status ${statusClass}">${statusText}</div>
|
||||||
|
</div>
|
||||||
|
<div class="pdf-item-actions">
|
||||||
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add event listeners to buttons
|
/**
|
||||||
|
* Setup event listeners for PDF list items
|
||||||
|
*/
|
||||||
|
function setupPDFListEventListeners() {
|
||||||
|
// Upload/Send buttons
|
||||||
pdfList.querySelectorAll('.btn-send-item').forEach(btn => {
|
pdfList.querySelectorAll('.btn-send-item').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
const id = (e.target as HTMLElement).dataset.id;
|
const id = (e.target as HTMLElement).dataset.id;
|
||||||
@@ -281,14 +377,29 @@ function renderPDFList() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Order buttons
|
||||||
|
pdfList.querySelectorAll('.btn-order-item').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const id = (e.target as HTMLElement).dataset.id;
|
||||||
|
if (id) handleOrderPDF(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh buttons
|
||||||
|
pdfList.querySelectorAll('.btn-refresh-item').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const id = (e.target as HTMLElement).dataset.id;
|
||||||
|
if (id) handleRefreshStatus(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dismiss/Cancel/Remove buttons
|
||||||
pdfList.querySelectorAll('.btn-dismiss').forEach(btn => {
|
pdfList.querySelectorAll('.btn-dismiss').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
const id = (e.target as HTMLElement).dataset.id;
|
const id = (e.target as HTMLElement).dataset.id;
|
||||||
if (id) handleDismissPDF(id);
|
if (id) handleDismissPDF(id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
hideStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,17 +407,80 @@ function renderPDFList() {
|
|||||||
*/
|
*/
|
||||||
function getStatusText(pdf: PDFQueueEntry): string {
|
function getStatusText(pdf: PDFQueueEntry): string {
|
||||||
switch (pdf.status) {
|
switch (pdf.status) {
|
||||||
|
case 'pending':
|
||||||
|
return formatTimestamp(pdf.timestamp);
|
||||||
case 'uploading':
|
case 'uploading':
|
||||||
return 'Uploading...';
|
return 'Uploading...';
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return pdf.errorMessage || 'Upload failed';
|
return pdf.errorMessage || 'Upload failed';
|
||||||
|
case 'in_basket':
|
||||||
|
return pdf.binectStatusText || 'Ready to order';
|
||||||
|
case 'ordering':
|
||||||
|
return 'Ordering...';
|
||||||
|
case 'in_production':
|
||||||
|
return pdf.binectStatusText || 'In production';
|
||||||
|
case 'sent':
|
||||||
|
return 'Sent successfully';
|
||||||
|
case 'canceled':
|
||||||
|
return 'Canceled';
|
||||||
default:
|
default:
|
||||||
return formatTimestamp(pdf.timestamp);
|
return formatTimestamp(pdf.timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle send PDF
|
* Get CSS class for status
|
||||||
|
*/
|
||||||
|
function getStatusClass(status: PDFStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'pending';
|
||||||
|
case 'uploading':
|
||||||
|
case 'ordering':
|
||||||
|
return 'uploading';
|
||||||
|
case 'failed':
|
||||||
|
return 'failed';
|
||||||
|
case 'in_basket':
|
||||||
|
return 'in-basket';
|
||||||
|
case 'in_production':
|
||||||
|
return 'in-production';
|
||||||
|
case 'sent':
|
||||||
|
return 'sent';
|
||||||
|
case 'canceled':
|
||||||
|
return 'canceled';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get icon for status
|
||||||
|
*/
|
||||||
|
function getStatusIcon(status: PDFStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return '📄';
|
||||||
|
case 'uploading':
|
||||||
|
return '⏳';
|
||||||
|
case 'failed':
|
||||||
|
return '❌';
|
||||||
|
case 'in_basket':
|
||||||
|
return '🛒';
|
||||||
|
case 'ordering':
|
||||||
|
return '⏳';
|
||||||
|
case 'in_production':
|
||||||
|
return '🏭';
|
||||||
|
case 'sent':
|
||||||
|
return '✅';
|
||||||
|
case 'canceled':
|
||||||
|
return '🚫';
|
||||||
|
default:
|
||||||
|
return '📄';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle send PDF (upload to Binect)
|
||||||
*/
|
*/
|
||||||
async function handleSendPDF(id: string) {
|
async function handleSendPDF(id: string) {
|
||||||
const pdf = pdfQueue.find(p => p.id === id);
|
const pdf = pdfQueue.find(p => p.id === id);
|
||||||
@@ -349,19 +523,38 @@ async function handleSendPDF(id: string) {
|
|||||||
// Update last use timestamp
|
// Update last use timestamp
|
||||||
await updateLastUse();
|
await updateLastUse();
|
||||||
|
|
||||||
// Update status to uploaded
|
// Extract price and recipient from document response
|
||||||
|
const meta: PDFStatusMeta = {
|
||||||
|
binectDocumentId: document.id,
|
||||||
|
binectStatus: document.status.code,
|
||||||
|
binectStatusText: document.status.text
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.letter?.letterData) {
|
||||||
|
meta.price = document.letter.letterData.price?.priceAfterTax;
|
||||||
|
meta.recipientAddress = document.letter.letterData.recipientAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status to in_basket (document is now SHIPPABLE)
|
||||||
await chrome.runtime.sendMessage({
|
await chrome.runtime.sendMessage({
|
||||||
action: 'updatePDFStatus',
|
action: 'updatePDFStatus',
|
||||||
id,
|
id,
|
||||||
status: 'uploaded',
|
status: 'in_basket',
|
||||||
meta: { binectDocumentId: document.id }
|
meta
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove from local queue
|
// Update local state
|
||||||
pdfQueue = pdfQueue.filter(p => p.id !== id);
|
pdf.status = 'in_basket';
|
||||||
|
pdf.binectDocumentId = document.id;
|
||||||
|
pdf.binectStatus = document.status.code;
|
||||||
|
pdf.binectStatusText = document.status.text;
|
||||||
|
if (document.letter?.letterData) {
|
||||||
|
pdf.price = document.letter.letterData.price?.priceAfterTax;
|
||||||
|
pdf.recipientAddress = document.letter.letterData.recipientAddress;
|
||||||
|
}
|
||||||
renderPDFList();
|
renderPDFList();
|
||||||
|
|
||||||
showStatus(`Sent! Document ID: ${document.id}`, 'success');
|
showStatus(`Uploaded! Ready to order (${(pdf.price || 0) / 100} €)`, 'success');
|
||||||
|
|
||||||
// Hide status after 3 seconds
|
// Hide status after 3 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -412,6 +605,154 @@ async function handleSendPDF(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle order PDF (ship document to production)
|
||||||
|
*/
|
||||||
|
async function handleOrderPDF(id: string) {
|
||||||
|
const pdf = pdfQueue.find(p => p.id === id);
|
||||||
|
if (!pdf || !currentCredentials || !pdf.binectDocumentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
pdf.status = 'ordering';
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
// Notify background
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'updatePDFStatus',
|
||||||
|
id,
|
||||||
|
status: 'ordering'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ship the document
|
||||||
|
const result = await chrome.runtime.sendMessage({
|
||||||
|
action: 'shipDocument',
|
||||||
|
documentId: pdf.binectDocumentId,
|
||||||
|
username: currentCredentials.username,
|
||||||
|
password: currentCredentials.password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to ship document');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status to in_production
|
||||||
|
const meta: PDFStatusMeta = {
|
||||||
|
binectStatus: result.status,
|
||||||
|
binectStatusText: result.statusText
|
||||||
|
};
|
||||||
|
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'updatePDFStatus',
|
||||||
|
id,
|
||||||
|
status: 'in_production',
|
||||||
|
meta
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
pdf.status = 'in_production';
|
||||||
|
pdf.binectStatus = result.status;
|
||||||
|
pdf.binectStatusText = result.statusText;
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
showStatus('Order placed! Document is in production.', 'success');
|
||||||
|
|
||||||
|
// Hide status after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
hideStatus();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Order failed';
|
||||||
|
|
||||||
|
// Revert to in_basket status
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'updatePDFStatus',
|
||||||
|
id,
|
||||||
|
status: 'in_basket',
|
||||||
|
meta: { errorMessage }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
pdf.status = 'in_basket';
|
||||||
|
pdf.errorMessage = errorMessage;
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
showStatus(errorMessage, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle refresh document status
|
||||||
|
*/
|
||||||
|
async function handleRefreshStatus(id: string) {
|
||||||
|
const pdf = pdfQueue.find(p => p.id === id);
|
||||||
|
if (!pdf || !currentCredentials || !pdf.binectDocumentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current status from Binect
|
||||||
|
const result = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getDocumentStatus',
|
||||||
|
documentId: pdf.binectDocumentId,
|
||||||
|
username: currentCredentials.username,
|
||||||
|
password: currentCredentials.password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to get status');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine new local status based on Binect status code
|
||||||
|
let newStatus: PDFStatus = pdf.status;
|
||||||
|
if (result.status === 5) {
|
||||||
|
newStatus = 'sent';
|
||||||
|
} else if (result.status === 6) {
|
||||||
|
newStatus = 'canceled';
|
||||||
|
} else if (result.status === 3 || result.status === 4) {
|
||||||
|
newStatus = 'in_production';
|
||||||
|
} else if (result.status === 2) {
|
||||||
|
newStatus = 'in_basket';
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta: PDFStatusMeta = {
|
||||||
|
binectStatus: result.status,
|
||||||
|
binectStatusText: result.statusText,
|
||||||
|
price: result.price,
|
||||||
|
recipientAddress: result.recipientAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update in background
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'updatePDFStatus',
|
||||||
|
id,
|
||||||
|
status: newStatus,
|
||||||
|
meta
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
pdf.status = newStatus;
|
||||||
|
pdf.binectStatus = result.status;
|
||||||
|
pdf.binectStatusText = result.statusText;
|
||||||
|
if (result.price) pdf.price = result.price;
|
||||||
|
if (result.recipientAddress) pdf.recipientAddress = result.recipientAddress;
|
||||||
|
renderPDFList();
|
||||||
|
|
||||||
|
showStatus(`Status: ${result.statusText}`, 'success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hideStatus();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to refresh';
|
||||||
|
showStatus(errorMessage, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle dismiss PDF
|
* Handle dismiss PDF
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -325,3 +325,151 @@ export async function testConnection(username: string, password: string): Promis
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document status information returned by getDocumentStatus
|
||||||
|
*/
|
||||||
|
export interface DocumentStatusInfo {
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
price?: number;
|
||||||
|
recipientAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ship a document (place order for production)
|
||||||
|
*
|
||||||
|
* This announces the document for delivery, transitioning it from
|
||||||
|
* SHIPPABLE to PRODUCTION_QUEUE.
|
||||||
|
*
|
||||||
|
* @param documentId - Binect document ID
|
||||||
|
* @param username - Binect username
|
||||||
|
* @param password - Binect password
|
||||||
|
* @returns Updated status info
|
||||||
|
*/
|
||||||
|
export async function shipDocument(
|
||||||
|
documentId: number,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<DocumentStatusInfo> {
|
||||||
|
console.log('[Binect API] Shipping document:', documentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new BinectClient({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the document for production
|
||||||
|
const sending = await client.sendings.send(String(documentId));
|
||||||
|
|
||||||
|
console.log('[Binect API] Document shipped successfully');
|
||||||
|
console.log('[Binect API] New status:', sending.status);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: sending.status,
|
||||||
|
statusText: getStatusText(sending.status),
|
||||||
|
price: sending.price,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Binect API] Ship error:', error);
|
||||||
|
|
||||||
|
if (error instanceof BinectAuthError) {
|
||||||
|
throw new BinectAPIError('Invalid credentials', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectApiError) {
|
||||||
|
// Check for insufficient balance (error code 2330)
|
||||||
|
if (error.message.includes('2330') || error.message.includes('balance')) {
|
||||||
|
throw new BinectAPIError('Insufficient account balance', 402);
|
||||||
|
}
|
||||||
|
throw BinectAPIError.fromBinectError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectAPIError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BinectAPIError(
|
||||||
|
`Failed to ship document: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current status of a document
|
||||||
|
*
|
||||||
|
* @param documentId - Binect document ID
|
||||||
|
* @param username - Binect username
|
||||||
|
* @param password - Binect password
|
||||||
|
* @returns Current status info
|
||||||
|
*/
|
||||||
|
export async function getDocumentStatus(
|
||||||
|
documentId: number,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<DocumentStatusInfo> {
|
||||||
|
console.log('[Binect API] Getting document status:', documentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new BinectClient({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch document details
|
||||||
|
const doc = await client.documents.get(String(documentId));
|
||||||
|
|
||||||
|
console.log('[Binect API] Document status:', doc.status);
|
||||||
|
|
||||||
|
// Extract price and recipient if available
|
||||||
|
let price: number | undefined;
|
||||||
|
let recipientAddress: string | undefined;
|
||||||
|
|
||||||
|
if (doc.letter?.letterData) {
|
||||||
|
price = doc.letter.letterData.price?.priceAfterTax;
|
||||||
|
recipientAddress = doc.letter.letterData.recipientAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: doc.status.code,
|
||||||
|
statusText: doc.status.text || getStatusText(doc.status.code),
|
||||||
|
price,
|
||||||
|
recipientAddress,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Binect API] Get status error:', error);
|
||||||
|
|
||||||
|
if (error instanceof BinectAuthError) {
|
||||||
|
throw new BinectAPIError('Invalid credentials', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectApiError) {
|
||||||
|
throw BinectAPIError.fromBinectError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BinectAPIError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BinectAPIError(
|
||||||
|
`Failed to get document status: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable status text for a Binect status code
|
||||||
|
*/
|
||||||
|
function getStatusText(statusCode: number): string {
|
||||||
|
switch (statusCode) {
|
||||||
|
case 1: return 'In preparation';
|
||||||
|
case 2: return 'Ready to ship';
|
||||||
|
case 3: return 'In production queue';
|
||||||
|
case 4: return 'Printing';
|
||||||
|
case 5: return 'Sent';
|
||||||
|
case 6: return 'Canceled';
|
||||||
|
case 7: return 'Has errors';
|
||||||
|
default: return 'Unknown status';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,16 +9,29 @@ import { DetectedPDF } from './pdf-detector';
|
|||||||
|
|
||||||
const STORAGE_KEY = 'pdfQueue';
|
const STORAGE_KEY = 'pdfQueue';
|
||||||
const MAX_ENTRIES = 50;
|
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
|
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 {
|
export interface PDFQueueEntry extends DetectedPDF {
|
||||||
status: PDFStatus;
|
status: PDFStatus;
|
||||||
uploadedAt?: number;
|
|
||||||
binectDocumentId?: 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;
|
errorMessage?: string;
|
||||||
|
uploadedAt?: number;
|
||||||
|
orderedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PDFQueueState {
|
interface PDFQueueState {
|
||||||
@@ -57,7 +70,9 @@ export async function addPDF(pdf: DetectedPDF): Promise<PDFQueueEntry | null> {
|
|||||||
// Check for duplicate by URL
|
// Check for duplicate by URL
|
||||||
const existing = state.entries.find(e => e.url === pdf.url);
|
const existing = state.entries.find(e => e.url === pdf.url);
|
||||||
if (existing) {
|
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);
|
console.log('[PDF Queue] PDF already uploaded, skipping:', pdf.filename);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -82,13 +97,25 @@ export async function addPDF(pdf: DetectedPDF): Promise<PDFQueueEntry | null> {
|
|||||||
return entry;
|
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
|
* Update the status of a PDF in the queue
|
||||||
*/
|
*/
|
||||||
export async function updatePDFStatus(
|
export async function updatePDFStatus(
|
||||||
id: string,
|
id: string,
|
||||||
status: PDFStatus,
|
status: PDFStatus,
|
||||||
meta?: { binectDocumentId?: number; errorMessage?: string }
|
meta?: PDFStatusMeta
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const state = await loadQueue();
|
const state = await loadQueue();
|
||||||
const entry = state.entries.find(e => e.id === id);
|
const entry = state.entries.find(e => e.id === id);
|
||||||
@@ -100,15 +127,32 @@ export async function updatePDFStatus(
|
|||||||
|
|
||||||
entry.status = status;
|
entry.status = status;
|
||||||
|
|
||||||
if (status === 'uploaded') {
|
// Update Binect-specific fields
|
||||||
entry.uploadedAt = Date.now();
|
if (meta?.binectDocumentId !== undefined) {
|
||||||
if (meta?.binectDocumentId) {
|
entry.binectDocumentId = meta.binectDocumentId;
|
||||||
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) {
|
// Set timestamps based on status
|
||||||
entry.errorMessage = meta.errorMessage;
|
if (status === 'in_basket' && !entry.uploadedAt) {
|
||||||
|
entry.uploadedAt = Date.now();
|
||||||
|
}
|
||||||
|
if (status === 'in_production' && !entry.orderedAt) {
|
||||||
|
entry.orderedAt = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveQueue(state);
|
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();
|
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> {
|
export async function getPendingCount(): Promise<number> {
|
||||||
const pending = await getPendingPDFs();
|
return getActionableCount();
|
||||||
return pending.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,15 +227,21 @@ export async function cleanupOldEntries(): Promise<void> {
|
|||||||
const initialCount = state.entries.length;
|
const initialCount = state.entries.length;
|
||||||
|
|
||||||
state.entries = state.entries.filter(entry => {
|
state.entries = state.entries.filter(entry => {
|
||||||
// Always keep pending entries
|
// Always keep active entries
|
||||||
if (entry.status === 'pending' || entry.status === 'uploading') {
|
if (
|
||||||
|
entry.status === 'pending' ||
|
||||||
|
entry.status === 'uploading' ||
|
||||||
|
entry.status === 'in_basket' ||
|
||||||
|
entry.status === 'ordering' ||
|
||||||
|
entry.status === 'in_production'
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove uploaded entries older than 7 days
|
// Remove sent/canceled entries older than 7 days
|
||||||
if (entry.status === 'uploaded' && entry.uploadedAt) {
|
if (entry.status === 'sent' || entry.status === 'canceled') {
|
||||||
const age = now - entry.uploadedAt;
|
const age = now - (entry.orderedAt || entry.uploadedAt || entry.timestamp);
|
||||||
if (age > UPLOADED_MAX_AGE_MS) {
|
if (age > SENT_MAX_AGE_MS) {
|
||||||
return false;
|
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> {
|
async function enforceMaxEntries(state: PDFQueueState): Promise<void> {
|
||||||
|
const terminalStatuses: PDFStatus[] = ['sent', 'canceled', 'failed'];
|
||||||
|
|
||||||
while (state.entries.length > MAX_ENTRIES) {
|
while (state.entries.length > MAX_ENTRIES) {
|
||||||
// Find oldest uploaded entry
|
|
||||||
let removeIndex = -1;
|
let removeIndex = -1;
|
||||||
let oldestTime = Infinity;
|
let oldestTime = Infinity;
|
||||||
|
|
||||||
|
// Find oldest terminal entry (sent, canceled, failed)
|
||||||
for (let i = state.entries.length - 1; i >= 0; i--) {
|
for (let i = state.entries.length - 1; i >= 0; i--) {
|
||||||
const entry = state.entries[i];
|
const entry = state.entries[i];
|
||||||
if (entry.status === 'uploaded' && entry.uploadedAt && entry.uploadedAt < oldestTime) {
|
if (terminalStatuses.includes(entry.status)) {
|
||||||
oldestTime = entry.uploadedAt;
|
const entryTime = entry.orderedAt || entry.uploadedAt || entry.timestamp;
|
||||||
removeIndex = i;
|
if (entryTime < oldestTime) {
|
||||||
}
|
oldestTime = entryTime;
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
removeIndex = i;
|
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) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user