feat(pos): add 3 improvements — Spanish translations, PDF quotes, push notifications
1. Spanish translations for TecDoc catalog (translations.py) applied to catalog_service.py and dashboard server.py endpoints 2. Printable quotation HTML endpoint (/pos/api/quotations/<id>/pdf) with @media print CSS for clean browser-to-PDF output 3. Web Push notifications to owner/admin on sale cancellation, stock zero, and cash register differences > $500. Includes service worker, VAPID key management, and subscription endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
pos/static/js/push.js
Normal file
95
pos/static/js/push.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* push.js — Web Push notification setup for Nexus POS
|
||||
*
|
||||
* Registers a service worker and subscribes to push notifications.
|
||||
* Only activates for owner/admin roles.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Only set up push for owner/admin
|
||||
var employee = {};
|
||||
try { employee = JSON.parse(localStorage.getItem('pos_employee') || '{}'); } catch(e) {}
|
||||
var role = employee.role || '';
|
||||
if (role !== 'owner' && role !== 'admin') return;
|
||||
|
||||
// Check browser support
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||
console.log('[Push] Browser does not support push notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
var token = localStorage.getItem('pos_token');
|
||||
if (!token) return;
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
var padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
var rawData = window.atob(base64);
|
||||
var outputArray = new Uint8Array(rawData.length);
|
||||
for (var i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
async function setupPush() {
|
||||
try {
|
||||
// Register service worker
|
||||
var registration = await navigator.serviceWorker.register('/pos/static/sw-push.js', {
|
||||
scope: '/pos/'
|
||||
});
|
||||
console.log('[Push] Service worker registered');
|
||||
|
||||
// Get VAPID key from server
|
||||
var resp = await fetch('/pos/api/push/vapid-key', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.log('[Push] VAPID key not available:', resp.status);
|
||||
return;
|
||||
}
|
||||
var data = await resp.json();
|
||||
var vapidKey = data.public_key;
|
||||
if (!vapidKey) return;
|
||||
|
||||
// Request permission
|
||||
var permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') {
|
||||
console.log('[Push] Permission denied');
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe
|
||||
var subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidKey)
|
||||
});
|
||||
|
||||
// Send subscription to server
|
||||
var subResp = await fetch('/pos/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({ subscription: subscription.toJSON() })
|
||||
});
|
||||
|
||||
if (subResp.ok) {
|
||||
console.log('[Push] Subscribed successfully');
|
||||
}
|
||||
} catch(err) {
|
||||
console.log('[Push] Setup error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Delay push setup to not block page load
|
||||
if (document.readyState === 'complete') {
|
||||
setTimeout(setupPush, 2000);
|
||||
} else {
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(setupPush, 2000);
|
||||
});
|
||||
}
|
||||
})();
|
||||
55
pos/static/sw-push.js
Normal file
55
pos/static/sw-push.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* sw-push.js — Service Worker for Nexus POS push notifications
|
||||
*/
|
||||
|
||||
self.addEventListener('push', function(event) {
|
||||
var data = { title: 'Nexus POS', body: '', url: '/pos', icon: '/pos/static/icons/icon-192.png' };
|
||||
|
||||
if (event.data) {
|
||||
try {
|
||||
data = Object.assign(data, event.data.json());
|
||||
} catch(e) {
|
||||
data.body = event.data.text();
|
||||
}
|
||||
}
|
||||
|
||||
var options = {
|
||||
body: data.body,
|
||||
icon: data.icon || '/pos/static/icons/icon-192.png',
|
||||
badge: data.badge || '/pos/static/icons/badge-72.png',
|
||||
vibrate: [200, 100, 200],
|
||||
data: { url: data.url || '/pos' },
|
||||
actions: [
|
||||
{ action: 'open', title: 'Ver' },
|
||||
{ action: 'dismiss', title: 'Cerrar' }
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title, options)
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'dismiss') return;
|
||||
|
||||
var url = (event.notification.data && event.notification.data.url) || '/pos';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
|
||||
// Focus existing window if open
|
||||
for (var i = 0; i < clientList.length; i++) {
|
||||
var client = clientList[i];
|
||||
if (client.url.indexOf('/pos') !== -1 && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// Open new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user