diff --git a/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.js b/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.js new file mode 100644 index 00000000000..c7e1c6fb1cf --- /dev/null +++ b/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.js @@ -0,0 +1,60 @@ +/** @odoo-module **/ + +import { _lt } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { formatDuration } from "@web/views/fields/formatters"; +import { + preloadStatusBar, + statusBarField, + StatusBarField, +} from "@web/views/fields/statusbar/statusbar_field"; + +export class StatusBarDurationField extends StatusBarField { + static template = "mail.StatusBarDurationField"; + + computeItems(grouped = true) { + const items = super.computeItems(grouped); + if (!grouped) { + return items; + } + if (this.props.record.data.duration_tracking) { + for (const property in items) { + this.setStatusbarDuration(items[property]); + } + } + return items; + } + + setStatusbarDuration(stages) { + const durationTracking = this.props.record.data.duration_tracking; + if (!Object.keys(durationTracking).length) { + return; + } + for (const stage of stages) { + if (stage.id in durationTracking && durationTracking[stage.id] > 0) { + stage.shortTimeInStage = formatDuration(durationTracking[stage.id], false); + stage.fullTimeInStage = formatDuration(durationTracking[stage.id], true); + } else { + stage.shortTimeInStage = false; + } + } + } +} + +export const statusBarDurationField = { + ...statusBarField, + component: StatusBarDurationField, + displayName: _lt("Status with time"), + supportedTypes: ["many2one"], + fieldDependencies: [{ name: "duration_tracking", type: "JSON" }], +}; + +registry.category("fields").add("statusbar_duration", statusBarDurationField); + +registry.category("preloadedData").add("statusbar_duration", { + loadOnTypes: ["many2one"], + extraMemoizationKey: (record, fieldName) => { + return record.data[fieldName]; + }, + preload: preloadStatusBar, +}); diff --git a/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.scss b/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.scss new file mode 100644 index 00000000000..fb1a9a1ac4d --- /dev/null +++ b/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.scss @@ -0,0 +1,3 @@ +.o_field_statusbar_duration { + @extend .o_field_statusbar; +} diff --git a/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.xml b/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.xml new file mode 100644 index 00000000000..22e4d984fb6 --- /dev/null +++ b/addons/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/mail/static/tests/views/fields/statusbar_duration_field_tests.js b/addons/mail/static/tests/views/fields/statusbar_duration_field_tests.js new file mode 100644 index 00000000000..e0876d4a418 --- /dev/null +++ b/addons/mail/static/tests/views/fields/statusbar_duration_field_tests.js @@ -0,0 +1,100 @@ +/** @odoo-module **/ + +import { getFixture } from "@web/../tests/helpers/utils"; +import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; + +let serverData; +let target; + +QUnit.module("Fields", (hooks) => { + hooks.beforeEach(() => { + target = getFixture(); + serverData = { + models: { + partner: { + fields: { + display_name: { string: "Displayed name", type: "char" }, + stage_id: { string: "Stage", type: "many2one", relation: "stage_model" }, + duration_tracking: { + string: "Time per stage", + type: "char", + default: "{}", + }, + }, + records: [ + { + id: 1, + display_name: "first record", + stage_id: 30, + // 7 days, 30 minutes - 3 hours - 2 days, 5 hours + duration_tracking: { + 10: 7 * 24 * 60 * 60 + 30 * 60, + 20: 3 * 60 * 60, + 40: 24 * 2 * 60 * 60 + 5 * 60 * 60, + }, + }, + ], + }, + stage_model: { + fields: { + name: { string: "Stage Name", type: "char" }, + }, + records: [ + { + id: 10, + display_name: "New", + }, + { + id: 20, + display_name: "Qualified", + }, + { + id: 30, + display_name: "Proposition", + }, + { + id: 40, + display_name: "Won", + }, + ], + }, + }, + }; + + setupViewRegistries(); + }); + + QUnit.module("StatusBarDurationField"); + + QUnit.test("StatusBarDurationField in a form view", async function (assert) { + await makeView({ + serverData, + type: "form", + resModel: "partner", + arch: ` +
+
+ +
+
`, + resId: 1, + }); + + // Check that time is put in the stages + const firstButton = target.querySelector("button[data-value='10'"); + assert.strictEqual(firstButton.innerText, "New7d"); + assert.strictEqual(firstButton.querySelectorAll("span")[1].title, "7 days, 30 minutes"); + + const secondButton = target.querySelector("button[data-value='20']"); + assert.strictEqual(secondButton.innerText, "Qualified3h"); + assert.strictEqual(secondButton.querySelectorAll("span")[1].title, "3 hours"); + + const thirdButton = target.querySelector("button[data-value='30']"); + assert.strictEqual(thirdButton.innerText, "Proposition"); + assert.strictEqual(thirdButton.title, ""); + + const fourthButton = target.querySelector("button[data-value='40']"); + assert.strictEqual(fourthButton.innerText, "Won2d"); + assert.strictEqual(fourthButton.querySelectorAll("span")[1].title, "2 days, 5 hours"); + }); +}); diff --git a/addons/web/static/src/views/fields/formatters.js b/addons/web/static/src/views/fields/formatters.js index b16e1e3b33a..197fe8fb485 100644 --- a/addons/web/static/src/views/fields/formatters.js +++ b/addons/web/static/src/views/fields/formatters.js @@ -154,6 +154,51 @@ export function formatChar(value, options) { return value; } +/** + * Converts a given duration in seconds into a human-readable format. + * + * The function takes a duration in seconds and converts it into a human-readable form, + * such as "1h" or "1 hour, 30 minutes", depending on the value of the `showFullDuration` parameter. + * If the `showFullDuration` is set to true, the function will display up to two non-zero duration + * components in long form (e.g: hours, minutes). + * Otherwise, it will show just the largest non-zero duration component in narrow form (e.g: y or h). + * Luxon takes care of translations given the current locale. + * + * @param {number} seconds - The duration in seconds to be converted. + * @param {boolean} showFullDuration - If true, the output will have two components in long form. + * Otherwise, just one component will be displayed in narrow form. + * + * @returns {string} A human-readable string representation of the duration. + * + * @example + * // Sample usage + * const durationInSeconds = 7320; // 2 hours and 2 minutes (2 * 3600 + 2 * 60) + * const fullDuration = humanizeDuration(durationInSeconds, true); + * console.log(fullDuration); // Output: "2 hours, 2 minutes" + * + * const shortDuration = humanizeDuration(durationInSeconds, false); + * console.log(shortDuration); // Output: "2h" + */ +export function formatDuration(seconds, showFullDuration) { + const displayStyle = showFullDuration ? "long" : "narrow"; + const numberOfValuesToDisplay = showFullDuration ? 2 : 1; + const durationKeys = ["years", "months", "days", "hours", "minutes"]; + + if (seconds < 60) { + seconds = 60; + } + seconds -= seconds % 60; + + let duration = luxon.Duration.fromObject({ seconds: seconds }).shiftTo(...durationKeys); + duration = duration.shiftTo(...durationKeys.filter((key) => duration.get(key))); + const durationSplit = duration.toHuman({ unitDisplay: displayStyle }).split(","); + + if (!showFullDuration && duration.loc.locale.includes("en") && duration.months > 0) { + durationSplit[0] = durationSplit[0].replace("m", "M"); + } + return durationSplit.slice(0, numberOfValuesToDisplay).join(","); +} + /** * Returns a string representing a float. The result takes into account the * user settings (to display the correct decimal separator). diff --git a/addons/web/static/src/views/fields/statusbar/statusbar_field.scss b/addons/web/static/src/views/fields/statusbar/statusbar_field.scss index 75816704612..dab8fd862c6 100644 --- a/addons/web/static/src/views/fields/statusbar/statusbar_field.scss +++ b/addons/web/static/src/views/fields/statusbar/statusbar_field.scss @@ -50,7 +50,8 @@ &.disabled { opacity: 1; - pointer-events: none; + pointer-events: auto; + cursor: default; &:not(.o_arrow_button_current) { &, &:hover, &:focus { @@ -76,4 +77,4 @@ } } } -} \ No newline at end of file +}