diff --git a/fusion_authorizer_portal/controllers/portal_main.py b/fusion_authorizer_portal/controllers/portal_main.py index f10dbb2b..1b7214d3 100644 --- a/fusion_authorizer_portal/controllers/portal_main.py +++ b/fusion_authorizer_portal/controllers/portal_main.py @@ -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 diff --git a/fusion_clock/controllers/__pycache__/portal_clock.cpython-312.pyc b/fusion_clock/controllers/__pycache__/portal_clock.cpython-312.pyc index e2816935..9d5f15ad 100644 Binary files a/fusion_clock/controllers/__pycache__/portal_clock.cpython-312.pyc and b/fusion_clock/controllers/__pycache__/portal_clock.cpython-312.pyc differ diff --git a/fusion_clock/controllers/clock_api.py b/fusion_clock/controllers/clock_api.py index 38aeba1c..3c1095c1 100644 --- a/fusion_clock/controllers/clock_api.py +++ b/fusion_clock/controllers/clock_api.py @@ -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, }) diff --git a/fusion_clock/controllers/portal_clock.py b/fusion_clock/controllers/portal_clock.py index fc209af6..a6876ea7 100644 --- a/fusion_clock/controllers/portal_clock.py +++ b/fusion_clock/controllers/portal_clock.py @@ -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, diff --git a/fusion_clock/static/src/js/fusion_clock_portal.js b/fusion_clock/static/src/js/fusion_clock_portal.js index 3eceede7..c570eba6 100644 --- a/fusion_clock/static/src/js/fusion_clock_portal.js +++ b/fusion_clock/static/src/js/fusion_clock_portal.js @@ -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) { diff --git a/fusion_clock/views/portal_clock_templates.xml b/fusion_clock/views/portal_clock_templates.xml index d84c58cb..a5060f2f 100644 --- a/fusion_clock/views/portal_clock_templates.xml +++ b/fusion_clock/views/portal_clock_templates.xml @@ -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 @@ View All
- +
- +
- +
- +
- - - + + -
- h + h
diff --git a/fusion_clock/views/portal_timesheet_templates.xml b/fusion_clock/views/portal_timesheet_templates.xml index fff9a016..3f17deaf 100644 --- a/fusion_clock/views/portal_timesheet_templates.xml +++ b/fusion_clock/views/portal_timesheet_templates.xml @@ -66,19 +66,19 @@ - + - + - + - + - - - + + + AUTO @@ -86,18 +86,18 @@ Active - m + m - h + h - + Correct diff --git a/fusion_schedule/controllers/portal_schedule.py b/fusion_schedule/controllers/portal_schedule.py index bf01ba76..4b2db39b 100644 --- a/fusion_schedule/controllers/portal_schedule.py +++ b/fusion_schedule/controllers/portal_schedule.py @@ -1213,7 +1213,7 @@ class PortalSchedule(CustomerPortal): 'response_type': 'code', 'scope': MICROSOFT_SCOPES, 'response_mode': 'query', - 'prompt': 'consent', + 'prompt': 'select_account', 'state': state, } diff --git a/fusion_tasks/static/src/css/fusion_task_map_view.scss b/fusion_tasks/static/src/css/fusion_task_map_view.scss index 31c3d696..f3b9b097 100644 --- a/fusion_tasks/static/src/css/fusion_task_map_view.scss +++ b/fusion_tasks/static/src/css/fusion_task_map_view.scss @@ -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; diff --git a/fusion_tasks/static/src/js/fusion_task_map_view.js b/fusion_tasks/static/src/js/fusion_task_map_view.js index c575e123..38bec401 100644 --- a/fusion_tasks/static/src/js/fusion_task_map_view.js +++ b/fusion_tasks/static/src/js/fusion_task_map_view.js @@ -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++; diff --git a/fusion_tasks/static/src/xml/fusion_task_map_view.xml b/fusion_tasks/static/src/xml/fusion_task_map_view.xml index f799ba5d..7e99b8fe 100644 --- a/fusion_tasks/static/src/xml/fusion_task_map_view.xml +++ b/fusion_tasks/static/src/xml/fusion_task_map_view.xml @@ -109,6 +109,11 @@
+ +
+ +
+