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

@@ -43,8 +43,24 @@
.tech-clock-card {
background: var(--o-main-card-bg, #fff);
border: 1px solid var(--o-main-border-color, #e9ecef);
border-radius: 14px;
padding: 0.875rem 1rem;
border-radius: 16px;
padding: 1.125rem 1.25rem;
transition: border-color 0.3s, box-shadow 0.3s;
}
.tech-clock-layout {
display: flex;
align-items: center;
justify-content: space-between;
}
.tech-clock-info {
flex: 1;
min-width: 0;
}
.tech-clock-status-line {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 2px;
}
.tech-clock-dot {
width: 10px;
@@ -52,6 +68,7 @@
border-radius: 50%;
background: #adb5bd;
flex-shrink: 0;
transition: all 0.3s;
}
.tech-clock-dot--active {
background: #10b981;
@@ -60,61 +77,199 @@
}
@keyframes tech-clock-pulse {
0%, 100% { box-shadow: 0 0 6px rgba(16, 185, 129, 0.5); }
50% { box-shadow: 0 0 12px rgba(16, 185, 129, 0.8); }
50% { box-shadow: 0 0 14px rgba(16, 185, 129, 0.8); }
}
.tech-clock-status {
font-size: 0.85rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--o-main-text-color, #212529);
line-height: 1.2;
}
.tech-clock-timer {
font-size: 0.75rem;
font-weight: 600;
font-size: 1.6rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
color: var(--o-main-text-color, #212529);
letter-spacing: 1px;
line-height: 1.3;
}
.tech-clock-hours {
font-size: 0.72rem;
color: #6c757d;
font-weight: 500;
}
.tech-clock-btn {
display: inline-flex;
/* Circular orb button */
.tech-clock-action {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
border-radius: 10px;
gap: 5px;
flex-shrink: 0;
margin-left: 12px;
}
.tech-clock-orb-wrap {
position: relative;
width: 68px;
height: 68px;
display: flex;
align-items: center;
justify-content: center;
}
.tech-clock-orb {
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
font-weight: 600;
font-size: 0.85rem;
background: linear-gradient(135deg, #10b981, #059669);
color: #fff;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 2;
box-shadow: 0 4px 18px rgba(16, 185, 129, 0.35);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
-webkit-tap-highlight-color: transparent;
}
.tech-clock-btn:active { transform: scale(0.96); }
.tech-clock-btn--in {
background: #10b981;
color: #fff;
.tech-clock-orb:hover {
transform: scale(1.08);
box-shadow: 0 6px 24px rgba(16, 185, 129, 0.45);
}
.tech-clock-btn--in:hover { background: #059669; }
.tech-clock-btn--out {
background: #ef4444;
color: #fff;
.tech-clock-orb:active {
transform: scale(0.92);
}
.tech-clock-btn--out:hover { background: #dc2626; }
.tech-clock-btn:disabled {
opacity: 0.6;
.tech-clock-orb--out {
background: linear-gradient(135deg, #ef4444, #dc2626);
box-shadow: 0 4px 18px rgba(239, 68, 68, 0.35);
}
.tech-clock-orb--out:hover {
box-shadow: 0 6px 24px rgba(239, 68, 68, 0.45);
}
.tech-clock-orb:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none !important;
}
.tech-clock-orb-icon {
display: flex;
align-items: center;
justify-content: center;
}
svg#clockIconPlay {
transform: translateX(1px);
}
/* Wave animation rings - active when clocked in */
.tech-clock-wave {
position: absolute;
top: 50%;
left: 50%;
width: 56px;
height: 56px;
border-radius: 50%;
border: 2px solid transparent;
transform: translate(-50%, -50%) scale(1);
pointer-events: none;
z-index: 1;
opacity: 0;
}
.tech-clock-orb-wrap--active .tech-clock-wave--1 {
animation: tech-clock-wave 2.4s ease-out infinite;
border-color: rgba(16, 185, 129, 0.5);
}
.tech-clock-orb-wrap--active .tech-clock-wave--2 {
animation: tech-clock-wave 2.4s ease-out 0.8s infinite;
border-color: rgba(16, 185, 129, 0.4);
}
.tech-clock-orb-wrap--active .tech-clock-wave--3 {
animation: tech-clock-wave 2.4s ease-out 1.6s infinite;
border-color: rgba(16, 185, 129, 0.3);
}
@keyframes tech-clock-wave {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.6;
}
100% {
transform: translate(-50%, -50%) scale(2.2);
opacity: 0;
}
}
.tech-clock-label {
font-size: 0.65rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.tech-clock-error {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.5rem;
padding: 0.4rem 0.75rem;
border-radius: 8px;
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 10px;
background: #fef2f2;
color: #dc2626;
font-size: 0.8rem;
font-size: 0.78rem;
font-weight: 500;
}
/* Missed clock-out reason modal */
.tech-reason-overlay {
position: fixed;
inset: 0;
z-index: 1060;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.tech-reason-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.tech-reason-dialog {
position: relative;
background: var(--o-main-card-bg, #fff);
border: 1px solid var(--o-main-border-color, #e5e7eb);
border-radius: 16px;
width: 100%;
max-width: 400px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
animation: techReasonIn 0.25s ease;
}
@keyframes techReasonIn {
from { opacity: 0; transform: scale(0.95) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.tech-reason-header {
text-align: center;
padding: 1.25rem 1.25rem 0.75rem;
border-bottom: 1px solid var(--o-main-border-color, #e9ecef);
}
.tech-reason-header h5 {
margin: 0.5rem 0 0.25rem;
font-weight: 700;
}
.tech-reason-body {
padding: 1rem 1.25rem;
}
.tech-reason-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.75rem 1.25rem 1rem;
border-top: 1px solid var(--o-main-border-color, #e9ecef);
}
/* ---- Quick Links (All Tasks / Tomorrow / Repair Form) ---- */
.tech-quick-links {
display: flex;
@@ -430,6 +585,40 @@
padding: 0.75rem;
}
/* ---- Order Details Card ---- */
.tech-order-card {
border-left: 3px solid #17a2b8;
}
.tech-order-card .bg-purple-subtle {
background: #f3e8ff;
}
.tech-order-card .text-purple {
color: #7c3aed;
}
.tech-order-lines {
border-top: 1px solid var(--o-main-border-color, #eee);
padding-top: 0.75rem;
margin-top: 0.5rem;
}
.tech-order-line-item {
padding: 0.625rem 0;
}
.tech-order-line-border {
border-bottom: 1px solid var(--o-main-border-color, #f0f0f0);
}
.tech-qty-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
border-radius: 8px;
background: #e9ecef;
font-weight: 700;
font-size: 0.9rem;
color: #495057;
}
/* ---- Action Buttons (Large Touch Targets) ---- */
.tech-action-btn {
display: inline-flex;

View File

@@ -1,343 +0,0 @@
(function () {
'use strict';
var dateInput = document.getElementById('bookingDate');
var slotsContainer = document.getElementById('slotsContainer');
var slotsGrid = document.getElementById('slotsGrid');
var slotsLoading = document.getElementById('slotsLoading');
var noSlots = document.getElementById('noSlots');
var slotDatetimeInput = document.getElementById('slotDatetime');
var slotDurationInput = document.getElementById('slotDuration');
var submitBtn = document.getElementById('btnSubmitBooking');
var typeSelect = document.getElementById('appointmentTypeSelect');
var selectedSlotBtn = null;
var weekContainer = document.getElementById('weekCalendarContainer');
var weekLoading = document.getElementById('weekCalendarLoading');
var weekGrid = document.getElementById('weekCalendarGrid');
var weekHeader = document.getElementById('weekCalendarHeader');
var weekBody = document.getElementById('weekCalendarBody');
var weekEmpty = document.getElementById('weekCalendarEmpty');
function getAppointmentTypeId() {
if (typeSelect) return typeSelect.value;
var hidden = document.querySelector('input[name="appointment_type_id"]');
return hidden ? hidden.value : null;
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function truncate(str, max) {
if (!str) return '';
return str.length > max ? str.substring(0, max) + '...' : str;
}
function fetchWeekEvents(date) {
if (!weekContainer || !date) return;
weekContainer.style.display = 'block';
weekLoading.style.display = 'block';
weekGrid.style.display = 'none';
weekEmpty.style.display = 'none';
fetch('/my/schedule/week-events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: { selected_date: date },
}),
})
.then(function (resp) { return resp.json(); })
.then(function (data) {
weekLoading.style.display = 'none';
var result = data.result || {};
var events = result.events || [];
var weekDays = result.week_days || [];
if (result.error || !weekDays.length) {
weekEmpty.style.display = 'block';
return;
}
renderWeekCalendar(weekDays, events, date);
})
.catch(function () {
weekLoading.style.display = 'none';
weekEmpty.textContent = 'Failed to load calendar. Please try again.';
weekEmpty.style.display = 'block';
});
}
function renderWeekCalendar(weekDays, events, selectedDate) {
weekHeader.innerHTML = '';
weekBody.innerHTML = '';
var eventsByDate = {};
events.forEach(function (ev) {
if (!eventsByDate[ev.date]) eventsByDate[ev.date] = [];
eventsByDate[ev.date].push(ev);
});
var hasAnyEvents = events.length > 0;
weekDays.forEach(function (day) {
var isSelected = day.date === selectedDate;
var isWeekend = day.label === 'Sat' || day.label === 'Sun';
var dayEvents = eventsByDate[day.date] || [];
var headerCell = document.createElement('div');
headerCell.className = 'text-center py-2 flex-fill';
headerCell.style.cssText = 'min-width: 0; font-size: 12px; border-right: 1px solid #dee2e6;';
if (isSelected) {
headerCell.style.backgroundColor = '#e8f4fd';
}
if (isWeekend) {
headerCell.style.opacity = '0.6';
}
var labelEl = document.createElement('div');
labelEl.className = 'fw-semibold text-muted';
labelEl.textContent = day.label;
var numEl = document.createElement('div');
numEl.className = isSelected ? 'fw-bold text-primary' : 'fw-semibold';
numEl.style.fontSize = '14px';
numEl.textContent = day.day_num;
headerCell.appendChild(labelEl);
headerCell.appendChild(numEl);
weekHeader.appendChild(headerCell);
var bodyCell = document.createElement('div');
bodyCell.className = 'flex-fill p-1';
bodyCell.style.cssText = 'min-width: 0; min-height: 70px; border-right: 1px solid #dee2e6; overflow: hidden;';
if (isSelected) {
bodyCell.style.backgroundColor = '#f0f8ff';
}
if (dayEvents.length) {
dayEvents.forEach(function (ev) {
var card = document.createElement('div');
card.className = 'mb-1 px-1 py-1 rounded';
card.style.cssText = 'font-size: 11px; background: #eef6ff; border-left: 3px solid #3a8fb7; overflow: hidden; cursor: default;';
card.title = ev.start_time + ' - ' + ev.end_time + '\n' + ev.name + (ev.location ? '\n' + ev.location : '');
var timeEl = document.createElement('div');
timeEl.className = 'fw-semibold text-primary';
timeEl.style.fontSize = '10px';
timeEl.textContent = ev.start_time;
var nameEl = document.createElement('div');
nameEl.className = 'text-truncate';
nameEl.style.fontSize = '10px';
nameEl.textContent = truncate(ev.name, 18);
card.appendChild(timeEl);
card.appendChild(nameEl);
bodyCell.appendChild(card);
});
}
weekBody.appendChild(bodyCell);
});
if (hasAnyEvents) {
weekGrid.style.display = 'block';
weekEmpty.style.display = 'none';
} else {
weekGrid.style.display = 'none';
weekEmpty.style.display = 'block';
}
}
function fetchSlots(date) {
var typeId = getAppointmentTypeId();
if (!typeId || !date) return;
slotsContainer.style.display = 'block';
slotsLoading.style.display = 'block';
slotsGrid.innerHTML = '';
noSlots.style.display = 'none';
slotDatetimeInput.value = '';
if (submitBtn) submitBtn.disabled = true;
selectedSlotBtn = null;
fetch('/my/schedule/available-slots', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
appointment_type_id: parseInt(typeId),
selected_date: date,
},
}),
})
.then(function (resp) { return resp.json(); })
.then(function (data) {
slotsLoading.style.display = 'none';
var result = data.result || {};
var slots = result.slots || [];
if (result.error) {
noSlots.textContent = result.error;
noSlots.style.display = 'block';
return;
}
if (!slots.length) {
noSlots.style.display = 'block';
return;
}
var morningSlots = [];
var afternoonSlots = [];
slots.forEach(function (slot) {
var hour = parseInt(slot.start_hour);
if (isNaN(hour)) {
var match = slot.start_hour.match(/(\d+)/);
hour = match ? parseInt(match[1]) : 0;
if (slot.start_hour.toLowerCase().indexOf('pm') > -1 && hour !== 12) hour += 12;
if (slot.start_hour.toLowerCase().indexOf('am') > -1 && hour === 12) hour = 0;
}
if (hour < 12) {
morningSlots.push(slot);
} else {
afternoonSlots.push(slot);
}
});
function renderGroup(label, icon, groupSlots) {
if (!groupSlots.length) return;
var header = document.createElement('div');
header.className = 'w-100 mt-2 mb-1';
header.innerHTML = '<small class="text-muted fw-semibold"><i class="fa ' + icon + ' me-1"></i>' + label + '</small>';
slotsGrid.appendChild(header);
groupSlots.forEach(function (slot) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-outline-primary btn-sm slot-btn';
btn.style.cssText = 'min-width: 100px; border-radius: 8px; padding: 8px 14px;';
btn.textContent = slot.start_hour;
btn.dataset.datetime = slot.datetime;
btn.dataset.duration = slot.duration;
btn.addEventListener('click', function () {
if (selectedSlotBtn) {
selectedSlotBtn.classList.remove('btn-primary');
selectedSlotBtn.classList.add('btn-outline-primary');
}
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-primary');
selectedSlotBtn = btn;
slotDatetimeInput.value = slot.datetime;
slotDurationInput.value = slot.duration;
if (submitBtn) submitBtn.disabled = false;
});
slotsGrid.appendChild(btn);
});
}
renderGroup('Morning', 'fa-sun-o', morningSlots);
renderGroup('Afternoon', 'fa-cloud', afternoonSlots);
})
.catch(function (err) {
slotsLoading.style.display = 'none';
noSlots.textContent = 'Failed to load slots. Please try again.';
noSlots.style.display = 'block';
});
}
if (dateInput) {
dateInput.addEventListener('change', function () {
var val = this.value;
fetchWeekEvents(val);
fetchSlots(val);
});
}
if (typeSelect) {
typeSelect.addEventListener('change', function () {
if (dateInput && dateInput.value) {
fetchSlots(dateInput.value);
}
});
}
var bookingForm = document.getElementById('bookingForm');
if (bookingForm) {
bookingForm.addEventListener('submit', function (e) {
if (!slotDatetimeInput || !slotDatetimeInput.value) {
e.preventDefault();
alert('Please select a time slot before booking.');
return false;
}
var clientName = bookingForm.querySelector('input[name="client_name"]');
if (!clientName || !clientName.value.trim()) {
e.preventDefault();
alert('Please enter the client name.');
return false;
}
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i> Booking...';
}
});
}
window.initScheduleAddressAutocomplete = function () {
var streetInput = document.getElementById('clientStreet');
if (!streetInput) return;
var autocomplete = new google.maps.places.Autocomplete(streetInput, {
componentRestrictions: { country: 'ca' },
types: ['address'],
});
autocomplete.addListener('place_changed', function () {
var place = autocomplete.getPlace();
if (!place.address_components) return;
var streetNumber = '';
var streetName = '';
var city = '';
var province = '';
var postalCode = '';
for (var i = 0; i < place.address_components.length; i++) {
var component = place.address_components[i];
var types = component.types;
if (types.indexOf('street_number') > -1) {
streetNumber = component.long_name;
} else if (types.indexOf('route') > -1) {
streetName = component.long_name;
} else if (types.indexOf('locality') > -1) {
city = component.long_name;
} else if (types.indexOf('administrative_area_level_1') > -1) {
province = component.long_name;
} else if (types.indexOf('postal_code') > -1) {
postalCode = component.long_name;
}
}
streetInput.value = (streetNumber + ' ' + streetName).trim();
var cityInput = document.getElementById('clientCity');
if (cityInput) cityInput.value = city;
var provInput = document.getElementById('clientProvince');
if (provInput) provInput.value = province;
var postalInput = document.getElementById('clientPostal');
if (postalInput) postalInput.value = postalCode;
});
};
})();

View File

@@ -0,0 +1,34 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
publicWidget.registry.TimezoneAutoDetect = publicWidget.Widget.extend({
selector: 'body',
start() {
this._super(...arguments);
this._detectAndSaveTimezone();
},
_detectAndSaveTimezone() {
let detectedTz;
try {
detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
return;
}
if (!detectedTz) return;
const cookieTz = this._getCookie('tz');
if (cookieTz === detectedTz) return;
document.cookie = `tz=${detectedTz};path=/;max-age=${60 * 60 * 24 * 365};SameSite=Lax`;
this._rpc('/my/timezone/detect', { timezone: detectedTz }).catch(() => {});
},
_getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
},
});