changes
This commit is contained in:
@@ -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 ---- */
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user