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),
], limit=1)
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 ''
from datetime import datetime

View File

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

View File

@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
from odoo import http, fields, _
from odoo.http import request
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__)
@@ -119,11 +119,20 @@ class FusionClockPortal(CustomerPortal):
])
week_hours = sum(a.x_fclk_net_hours or 0 for a in week_atts)
# Recent activity
recent = request.env['hr.attendance'].sudo().search([
# Recent activity with local-timezone formatted times
recent_raw = request.env['hr.attendance'].sudo().search([
('employee_id', '=', employee.id),
('check_out', '!=', False),
], 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
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)
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 = {
'employee': employee,
'attendances': attendances,
'attendances': ts_attendances,
'period_start': period_start,
'period_end': period_end,
'period': period,

View File

@@ -39,6 +39,7 @@ export class FusionClockPortal extends Interaction {
}
if (this.locations.length > 0) {
this.selectedLocationId = this.locations[0].id;
this._restoreSelectedLocation();
}
// Start live clock
@@ -50,7 +51,15 @@ export class FusionClockPortal extends Interaction {
this._startTimer();
this._updateUIForClockIn({
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();
@@ -121,7 +130,9 @@ export class FusionClockPortal extends Interaction {
document.querySelectorAll(".fclk-modal-item").forEach((item) => {
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 addrEl = document.getElementById("fclk-location-address");
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
// =========================================================================
@@ -262,6 +327,7 @@ export class FusionClockPortal extends Interaction {
this._playSound("in");
this._showToast(result.message, "success");
this._saveState();
if (this.selectedLocationId) this._saveSelectedLocation(this.selectedLocationId);
} else if (result.action === "clock_out") {
this.isCheckedIn = false;
this._updateUIForClockOut(result);
@@ -302,6 +368,10 @@ export class FusionClockPortal extends Interaction {
const locEl = document.getElementById("fclk-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) {
@@ -522,6 +592,27 @@ export class FusionClockPortal extends Interaction {
} 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
// =========================================================================
@@ -607,7 +698,7 @@ export class FusionClockPortal extends Interaction {
if (result.is_checked_in && !this.isCheckedIn) {
this.isCheckedIn = true;
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._saveState();
} 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-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-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-google-maps-key="google_maps_key or ''">
@@ -222,27 +224,27 @@
<a href="/my/clock/timesheets" class="fclk-view-all">View All</a>
</div>
<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-date">
<div class="fclk-recent-day-name">
<t t-esc="context_timestamp(att.check_in).strftime('%a')"/>
<t t-esc="entry['day_name']"/>
</div>
<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 class="fclk-recent-info">
<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 class="fclk-recent-times">
<t t-esc="context_timestamp(att.check_in).strftime('%I:%M %p')"/>
- <t t-esc="context_timestamp(att.check_out).strftime('%I:%M %p') if att.check_out else '--'"/>
<t t-esc="entry['time_in']"/>
- <t t-esc="entry['time_out']"/>
</div>
</div>
<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>
</t>

View File

@@ -66,19 +66,19 @@
</tr>
</thead>
<tbody>
<t t-foreach="attendances" t-as="att">
<t t-foreach="attendances" t-as="entry">
<tr>
<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;">
<t t-esc="context_timestamp(att.check_in).strftime('%b %d')"/>
<t t-esc="entry['day_date']"/>
</span>
</td>
<td><t t-esc="context_timestamp(att.check_in).strftime('%I:%M %p')"/></td>
<td><t t-esc="entry['time_in']"/></td>
<td>
<t t-if="att.check_out">
<t t-esc="context_timestamp(att.check_out).strftime('%I:%M %p')"/>
<t t-if="att.x_fclk_auto_clocked_out">
<t t-if="entry['att'].check_out">
<t t-esc="entry['time_out']"/>
<t t-if="entry['att'].x_fclk_auto_clocked_out">
<span class="fclk-ts-badge-auto">AUTO</span>
</t>
</t>
@@ -86,18 +86,18 @@
<span style="color:#f59e0b;">Active</span>
</t>
</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;">
<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 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>
<a href="#" class="fclk-correction-link"
t-att-data-att-id="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-out="att.check_out.strftime('%Y-%m-%d %H:%M:%S') if att.check_out else ''"
t-att-data-att-id="entry['att'].id"
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="entry['att'].check_out.strftime('%Y-%m-%d %H:%M:%S') if entry['att'].check_out else ''"
style="font-size:11px; color:#6b7280;">
Correct
</a>

View File

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

View File

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

View File

@@ -241,6 +241,9 @@ function groupTasks(tasksData, localInstanceId, visibleTechIds) {
const src = task.x_fc_sync_source || localInstanceId || "";
task._sourceLabel = src ? src.charAt(0).toUpperCase() + src.slice(1) : "";
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;
groups[g].tasks.push(task);
groups[g].count++;

View File

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