USV Label Maker ⚠️ Missing Information
The following fields could not be found in the PDF. Please fill them in before generating labels.
📄
Drop PDF here or tap to browse
Order forms, work orders, invoices
How it works
1 Upload your order PDF
2 AI extracts all blind details — you'll be prompted for anything missing
3 Labels are auto-generated and saved to Google Drive
🏷
No labels in queue
Fill out the form or upload a PDF to get started📁 Saved Jobs
📂
No saved jobs yet
Jobs save automatically when added to the print queue
Connect Google Drive to enable job history
');
win.document.close();
win.onload = function() { win.focus(); win.print(); };
setTimeout(function() { try { win.focus(); win.print(); } catch(e){} }, 900);
} function printAll() {
if (!queue.length) return;
const labels = [];
queue.forEach(function(b) {
getLabelTabs(b.type).forEach(function(t) { labels.push(buildLabelHTML(b, t.key)); });
});
openPrintWindow(labels);
} function printSingleLabel(groupIdx, labelType) {
const b = queue[groupIdx];
if (!b) return;
openPrintWindow([buildLabelHTML(b, labelType)]);
} function reprintGroup(i) {
openReprintModal(queue[i], i);
} // ── REPRINT MODAL ───────────────────────────────────────────────────
function openReprintModal(blindData, queueIdx) {
const tabs = getLabelTabs(blindData.type);
document.getElementById('reprint-modal-title').textContent =
`Reprint — ${blindData.customer} Blind ${blindData.blindNum}/${blindData.blindTotal}`;
document.getElementById('reprint-modal-sub').textContent =
`Select a label to reprint. It will be marked as REPRINT.`; const grid = document.getElementById('reprint-labels-grid');
grid.innerHTML = tabs.map(t => `
${t.label}
${buildLabelHTML(blindData, t.key, true)}
`).join(''); document.getElementById('reprint-modal').classList.add('open');
} function doPrintReprint(groupIdx, labelType) {
closeReprintModal();
const b = groupIdx >= 0 ? queue[groupIdx] : (window._reprintFromHistory && window._reprintFromHistory.blind);
if (!b) return;
setTimeout(function() { openPrintWindow([buildLabelHTML(b, labelType, true)]); }, 300);
} function closeReprintModal() {
document.getElementById('reprint-modal').classList.remove('open');
} // ── FORM HELPERS ─────────────────────────────────────────────────── // Clears per-blind fields only — keeps customer/dealer/order/date/total intact
function clearBlindFields() {
['f-width','f-drop','f-material','f-room','f-comment'].forEach(id => {
document.getElementById(id).value = '';
});
const vf = document.getElementById('f-valwidth');
vf.value = '';
delete vf.dataset.userEdited;
document.getElementById('f-carriers').value = '';
document.getElementById('f-type').value = 'COMPLETE';
document.getElementById('f-control').value = 'ONE-WAY';
setReturn('IB');
onTypeChange();
} // Full reset — everything
function clearForm() {
['f-dealer','f-customer','f-order','f-width','f-drop','f-material','f-room','f-comment','f-valwidth'].forEach(id => {
const el = document.getElementById(id);
el.value = '';
delete el.dataset.userEdited;
});
document.getElementById('f-date').value = today();
document.getElementById('f-blindnum').value = 1;
document.getElementById('f-blindtotal').value = 1;
document.getElementById('f-type').value = 'COMPLETE';
document.getElementById('f-control').value = 'ONE-WAY';
document.getElementById('f-carriers').value = '';
setReturn('IB');
onTypeChange();
addCount = 1;
document.getElementById('add-count').textContent = '1';
currentJobId = null;
currentJobDriveFileId = null;
} // ── PDF UPLOAD ──────────────────────────────────────────────────────
function setupDragDrop() {
const zone = document.getElementById('upload-zone');
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag-over'); });
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
zone.addEventListener('drop', e => {
e.preventDefault(); zone.classList.remove('drag-over');
const f = e.dataTransfer.files[0];
if (f?.type === 'application/pdf') processPdf(f);
});
} function handlePdfUpload(e) {
const f = e.target.files[0];
if (f) processPdf(f);
e.target.value = '';
} function setPdfStatus(msg, type) {
const el = document.getElementById('pdf-status');
el.textContent = msg;
el.className = `pdf-status visible ${type}`;
} async function processPdf(file) {
setPdfStatus('⏳ Reading PDF...', 'loading');
try {
const base64 = await fileToBase64(file);
setPdfStatus('🤖 Extracting order data with AI...', 'loading'); const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 3000,
messages: [{
role: 'user',
content: [
{ type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: base64 } },
{ type: 'text', text: `Extract all vertical blind order information from this PDF.
Return ONLY a valid JSON object with these exact keys (no markdown, no backticks):
{
"dealer": string,
"customer": string (UPPERCASE),
"orderNum": string (look for order number starting with "O-"),
"date": string (YYYY-MM-DD),
"blinds": [
{
"blindNum": number,
"blindTotal": number,
"type": "COMPLETE"|"SLATS"|"TRACK"|"VALANCE",
"width": string (e.g. "35 1/2"),
"drop": string,
"material": string (UPPERCASE),
"control": "ONE-WAY"|"SPLIT",
"valWidth": string (window width + 1 inch, or "NO"),
"returns": "IB"|"OB",
"room": string (UPPERCASE, or ""),
"comment": string (UPPERCASE, or "")
}
],
"missingFields": ["list","of","fields","that","are","unknown"]
}
Use empty string "" for unknown text fields. Use null for unknown numbers. List any truly unknown fields in missingFields array.` }
]
}]
})
}); const data = await response.json();
if (data.error) throw new Error(data.error.message); const text = data.content.find(b => b.type === 'text')?.text || '';
const clean = text.replace(/```json|```/g, '').trim();
const parsed = JSON.parse(clean.match(/\{[\s\S]*\}/)?.[0] || clean); if (!parsed.blinds?.length) throw new Error('No blind orders found in the PDF'); setPdfStatus(`✅ Found ${parsed.blinds.length} blind(s). Checking for missing info...`, 'success'); // Check for missing fields
const missing = parsed.missingFields || [];
const criticalMissing = missing.filter(f =>
['customer','width','drop','blindTotal'].includes(f)); if (criticalMissing.length > 0) {
pendingPdfData = parsed;
openMissingModal(parsed, criticalMissing);
} else {
applyPdfData(parsed);
} } catch(err) {
console.error(err);
setPdfStatus(`❌ Error: ${err.message}`, 'error');
}
} function fileToBase64(file) {
return new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(r.result.split(',')[1]);
r.onerror = rej;
r.readAsDataURL(file);
});
} // ── MISSING FIELDS MODAL ─────────────────────────────────────────────
const FIELD_LABELS = {
customer: 'Customer Name',
orderNum: 'Order Number (e.g. O-12345)',
width: 'Width (e.g. 35 1/2)',
drop: 'Drop / Length',
blindTotal: 'Total # of Blinds in Order',
material: 'Material / Style',
room: 'Room / Location',
}; function openMissingModal(pdfData, missingList) {
const form = document.getElementById('missing-fields-form');
form.innerHTML = missingList.map(f => `
`).join('');
form.dataset.missing = JSON.stringify(missingList);
document.getElementById('missing-modal').classList.add('open');
} function closeMissingModal() {
document.getElementById('missing-modal').classList.remove('open');
pendingPdfData = null;
} function submitMissingFields() {
if (!pendingPdfData) return;
const missing = JSON.parse(document.getElementById('missing-fields-form').dataset.missing || '[]');
const overrides = {};
for (const f of missing) {
const val = document.getElementById(`mf-${f}`)?.value?.trim();
if (val) overrides[f] = val.toUpperCase();
}
const merged = { ...pendingPdfData, ...overrides };
// Apply overrides to each blind too
merged.blinds = merged.blinds.map(b => ({ ...b, ...overrides }));
closeMissingModal();
applyPdfData(merged);
} function applyPdfData(data) {
const blinds = data.blinds || [];
for (const b of blinds) {
const blindData = {
dealer: (data.dealer || 'USV').toUpperCase(),
customer: (data.customer || b.customer || '').toUpperCase(),
orderNum: (data.orderNum || '').toUpperCase(),
date: data.date || today(),
blindNum: b.blindNum || 1,
blindTotal: b.blindTotal || blinds.length,
type: b.type || 'COMPLETE',
width: b.width || '',
drop: b.drop || '',
material: (b.material || '').toUpperCase(),
control: b.control || 'ONE-WAY',
carriers: calcCarriers(b.width, b.control || 'ONE-WAY'),
valWidth: b.valWidth || (parseFraction(b.width) ? formatFraction(parseFraction(b.width) + 1) : ''),
returns: b.returns || 'IB',
room: (b.room || '').toUpperCase(),
comment: (b.comment || '').toUpperCase(),
};
queue.push(blindData);
}
renderQueue();
setPdfStatus(`✅ ${blinds.length} label group(s) added to queue`, 'success');
switchTab('manual');
showView('maker');
autoSaveJob();
showToast(`✅ ${blinds.length} blind(s) imported from PDF`, 'success');
} // ── SAVE JOB ────────────────────────────────────────────────────────
function buildJobSnapshot() {
if (!queue.length) return null;
const b0 = queue[0];
const safeName = (b0.customer || 'UNKNOWN').replace(/[^A-Z0-9]/g,'_');
const safeDate = (b0.date || '').replace(/-/g,'');
const safeOrder = b0.orderNum || 'NO-ORDER';
const filename = safeName + '_' + safeDate + '_' + safeOrder + '.json';
if (!currentJobId) currentJobId = Date.now().toString();
return {
id: currentJobId,
filename,
savedAt: new Date().toISOString(),
customer: b0.customer,
orderNum: b0.orderNum,
date: b0.date,
dealer: b0.dealer,
blindCount: queue.length,
blinds: queue.map(b => ({ ...b }))
};
} function autoSaveJob() {
const job = buildJobSnapshot();
if (!job) return;
saveJobToGDrive(job);
} function saveWithoutPrinting() {
const job = buildJobSnapshot();
if (!job) { showToast('No labels in queue to save', 'error'); return; }
saveJobToGDrive(job, true);
} // ── GOOGLE DRIVE ─────────────────────────────────────────────────────
function initGDrive() {
// Load gapi client for Drive API calls (no auth2)
gapi.load('client', async () => {
try {
await gapi.client.init({
discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
});
setDriveStatus('disconnected');
} catch(e) {
console.error('gapi init error', e);
}
});
// Set up GIS token client for sign-in popup
gisTokenClient = google.accounts.oauth2.initTokenClient({
client_id: GOOGLE_CLIENT_ID,
scope: SCOPES,
callback: async (resp) => {
if (resp.error) {
setDriveStatus('error');
showToast('Google Drive sign-in failed', 'error');
return;
}
gdriveAccessToken = resp.access_token;
gapi.client.setToken({ access_token: gdriveAccessToken });
await onDriveSignedIn();
}
});
} function connectGDrive() {
if (!gisTokenClient) { showToast('Auth not ready, please wait', 'error'); return; }
gisTokenClient.requestAccessToken({ prompt: 'consent' });
} async function onDriveSignedIn() {
gdriveReady = true;
setDriveStatus('connected');
gdriveFolderId = await getOrCreateFolder(GDRIVE_FOLDER_NAME);
showToast('Google Drive connected', 'success');
if (document.getElementById('view-history').classList.contains('active')) loadJobHistory();
} function setDriveStatus(state) {
const dot = document.getElementById('gdrive-dot');
const lbl = document.getElementById('gdrive-label');
if (state === 'connected') { dot.className='gdrive-dot connected'; lbl.style.display=''; lbl.textContent='Drive'; }
else if (state === 'error') { dot.className='gdrive-dot error'; lbl.style.display=''; lbl.textContent='Drive'; }
else if (state === 'disconnected') { dot.className='gdrive-dot'; lbl.style.display=''; lbl.textContent='Drive'; }
else { dot.className='gdrive-dot'; lbl.style.display='none'; }
} async function getOrCreateFolder(name) {
try {
const res = await gapi.client.drive.files.list({
q: "name='" + name + "' and mimeType='application/vnd.google-apps.folder' and trashed=false",
fields: 'files(id,name)'
});
if (res.result.files.length > 0) return res.result.files[0].id;
const created = await gapi.client.drive.files.create({
resource: { name, mimeType: 'application/vnd.google-apps.folder' },
fields: 'id'
});
return created.result.id;
} catch(e) {
console.error('Folder error', e);
return null;
}
} async function saveJobToGDrive(job, manual = false) {
saveJobLocal(job);
if (!gdriveReady || !gdriveAccessToken) {
if (manual) showToast('Saved locally (Drive not connected)', 'info');
return;
}
try {
const body = JSON.stringify(job, null, 2);
if (currentJobDriveFileId) {
// Update existing file — no duplicate created
await fetch('https://www.googleapis.com/upload/drive/v3/files/' + currentJobDriveFileId + '?uploadType=media', {
method: 'PATCH',
headers: { Authorization: 'Bearer ' + gdriveAccessToken, 'Content-Type': 'application/json' },
body
});
} else {
// First save for this order — create new file
const metadata = { name: job.filename, mimeType: 'application/json', parents: gdriveFolderId ? [gdriveFolderId] : [] };
const form = new FormData();
form.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
form.append('file', new Blob([body], { type: 'application/json' }));
const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', {
method: 'POST',
headers: { Authorization: 'Bearer ' + gdriveAccessToken },
body: form
});
const created = await res.json();
if (created.id) currentJobDriveFileId = created.id;
}
if (manual) showToast('Job saved to Google Drive', 'success');
} catch(e) {
console.error('Drive save error', e);
if (manual) showToast('Saved locally (Drive error)', 'info');
}
} // ── LOCAL STORAGE FALLBACK ───────────────────────────────────────────
function saveJobLocal(job) {
const jobs = JSON.parse(localStorage.getItem('usv_jobs') || '[]');
const others = jobs.filter(j => j.id !== job.id);
others.unshift(job);
localStorage.setItem('usv_jobs', JSON.stringify(others.slice(0, 200)));
} function getLocalJobs() {
return JSON.parse(localStorage.getItem('usv_jobs') || '[]');
} // ── LOAD JOB HISTORY ─────────────────────────────────────────────────
async function loadJobHistory() {
const area = document.getElementById('history-area');
area.innerHTML = '
Loading jobs...
';
let jobs = [];
if (gdriveReady && gdriveFolderId && gdriveAccessToken) {
try {
const res = await gapi.client.drive.files.list({
q: "'" + gdriveFolderId + "' in parents and mimeType='application/json' and trashed=false",
fields: 'files(id,name,createdTime)',
orderBy: 'createdTime desc',
pageSize: 200
});
for (const f of (res.result.files || [])) {
try {
const r = await fetch('https://www.googleapis.com/drive/v3/files/' + f.id + '?alt=media', {
headers: { Authorization: 'Bearer ' + gdriveAccessToken }
});
jobs.push(await r.json());
} catch(e) {}
}
} catch(e) { console.error('Drive load error', e); }
}
const seen = new Set();
const all = [...jobs, ...getLocalJobs()].filter(j => { if (!j.id||seen.has(j.id)) return false; seen.add(j.id); return true; });
renderJobHistory(all);
} function renderJobHistory(jobs) {
const area = document.getElementById('history-area');
if (!jobs.length) {
area.innerHTML = `
📂
No saved jobs yet
Jobs auto-save when added to the print queue
${!gdriveReady ? `
` : ''}
`;
return;
} area.innerHTML = jobs.map((job, i) => `
${job.dealer||''} | Saved: ${formatDateTime(job.savedAt)}
${(job.blinds||[]).map((b,bi) => `
Blind ${b.blindNum}/${b.blindTotal} — ${b.width||''}×${b.drop||''}" ${b.material||''}
`).join('')}
`).join(''); // Store jobs for later reference
window._historyJobs = jobs;
} function toggleJob(i) {
const card = document.getElementById(`jcard-${i}`);
card.classList.toggle('open');
} function reloadJob(i) {
const job = window._historyJobs?.[i];
if (!job) return;
if (queue.length && !confirm('This will replace your current queue. Continue?')) return;
queue = (job.blinds || []).map(b => ({ ...b }));
renderQueue();
showView('maker');
showToast('Job loaded to queue', 'info');
} function reprintEntireJob(i) {
const job = window._historyJobs?.[i];
if (!job) return;
// Load to queue temporarily and print
const prev = [...queue];
queue = (job.blinds || []).map(b => ({ ...b }));
renderQueue();
setTimeout(() => {
printAll();
queue = prev;
renderQueue();
}, 300);
} function openJobBlindReprint(jobIdx, blindIdx) {
const job = window._historyJobs?.[jobIdx];
if (!job) return;
const blind = job.blinds?.[blindIdx];
if (!blind) return;
openReprintModal(blind, -1);
// Override doPrintReprint for history context
window._reprintFromHistory = { blind, jobIdx, blindIdx };
} function formatDateTime(iso) {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
} // ── TOAST ────────────────────────────────────────────────────────────
function showToast(msg, type = 'info') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = `toast ${type} show`;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { el.classList.remove('show'); }, 3500);
} // ── CONTROL CHANGE ───────────────────────────────────────────────────