feat: separate fusion field service and LTC into standalone modules, update core modules
- fusion_claims: separated field service logic, updated controllers/views - fusion_tasks: updated task views and map integration - fusion_authorizer_portal: added page 11 signing, schedule booking, migrations - fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator) - fusion_ltc_management: new standalone LTC management module
This commit is contained in:
@@ -188,7 +188,90 @@
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
<!-- My Schedule (All portal roles) -->
|
||||
<div class="col-md-6">
|
||||
<a href="/my/schedule" class="card h-100 border-0 shadow-sm text-decoration-none" style="border-radius: 12px; min-height: 100px;">
|
||||
<div class="card-body d-flex align-items-center p-4">
|
||||
<div class="me-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center" t-attf-style="width: 50px; height: 50px; background: {{fc_gradient}};">
|
||||
<i class="fa fa-calendar-check-o fa-lg text-white"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-1 text-dark">My Schedule</h5>
|
||||
<small class="text-muted">View and book appointments</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 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 +334,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 +1191,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 +1517,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>
|
||||
|
||||
@@ -3699,4 +3946,232 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TASK-LEVEL POD SIGNATURE (works for shadow + regular tasks) -->
|
||||
<!-- ============================================================ -->
|
||||
<template id="portal_task_pod_signature" name="Task POD Signature Capture">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="False"/>
|
||||
<t t-set="no_breadcrumbs" t-value="True"/>
|
||||
|
||||
<div class="container mt-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="/my/home">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/my/technician">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="/my/technician/tasks">Tasks</a></li>
|
||||
<li class="breadcrumb-item"><a t-attf-href="/my/technician/task/#{task.id}"><t t-out="task.name"/></a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Collect POD Signature</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
<i class="fa fa-pencil-square-o me-2"/>
|
||||
Proof of Delivery - <t t-out="task.name"/>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="fa fa-truck me-2"/>Delivery Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Client:</strong> <t t-out="task.client_display_name or 'N/A'"/></p>
|
||||
<p><strong>Task:</strong> <t t-out="task.name"/></p>
|
||||
<p><strong>Type:</strong>
|
||||
<t t-out="dict(task._fields['task_type'].selection).get(task.task_type, '')"/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Delivery Address:</strong></p>
|
||||
<p class="mb-0 text-muted">
|
||||
<t t-out="task.address_display or 'No address'"/>
|
||||
</p>
|
||||
<t t-if="task.scheduled_date">
|
||||
<p class="mt-2"><strong>Scheduled:</strong>
|
||||
<t t-out="task.scheduled_date" t-options="{'widget': 'date'}"/>
|
||||
<t t-out="task.time_start_display"/> - <t t-out="task.time_end_display"/>
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="card" id="task-pod-signature-section">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="fa fa-pencil me-2"/>Client Signature</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<t t-if="has_existing_signature">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
A signature has already been collected. Submitting a new one will replace it.
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<form id="taskPodSignatureForm">
|
||||
<div class="mb-3">
|
||||
<label for="task_client_name" class="form-label">
|
||||
Client Name <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="task_client_name"
|
||||
name="client_name" required=""
|
||||
t-att-value="task.pod_client_name or task.client_display_name or ''"
|
||||
placeholder="Enter the client's full name"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="task_signature_date" class="form-label">Signature Date</label>
|
||||
<input type="date" class="form-control" id="task_signature_date" name="signature_date"/>
|
||||
<script>document.getElementById('task_signature_date').value = new Date().toISOString().slice(0,10);</script>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Signature <span class="text-danger">*</span></label>
|
||||
<div class="border rounded p-2 bg-white">
|
||||
<canvas id="task-signature-canvas"
|
||||
style="width:100%;height:200px;border:1px dashed #ccc;border-radius:4px;touch-action:none;">
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<small class="text-muted">Draw signature above</small>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="clearTaskSignature()">
|
||||
<i class="fa fa-eraser me-1"/>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-success btn-lg"
|
||||
onclick="submitTaskPODSignature()">
|
||||
<i class="fa fa-check me-2"/>Submit Signature
|
||||
</button>
|
||||
<a t-attf-href="/my/technician/task/#{task.id}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tpod-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%;
|
||||
background:rgba(0,0,0,0.65); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
|
||||
z-index:9999; align-items:center; justify-content:center; }
|
||||
.tpod-overlay.show { display:flex; }
|
||||
.tpod-overlay-card { background:#fff; border-radius:20px; padding:2.5rem 2rem;
|
||||
max-width:420px; width:90%; text-align:center; animation:tpodSlideUp 0.3s ease;
|
||||
box-shadow:0 8px 32px rgba(0,0,0,0.2); }
|
||||
.tpod-overlay-icon { font-size:3.5rem; margin-bottom:1rem; }
|
||||
@keyframes tpodSlideUp { from { opacity:0; transform:translateY(30px); } to { opacity:1; transform:translateY(0); } }
|
||||
</style>
|
||||
<div id="taskPodOverlay" class="tpod-overlay">
|
||||
<div class="tpod-overlay-card">
|
||||
<div class="tpod-overlay-icon" id="tpodIcon"></div>
|
||||
<h4 id="tpodTitle" style="font-weight:700;"></h4>
|
||||
<p id="tpodMsg" style="color:#6c757d;margin-bottom:1.5rem;"></p>
|
||||
<div id="tpodActions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var tCanvas, tCtx, tIsDrawing = false;
|
||||
|
||||
function showTaskPodOverlay(type, title, msg, url) {
|
||||
var ov = document.getElementById('taskPodOverlay');
|
||||
document.getElementById('tpodIcon').innerHTML = type === 'success'
|
||||
? '<i class="fa fa-check-circle text-success"></i>'
|
||||
: '<i class="fa fa-exclamation-circle text-danger"></i>';
|
||||
document.getElementById('tpodTitle').textContent = title;
|
||||
document.getElementById('tpodTitle').className = type === 'success' ? 'text-success' : 'text-danger';
|
||||
document.getElementById('tpodMsg').textContent = msg;
|
||||
var acts = document.getElementById('tpodActions');
|
||||
if (type === 'success' && url) {
|
||||
acts.innerHTML = '<a href="' + url + '" class="btn btn-success w-100 rounded-pill mb-2">Continue</a>' +
|
||||
'<p class="text-muted small mb-0">Redirecting in <span id="tpodCD">3</span>s...</p>';
|
||||
ov.classList.add('show');
|
||||
var s = 3, t = setInterval(function() { s--; var c = document.getElementById('tpodCD');
|
||||
if (c) c.textContent = s; if (s <= 0) { clearInterval(t); window.location.href = url; } }, 1000);
|
||||
} else {
|
||||
acts.innerHTML = '<button class="btn btn-outline-secondary w-100 rounded-pill" onclick="document.getElementById(\'taskPodOverlay\').classList.remove(\'show\')">OK</button>';
|
||||
ov.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
tCanvas = document.getElementById('task-signature-canvas');
|
||||
if (!tCanvas) return;
|
||||
tCtx = tCanvas.getContext('2d');
|
||||
var r = tCanvas.getBoundingClientRect();
|
||||
tCanvas.width = r.width * 2; tCanvas.height = r.height * 2;
|
||||
tCtx.scale(2, 2); tCtx.lineCap = 'round'; tCtx.lineJoin = 'round';
|
||||
tCtx.lineWidth = 2; tCtx.strokeStyle = '#000';
|
||||
tCanvas.addEventListener('mousedown', tStart);
|
||||
tCanvas.addEventListener('mousemove', tDraw);
|
||||
tCanvas.addEventListener('mouseup', tStop);
|
||||
tCanvas.addEventListener('mouseout', tStop);
|
||||
tCanvas.addEventListener('touchstart', function(e) { e.preventDefault(); tStart(e); }, {passive:false});
|
||||
tCanvas.addEventListener('touchmove', function(e) { e.preventDefault(); tDraw(e); }, {passive:false});
|
||||
tCanvas.addEventListener('touchend', tStop);
|
||||
var sec = document.getElementById('task-pod-signature-section');
|
||||
if (sec) setTimeout(function() { sec.scrollIntoView({behavior:'smooth', block:'start'}); }, 300);
|
||||
});
|
||||
|
||||
function tPos(e) {
|
||||
var r = tCanvas.getBoundingClientRect();
|
||||
if (e.touches) return { x: e.touches[0].clientX - r.left, y: e.touches[0].clientY - r.top };
|
||||
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
||||
}
|
||||
function tStart(e) { tIsDrawing = true; var p = tPos(e); tCtx.beginPath(); tCtx.moveTo(p.x, p.y); }
|
||||
function tDraw(e) { if (!tIsDrawing) return; var p = tPos(e); tCtx.lineTo(p.x, p.y); tCtx.stroke(); }
|
||||
function tStop() { tIsDrawing = false; }
|
||||
function clearTaskSignature() { if (tCtx) tCtx.clearRect(0, 0, tCanvas.width, tCanvas.height); }
|
||||
|
||||
function submitTaskPODSignature() {
|
||||
var name = document.getElementById('task_client_name').value.trim();
|
||||
var sigDate = document.getElementById('task_signature_date').value;
|
||||
if (!name) { showTaskPodOverlay('error', 'Missing Information', 'Please enter the client name.'); return; }
|
||||
var blank = document.createElement('canvas');
|
||||
blank.width = tCanvas.width; blank.height = tCanvas.height;
|
||||
if (tCanvas.toDataURL() === blank.toDataURL()) {
|
||||
showTaskPodOverlay('error', 'Missing Signature', 'Please draw a signature before submitting.'); return;
|
||||
}
|
||||
var sigData = tCanvas.toDataURL('image/png');
|
||||
var btn = document.querySelector('button[onclick="submitTaskPODSignature()"]');
|
||||
var orig = btn.innerHTML; btn.innerHTML = '<i class="fa fa-spinner fa-spin me-2"></i>Saving...'; btn.disabled = true;
|
||||
fetch('<t t-out="'/my/technician/task/' + str(task.id) + '/pod/sign'"/>', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ jsonrpc:'2.0', method:'call',
|
||||
params: { client_name: name, signature_data: sigData, signature_date: sigDate || null },
|
||||
id: Math.floor(Math.random()*1000000) })
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (d.result && d.result.success) {
|
||||
showTaskPodOverlay('success', 'Signature Saved!', 'Proof of Delivery recorded.', d.result.redirect_url);
|
||||
} else {
|
||||
showTaskPodOverlay('error', 'Error', d.result?.error || 'Unknown error');
|
||||
btn.innerHTML = orig; btn.disabled = false;
|
||||
}
|
||||
}).catch(function() {
|
||||
showTaskPodOverlay('error', 'Connection Error', 'Please check your connection.');
|
||||
btn.innerHTML = orig; btn.disabled = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user