This commit is contained in:
gsinghpal
2026-02-23 00:32:20 -05:00
parent d6bac8e623
commit e8e554de95
549 changed files with 1330 additions and 124935 deletions

View File

@@ -66,18 +66,12 @@ html.o_dark .fclk-app,
box-sizing: border-box;
}
/* Hide portal navigation and footer on clock page */
/* Hide portal navigation */
.fclk-app ~ .o_portal_navbar,
.fclk-app .o_portal_navbar {
display: none !important;
}
.o_portal_wrap:has(.fclk-app) ~ footer,
.o_portal_wrap:has(.fclk-app) ~ #bottom,
body:has(.fclk-app) footer#bottom {
display: none !important;
}
.fclk-container {
max-width: 480px;
margin: 0 auto;
@@ -230,12 +224,11 @@ body:has(.fclk-app) footer#bottom {
.fclk-timer {
color: var(--fclk-text);
font-size: clamp(36px, 10vw, 52px);
font-size: 52px;
font-weight: 300;
letter-spacing: 1px;
letter-spacing: 4px;
font-variant-numeric: tabular-nums;
font-family: 'SF Mono', 'Fira Code', 'Courier New', monospace;
white-space: nowrap;
}
/* ---- Clock Button ---- */
@@ -340,13 +333,11 @@ body:has(.fclk-app) footer#bottom {
border-radius: 16px;
padding: 16px;
box-shadow: var(--fclk-shadow);
text-align: center;
}
.fclk-stat-header {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--fclk-text-muted);
font-size: 12px;
@@ -670,6 +661,9 @@ body:has(.fclk-app) footer#bottom {
/* ---- Responsive ---- */
@media (max-width: 380px) {
.fclk-timer {
font-size: 40px;
}
.fclk-clock-btn {
width: 100px;
height: 100px;
@@ -677,10 +671,6 @@ body:has(.fclk-app) footer#bottom {
.fclk-stat-value {
font-size: 26px;
}
.fclk-timer-label {
font-size: 11px;
letter-spacing: 0.5px;
}
}
/* ---- Timesheet Page ---- */

View File

@@ -1,300 +0,0 @@
/** @odoo-module **/
/**
* Fusion Clock - Location Widgets (Odoo 19)
*
* 1) fclk_places_address - CharField with Google Places Autocomplete.
* Selecting a place auto-fills lat/lng on the record (no manual geocode).
*
* 2) fclk_map - Interactive map view widget.
* Reacts live to lat/lng/radius changes on the record.
* Dragging the pin updates the record directly (no separate save).
*/
import { Component, onMounted, onWillUnmount, useRef, useState, useEffect } from "@odoo/owl";
import { CharField, charField } from "@web/views/fields/char/char_field";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
// ---------------------------------------------------------------------------
// Google Maps JS API singleton loader
// ---------------------------------------------------------------------------
let _gmapsPromise = null;
function loadGoogleMapsAPI(apiKey) {
if (window.google?.maps?.places) {
return Promise.resolve();
}
if (_gmapsPromise) {
return _gmapsPromise;
}
_gmapsPromise = new Promise((resolve, reject) => {
window.__fclkGmapsReady = () => {
delete window.__fclkGmapsReady;
resolve();
};
const s = document.createElement("script");
s.src =
"https://maps.googleapis.com/maps/api/js?key=" +
encodeURIComponent(apiKey) +
"&libraries=places&callback=__fclkGmapsReady";
s.async = true;
s.onerror = () => reject(new Error("Failed to load Google Maps JS API"));
document.head.appendChild(s);
});
return _gmapsPromise;
}
// ===========================================================================
// 1) fclk_map - Interactive map view widget
// ===========================================================================
export class FclkLocationMap extends Component {
static template = "fusion_clock.LocationMap";
static props = { ...standardWidgetProps };
setup() {
this.mapContainerRef = useRef("mapContainer");
this.searchRef = useRef("searchInput");
this.notification = useService("notification");
this.state = useState({
loaded: false,
error: null,
});
this.map = null;
this.marker = null;
this.circle = null;
this._skipSync = false;
onMounted(() => this._init());
onWillUnmount(() => this._cleanup());
// React to record data changes (lat, lng, radius)
useEffect(
() => {
this._syncMapToRecord();
},
() => {
const d = this.props.record.data;
return [d.latitude, d.longitude, d.radius];
}
);
}
get record() {
return this.props.record;
}
// ------------------------------------------------------------------
// Initialise
// ------------------------------------------------------------------
async _init() {
try {
const settings = await rpc("/fusion_clock/get_settings");
const apiKey = settings?.google_maps_api_key;
if (!apiKey) {
this.state.error =
"Google Maps API key not configured. Go to Fusion Clock Settings.";
return;
}
await loadGoogleMapsAPI(apiKey);
this.state.loaded = true;
await new Promise((r) => requestAnimationFrame(r));
this._buildMap();
} catch (e) {
this.state.error = "Could not load Google Maps: " + (e.message || e);
}
}
// ------------------------------------------------------------------
// Build the map + marker + circle + search
// ------------------------------------------------------------------
_buildMap() {
const lat = this.record.data.latitude || 43.65;
const lng = this.record.data.longitude || -79.38;
const radius = this.record.data.radius || 100;
const hasCoords =
this.record.data.latitude !== 0 || this.record.data.longitude !== 0;
const el = this.mapContainerRef.el;
if (!el) return;
this.map = new google.maps.Map(el, {
center: { lat, lng },
zoom: hasCoords ? 17 : 12,
mapTypeControl: true,
streetViewControl: false,
fullscreenControl: true,
});
this.marker = new google.maps.Marker({
position: { lat, lng },
map: this.map,
draggable: !this.props.readonly,
animation: google.maps.Animation.DROP,
title: "Drag to fine-tune location",
});
this.circle = new google.maps.Circle({
map: this.map,
center: { lat, lng },
radius,
fillColor: "#10B981",
fillOpacity: 0.12,
strokeColor: "#10B981",
strokeWeight: 2,
strokeOpacity: 0.45,
});
// Pin drag -> update record directly (saves with form Save button)
this.marker.addListener("dragend", () => {
const pos = this.marker.getPosition();
this.circle.setCenter(pos);
this._skipSync = true;
this.record.update({
latitude: pos.lat(),
longitude: pos.lng(),
});
});
// Places Autocomplete search inside the map widget
if (this.searchRef.el && !this.props.readonly) {
const autocomplete = new google.maps.places.Autocomplete(
this.searchRef.el,
{ types: ["establishment", "geocode"] }
);
autocomplete.bindTo("bounds", this.map);
autocomplete.addListener("place_changed", () => {
const place = autocomplete.getPlace();
if (!place.geometry?.location) return;
const loc = place.geometry.location;
this.map.setCenter(loc);
this.map.setZoom(17);
this.marker.setPosition(loc);
this.circle.setCenter(loc);
this._skipSync = true;
const updates = {
latitude: loc.lat(),
longitude: loc.lng(),
};
if (place.formatted_address) {
updates.address = place.formatted_address;
}
this.record.update(updates);
});
}
}
// ------------------------------------------------------------------
// Sync map when record lat/lng/radius changes externally
// (e.g. from the address field's Places autocomplete)
// ------------------------------------------------------------------
_syncMapToRecord() {
if (!this.map || !this.marker || !this.circle) return;
// Skip if this change originated from our own map interaction
if (this._skipSync) {
this._skipSync = false;
return;
}
const lat = this.record.data.latitude || 43.65;
const lng = this.record.data.longitude || -79.38;
const radius = this.record.data.radius || 100;
const hasCoords =
this.record.data.latitude !== 0 || this.record.data.longitude !== 0;
const pos = { lat, lng };
this.map.setCenter(pos);
if (hasCoords) {
this.map.setZoom(17);
}
this.marker.setPosition(pos);
this.circle.setCenter(pos);
this.circle.setRadius(radius);
}
// ------------------------------------------------------------------
// Cleanup
// ------------------------------------------------------------------
_cleanup() {
this.map = null;
this.marker = null;
this.circle = null;
}
}
registry.category("view_widgets").add("fclk_map", {
component: FclkLocationMap,
});
// ===========================================================================
// 2) fclk_places_address - CharField with Google Places Autocomplete
// ===========================================================================
export class FclkPlacesAddress extends CharField {
static template = "fusion_clock.PlacesAddress";
setup() {
super.setup();
this._autocomplete = null;
onMounted(() => this._attachPlaces());
onWillUnmount(() => this._detachPlaces());
}
async _attachPlaces() {
if (this.props.readonly) return;
try {
const settings = await rpc("/fusion_clock/get_settings");
const apiKey = settings?.google_maps_api_key;
if (!apiKey) return;
await loadGoogleMapsAPI(apiKey);
const inputEl = this.input?.el;
if (!inputEl) return;
this._autocomplete = new google.maps.places.Autocomplete(inputEl, {
types: ["establishment", "geocode"],
});
this._autocomplete.addListener("place_changed", () => {
const place = this._autocomplete.getPlace();
if (!place.geometry?.location) return;
const lat = place.geometry.location.lat();
const lng = place.geometry.location.lng();
const addr = place.formatted_address || place.name || "";
// Auto-fill address + coordinates on the record
this.props.record.update({
[this.props.name]: addr,
latitude: lat,
longitude: lng,
});
});
} catch (e) {
// Silently degrade - Places is non-critical
}
}
_detachPlaces() {
if (this._autocomplete) {
google.maps.event.clearInstanceListeners(this._autocomplete);
this._autocomplete = null;
}
}
}
const fclkPlacesAddress = {
...charField,
component: FclkPlacesAddress,
};
registry.category("fields").add("fclk_places_address", fclkPlacesAddress);

View File

@@ -34,9 +34,6 @@ export class FusionClockPortal extends Interaction {
this.selectedLocationId = this.locations[0].id;
}
// Auto-detect nearest location in background
this._autoSelectNearestLocation();
// Restore localStorage state
this._restoreState();
@@ -226,7 +223,7 @@ export class FusionClockPortal extends Interaction {
if (btn) btn.classList.remove("fclk-clock-btn-out");
if (playIcon) playIcon.style.display = "block";
if (stopIcon) stopIcon.style.display = "none";
if (timer) timer.textContent = "00:00:00";
if (timer) timer.textContent = "00 : 00 : 00";
if (data.net_hours !== undefined) {
const todayEl = document.getElementById("fclk-today-hours");
@@ -271,7 +268,7 @@ export class FusionClockPortal extends Interaction {
const pad = (n) => (n < 10 ? "0" + n : "" + n);
const timerEl = document.getElementById("fclk-timer");
if (timerEl) {
timerEl.textContent = pad(h) + ":" + pad(m) + ":" + pad(s);
timerEl.textContent = pad(h) + " : " + pad(m) + " : " + pad(s);
}
}
@@ -300,53 +297,6 @@ export class FusionClockPortal extends Interaction {
el.textContent = h + ":" + (m < 10 ? "0" : "") + m + " " + ampm;
}
// =========================================================================
// Auto-detect nearest location
// =========================================================================
_autoSelectNearestLocation() {
if (this.locations.length < 1) return;
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
(pos) => {
const userLat = pos.coords.latitude;
const userLng = pos.coords.longitude;
let nearest = this.locations[0];
let minDist = Infinity;
for (const loc of this.locations) {
const d = this._haversine(userLat, userLng, loc.latitude, loc.longitude);
if (d < minDist) {
minDist = d;
nearest = loc;
}
}
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 || "";
},
() => {
// Silently fall back to the first location
},
{ enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 }
);
}
_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) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// =========================================================================
// Sound Effects
// =========================================================================

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<!-- Places Autocomplete Address Field (inherits CharField, turns off browser autocomplete) -->
<t t-name="fusion_clock.PlacesAddress" t-inherit="web.CharField" t-inherit-mode="primary">
<xpath expr="//input" position="attributes">
<attribute name="autocomplete">off</attribute>
</xpath>
</t>
<!-- Interactive Map Widget -->
<t t-name="fusion_clock.LocationMap">
<div class="fclk-map-widget">
<!-- Search box (edit mode only) -->
<div t-if="state.loaded and !props.readonly" class="mb-2">
<input t-ref="searchInput" type="text"
class="form-control"
placeholder="Search for a place or address..."/>
</div>
<!-- Map container -->
<div t-ref="mapContainer"
style="width:100%; height:400px; border-radius:8px; border:1px solid var(--o-border-color, #dee2e6);"/>
<!-- Coordinate display -->
<div t-if="state.loaded and !props.readonly"
class="mt-2 text-muted small">
<i class="fa fa-info-circle me-1"/>
Drag the pin or use the search box to adjust. Changes save with the form.
</div>
<!-- Loading -->
<div t-if="!state.loaded and !state.error"
class="text-center p-4 text-muted">
<i class="fa fa-spinner fa-spin fa-2x"/>
<div class="mt-2">Loading map...</div>
</div>
<!-- Error -->
<div t-if="state.error" class="alert alert-warning mt-2 mb-0">
<i class="fa fa-exclamation-triangle me-1"/>
<t t-esc="state.error"/>
</div>
</div>
</t>
</templates>