changes
This commit is contained in:
Binary file not shown.
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user