changes
This commit is contained in:
@@ -18,6 +18,41 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Clock In/Out -->
|
||||
<t t-if="clock_enabled">
|
||||
<div class="tech-clock-card mb-3"
|
||||
id="techClockCard"
|
||||
t-att-data-checked-in="'true' if clock_checked_in else 'false'"
|
||||
t-att-data-check-in-time="clock_check_in_time or ''">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="tech-clock-dot" t-att-class="'tech-clock-dot--active' if clock_checked_in else ''"/>
|
||||
<div>
|
||||
<div class="tech-clock-status" id="clockStatusText">
|
||||
<t t-if="clock_checked_in">Clocked In</t>
|
||||
<t t-else="">Not Clocked In</t>
|
||||
</div>
|
||||
<div class="tech-clock-timer" id="clockTimer">00:00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="tech-clock-btn" id="clockActionBtn"
|
||||
t-att-class="'tech-clock-btn--out' if clock_checked_in else 'tech-clock-btn--in'"
|
||||
onclick="handleClockAction()">
|
||||
<t t-if="clock_checked_in">
|
||||
<i class="fa fa-stop-circle-o"/> Clock Out
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-play-circle-o"/> Clock In
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tech-clock-error" id="clockError" style="display:none;">
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
<span id="clockErrorText"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Quick Stats Bar -->
|
||||
<div class="tech-stats-bar mb-4">
|
||||
<div class="tech-stat-card tech-stat-total">
|
||||
@@ -32,10 +67,6 @@
|
||||
<div class="stat-number"><t t-out="completed_today"/></div>
|
||||
<div class="stat-label">Done</div>
|
||||
</div>
|
||||
<div class="tech-stat-card tech-stat-travel">
|
||||
<div class="stat-number"><t t-out="total_travel"/></div>
|
||||
<div class="stat-label">Travel min</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current / Next Task Hero Card -->
|
||||
@@ -61,8 +92,11 @@
|
||||
<p class="mb-2 small"><t t-out="current_task.description"/></p>
|
||||
</t>
|
||||
<div class="d-flex gap-2 flex-wrap mt-3">
|
||||
<a t-if="current_task.get_google_maps_url()" t-att-href="current_task.get_google_maps_url()"
|
||||
class="tech-action-btn tech-btn-navigate" target="_blank">
|
||||
<a t-if="current_task.get_google_maps_url()"
|
||||
href="#" class="tech-action-btn tech-btn-navigate"
|
||||
t-att-data-nav-url="current_task.get_google_maps_url()"
|
||||
t-att-data-nav-addr="current_task.address_display or ''"
|
||||
onclick="openGoogleMapsNav(this); return false;">
|
||||
<i class="fa fa-location-arrow"/>Navigate
|
||||
</a>
|
||||
<a t-attf-href="/my/technician/task/#{current_task.id}"
|
||||
@@ -102,8 +136,11 @@
|
||||
</p>
|
||||
</t>
|
||||
<div class="d-flex gap-2 flex-wrap mt-3">
|
||||
<a t-if="next_task.get_google_maps_url()" t-att-href="next_task.get_google_maps_url()"
|
||||
class="tech-action-btn tech-btn-navigate" target="_blank">
|
||||
<a t-if="next_task.get_google_maps_url()"
|
||||
href="#" class="tech-action-btn tech-btn-navigate"
|
||||
t-att-data-nav-url="next_task.get_google_maps_url()"
|
||||
t-att-data-nav-addr="next_task.address_display or ''"
|
||||
onclick="openGoogleMapsNav(this); return false;">
|
||||
<i class="fa fa-location-arrow"/>Navigate
|
||||
</a>
|
||||
<button class="tech-action-btn tech-btn-enroute"
|
||||
@@ -170,25 +207,22 @@
|
||||
</t>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-4">
|
||||
<a href="/my/technician/tasks" class="btn btn-outline-primary w-100 py-3">
|
||||
<i class="fa fa-list me-1"/>All Tasks
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="/my/technician/tomorrow" class="btn btn-outline-secondary w-100 py-3">
|
||||
<i class="fa fa-calendar me-1"/>Tomorrow
|
||||
<t t-if="tomorrow_count">
|
||||
<span class="badge bg-primary ms-1"><t t-out="tomorrow_count"/></span>
|
||||
</t>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="/repair-form" class="btn btn-outline-warning w-100 py-3">
|
||||
<i class="fa fa-wrench me-1"/>Repair Form
|
||||
</a>
|
||||
</div>
|
||||
<div class="tech-quick-links mb-4">
|
||||
<a href="/my/technician/tasks" class="tech-quick-link tech-quick-link-primary">
|
||||
<i class="fa fa-list"/>
|
||||
<span>All Tasks</span>
|
||||
</a>
|
||||
<a href="/my/technician/tomorrow" class="tech-quick-link tech-quick-link-secondary">
|
||||
<i class="fa fa-calendar"/>
|
||||
<span>Tomorrow</span>
|
||||
<t t-if="tomorrow_count">
|
||||
<span class="tech-quick-link-badge"><t t-out="tomorrow_count"/></span>
|
||||
</t>
|
||||
</a>
|
||||
<a href="/repair-form" class="tech-quick-link tech-quick-link-warning">
|
||||
<i class="fa fa-wrench"/>
|
||||
<span>Repair Form</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- My Start Location -->
|
||||
@@ -221,30 +255,146 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clock In/Out JS -->
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var card = document.getElementById('techClockCard');
|
||||
if (!card) return;
|
||||
|
||||
var isCheckedIn = card.dataset.checkedIn === 'true';
|
||||
var checkInTime = card.dataset.checkInTime ? new Date(card.dataset.checkInTime + 'Z') : null;
|
||||
var timerInterval = null;
|
||||
|
||||
function updateTimer() {
|
||||
if (!checkInTime) return;
|
||||
var diff = Math.max(0, Math.floor((new Date() - checkInTime) / 1000));
|
||||
var h = Math.floor(diff / 3600);
|
||||
var m = Math.floor((diff % 3600) / 60);
|
||||
var s = diff % 60;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
document.getElementById('clockTimer').textContent = pad(h) + ':' + pad(m) + ':' + pad(s);
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
stopTimer();
|
||||
updateTimer();
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||
}
|
||||
|
||||
function applyState() {
|
||||
var dot = card.querySelector('.tech-clock-dot');
|
||||
var statusEl = document.getElementById('clockStatusText');
|
||||
var btn = document.getElementById('clockActionBtn');
|
||||
var timerEl = document.getElementById('clockTimer');
|
||||
|
||||
if (dot) dot.className = 'tech-clock-dot' + (isCheckedIn ? ' tech-clock-dot--active' : '');
|
||||
if (statusEl) statusEl.textContent = isCheckedIn ? 'Clocked In' : 'Not Clocked In';
|
||||
if (btn) {
|
||||
btn.className = 'tech-clock-btn ' + (isCheckedIn ? 'tech-clock-btn--out' : 'tech-clock-btn--in');
|
||||
btn.innerHTML = isCheckedIn
|
||||
? '<i class="fa fa-stop-circle-o"></i> Clock Out'
|
||||
: '<i class="fa fa-play-circle-o"></i> Clock In';
|
||||
}
|
||||
if (!isCheckedIn && timerEl) timerEl.textContent = '00:00:00';
|
||||
}
|
||||
|
||||
if (isCheckedIn && checkInTime) startTimer();
|
||||
|
||||
window.handleClockAction = function() {
|
||||
var btn = document.getElementById('clockActionBtn');
|
||||
var errEl = document.getElementById('clockError');
|
||||
var errText = document.getElementById('clockErrorText');
|
||||
btn.disabled = true;
|
||||
errEl.style.display = 'none';
|
||||
|
||||
window.fusionGetLocation().then(function(coords) {
|
||||
fetch('/fusion_clock/clock_action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
accuracy: coords.accuracy,
|
||||
source: 'portal'
|
||||
}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var result = data.result || {};
|
||||
if (result.error) {
|
||||
errText.textContent = result.error;
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (result.action === 'clock_in') {
|
||||
isCheckedIn = true;
|
||||
checkInTime = new Date(result.check_in + 'Z');
|
||||
startTimer();
|
||||
} else {
|
||||
isCheckedIn = false;
|
||||
checkInTime = null;
|
||||
stopTimer();
|
||||
}
|
||||
applyState();
|
||||
btn.disabled = false;
|
||||
})
|
||||
.catch(function() {
|
||||
errText.textContent = 'Network error. Please try again.';
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
});
|
||||
}).catch(function() {
|
||||
errText.textContent = 'Location access is required for clock in/out.';
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Inline JS for task actions -->
|
||||
<script type="text/javascript">
|
||||
function techTaskAction(btn, action) {
|
||||
var taskId = btn.dataset.taskId;
|
||||
var origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ...';
|
||||
fetch('/my/technician/task/' + taskId + '/action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {action: action}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.result && data.result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.result ? data.result.error : 'Error');
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Getting location...';
|
||||
window.fusionGetLocation().then(function(coords) {
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ...';
|
||||
fetch('/my/technician/task/' + taskId + '/action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {
|
||||
action: action,
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
accuracy: coords.accuracy
|
||||
}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.result && data.result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.result ? data.result.error : 'Error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
alert('Network error. Please try again.');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa fa-road"></i> En Route';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
btn.innerHTML = origHtml;
|
||||
});
|
||||
}).catch(function() {
|
||||
alert('Location access is required. Please enable GPS and try again.');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa fa-road"></i> En Route';
|
||||
btn.innerHTML = origHtml;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -462,7 +612,10 @@
|
||||
<!-- ===== QUICK ACTIONS ROW ===== -->
|
||||
<div class="tech-quick-actions mb-3">
|
||||
<t t-if="task.get_google_maps_url()">
|
||||
<a t-att-href="task.get_google_maps_url()" class="tech-quick-btn" target="_blank">
|
||||
<a href="#" class="tech-quick-btn"
|
||||
t-att-data-nav-url="task.get_google_maps_url()"
|
||||
t-att-data-nav-addr="task.address_display or ''"
|
||||
onclick="openGoogleMapsNav(this); return false;">
|
||||
<i class="fa fa-location-arrow"/>
|
||||
<span>Navigate</span>
|
||||
</a>
|
||||
@@ -695,8 +848,11 @@
|
||||
</button>
|
||||
</t>
|
||||
<t t-if="task.status == 'en_route'">
|
||||
<a t-if="task.get_google_maps_url()" t-att-href="task.get_google_maps_url()"
|
||||
class="tech-action-btn tech-btn-navigate" target="_blank">
|
||||
<a t-if="task.get_google_maps_url()"
|
||||
href="#" class="tech-action-btn tech-btn-navigate"
|
||||
t-att-data-nav-url="task.get_google_maps_url()"
|
||||
t-att-data-nav-addr="task.address_display or ''"
|
||||
onclick="openGoogleMapsNav(this); return false;">
|
||||
<i class="fa fa-location-arrow"/>Navigate
|
||||
</a>
|
||||
<button class="tech-action-btn tech-btn-start"
|
||||
@@ -750,46 +906,78 @@
|
||||
var recordingSeconds = 0;
|
||||
|
||||
function techTaskAction(btn, action) {
|
||||
var origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||||
fetch('/my/technician/task/' + taskId + '/action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {action: action}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.result && data.result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.result ? data.result.error : 'Error');
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Getting location...';
|
||||
window.fusionGetLocation().then(function(coords) {
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
||||
fetch('/my/technician/task/' + taskId + '/action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {
|
||||
action: action,
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
accuracy: coords.accuracy
|
||||
}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.result && data.result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.result ? data.result.error : 'Error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
alert('Network error. Please try again.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
btn.innerHTML = origHtml;
|
||||
});
|
||||
}).catch(function() {
|
||||
alert('Location access is required. Please enable GPS and try again.');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
});
|
||||
}
|
||||
|
||||
function techCompleteTask(btn) {
|
||||
var origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Completing...';
|
||||
fetch('/my/technician/task/' + taskId + '/action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {action: 'complete'}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.result && data.result.success) {
|
||||
showCompletionOverlay(data.result);
|
||||
} else {
|
||||
alert(data.result ? data.result.error : 'Error completing task');
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Getting location...';
|
||||
window.fusionGetLocation().then(function(coords) {
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Completing...';
|
||||
fetch('/my/technician/task/' + taskId + '/action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {
|
||||
action: 'complete',
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
accuracy: coords.accuracy
|
||||
}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.result && data.result.success) {
|
||||
showCompletionOverlay(data.result);
|
||||
} else {
|
||||
alert(data.result ? data.result.error : 'Error completing task');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
alert('Network error. Please try again.');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa fa-check-circle"></i> Complete Task';
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
alert('Network error. Please try again.');
|
||||
btn.innerHTML = origHtml;
|
||||
});
|
||||
}).catch(function() {
|
||||
alert('Location access is required. Please enable GPS and try again.');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa fa-check-circle"></i> Complete Task';
|
||||
btn.innerHTML = origHtml;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1184,10 +1372,16 @@
|
||||
btns.forEach(function(b){b.disabled = true;});
|
||||
|
||||
try {
|
||||
var coords = await window.fusionGetLocation();
|
||||
var resp = await fetch('/my/technician/task/' + taskId + '/voice-complete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {transcription: text}})
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {
|
||||
transcription: text,
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
accuracy: coords.accuracy
|
||||
}})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.result && data.result.success) {
|
||||
@@ -1197,7 +1391,11 @@
|
||||
btns.forEach(function(b){b.disabled = false;});
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
if (err instanceof GeolocationPositionError || err.code) {
|
||||
alert('Location access is required. Please enable GPS and try again.');
|
||||
} else {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
btns.forEach(function(b){b.disabled = false;});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,73 @@
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
<!-- Clock In/Out -->
|
||||
<t t-if="clock_enabled">
|
||||
<div class="col-md-6">
|
||||
<style>
|
||||
@keyframes hcPulseGreen {
|
||||
0% { box-shadow: 0 0 0 0 rgba(16,185,129,0.5); }
|
||||
70% { box-shadow: 0 0 0 14px rgba(16,185,129,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); }
|
||||
}
|
||||
@keyframes hcPulseRed {
|
||||
0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
|
||||
70% { box-shadow: 0 0 0 14px rgba(239,68,68,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); }
|
||||
}
|
||||
.hc-btn-ring {
|
||||
width: 56px; height: 56px; border-radius: 50%; border: none;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0; pointer-events: none;
|
||||
}
|
||||
.hc-btn-ring--in {
|
||||
background: #10b981;
|
||||
animation: hcPulseGreen 2s ease-in-out infinite;
|
||||
}
|
||||
.hc-btn-ring--out {
|
||||
background: #ef4444;
|
||||
animation: hcPulseRed 2s ease-in-out infinite;
|
||||
}
|
||||
.hc-btn-ring i { color: #fff; font-size: 1.4rem; }
|
||||
.hc-btn-ring--in i { padding-left: 3px; }
|
||||
.hc-timer-badge {
|
||||
display: inline-block; font-family: monospace; font-size: 0.75rem; font-weight: 700;
|
||||
color: #10b981; background: rgba(16,185,129,0.1); border-radius: 20px;
|
||||
padding: 2px 10px; letter-spacing: 0.05em;
|
||||
}
|
||||
.hc-clock-link { text-decoration: none; }
|
||||
.hc-clock-link:hover { text-decoration: none; }
|
||||
.hc-clock-link:hover .card { box-shadow: 0 4px 16px rgba(0,0,0,0.12) !important; }
|
||||
.hc-clock-link:active .hc-btn-ring { transform: scale(0.92); }
|
||||
</style>
|
||||
<a href="/my/clock" class="hc-clock-link"
|
||||
id="homeClockCard"
|
||||
t-att-data-checked-in="'true' if clock_checked_in else 'false'"
|
||||
t-att-data-check-in-time="clock_check_in_time or ''">
|
||||
<div class="card h-100 border-0 shadow-sm" style="border-radius: 12px; min-height: 100px;">
|
||||
<div class="card-body d-flex align-items-center p-4">
|
||||
<div class="me-3">
|
||||
<div t-attf-class="hc-btn-ring #{clock_checked_in and 'hc-btn-ring--out' or 'hc-btn-ring--in'}">
|
||||
<i t-attf-class="fa #{clock_checked_in and 'fa-stop' or 'fa-play'}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="min-width: 0;">
|
||||
<h5 class="mb-0 text-dark" id="homeClockStatus">
|
||||
<t t-if="clock_checked_in">Clocked In</t>
|
||||
<t t-else="">Clock In</t>
|
||||
</h5>
|
||||
<div id="homeClockTimer">
|
||||
<t t-if="clock_checked_in"><span class="hc-timer-badge">00:00:00</span></t>
|
||||
<t t-else=""><small class="text-muted">Tap to start your shift</small></t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Funding Claims (Clients/Authorizers) -->
|
||||
<t t-if="request.env.user.partner_id.is_client_portal or request.env.user.partner_id.is_authorizer">
|
||||
<div class="col-md-6">
|
||||
@@ -251,6 +317,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Home Clock Timer (display only, links to /my/clock) -->
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var card = document.getElementById('homeClockCard');
|
||||
if (!card) return;
|
||||
var isCheckedIn = card.dataset.checkedIn === 'true';
|
||||
var checkInTime = card.dataset.checkInTime ? new Date(card.dataset.checkInTime + 'Z') : null;
|
||||
if (!isCheckedIn || !checkInTime) return;
|
||||
|
||||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
var badge = document.querySelector('#homeClockTimer .hc-timer-badge');
|
||||
if (!badge) return;
|
||||
|
||||
function tick() {
|
||||
var diff = Math.max(0, Math.floor((new Date() - checkInTime) / 1000));
|
||||
badge.textContent = pad(Math.floor(diff / 3600)) + ':' + pad(Math.floor((diff % 3600) / 60)) + ':' + pad(diff % 60);
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
})();
|
||||
</script>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
@@ -1086,7 +1174,42 @@
|
||||
<p class="text-muted">Welcome back, <t t-out="partner.name"/>!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Clock In/Out -->
|
||||
<t t-if="clock_enabled">
|
||||
<div class="tech-clock-card mb-3"
|
||||
id="techClockCard"
|
||||
t-att-data-checked-in="'true' if clock_checked_in else 'false'"
|
||||
t-att-data-check-in-time="clock_check_in_time or ''">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="tech-clock-dot" t-att-class="'tech-clock-dot--active' if clock_checked_in else ''"/>
|
||||
<div>
|
||||
<div class="tech-clock-status" id="clockStatusText">
|
||||
<t t-if="clock_checked_in">Clocked In</t>
|
||||
<t t-else="">Not Clocked In</t>
|
||||
</div>
|
||||
<div class="tech-clock-timer" id="clockTimer">00:00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="tech-clock-btn" id="clockActionBtn"
|
||||
t-att-class="'tech-clock-btn--out' if clock_checked_in else 'tech-clock-btn--in'"
|
||||
onclick="handleClockAction()">
|
||||
<t t-if="clock_checked_in">
|
||||
<i class="fa fa-stop-circle-o"/> Clock Out
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-play-circle-o"/> Clock In
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tech-clock-error" id="clockError" style="display:none;">
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
<span id="clockErrorText"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Stats Cards - 2x2 on mobile, 4 columns on desktop -->
|
||||
<div class="row mb-3 g-2">
|
||||
<div class="col-6 col-md-3">
|
||||
@@ -1377,6 +1500,113 @@
|
||||
<!-- Include loaner modals -->
|
||||
<t t-call="fusion_authorizer_portal.loaner_checkout_modal"/>
|
||||
<t t-call="fusion_authorizer_portal.loaner_return_modal"/>
|
||||
|
||||
<!-- Clock In/Out JS -->
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var card = document.getElementById('techClockCard');
|
||||
if (!card) return;
|
||||
|
||||
var isCheckedIn = card.dataset.checkedIn === 'true';
|
||||
var checkInTime = card.dataset.checkInTime ? new Date(card.dataset.checkInTime + 'Z') : null;
|
||||
var timerInterval = null;
|
||||
|
||||
function updateTimer() {
|
||||
if (!checkInTime) return;
|
||||
var diff = Math.max(0, Math.floor((new Date() - checkInTime) / 1000));
|
||||
var h = Math.floor(diff / 3600);
|
||||
var m = Math.floor((diff % 3600) / 60);
|
||||
var s = diff % 60;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
document.getElementById('clockTimer').textContent = pad(h) + ':' + pad(m) + ':' + pad(s);
|
||||
}
|
||||
|
||||
function startTimer() { stopTimer(); updateTimer(); timerInterval = setInterval(updateTimer, 1000); }
|
||||
function stopTimer() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } }
|
||||
|
||||
function applyState() {
|
||||
var dot = card.querySelector('.tech-clock-dot');
|
||||
var statusEl = document.getElementById('clockStatusText');
|
||||
var btn = document.getElementById('clockActionBtn');
|
||||
var timerEl = document.getElementById('clockTimer');
|
||||
if (dot) dot.className = 'tech-clock-dot' + (isCheckedIn ? ' tech-clock-dot--active' : '');
|
||||
if (statusEl) statusEl.textContent = isCheckedIn ? 'Clocked In' : 'Not Clocked In';
|
||||
if (btn) {
|
||||
btn.className = 'tech-clock-btn ' + (isCheckedIn ? 'tech-clock-btn--out' : 'tech-clock-btn--in');
|
||||
btn.innerHTML = isCheckedIn
|
||||
? '<i class="fa fa-stop-circle-o"></i> Clock Out'
|
||||
: '<i class="fa fa-play-circle-o"></i> Clock In';
|
||||
}
|
||||
if (!isCheckedIn && timerEl) timerEl.textContent = '00:00:00';
|
||||
}
|
||||
|
||||
if (isCheckedIn && checkInTime) startTimer();
|
||||
|
||||
window.handleClockAction = function() {
|
||||
var btn = document.getElementById('clockActionBtn');
|
||||
var errEl = document.getElementById('clockError');
|
||||
var errText = document.getElementById('clockErrorText');
|
||||
btn.disabled = true;
|
||||
errEl.style.display = 'none';
|
||||
|
||||
function doClockAction(lat, lng, acc) {
|
||||
fetch('/fusion_clock/clock_action', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: {
|
||||
latitude: lat, longitude: lng, accuracy: acc, source: 'portal'
|
||||
}})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var result = data.result || {};
|
||||
if (result.error) {
|
||||
errText.textContent = result.error;
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (result.action === 'clock_in') {
|
||||
isCheckedIn = true;
|
||||
checkInTime = new Date(result.check_in + 'Z');
|
||||
startTimer();
|
||||
} else {
|
||||
isCheckedIn = false;
|
||||
checkInTime = null;
|
||||
stopTimer();
|
||||
}
|
||||
applyState();
|
||||
btn.disabled = false;
|
||||
})
|
||||
.catch(function() {
|
||||
errText.textContent = 'Network error. Please try again.';
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (window.fusionGetLocation) {
|
||||
window.fusionGetLocation().then(function(coords) {
|
||||
doClockAction(coords.latitude, coords.longitude, coords.accuracy);
|
||||
}).catch(function() {
|
||||
errText.textContent = 'Location access is required for clock in/out.';
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
});
|
||||
} else if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(function(pos) {
|
||||
doClockAction(pos.coords.latitude, pos.coords.longitude, pos.coords.accuracy);
|
||||
}, function() {
|
||||
errText.textContent = 'Location access is required for clock in/out.';
|
||||
errEl.style.display = 'flex';
|
||||
btn.disabled = false;
|
||||
}, {enableHighAccuracy: true, timeout: 15000});
|
||||
} else {
|
||||
doClockAction(0, 0, 0);
|
||||
}
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user