This commit is contained in:
gsinghpal
2026-03-17 13:32:08 -04:00
parent e56974d46f
commit 595dccc17d
11 changed files with 159 additions and 28 deletions

View File

@@ -1148,7 +1148,7 @@ class AuthorizerPortal(CustomerPortal):
('check_out', '=', False), ('check_out', '=', False),
], limit=1) ], limit=1)
if att: if att:
check_in_time = (att.check_in.isoformat() + 'Z') if att.check_in else '' check_in_time = att.check_in.isoformat() if att.check_in else ''
location_name = att.x_fclk_location_id.name if att.x_fclk_location_id else '' location_name = att.x_fclk_location_id.name if att.x_fclk_location_id else ''
from datetime import datetime from datetime import datetime

View File

@@ -343,6 +343,7 @@ class FusionClockAPI(http.Controller):
'attendance_id': attendance.id, 'attendance_id': attendance.id,
'check_in': fields.Datetime.to_string(attendance.check_in), 'check_in': fields.Datetime.to_string(attendance.check_in),
'location_name': location.name, 'location_name': location.name,
'location_address': location.address or '',
'message': f'Clocked in at {location.name}', 'message': f'Clocked in at {location.name}',
'streak': employee.x_fclk_ontime_streak, 'streak': employee.x_fclk_ontime_streak,
} }
@@ -389,6 +390,7 @@ class FusionClockAPI(http.Controller):
'break_minutes': attendance.x_fclk_break_minutes, 'break_minutes': attendance.x_fclk_break_minutes,
'overtime_hours': round(attendance.x_fclk_overtime_hours or 0, 2), 'overtime_hours': round(attendance.x_fclk_overtime_hours or 0, 2),
'location_name': location.name, 'location_name': location.name,
'location_address': location.address or '',
'message': f'Clocked out from {location.name}', 'message': f'Clocked out from {location.name}',
} }
@@ -527,6 +529,7 @@ class FusionClockAPI(http.Controller):
'attendance_id': att.id, 'attendance_id': att.id,
'check_in': fields.Datetime.to_string(att.check_in), 'check_in': fields.Datetime.to_string(att.check_in),
'location_name': att.x_fclk_location_id.name or '', 'location_name': att.x_fclk_location_id.name or '',
'location_address': att.x_fclk_location_id.address or '',
'location_id': att.x_fclk_location_id.id or False, 'location_id': att.x_fclk_location_id.id or False,
}) })

View File

@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
from odoo import http, fields, _ from odoo import http, fields, _
from odoo.http import request from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.fusion_clock.models.tz_utils import get_local_today, get_local_day_boundaries from odoo.addons.fusion_clock.models.tz_utils import get_local_today, get_local_day_boundaries, utc_to_local_str
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -119,11 +119,20 @@ class FusionClockPortal(CustomerPortal):
]) ])
week_hours = sum(a.x_fclk_net_hours or 0 for a in week_atts) week_hours = sum(a.x_fclk_net_hours or 0 for a in week_atts)
# Recent activity # Recent activity with local-timezone formatted times
recent = request.env['hr.attendance'].sudo().search([ recent_raw = request.env['hr.attendance'].sudo().search([
('employee_id', '=', employee.id), ('employee_id', '=', employee.id),
('check_out', '!=', False), ('check_out', '!=', False),
], order='check_in desc', limit=10) ], order='check_in desc', limit=10)
recent = []
for att in recent_raw:
recent.append({
'att': att,
'day_name': utc_to_local_str(att.check_in, request.env, employee, '%a'),
'day_num': utc_to_local_str(att.check_in, request.env, employee, '%d'),
'time_in': utc_to_local_str(att.check_in, request.env, employee, '%I:%M %p'),
'time_out': utc_to_local_str(att.check_out, request.env, employee, '%I:%M %p') if att.check_out else '--',
})
# Prepare locations JSON for JS # Prepare locations JSON for JS
locations_json = json.dumps([{ locations_json = json.dumps([{
@@ -203,9 +212,19 @@ class FusionClockPortal(CustomerPortal):
net_hours = sum(a.x_fclk_net_hours or 0 for a in attendances if a.check_out) net_hours = sum(a.x_fclk_net_hours or 0 for a in attendances if a.check_out)
total_breaks = sum(a.x_fclk_break_minutes or 0 for a in attendances if a.check_out) total_breaks = sum(a.x_fclk_break_minutes or 0 for a in attendances if a.check_out)
ts_attendances = []
for att in attendances:
ts_attendances.append({
'att': att,
'day_name': utc_to_local_str(att.check_in, request.env, employee, '%a'),
'day_date': utc_to_local_str(att.check_in, request.env, employee, '%b %d'),
'time_in': utc_to_local_str(att.check_in, request.env, employee, '%I:%M %p'),
'time_out': utc_to_local_str(att.check_out, request.env, employee, '%I:%M %p') if att.check_out else '',
})
values = { values = {
'employee': employee, 'employee': employee,
'attendances': attendances, 'attendances': ts_attendances,
'period_start': period_start, 'period_start': period_start,
'period_end': period_end, 'period_end': period_end,
'period': period, 'period': period,

View File

@@ -39,6 +39,7 @@ export class FusionClockPortal extends Interaction {
} }
if (this.locations.length > 0) { if (this.locations.length > 0) {
this.selectedLocationId = this.locations[0].id; this.selectedLocationId = this.locations[0].id;
this._restoreSelectedLocation();
} }
// Start live clock // Start live clock
@@ -50,7 +51,15 @@ export class FusionClockPortal extends Interaction {
this._startTimer(); this._startTimer();
this._updateUIForClockIn({ this._updateUIForClockIn({
location_name: this.el.dataset.locationName || "", location_name: this.el.dataset.locationName || "",
location_address: this.el.dataset.locationAddress || "",
}); });
const locId = parseInt(this.el.dataset.locationId || "0");
if (locId) {
this.selectedLocationId = locId;
this._saveSelectedLocation(locId);
}
} else if (this.locations.length > 1) {
this._detectNearestLocation();
} }
this._updateDateDisplay(); this._updateDateDisplay();
@@ -121,7 +130,9 @@ export class FusionClockPortal extends Interaction {
document.querySelectorAll(".fclk-modal-item").forEach((item) => { document.querySelectorAll(".fclk-modal-item").forEach((item) => {
item.addEventListener("click", () => { item.addEventListener("click", () => {
this.selectedLocationId = parseInt(item.dataset.id); const locId = parseInt(item.dataset.id);
this.selectedLocationId = locId;
this._saveSelectedLocation(locId);
const nameEl = document.getElementById("fclk-location-name"); const nameEl = document.getElementById("fclk-location-name");
const addrEl = document.getElementById("fclk-location-address"); const addrEl = document.getElementById("fclk-location-address");
if (nameEl) nameEl.textContent = item.dataset.name; if (nameEl) nameEl.textContent = item.dataset.name;
@@ -132,6 +143,60 @@ export class FusionClockPortal extends Interaction {
}); });
} }
// =========================================================================
// Nearest Location Detection
// =========================================================================
_detectNearestLocation() {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
(pos) => this._selectNearestFromCoords(pos.coords.latitude, pos.coords.longitude),
async () => {
try {
const resp = await fetch("https://ipapi.co/json/");
if (resp.ok) {
const data = await resp.json();
if (data.latitude && data.longitude) {
this._selectNearestFromCoords(data.latitude, data.longitude);
}
}
} catch { /* silent */ }
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
);
}
_selectNearestFromCoords(lat, lng) {
let nearest = null;
let minDist = Infinity;
for (const loc of this.locations) {
if (!loc.latitude || !loc.longitude) continue;
const dist = this._haversine(lat, lng, loc.latitude, loc.longitude);
if (dist < minDist) {
minDist = dist;
nearest = loc;
}
}
if (!nearest) return;
this.selectedLocationId = nearest.id;
const nameEl = document.getElementById("fclk-location-name");
const addrEl = document.getElementById("fclk-location-address");
if (nameEl) nameEl.textContent = nearest.name;
if (addrEl) addrEl.textContent = nearest.address || "";
}
_haversine(lat1, lon1, lat2, lon2) {
const R = 6371000;
const toRad = (v) => (v * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// ========================================================================= // =========================================================================
// Clock Action // Clock Action
// ========================================================================= // =========================================================================
@@ -262,6 +327,7 @@ export class FusionClockPortal extends Interaction {
this._playSound("in"); this._playSound("in");
this._showToast(result.message, "success"); this._showToast(result.message, "success");
this._saveState(); this._saveState();
if (this.selectedLocationId) this._saveSelectedLocation(this.selectedLocationId);
} else if (result.action === "clock_out") { } else if (result.action === "clock_out") {
this.isCheckedIn = false; this.isCheckedIn = false;
this._updateUIForClockOut(result); this._updateUIForClockOut(result);
@@ -302,6 +368,10 @@ export class FusionClockPortal extends Interaction {
const locEl = document.getElementById("fclk-location-name"); const locEl = document.getElementById("fclk-location-name");
if (locEl) locEl.textContent = data.location_name; if (locEl) locEl.textContent = data.location_name;
} }
if (data.location_address) {
const addrEl = document.getElementById("fclk-location-address");
if (addrEl) addrEl.textContent = data.location_address;
}
} }
_updateUIForClockOut(data) { _updateUIForClockOut(data) {
@@ -522,6 +592,27 @@ export class FusionClockPortal extends Interaction {
} catch (e) {} } catch (e) {}
} }
_saveSelectedLocation(locId) {
try {
localStorage.setItem("fclk_selected_location", String(locId));
} catch (e) {}
}
_restoreSelectedLocation() {
try {
const saved = localStorage.getItem("fclk_selected_location");
if (!saved) return;
const savedId = parseInt(saved);
const loc = this.locations.find((l) => l.id === savedId);
if (!loc) return;
this.selectedLocationId = savedId;
const nameEl = document.getElementById("fclk-location-name");
const addrEl = document.getElementById("fclk-location-address");
if (nameEl) nameEl.textContent = loc.name;
if (addrEl) addrEl.textContent = loc.address || "";
} catch (e) {}
}
// ========================================================================= // =========================================================================
// Reason Modal & Leave Request // Reason Modal & Leave Request
// ========================================================================= // =========================================================================
@@ -607,7 +698,7 @@ export class FusionClockPortal extends Interaction {
if (result.is_checked_in && !this.isCheckedIn) { if (result.is_checked_in && !this.isCheckedIn) {
this.isCheckedIn = true; this.isCheckedIn = true;
this.checkInTime = new Date(result.check_in + "Z"); this.checkInTime = new Date(result.check_in + "Z");
this._updateUIForClockIn({ location_name: result.location_name }); this._updateUIForClockIn({ location_name: result.location_name, location_address: result.location_address || "" });
this._startTimer(); this._startTimer();
this._saveState(); this._saveState();
} else if (result.is_checked_in && this.isCheckedIn) { } else if (result.is_checked_in && this.isCheckedIn) {

View File

@@ -85,6 +85,8 @@
t-att-data-checked-in="'true' if is_checked_in else 'false'" t-att-data-checked-in="'true' if is_checked_in else 'false'"
t-att-data-check-in-time="current_attendance.check_in.isoformat() if current_attendance and current_attendance.check_in else ''" t-att-data-check-in-time="current_attendance.check_in.isoformat() if current_attendance and current_attendance.check_in else ''"
t-att-data-location-name="current_attendance.x_fclk_location_id.name if current_attendance and current_attendance.x_fclk_location_id else ''" t-att-data-location-name="current_attendance.x_fclk_location_id.name if current_attendance and current_attendance.x_fclk_location_id else ''"
t-att-data-location-address="current_attendance.x_fclk_location_id.address if current_attendance and current_attendance.x_fclk_location_id else ''"
t-att-data-location-id="current_attendance.x_fclk_location_id.id if current_attendance and current_attendance.x_fclk_location_id else '0'"
t-att-data-enable-sounds="'true' if enable_sounds else 'false'" t-att-data-enable-sounds="'true' if enable_sounds else 'false'"
t-att-data-google-maps-key="google_maps_key or ''"> t-att-data-google-maps-key="google_maps_key or ''">
@@ -222,27 +224,27 @@
<a href="/my/clock/timesheets" class="fclk-view-all">View All</a> <a href="/my/clock/timesheets" class="fclk-view-all">View All</a>
</div> </div>
<div class="fclk-recent-list" id="fclk-recent-list"> <div class="fclk-recent-list" id="fclk-recent-list">
<t t-foreach="recent_attendances" t-as="att"> <t t-foreach="recent_attendances" t-as="entry">
<div class="fclk-recent-item"> <div class="fclk-recent-item">
<div class="fclk-recent-date"> <div class="fclk-recent-date">
<div class="fclk-recent-day-name"> <div class="fclk-recent-day-name">
<t t-esc="context_timestamp(att.check_in).strftime('%a')"/> <t t-esc="entry['day_name']"/>
</div> </div>
<div class="fclk-recent-day-num"> <div class="fclk-recent-day-num">
<t t-esc="context_timestamp(att.check_in).strftime('%d')"/> <t t-esc="entry['day_num']"/>
</div> </div>
</div> </div>
<div class="fclk-recent-info"> <div class="fclk-recent-info">
<div class="fclk-recent-location"> <div class="fclk-recent-location">
<t t-esc="att.x_fclk_location_id.name or 'Unknown'"/> <t t-esc="entry['att'].x_fclk_location_id.name or 'Unknown'"/>
</div> </div>
<div class="fclk-recent-times"> <div class="fclk-recent-times">
<t t-esc="context_timestamp(att.check_in).strftime('%I:%M %p')"/> <t t-esc="entry['time_in']"/>
- <t t-esc="context_timestamp(att.check_out).strftime('%I:%M %p') if att.check_out else '--'"/> - <t t-esc="entry['time_out']"/>
</div> </div>
</div> </div>
<div class="fclk-recent-hours"> <div class="fclk-recent-hours">
<t t-esc="'%.1f' % (att.x_fclk_net_hours or 0)"/>h <t t-esc="'%.1f' % (entry['att'].x_fclk_net_hours or 0)"/>h
</div> </div>
</div> </div>
</t> </t>

View File

@@ -66,19 +66,19 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<t t-foreach="attendances" t-as="att"> <t t-foreach="attendances" t-as="entry">
<tr> <tr>
<td> <td>
<strong><t t-esc="context_timestamp(att.check_in).strftime('%a')"/></strong> <strong><t t-esc="entry['day_name']"/></strong>
<span style="color:#9ca3af; margin-left:4px;"> <span style="color:#9ca3af; margin-left:4px;">
<t t-esc="context_timestamp(att.check_in).strftime('%b %d')"/> <t t-esc="entry['day_date']"/>
</span> </span>
</td> </td>
<td><t t-esc="context_timestamp(att.check_in).strftime('%I:%M %p')"/></td> <td><t t-esc="entry['time_in']"/></td>
<td> <td>
<t t-if="att.check_out"> <t t-if="entry['att'].check_out">
<t t-esc="context_timestamp(att.check_out).strftime('%I:%M %p')"/> <t t-esc="entry['time_out']"/>
<t t-if="att.x_fclk_auto_clocked_out"> <t t-if="entry['att'].x_fclk_auto_clocked_out">
<span class="fclk-ts-badge-auto">AUTO</span> <span class="fclk-ts-badge-auto">AUTO</span>
</t> </t>
</t> </t>
@@ -86,18 +86,18 @@
<span style="color:#f59e0b;">Active</span> <span style="color:#f59e0b;">Active</span>
</t> </t>
</td> </td>
<td><t t-esc="int(att.x_fclk_break_minutes or 0)"/>m</td> <td><t t-esc="int(entry['att'].x_fclk_break_minutes or 0)"/>m</td>
<td style="font-weight:600; color:#10B981;"> <td style="font-weight:600; color:#10B981;">
<t t-esc="'%.1f' % (att.x_fclk_net_hours or 0)"/>h <t t-esc="'%.1f' % (entry['att'].x_fclk_net_hours or 0)"/>h
</td> </td>
<td style="color:#9ca3af; font-size:12px;"> <td style="color:#9ca3af; font-size:12px;">
<t t-esc="att.x_fclk_location_id.name or ''"/> <t t-esc="entry['att'].x_fclk_location_id.name or ''"/>
</td> </td>
<td> <td>
<a href="#" class="fclk-correction-link" <a href="#" class="fclk-correction-link"
t-att-data-att-id="att.id" t-att-data-att-id="entry['att'].id"
t-att-data-check-in="att.check_in.strftime('%Y-%m-%d %H:%M:%S') if att.check_in else ''" t-att-data-check-in="entry['att'].check_in.strftime('%Y-%m-%d %H:%M:%S') if entry['att'].check_in else ''"
t-att-data-check-out="att.check_out.strftime('%Y-%m-%d %H:%M:%S') if att.check_out else ''" t-att-data-check-out="entry['att'].check_out.strftime('%Y-%m-%d %H:%M:%S') if entry['att'].check_out else ''"
style="font-size:11px; color:#6b7280;"> style="font-size:11px; color:#6b7280;">
Correct Correct
</a> </a>

View File

@@ -1213,7 +1213,7 @@ class PortalSchedule(CustomerPortal):
'response_type': 'code', 'response_type': 'code',
'scope': MICROSOFT_SCOPES, 'scope': MICROSOFT_SCOPES,
'response_mode': 'query', 'response_mode': 'query',
'prompt': 'consent', 'prompt': 'select_account',
'state': state, 'state': state,
} }

View File

@@ -343,6 +343,14 @@ $transition-speed: .25s;
.fa { opacity: .5; } .fa { opacity: .5; }
} }
.fc_task_date {
font-size: 11px;
color: #6366f1;
font-weight: 600;
margin-bottom: 3px;
.fa { opacity: .5; }
}
.fc_task_detail { .fc_task_detail {
font-size: 11px; font-size: 11px;
color: $body-color; color: $body-color;

View File

@@ -241,6 +241,9 @@ function groupTasks(tasksData, localInstanceId, visibleTechIds) {
const src = task.x_fc_sync_source || localInstanceId || ""; const src = task.x_fc_sync_source || localInstanceId || "";
task._sourceLabel = src ? src.charAt(0).toUpperCase() + src.slice(1) : ""; task._sourceLabel = src ? src.charAt(0).toUpperCase() + src.slice(1) : "";
task._sourceColor = SOURCE_COLORS[src] || "#6c757d"; task._sourceColor = SOURCE_COLORS[src] || "#6c757d";
task._dateLabel = task.scheduled_date
? new Date(task.scheduled_date + "T12:00:00").toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
: "No date";
task._hasCoords = task.address_lat && task.address_lng && task.address_lat !== 0 && task.address_lng !== 0; task._hasCoords = task.address_lat && task.address_lng && task.address_lat !== 0 && task.address_lng !== 0;
groups[g].tasks.push(task); groups[g].tasks.push(task);
groups[g].count++; groups[g].count++;

View File

@@ -109,6 +109,11 @@
<span><i class="fa fa-clock-o me-1"/><t t-esc="task._timeRange"/></span> <span><i class="fa fa-clock-o me-1"/><t t-esc="task._timeRange"/></span>
</div> </div>
<!-- Date -->
<div class="fc_task_date">
<i class="fa fa-calendar me-1"/><t t-esc="task._dateLabel"/>
</div>
<!-- Technician + address --> <!-- Technician + address -->
<div class="fc_task_detail"> <div class="fc_task_detail">
<span><i class="fa fa-user me-1"/><t t-esc="task._techName"/></span> <span><i class="fa fa-user me-1"/><t t-esc="task._techName"/></span>