This commit is contained in:
gsinghpal
2026-03-16 08:14:56 -04:00
parent fdca9518ab
commit e56974d46f
196 changed files with 19739 additions and 3471 deletions

View File

@@ -5,7 +5,7 @@
<record id="view_fusion_loaner_checkout_form_assessment" model="ir.ui.view">
<field name="name">fusion.loaner.checkout.form.assessment</field>
<field name="model">fusion.loaner.checkout</field>
<field name="inherit_id" ref="fusion_claims.view_fusion_loaner_checkout_form"/>
<field name="inherit_id" ref="fusion_loaners_management.view_fusion_loaner_checkout_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_partner']" position="before">
<button name="action_view_assessment" type="object"

View File

@@ -1,348 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ==================== SCHEDULE OVERVIEW PAGE ==================== -->
<template id="portal_schedule_page" name="My Schedule">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<div class="container py-4">
<!-- Success/Error Messages -->
<t t-if="request.params.get('success')">
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fa fa-check-circle me-2"/><t t-out="request.params.get('success')"/>
<button type="button" class="btn-close" data-bs-dismiss="alert"/>
</div>
</t>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<div>
<h3 class="mb-1"><i class="fa fa-calendar-check-o me-2"/>My Schedule</h3>
<p class="text-muted mb-0">View your appointments and book new ones</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<t t-if="share_url">
<div class="input-group" style="max-width: 350px;">
<input type="text" class="form-control form-control-sm" t-att-value="share_url"
id="shareBookingUrl" readonly="readonly" style="font-size: 13px;"/>
<button class="btn btn-outline-secondary btn-sm" type="button"
id="btnCopyShareUrl">
<i class="fa fa-copy" id="copyIcon"/> <span id="copyText">Copy</span>
</button>
<script type="text/javascript">
(function() {
var btn = document.getElementById('btnCopyShareUrl');
if (!btn) return;
btn.addEventListener('click', function() {
var url = document.getElementById('shareBookingUrl').value;
navigator.clipboard.writeText(url);
var icon = document.getElementById('copyIcon');
var text = document.getElementById('copyText');
icon.className = 'fa fa-check';
text.textContent = 'Copied';
setTimeout(function() {
icon.className = 'fa fa-copy';
text.textContent = 'Copy';
}, 2000);
});
})();
</script>
</div>
</t>
<a href="/my/schedule/book" class="btn btn-primary">
<i class="fa fa-plus me-1"/> Book Appointment
</a>
</div>
</div>
<!-- Today's Appointments -->
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
<div class="card-header bg-white border-bottom-0 pt-3 pb-2 px-4"
style="border-radius: 12px 12px 0 0;">
<h5 class="mb-0"><i class="fa fa-sun-o me-2 text-warning"/>Today's Appointments</h5>
</div>
<div class="card-body px-4 pb-4 pt-2">
<t t-if="today_events">
<div class="list-group list-group-flush">
<t t-foreach="today_events" t-as="event">
<div class="list-group-item px-0 py-3 border-start-0 border-end-0">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="rounded-3 text-center px-3 py-2 me-3"
t-attf-style="background: #{portal_gradient}; min-width: 70px;">
<div class="text-white fw-bold" style="font-size: 14px;">
<t t-out="event.start.astimezone(user_tz).strftime('%I:%M')"/>
</div>
<div class="text-white" style="font-size: 10px;">
<t t-out="event.start.astimezone(user_tz).strftime('%p')"/>
</div>
</div>
<div>
<h6 class="mb-0"><t t-out="event.name"/></h6>
<small class="text-muted">
<t t-if="event.location">
<i class="fa fa-map-marker me-1"/><t t-out="event.location"/>
</t>
</small>
</div>
</div>
<div class="text-end">
<span class="badge bg-light text-dark">
<t t-out="'%.0f' % (event.duration * 60)"/> min
</span>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-else="">
<p class="text-muted mb-0 py-3 text-center">
<i class="fa fa-calendar-o me-1"/> No appointments scheduled for today.
</p>
</t>
</div>
</div>
<!-- Upcoming Appointments -->
<div class="card border-0 shadow-sm" style="border-radius: 12px;">
<div class="card-header bg-white border-bottom-0 pt-3 pb-2 px-4"
style="border-radius: 12px 12px 0 0;">
<h5 class="mb-0"><i class="fa fa-calendar me-2 text-primary"/>Upcoming Appointments</h5>
</div>
<div class="card-body px-4 pb-4 pt-2">
<t t-if="upcoming_events">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="border-top:none;">Date</th>
<th style="border-top:none;">Time</th>
<th style="border-top:none;">Appointment</th>
<th style="border-top:none;">Location</th>
<th style="border-top:none;">Duration</th>
</tr>
</thead>
<tbody>
<t t-foreach="upcoming_events" t-as="event">
<tr>
<td>
<strong><t t-out="event.start.astimezone(user_tz).strftime('%b %d')"/></strong>
<br/>
<small class="text-muted">
<t t-out="event.start.astimezone(user_tz).strftime('%A')"/>
</small>
</td>
<td>
<t t-out="event.start.astimezone(user_tz).strftime('%I:%M %p')"/>
</td>
<td><t t-out="event.name"/></td>
<td>
<t t-if="event.location">
<small><t t-out="event.location"/></small>
</t>
<t t-else="">
<small class="text-muted">-</small>
</t>
</td>
<td>
<span class="badge bg-light text-dark">
<t t-out="'%.0f' % (event.duration * 60)"/> min
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</t>
<t t-else="">
<p class="text-muted mb-0 py-3 text-center">
<i class="fa fa-calendar-o me-1"/> No upcoming appointments.
<a href="/my/schedule/book">Book one now</a>
</p>
</t>
</div>
</div>
</div>
</t>
</template>
<!-- ==================== BOOKING FORM ==================== -->
<template id="portal_schedule_book" name="Book Appointment">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<div class="container py-4" style="max-width: 800px;">
<!-- Error Messages -->
<t t-if="error">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fa fa-exclamation-circle me-2"/><t t-out="error"/>
<button type="button" class="btn-close" data-bs-dismiss="alert"/>
</div>
</t>
<!-- Header -->
<div class="mb-4">
<a href="/my/schedule" class="text-muted text-decoration-none mb-2 d-inline-block">
<i class="fa fa-arrow-left me-1"/> Back to Schedule
</a>
<h3 class="mb-1"><i class="fa fa-plus-circle me-2"/>Book Appointment</h3>
<p class="text-muted mb-0">Select a time slot and enter client details</p>
</div>
<form action="/my/schedule/book/submit" method="post" id="bookingForm">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<!-- Step 1: Appointment Type + Date/Time -->
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
<div class="card-header bg-white border-bottom pt-3 pb-2 px-4"
style="border-radius: 12px 12px 0 0;">
<h5 class="mb-0">
<span class="badge rounded-pill me-2"
t-attf-style="background: #{portal_gradient};">1</span>
Date &amp; Time
</h5>
</div>
<div class="card-body px-4 pb-4">
<!-- Appointment Type (if multiple) -->
<t t-if="len(appointment_types) > 1">
<div class="mb-3">
<label class="form-label fw-semibold">Appointment Type</label>
<select name="appointment_type_id" class="form-select"
id="appointmentTypeSelect">
<t t-foreach="appointment_types" t-as="atype">
<option t-att-value="atype.id"
t-att-selected="atype.id == selected_type.id"
t-att-data-duration="atype.appointment_duration">
<t t-out="atype.name"/>
(<t t-out="'%.0f' % (atype.appointment_duration * 60)"/> min)
</option>
</t>
</select>
</div>
</t>
<t t-else="">
<input type="hidden" name="appointment_type_id"
t-att-value="selected_type.id"/>
</t>
<!-- Date Picker -->
<div class="mb-3">
<label class="form-label fw-semibold">Select Date</label>
<input type="date" class="form-control" id="bookingDate"
required="required"
t-att-min="now.strftime('%Y-%m-%d')"/>
</div>
<!-- Week Calendar Preview -->
<div id="weekCalendarContainer" class="mb-3" style="display: none;">
<label class="form-label fw-semibold">
<i class="fa fa-calendar me-1"/>Your Week
</label>
<div id="weekCalendarLoading" class="text-center py-3" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"/>
Loading calendar...
</div>
<div id="weekCalendarGrid" class="border rounded-3 overflow-hidden" style="display: none;">
<div id="weekCalendarHeader" class="d-flex bg-light border-bottom" style="min-height: 40px;"></div>
<div id="weekCalendarBody" class="d-flex" style="min-height: 80px;"></div>
</div>
<div id="weekCalendarEmpty" class="text-muted py-2 text-center" style="display: none;">
<i class="fa fa-calendar-o me-1"/> No events this week -- your schedule is open.
</div>
</div>
<!-- Available Slots -->
<div id="slotsContainer" style="display: none;">
<label class="form-label fw-semibold">Available Time Slots</label>
<div id="slotsLoading" class="text-center py-3" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"/>
Loading available slots...
</div>
<div id="slotsGrid" class="d-flex flex-wrap gap-2 mb-2"></div>
<div id="noSlots" class="text-muted py-2" style="display: none;">
<i class="fa fa-info-circle me-1"/> No available slots for this date.
Try another date.
</div>
<input type="hidden" name="slot_datetime" id="slotDatetime"/>
<input type="hidden" name="slot_duration" id="slotDuration"
t-att-value="selected_type.appointment_duration"/>
</div>
</div>
</div>
<!-- Step 2: Client Details -->
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
<div class="card-header bg-white border-bottom pt-3 pb-2 px-4"
style="border-radius: 12px 12px 0 0;">
<h5 class="mb-0">
<span class="badge rounded-pill me-2"
t-attf-style="background: #{portal_gradient};">2</span>
Client Details
</h5>
</div>
<div class="card-body px-4 pb-4">
<div class="mb-3">
<label class="form-label fw-semibold">Client Name <span class="text-danger">*</span></label>
<input type="text" name="client_name" class="form-control"
placeholder="Enter client's full name" required="required"/>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Address</label>
<input type="text" name="client_street" class="form-control mb-2"
id="clientStreet"
placeholder="Start typing address..."/>
</div>
<div class="row g-2 mb-3">
<div class="col-md-4">
<input type="text" name="client_city" class="form-control"
id="clientCity" placeholder="City"/>
</div>
<div class="col-md-4">
<input type="text" name="client_province" class="form-control"
id="clientProvince" placeholder="Province"/>
</div>
<div class="col-md-4">
<input type="text" name="client_postal" class="form-control"
id="clientPostal" placeholder="Postal Code"/>
</div>
</div>
<div class="mb-0">
<label class="form-label fw-semibold">Notes</label>
<textarea name="notes" class="form-control" rows="3"
placeholder="e.g. Equipment to bring, special instructions, reason for visit..."></textarea>
</div>
</div>
</div>
<!-- Submit -->
<div class="d-flex justify-content-between">
<a href="/my/schedule" class="btn btn-outline-secondary">
<i class="fa fa-arrow-left me-1"/> Cancel
</a>
<button type="submit" class="btn btn-primary btn-lg px-4" id="btnSubmitBooking"
disabled="disabled">
<i class="fa fa-calendar-check-o me-1"/> Book Appointment
</button>
</div>
</form>
</div>
<!-- Google Maps Places API -->
<t t-if="google_maps_api_key">
<script t-attf-src="https://maps.googleapis.com/maps/api/js?key=#{google_maps_api_key}&amp;libraries=places&amp;callback=initScheduleAddressAutocomplete"
async="async" defer="defer"></script>
</t>
<script t-attf-src="/fusion_authorizer_portal/static/src/js/portal_schedule_booking.js"></script>
</t>
</template>
</odoo>

View File

@@ -23,34 +23,79 @@
<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-att-data-check-in-time="clock_check_in_time or ''"
t-att-data-today-hours="clock_today_hours or 0">
<div class="tech-clock-layout">
<div class="tech-clock-info">
<div class="tech-clock-status-line">
<span t-attf-class="tech-clock-dot {{ 'tech-clock-dot--active' if clock_checked_in else '' }}"/>
<span 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>
</span>
</div>
<div class="tech-clock-timer" id="clockTimer">00:00:00</div>
<div class="tech-clock-hours" id="clockTodayHours">
Today: <t t-esc="'%.1fh' % (clock_today_hours or 0)"/>
</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 class="tech-clock-action">
<div t-attf-class="tech-clock-orb-wrap {{ 'tech-clock-orb-wrap--active' if clock_checked_in else '' }}" id="clockOrbWrap">
<span class="tech-clock-wave tech-clock-wave--1"/>
<span class="tech-clock-wave tech-clock-wave--2"/>
<span class="tech-clock-wave tech-clock-wave--3"/>
<button t-attf-class="tech-clock-orb {{ 'tech-clock-orb--out' if clock_checked_in else '' }}"
id="clockActionBtn" onclick="handleClockAction()">
<svg class="tech-clock-orb-icon" id="clockIconPlay" width="22" height="22" viewBox="0 0 24 24" fill="white"
t-attf-style="display:{{ 'none' if clock_checked_in else 'block' }}">
<polygon points="6 3 20 12 6 21"/>
</svg>
<svg class="tech-clock-orb-icon" id="clockIconStop" width="18" height="18" viewBox="0 0 24 24" fill="white"
t-attf-style="display:{{ 'block' if clock_checked_in else 'none' }}">
<rect x="4" y="4" width="16" height="16" rx="2"/>
</svg>
</button>
</div>
<span class="tech-clock-label" id="clockBtnLabel">
<t t-if="clock_checked_in">Clock Out</t>
<t t-else="">Clock In</t>
</span>
</div>
</div>
<div class="tech-clock-error" id="clockError" style="display:none;">
<i class="fa fa-exclamation-triangle"/>
<span id="clockErrorText"/>
</div>
</div>
<!-- Missed Clock-Out Reason Modal -->
<div class="tech-reason-overlay" id="clockReasonModal" style="display:none;">
<div class="tech-reason-backdrop" onclick="document.getElementById('clockReasonModal').style.display='none'"/>
<div class="tech-reason-dialog">
<div class="tech-reason-header">
<i class="fa fa-exclamation-triangle text-warning" style="font-size:1.5rem;"/>
<h5>Missed Clock-Out</h5>
<p class="text-muted small mb-0">You didn't clock out on your last shift. Please provide details before clocking in.</p>
</div>
<div class="tech-reason-body">
<label class="form-label small fw-semibold" for="clockReasonText">
Reason <span class="text-danger">*</span>
</label>
<textarea id="clockReasonText" class="form-control mb-2" rows="3"
placeholder="Please explain why you didn't clock out..."/>
<label class="form-label small fw-semibold" for="clockReasonTime">
Departure Time <span class="text-muted">(optional)</span>
</label>
<input type="datetime-local" id="clockReasonTime" class="form-control"/>
</div>
<div class="tech-reason-footer">
<button class="btn btn-sm btn-secondary" onclick="document.getElementById('clockReasonModal').style.display='none'">Cancel</button>
<button class="btn btn-sm btn-success" id="clockReasonSubmitBtn" onclick="submitClockReason()">
<i class="fa fa-check"/> Submit
</button>
</div>
</div>
</div>
</t>
<!-- Quick Stats Bar -->
@@ -290,20 +335,150 @@
var statusEl = document.getElementById('clockStatusText');
var btn = document.getElementById('clockActionBtn');
var timerEl = document.getElementById('clockTimer');
var labelEl = document.getElementById('clockBtnLabel');
var orbWrap = document.getElementById('clockOrbWrap');
var playIcon = document.getElementById('clockIconPlay');
var stopIcon = document.getElementById('clockIconStop');
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
? '&lt;i class="fa fa-stop-circle-o">&lt;/i> Clock Out'
: '&lt;i class="fa fa-play-circle-o">&lt;/i> Clock In';
btn.className = 'tech-clock-orb' + (isCheckedIn ? ' tech-clock-orb--out' : '');
}
if (orbWrap) {
orbWrap.className = 'tech-clock-orb-wrap' + (isCheckedIn ? ' tech-clock-orb-wrap--active' : '');
}
if (playIcon) playIcon.style.display = isCheckedIn ? 'none' : 'block';
if (stopIcon) stopIcon.style.display = isCheckedIn ? 'block' : 'none';
if (labelEl) labelEl.textContent = isCheckedIn ? 'Clock Out' : 'Clock In';
if (!isCheckedIn &amp;&amp; timerEl) timerEl.textContent = '00:00:00';
}
if (isCheckedIn &amp;&amp; checkInTime) startTimer();
function showClockError(msg) {
var errEl = document.getElementById('clockError');
var errText = document.getElementById('clockErrorText');
var btn = document.getElementById('clockActionBtn');
if (errText) errText.textContent = msg;
if (errEl) errEl.style.display = 'flex';
if (btn) btn.disabled = false;
}
function doClockAction(lat, lng, accuracy) {
var btn = document.getElementById('clockActionBtn');
var errEl = document.getElementById('clockError');
fetch('/fusion_clock/clock_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({jsonrpc: '2.0', id: 1, method: 'call', params: {
latitude: lat,
longitude: lng,
accuracy: accuracy,
source: 'portal'
}})
})
.then(function(r) {
if (!r.ok) {
throw new Error('HTTP ' + r.status);
}
return r.json();
})
.then(function(data) {
if (data.error) {
var msg = (data.error.data &amp;&amp; data.error.data.message) || data.error.message || 'Server error';
showClockError(msg);
return;
}
var result = data.result;
if (!result) {
showClockError('No response from server. Please try again.');
return;
}
if (result.error) {
showClockError(result.error);
return;
}
if (result.requires_reason) {
var modal = document.getElementById('clockReasonModal');
if (modal) modal.style.display = 'flex';
btn.disabled = false;
return;
}
if (result.action === 'clock_in') {
isCheckedIn = true;
checkInTime = new Date(result.check_in + 'Z');
startTimer();
} else if (result.action === 'clock_out') {
isCheckedIn = false;
checkInTime = null;
stopTimer();
} else {
showClockError(result.message || 'Unexpected response. Please try again.');
return;
}
applyState();
btn.disabled = false;
})
.catch(function(e) {
showClockError('Network error. Please try again.');
});
}
window.submitClockReason = function() {
var reasonEl = document.getElementById('clockReasonText');
var timeEl = document.getElementById('clockReasonTime');
var submitBtn = document.getElementById('clockReasonSubmitBtn');
var reason = reasonEl ? reasonEl.value.trim() : '';
if (!reason) {
showClockError('Please provide a reason.');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '&lt;i class="fa fa-spinner fa-spin">&lt;/i> Submitting...';
var rawTime = timeEl ? timeEl.value.trim() : '';
var depTime = rawTime ? new Date(rawTime).toISOString() : '';
fetch('/fusion_clock/submit_reason', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({jsonrpc: '2.0', id: 1, method: 'call', params: {
reason: reason,
departure_time: depTime
}})
})
.then(function(r) { return r.json(); })
.then(function(data) {
submitBtn.disabled = false;
submitBtn.innerHTML = '&lt;i class="fa fa-check">&lt;/i> Submit';
if (data.error) {
showClockError((data.error.data &amp;&amp; data.error.data.message) || data.error.message || 'Server error');
return;
}
var result = data.result || {};
if (result.success) {
var modal = document.getElementById('clockReasonModal');
if (modal) modal.style.display = 'none';
if (reasonEl) reasonEl.value = '';
if (timeEl) timeEl.value = '';
var errEl = document.getElementById('clockError');
if (errEl) errEl.style.display = 'none';
handleClockAction();
} else {
showClockError(result.error || 'Failed to submit reason.');
}
})
.catch(function() {
submitBtn.disabled = false;
submitBtn.innerHTML = '&lt;i class="fa fa-check">&lt;/i> Submit';
showClockError('Network error. Please try again.');
});
};
window.handleClockAction = function() {
var btn = document.getElementById('clockActionBtn');
var errEl = document.getElementById('clockError');
@@ -311,48 +486,29 @@
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;
});
if (!navigator.geolocation) {
doClockAction(0, 0, 0);
return;
}
navigator.geolocation.getCurrentPosition(
function(pos) {
doClockAction(pos.coords.latitude, pos.coords.longitude, pos.coords.accuracy);
},
function() {
fetch('https://ipapi.co/json/')
.then(function(r) { return r.json(); })
.then(function(ipData) {
var lat = (ipData.latitude &amp;&amp; ipData.longitude) ? ipData.latitude : 0;
var lng = (ipData.latitude &amp;&amp; ipData.longitude) ? ipData.longitude : 0;
doClockAction(lat, lng, lat ? 5000 : 0);
})
.catch(function() {
doClockAction(0, 0, 0);
});
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
);
};
})();
</script>
@@ -675,46 +831,107 @@
</div>
</div>
<!-- ===== TASK DETAILS (collapsible) ===== -->
<t t-if="task.description or task.equipment_needed">
<!-- ===== INSTRUCTIONS ===== -->
<t t-if="task.description">
<div class="tech-card mb-3">
<t t-if="task.description">
<div class="mb-2">
<div class="text-muted small text-uppercase fw-semibold mb-1">
<i class="fa fa-file-text-o me-1"/>Instructions
<div class="text-muted small text-uppercase fw-semibold mb-1">
<i class="fa fa-file-text-o me-1"/>Instructions
</div>
<div style="white-space:pre-wrap;font-size:0.9rem;"><t t-out="task.description"/></div>
</div>
</t>
<!-- ===== ORDER DETAILS (Sale Order or Purchase Order) ===== -->
<t t-if="linked_order">
<div class="tech-card tech-order-card mb-3">
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="d-flex align-items-center">
<div t-attf-class="tech-card-icon #{linked_order_type == 'sale' and 'bg-info-subtle text-info' or 'bg-purple-subtle text-purple'}">
<i t-attf-class="fa #{linked_order_type == 'sale' and 'fa-shopping-cart' or 'fa-truck'}"/>
</div>
<div style="white-space:pre-wrap;font-size:0.9rem;"><t t-out="task.description"/></div>
<div>
<div class="text-muted small text-uppercase fw-semibold" style="font-size:0.68rem;letter-spacing:0.05em;">
<t t-if="linked_order_type == 'sale'">Sale Order</t>
<t t-else="">Purchase Order</t>
</div>
<div class="fw-bold" style="font-size:0.95rem;"><t t-out="linked_order.name"/></div>
</div>
</div>
<t t-if="linked_order_type == 'sale'">
<a t-attf-href="/my/orders/#{linked_order.id}" class="btn btn-sm btn-outline-secondary rounded-pill"
style="font-size:0.75rem;">
<i class="fa fa-external-link me-1"/>View
</a>
</t>
</div>
<!-- Order Lines: Products & Services -->
<t t-if="order_lines">
<div class="tech-order-lines">
<div class="text-muted small text-uppercase fw-semibold mb-2" style="font-size:0.68rem;letter-spacing:0.05em;">
<i class="fa fa-cube me-1"/>Items (<t t-out="len(order_lines)"/>)
</div>
<t t-foreach="order_lines" t-as="line">
<div t-attf-class="tech-order-line-item #{not line_last and 'tech-order-line-border' or ''}">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1 me-2">
<div class="fw-semibold" style="font-size:0.88rem;">
<t t-out="line.product_id.name"/>
</div>
<!-- Product description (differs from product name) -->
<t t-if="linked_order_type == 'sale'">
<t t-set="line_desc" t-value="line.name or ''"/>
</t>
<t t-else="">
<t t-set="line_desc" t-value="line.name or ''"/>
</t>
<t t-if="line_desc and line_desc != line.product_id.name">
<div class="text-muted" style="font-size:0.8rem;white-space:pre-wrap;line-height:1.35;">
<t t-out="line_desc"/>
</div>
</t>
<!-- Serial number if available -->
<t t-if="linked_order_type == 'sale' and line.sudo()._fields.get('x_fc_serial_number') and line.x_fc_serial_number">
<div class="mt-1">
<span class="badge text-bg-light border" style="font-size:0.72rem;">
<i class="fa fa-barcode me-1"/>S/N: <t t-out="line.x_fc_serial_number"/>
</span>
</div>
</t>
</div>
<div class="text-end flex-shrink-0">
<span class="tech-qty-badge">
<t t-if="linked_order_type == 'sale'">
<t t-out="int(line.product_uom_qty)"/>
</t>
<t t-else="">
<t t-out="int(line.product_qty)"/>
</t>
</span>
<div class="text-muted" style="font-size:0.68rem;">qty</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-if="task.equipment_needed">
<div class="tech-equipment-tag">
<div class="text-muted small text-uppercase fw-semibold mb-1">
<i class="fa fa-wrench me-1"/>Equipment Needed
</div>
<div style="white-space:pre-wrap;font-size:0.9rem;"><t t-out="task.equipment_needed"/></div>
<t t-if="not order_lines">
<div class="text-muted small text-center py-2">
<i class="fa fa-info-circle me-1"/>No line items on this order
</div>
</t>
</div>
</t>
<!-- ===== PRODUCTS / ITEMS ===== -->
<t t-if="order_lines">
<!-- ===== EQUIPMENT NEEDED ===== -->
<t t-if="task.equipment_needed">
<div class="tech-card mb-3">
<div class="text-muted small text-uppercase fw-semibold mb-2">
<i class="fa fa-cube me-1"/>Products
</div>
<t t-foreach="order_lines" t-as="line">
<div class="d-flex justify-content-between align-items-center py-2"
t-attf-style="#{not line_last and 'border-bottom:1px solid var(--o-main-border-color, #eee);' or ''}">
<div>
<div class="fw-medium" style="font-size:0.9rem;"><t t-out="line.product_id.name"/></div>
<t t-if="line.sudo()._fields.get('x_fc_serial_number') and line.x_fc_serial_number">
<div class="text-muted small">S/N: <t t-out="line.x_fc_serial_number"/></div>
</t>
</div>
<span class="badge text-bg-secondary rounded-pill">x<t t-out="int(line.product_uom_qty)"/></span>
<div class="tech-equipment-tag">
<div class="text-muted small text-uppercase fw-semibold mb-1">
<i class="fa fa-wrench me-1"/>Equipment Needed
</div>
</t>
<div style="white-space:pre-wrap;font-size:0.9rem;"><t t-out="task.equipment_needed"/></div>
</div>
</div>
</t>

View File

@@ -189,23 +189,6 @@
</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">