Get Started
NOW
Receive your estimate
USV Label Maker
Label Maker
Order Info
Blind Details
Track
Valance
IB = Inside Mount
OB = Outside Mount
Location
Quick Add
Add same order multiple times (blind # auto-increments)
1
blind(s)
📄

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

Label Queue

0
🏷

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

USV Labels ${labelHTMLArray.join('')}`); win.document.close(); // Wait for fonts then print win.onload = () => { win.focus(); win.print(); }; setTimeout(() => { try { win.focus(); win.print(); } catch(e){} }, 800); } function printAll() { if (!queue.length) return; const labels = []; queue.forEach(b => { const tabs = getLabelTabs(b.type); tabs.forEach(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?.blind); if (!b) return; setTimeout(() => openPrintWindow([buildLabelHTML(b, labelType, true)]), 200); } function closeReprintModal() { document.getElementById('reprint-modal').classList.remove('open'); } // ── FORM HELPERS ─────────────────────────────────────────────────── // Clears only per-blind measurement fields; keeps order-level info intact function clearPerBlindFields() { ['f-width','f-drop','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 — clears everything including order-level fields 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'; currentJobDriveFileId = null; currentJobId = 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 ──────────────────────────────────────────────────────── // One Drive file per order session. currentJobId is stable across all // blinds added in the same session so we update rather than create new files. function buildJobSnapshot() { if (!queue.length) return null; const b0 = queue[0]; const name = `${b0.customer.replace(/[^A-Z0-9]/g,'_')}_${b0.date.replace(/-/g,'')}_${b0.orderNum || 'NO-ORDER'}`; // Re-use existing job ID if we have one; only create new ID for a new order if (!currentJobId) currentJobId = Date.now().toString(); return { id: currentJobId, filename: `${name}.json`, 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, false); } function saveWithoutPrinting() { const job = buildJobSnapshot(); if (!job) { showToast('No labels in queue to save', 'error'); return; } saveJobToGDrive(job, true); } // ── GOOGLE DRIVE ───────────────────────────────────────────────────── // ── GOOGLE DRIVE (Google Identity Services — modern auth) ──────────── // Uses GIS token client (popup) + gapi for Drive REST calls. // No deprecated gapi.auth2 anywhere. function initGDrive() { if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID_HERE.apps.googleusercontent.com') { setDriveStatus('not-configured'); return; } // Load the gapi client library (no auth2) gapi.load('client', async () => { try { await gapi.client.init({ // No clientId here — auth is handled by GIS separately discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'] }); setDriveStatus('disconnected'); } catch(e) { console.error('gapi client init error', e); setDriveStatus('error'); } }); // Set up GIS token client gisTokenClient = google.accounts.oauth2.initTokenClient({ client_id: GOOGLE_CLIENT_ID, scope: SCOPES, callback: async (tokenResponse) => { if (tokenResponse.error) { console.error('GIS token error:', tokenResponse); setDriveStatus('error'); showToast('Google Drive sign-in failed', 'error'); return; } gdriveAccessToken = tokenResponse.access_token; // Give gapi the token so it can use Drive API gapi.client.setToken({ access_token: gdriveAccessToken }); await onDriveSignedIn(); } }); } function connectGDrive() { if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID_HERE.apps.googleusercontent.com') { alert('Google Drive is not configured yet.\n\nTo enable it:\n1. Go to console.cloud.google.com\n2. Create a project and enable the Drive API\n3. Create OAuth 2.0 credentials\n4. Replace YOUR_GOOGLE_CLIENT_ID_HERE in the app code with your Client ID'); return; } if (!gisTokenClient) { showToast('Auth not ready yet — please wait a moment and try again', 'error'); return; } // Request access token — shows Google account picker popup gisTokenClient.requestAccessToken({ prompt: 'consent' }); } async function onDriveSignedIn() { gdriveReady = true; setDriveStatus('connected'); gdriveFolderId = await getOrCreateFolder(GDRIVE_FOLDER_NAME); showToast('✅ Google Drive connected', 'success'); // Refresh history if on that view 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 /* not-configured */ { 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); // always keep local copy 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 — PATCH body only (no metadata change needed) 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 { // 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(); currentJobDriveFileId = created.id; // store so future saves update this file } 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') || '[]'); // Avoid duplicates by id const filtered = jobs.filter(j => j.id !== job.id); filtered.unshift(job); localStorage.setItem('usv_jobs', JSON.stringify(filtered.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 }); const files = res.result.files || []; for (const f of files) { try { const r = await fetch(`https://www.googleapis.com/drive/v3/files/${f.id}?alt=media`, { headers: { Authorization: `Bearer ${gdriveAccessToken}` } }); const job = await r.json(); jobs.push(job); } catch(e) { /* skip unreadable files */ } } } catch(e) { console.error('Drive load error', e); showToast('Could not load from Drive — showing local jobs', 'info'); } } // Merge local jobs, deduplicate by id const localJobs = getLocalJobs(); const allJobs = [...jobs, ...localJobs]; const seen = new Set(); jobs = allJobs.filter(j => { if (!j.id || seen.has(j.id)) return false; seen.add(j.id); return true; }); renderJobHistory(jobs); } 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) => `
${formatDate(job.date)}
${job.customer || 'Unknown'}
${job.orderNum || 'No Order #'}  |  ${job.blindCount || job.blinds?.length || 0} blind(s)  |  ${formatDateTime(job.savedAt)}
${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 ───────────────────────────────────────────────────



Special Offer
FREE TEMPORARY SHADES WITH EVERY ORDER
GET THIS DEAL



US Verticals Service

Albert G.


After lots of call to other companies, US Verticals was the least expensive with a much better product. The Shutters…

US Verticals Service

Ali R.


The whole process from A to Z when I was dealing with US verticals was seamless and hassle free. I…

US Verticals Service

Andy See


The techs came and took measurement and gave us a quote which is very reasonable. It was $928 plus tax.…

US Verticals Service

Anne Marie E.


From the initial call and scheduling an appointment, Kevin and Dan arrived promptly, explaining the difference in their products, taking…

US Verticals Service

Birma Montes


Very friendly and professional service. The assistance was great since we visited the store until the installation of the verticals…

US Verticals Service

Carlos Level


The word perfect, it is not enough. The work was excellent. On time, and a perfect and fast job.

US Verticals Service

Christine Batista


Re-did Vertical Blinds in living room and formal dining area. This time around we decided to not only change the…

US Verticals Service

Donna Becherer


As always, these folks do a great job! Thanks for keeping the quality and service topnotch.

US Verticals Service

Eric Goldman


We recently purchased a new home and saw US VERTICALS at a neighbors house. I stopped by and asked if…




guarantee
Ask about our unconditional
Price Match Guarantee
See Details



icon Always on Time
icon Honest PRICING
icon 25+ Years
icon Best Warranty