This commit is contained in:
gsinghpal
2026-02-27 14:32:32 -05:00
parent b649246e81
commit b925766966
80 changed files with 7831 additions and 1041 deletions

View File

@@ -14,16 +14,12 @@
.tech-stats-bar {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding-bottom: 0.5rem;
scrollbar-width: none;
}
.tech-stats-bar::-webkit-scrollbar { display: none; }
.tech-stat-card {
flex: 0 0 auto;
min-width: 100px;
padding: 0.75rem 1rem;
flex: 1 1 0;
min-width: 0;
padding: 0.75rem 0.5rem;
border-radius: 12px;
text-align: center;
color: #fff;
@@ -42,7 +38,145 @@
.tech-stat-total { background: linear-gradient(135deg, #5ba848, #3a8fb7); }
.tech-stat-remaining { background: linear-gradient(135deg, #3498db, #2980b9); }
.tech-stat-completed { background: linear-gradient(135deg, #27ae60, #219a52); }
.tech-stat-travel { background: linear-gradient(135deg, #8e44ad, #7d3c98); }
/* ---- Clock In/Out Card ---- */
.tech-clock-card {
background: var(--o-main-card-bg, #fff);
border: 1px solid var(--o-main-border-color, #e9ecef);
border-radius: 14px;
padding: 0.875rem 1rem;
}
.tech-clock-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #adb5bd;
flex-shrink: 0;
}
.tech-clock-dot--active {
background: #10b981;
box-shadow: 0 0 6px rgba(16, 185, 129, 0.5);
animation: tech-clock-pulse 2s ease-in-out infinite;
}
@keyframes tech-clock-pulse {
0%, 100% { box-shadow: 0 0 6px rgba(16, 185, 129, 0.5); }
50% { box-shadow: 0 0 12px rgba(16, 185, 129, 0.8); }
}
.tech-clock-status {
font-size: 0.85rem;
font-weight: 600;
color: var(--o-main-text-color, #212529);
line-height: 1.2;
}
.tech-clock-timer {
font-size: 0.75rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: #6c757d;
}
.tech-clock-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
border-radius: 10px;
border: none;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.tech-clock-btn:active { transform: scale(0.96); }
.tech-clock-btn--in {
background: #10b981;
color: #fff;
}
.tech-clock-btn--in:hover { background: #059669; }
.tech-clock-btn--out {
background: #ef4444;
color: #fff;
}
.tech-clock-btn--out:hover { background: #dc2626; }
.tech-clock-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.tech-clock-error {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.5rem;
padding: 0.4rem 0.75rem;
border-radius: 8px;
background: #fef2f2;
color: #dc2626;
font-size: 0.8rem;
font-weight: 500;
}
/* ---- Quick Links (All Tasks / Tomorrow / Repair Form) ---- */
.tech-quick-links {
display: flex;
gap: 0.5rem;
}
.tech-quick-link {
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.35rem;
padding: 0.875rem 0.5rem;
border-radius: 12px;
border: 1.5px solid;
text-decoration: none !important;
font-weight: 600;
font-size: 0.8rem;
transition: all 0.15s;
position: relative;
}
.tech-quick-link:active { transform: scale(0.97); }
.tech-quick-link i { font-size: 1.1rem; }
.tech-quick-link-primary {
border-color: #3498db;
color: #3498db !important;
background: rgba(52, 152, 219, 0.04);
}
.tech-quick-link-primary:hover { background: rgba(52, 152, 219, 0.1); }
.tech-quick-link-secondary {
border-color: #6c757d;
color: #6c757d !important;
background: rgba(108, 117, 125, 0.04);
}
.tech-quick-link-secondary:hover { background: rgba(108, 117, 125, 0.1); }
.tech-quick-link-warning {
border-color: #e67e22;
color: #e67e22 !important;
background: rgba(230, 126, 34, 0.04);
}
.tech-quick-link-warning:hover { background: rgba(230, 126, 34, 0.1); }
.tech-quick-link-badge {
position: absolute;
top: -6px;
right: -6px;
background: #3498db;
color: #fff;
font-size: 0.65rem;
font-weight: 700;
min-width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border-radius: 9px;
padding: 0 4px;
}
/* ---- Hero Card (Dashboard Current Task) ---- */
.tech-hero-card {
@@ -475,12 +609,18 @@
gap: 1rem;
}
.tech-stat-card {
min-width: 130px;
padding: 1rem 1.5rem;
}
.tech-stat-card .stat-number {
font-size: 2rem;
}
.tech-quick-links {
gap: 1rem;
}
.tech-quick-link {
padding: 1rem 0.75rem;
font-size: 0.85rem;
}
.tech-bottom-bar {
position: static;
box-shadow: none;

View File

@@ -28,6 +28,9 @@ patch(Chatter.prototype, {
[thread.id],
);
if (result && result.type === "ir.actions.act_window") {
if (!result.views && result.view_mode) {
result.views = result.view_mode.split(",").map(v => [false, v.trim()]);
}
this._fapActionService.doAction(result);
}
} catch (e) {

View File

@@ -1,15 +1,144 @@
/**
* Technician Location Logger
* Logs GPS location every 5 minutes during working hours (9 AM - 6 PM)
* Only logs while the browser tab is visible.
* Technician Location Services
*
* 1. Background logger -- logs GPS every 5 minutes during working hours.
* 2. getLocation() -- returns a Promise that resolves to {latitude, longitude, accuracy}.
* If the user denies permission or the request times out a blocking modal is shown
* and the promise is rejected.
* 3. Blocking modal -- cannot be dismissed; forces the technician to grant permission.
*/
(function () {
'use strict';
var INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
var INTERVAL_MS = 5 * 60 * 1000;
var STORE_OPEN_HOUR = 9;
var STORE_CLOSE_HOUR = 18;
var locationTimer = null;
var permissionDenied = false;
// =====================================================================
// BLOCKING MODAL
// =====================================================================
var modalEl = null;
function ensureModal() {
if (modalEl) return;
var div = document.createElement('div');
div.id = 'fusionLocationModal';
div.innerHTML =
'<div style="position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:99999;display:flex;align-items:center;justify-content:center;">' +
'<div style="background:#fff;border-radius:16px;max-width:400px;width:90%;padding:2rem;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.3);">' +
'<div style="font-size:3rem;color:#dc3545;margin-bottom:1rem;"><i class="fa fa-map-marker"></i></div>' +
'<h4 style="margin-bottom:0.5rem;">Location Required</h4>' +
'<p style="color:#666;font-size:0.95rem;">Your GPS location is mandatory to perform this action. ' +
'Please allow location access in your browser settings and try again.</p>' +
'<p style="color:#999;font-size:0.85rem;">If you previously denied access, open your browser settings ' +
'and reset the location permission for this site.</p>' +
'<button id="fusionLocationRetryBtn" style="background:#0d6efd;color:#fff;border:none;border-radius:12px;padding:0.75rem 2rem;font-size:1rem;cursor:pointer;margin-top:0.5rem;width:100%;">' +
'<i class="fa fa-refresh" style="margin-right:6px;"></i>Try Again' +
'</button>' +
'</div>' +
'</div>';
document.body.appendChild(div);
modalEl = div;
document.getElementById('fusionLocationRetryBtn').addEventListener('click', function () {
hideModal();
window.fusionGetLocation().catch(function () {
showModal();
});
});
}
function showModal() {
ensureModal();
modalEl.style.display = '';
}
function hideModal() {
if (modalEl) modalEl.style.display = 'none';
}
// =====================================================================
// PERMISSION-DENIED BANNER (persistent warning for background logger)
// =====================================================================
var bannerEl = null;
function showDeniedBanner() {
if (bannerEl) return;
bannerEl = document.createElement('div');
bannerEl.id = 'fusionLocationBanner';
bannerEl.style.cssText =
'position:fixed;top:0;left:0;right:0;z-index:9999;background:#dc3545;color:#fff;' +
'padding:10px 16px;text-align:center;font-size:0.9rem;font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,.2);';
bannerEl.innerHTML =
'<i class="fa fa-exclamation-triangle" style="margin-right:6px;"></i>' +
'Location access is denied. Your location is not being tracked. ' +
'Please enable location in browser settings.';
document.body.appendChild(bannerEl);
}
// =====================================================================
// getLocation() -- public API
// =====================================================================
function getLocation() {
return new Promise(function (resolve, reject) {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by this browser.'));
return;
}
navigator.geolocation.getCurrentPosition(
function (position) {
permissionDenied = false;
if (bannerEl) { bannerEl.remove(); bannerEl = null; }
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy || 0,
});
},
function (error) {
permissionDenied = true;
showDeniedBanner();
console.error('Fusion Location: GPS error', error.code, error.message);
reject(error);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 30000 }
);
});
}
window.fusionGetLocation = getLocation;
// =====================================================================
// NAVIGATE -- opens Google Maps app on iOS/Android, browser fallback
// =====================================================================
function openGoogleMapsNav(el) {
var addr = (el.dataset.navAddr || '').trim();
var fallbackUrl = el.dataset.navUrl || '';
if (!addr && !fallbackUrl) return;
var dest = encodeURIComponent(addr) || fallbackUrl.split('destination=')[1];
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
var isAndroid = /Android/i.test(navigator.userAgent);
if (isIOS) {
window.location.href = 'comgooglemaps://?daddr=' + dest + '&directionsmode=driving';
} else if (isAndroid) {
window.location.href = 'google.navigation:q=' + dest;
} else {
window.open(fallbackUrl, '_blank');
}
}
window.openGoogleMapsNav = openGoogleMapsNav;
// =====================================================================
// BACKGROUND LOGGER
// =====================================================================
function isWorkingHours() {
var now = new Date();
@@ -18,77 +147,54 @@
}
function isTechnicianPortal() {
// Check if we're on a technician portal page
return window.location.pathname.indexOf('/my/technician') !== -1;
}
function logLocation() {
if (!isWorkingHours()) {
return;
}
if (document.hidden) {
return;
}
if (!navigator.geolocation) {
return;
}
if (!isWorkingHours() || document.hidden || !navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
function (position) {
var data = {
getLocation().then(function (coords) {
fetch('/my/technician/location/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy || 0,
latitude: coords.latitude,
longitude: coords.longitude,
accuracy: coords.accuracy,
}
};
fetch('/my/technician/location/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).catch(function () {
// Silently fail - location logging is best-effort
});
},
function () {
// Geolocation permission denied or error - silently ignore
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
);
}),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.result && !data.result.success) {
console.warn('Fusion Location: server rejected log', data.result);
}
})
.catch(function (err) {
console.warn('Fusion Location: network error', err);
});
}).catch(function () {
/* permission denied -- banner already shown */
});
}
function startLocationLogging() {
if (!isTechnicianPortal()) {
return;
}
// Log immediately on page load
if (!isTechnicianPortal()) return;
logLocation();
// Set interval for periodic logging
locationTimer = setInterval(logLocation, INTERVAL_MS);
// Pause/resume on tab visibility change
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
// Tab hidden - clear interval to save battery
if (locationTimer) {
clearInterval(locationTimer);
locationTimer = null;
}
if (locationTimer) { clearInterval(locationTimer); locationTimer = null; }
} else {
// Tab visible again - log immediately and restart interval
logLocation();
if (!locationTimer) {
locationTimer = setInterval(logLocation, INTERVAL_MS);
}
if (!locationTimer) { locationTimer = setInterval(logLocation, INTERVAL_MS); }
}
});
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startLocationLogging);
} else {