diff --git a/fusion_claims/__manifest__.py b/fusion_claims/__manifest__.py index cd5a9da1..955503d4 100644 --- a/fusion_claims/__manifest__.py +++ b/fusion_claims/__manifest__.py @@ -178,6 +178,9 @@ # Dashboard: tokens MUST load before dashboard layout 'fusion_claims/static/src/scss/_fc_dashboard_tokens.scss', 'fusion_claims/static/src/scss/fc_dashboard.scss', + # Dashboard OWL countdown widget + 'fusion_claims/static/src/js/fc_posting_countdown.js', + 'fusion_claims/static/src/xml/fc_posting_countdown.xml', ], 'web.assets_web_dark': [ # Dark bundle recompiles the same SCSS with the dark diff --git a/fusion_claims/static/src/js/fc_posting_countdown.js b/fusion_claims/static/src/js/fc_posting_countdown.js new file mode 100644 index 00000000..40f8c71f --- /dev/null +++ b/fusion_claims/static/src/js/fc_posting_countdown.js @@ -0,0 +1,63 @@ +/** @odoo-module **/ +// Fusion Claims — Posting Period Countdown +// Reads the submission_deadline_dt field, computes "Nd Xh to cutoff" client-side, +// re-renders every 60 seconds, swaps colour class as the deadline approaches. +// Copyright 2026 Nexa Systems Inc. +// License OPL-1 + +import { Component, useState, onWillDestroy } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +class FcPostingCountdown extends Component { + static template = "fusion_claims.PostingCountdown"; + static props = { ...standardFieldProps }; + + setup() { + this.state = useState({ text: "", level: "info" }); + this._render(); + this._timer = setInterval(() => this._render(), 60_000); + onWillDestroy(() => { + if (this._timer) { + clearInterval(this._timer); + this._timer = null; + } + }); + } + + _render() { + const deadline = this.props.record.data[this.props.name]; + if (!deadline) { + this.state.text = ""; + this.state.level = "muted"; + return; + } + // Odoo provides a luxon DateTime for Datetime fields + const now = luxon.DateTime.now(); + const diff = deadline.diff(now, ["days", "hours", "minutes"]).toObject(); + + if (diff.days < 0 || (diff.days === 0 && diff.hours < 0)) { + this.state.text = "Cutoff passed"; + this.state.level = "muted"; + return; + } + + const days = Math.floor(diff.days); + const hours = Math.floor(diff.hours); + + if (days < 1) { + this.state.text = `${hours}h to cutoff`; + this.state.level = "danger"; + } else if (days < 3) { + this.state.text = `${days}d ${hours}h to cutoff`; + this.state.level = "warning"; + } else { + this.state.text = `${days} days to cutoff`; + this.state.level = "info"; + } + } +} + +registry.category("fields").add("fc_posting_countdown", { + component: FcPostingCountdown, +}); diff --git a/fusion_claims/static/src/xml/fc_posting_countdown.xml b/fusion_claims/static/src/xml/fc_posting_countdown.xml new file mode 100644 index 00000000..83c7b599 --- /dev/null +++ b/fusion_claims/static/src/xml/fc_posting_countdown.xml @@ -0,0 +1,7 @@ + + + + + +