update
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 & 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}&libraries=places&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>
|
||||
@@ -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
|
||||
? '<i class="fa fa-stop-circle-o"></i> Clock Out'
|
||||
: '<i class="fa fa-play-circle-o"></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 && timerEl) timerEl.textContent = '00:00:00';
|
||||
}
|
||||
|
||||
if (isCheckedIn && 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 && 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 = '<i class="fa fa-spinner fa-spin"></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 = '<i class="fa fa-check"></i> Submit';
|
||||
|
||||
if (data.error) {
|
||||
showClockError((data.error.data && 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 = '<i class="fa fa-check"></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 && ipData.longitude) ? ipData.latitude : 0;
|
||||
var lng = (ipData.latitude && 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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user