Add PWA manifest, fix AI multi-coat pricing, and improve catalog lookup
- PWA: manifest.json + minimal service worker so iOS/Android persist camera permission after "Add to Home Screen"; theme-color and apple meta tags in layout - PWA icons: 192x192 and 512x512 from transparent PCL logo; updated pcl-logo.png - AI pricing: apply AdditionalCoatLaborPercent per extra coat on AI items, matching the calculated-item path (was ignoring extra coats entirely) - AI wizard: live price recalc when coats are added/removed; session-expiry errors now show a clear "refresh and sign in" message instead of raw HTTP status; smooth-scroll to follow-up/results sections on AI response - Catalog lookup: exclude SKUs already in company inventory from results; pass currentId on edit so own entry still appears; vendor-scoped search with cross-vendor fallback; result count shown in multi-match modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
@@ -54,6 +54,8 @@
|
||||
const params = new URLSearchParams();
|
||||
if (searchTerm) params.set('q', searchTerm);
|
||||
if (manufacturer) params.set('vendor', manufacturer);
|
||||
const currentId = smartBtn.dataset.currentId;
|
||||
if (currentId) params.set('currentId', currentId);
|
||||
|
||||
const resp = await fetch(`${LOOKUP_URL}?${params}`);
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
@@ -358,7 +360,7 @@
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title"><i class="bi bi-list-ul me-2"></i>Multiple matches — pick one</h6>
|
||||
<h6 class="modal-title"><i class="bi bi-list-ul me-2"></i>${items.length} match${items.length !== 1 ? 'es' : ''} — pick one <span class="text-muted fw-normal small">(already in inventory excluded)</span></h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
|
||||
@@ -935,7 +935,9 @@ async function _aiRecalcPriceAsync() {
|
||||
const sqft = parseFloat(document.getElementById('ai_sqftOverride')?.value) || b.surfaceAreaSqFt;
|
||||
const minutes = parseInt(document.getElementById('ai_minutesOverride')?.value) || b.estimatedMinutes;
|
||||
const complexity = document.getElementById('ai_complexityOverride')?.value || b.complexity;
|
||||
const coatCount = b.coatCount || 1;
|
||||
// Use the actual step-3 coat count so the price preview reflects whatever the user
|
||||
// added in the coating layers screen, not the coat count fixed at AI analysis time.
|
||||
const coatCount = (wz.data.coats || []).length || b.coatCount || 1;
|
||||
|
||||
const recalcUrl = pageMeta.aiRecalcUrl || '/Quotes/AiRecalcPrice';
|
||||
const csrf = document.querySelector('input[name="__RequestVerificationToken"]')?.value || '';
|
||||
@@ -1280,14 +1282,22 @@ async function aiAnalyze() {
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Server returned ${resp.status} ${resp.statusText}`);
|
||||
if (resp.status === 401 || resp.status === 302 || resp.redirected) {
|
||||
throw new Error('Your session has expired. Please refresh the page and sign in again.');
|
||||
}
|
||||
throw new Error(`Server error (${resp.status}). Please try again.`);
|
||||
}
|
||||
const contentType = resp.headers.get('Content-Type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
// Server returned HTML (e.g. login redirect) instead of JSON
|
||||
throw new Error('Your session may have expired. Please refresh the page and sign in again.');
|
||||
}
|
||||
const result = await resp.json();
|
||||
aiHandleResult(result);
|
||||
} catch (err) {
|
||||
console.error('AI analyze error:', err);
|
||||
aiSetLoading(false);
|
||||
aiShowError('Error: ' + err.message);
|
||||
aiShowError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,14 +1344,21 @@ async function aiSendFollowup() {
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Server returned ${resp.status} ${resp.statusText}`);
|
||||
if (resp.status === 401 || resp.status === 302 || resp.redirected) {
|
||||
throw new Error('Your session has expired. Please refresh the page and sign in again.');
|
||||
}
|
||||
throw new Error(`Server error (${resp.status}). Please try again.`);
|
||||
}
|
||||
const contentType = resp.headers.get('Content-Type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw new Error('Your session may have expired. Please refresh the page and sign in again.');
|
||||
}
|
||||
const result = await resp.json();
|
||||
aiHandleResult(result);
|
||||
} catch (err) {
|
||||
console.error('AI follow-up error:', err);
|
||||
aiSetLoading(false);
|
||||
aiShowError('Error: ' + err.message);
|
||||
aiShowError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1363,6 +1380,9 @@ function aiHandleResult(result) {
|
||||
wz.ai.followUpQuestion = result.followUpQuestion || 'Can you provide more details?';
|
||||
renderStep(wz.step);
|
||||
document.getElementById('ai_followupAnswer')?.focus();
|
||||
requestAnimationFrame(() => {
|
||||
document.getElementById('ai_followupSection')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
} else {
|
||||
// Store result state and re-render step so elements are guaranteed fresh
|
||||
wz.ai.result = result;
|
||||
@@ -1371,6 +1391,9 @@ function aiHandleResult(result) {
|
||||
wz.ai.phase = 'result';
|
||||
renderStep(wz.step);
|
||||
document.getElementById('ai_acceptError')?.classList.add('d-none');
|
||||
requestAnimationFrame(() => {
|
||||
document.getElementById('ai_resultsSection')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1488,6 +1511,8 @@ function addCoatRow() {
|
||||
if (!container) return;
|
||||
container.insertAdjacentHTML('beforeend', buildCoatRowHtml(i, coat));
|
||||
updatePowderNeeded(i);
|
||||
// AI items: recalculate price preview so multi-coat surcharge is visible before saving
|
||||
if (wz.itemType === 'ai') aiRecalcPrice();
|
||||
}
|
||||
|
||||
const COAT_NAME_PRESETS = ['Primer', 'Base Coat', 'Mid Coat', 'Top Coat', 'Clear Coat'];
|
||||
@@ -1699,6 +1724,8 @@ function removeCoatRow(i) {
|
||||
container.insertAdjacentHTML('beforeend', buildCoatRowHtml(idx, coat));
|
||||
restoreCoatRow(idx, coat);
|
||||
});
|
||||
updateAllPowderNeeded();
|
||||
if (wz.itemType === 'ai') aiRecalcPrice();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "Powder Coating Logix",
|
||||
"short_name": "PCLogix",
|
||||
"description": "Powder coating shop management — jobs, quotes, inventory, and scheduling.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#1A1A1C",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/pwa-icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/pwa-icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Minimal service worker — required for PWA installability.
|
||||
// No caching: all requests pass through to the network normally.
|
||||
// This exists solely so browsers recognize the site as installable,
|
||||
// which causes iOS/Android to persist camera permissions after "Add to Home Screen."
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting());
|
||||
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
|
||||
self.addEventListener('fetch', e => e.respondWith(fetch(e.request)));
|
||||
Reference in New Issue
Block a user