update
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 135.46666 135.46667"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="icon.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.4378367"
|
||||
inkscape:cx="97.020753"
|
||||
inkscape:cy="192.65053"
|
||||
inkscape:window-width="3200"
|
||||
inkscape:window-height="1960"
|
||||
inkscape:window-x="-12"
|
||||
inkscape:window-y="-12"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g5" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g5"
|
||||
transform="matrix(1.1612554,0,0,1.1612119,-10.922364,-10.919417)"
|
||||
style="stroke-width:0.861153">
|
||||
<path
|
||||
id="path1"
|
||||
style="fill:#bc5fd3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.911393;stroke-linejoin:round;stroke-miterlimit:3.2"
|
||||
d="M 67.73333,15.268131 A 52.320512,52.320495 0 0 0 15.412774,67.588688 52.320512,52.320495 0 0 0 67.73333,119.90924 52.320512,52.320495 0 0 0 120.05389,67.588688 52.320512,52.320495 0 0 0 67.73333,15.268131 Z m 0,21.381665 A 30.939746,30.939746 0 0 1 98.672226,67.588688 30.939746,30.939746 0 0 1 67.73333,98.528682 30.939746,30.939746 0 0 1 36.793336,67.588688 30.939746,30.939746 0 0 1 67.73333,36.649796 Z" />
|
||||
<rect
|
||||
style="mix-blend-mode:multiply;fill:#71baf4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.911385;stroke-linejoin:round;stroke-miterlimit:3.2"
|
||||
id="rect4"
|
||||
width="74.61174"
|
||||
height="24.603153"
|
||||
x="85.679924"
|
||||
y="-11.51065"
|
||||
ry="5.8131294"
|
||||
transform="rotate(45)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
export const QUICKBOARD_BG_COLORS = {
|
||||
"def": ["#845ec2","#d65db1","#ff6f91","#ff9671","#ffc75f","#2c73d2","#0081cf","#0089ba","#008e9b","#008f7a"],
|
||||
"alt": ["#005f73","#ee9b00","#94d2bd","#ca6702","#e9d8a6","#bb3e03","#0a9396","#9b2226","#ae2012","#c0d896"],
|
||||
"cld": ["#a9d6e5","#89c2d9","#61a5c2","#468faf","#2c7da0","#2a6f97","#014f86","#01497c","#013a63","#012a4a"],
|
||||
"hot": ["#ffb950","#ffad33","#ff931f","#ff7e33","#fa5e1f","#ec3f13","#b81702","#a50104","#8e0103","#7a0103"],
|
||||
"ert": ["#bfc882","#7b4618","#a4b75c","#532a09","#647332","#915c27","#3e4c22","#ad8042","#2e401c","#bfab67"],
|
||||
"clr": ["#1f2ba0","#0063ff","#0087ff","#19b6ec","#038659","#006f26","#563c0c","#803c00","#ed9180","#ff1002"],
|
||||
"ptl": ["#66c5cc","#f6cf71","#f89c74","#dcb0f2","#87c55f","#9eb9f3","#fe88b1","#c9db74","#8be0a4","#b497e7"],
|
||||
"pur": ["#f992ad","#fbbcee","#fab4c8","#f78ecf","#cfb9f7","#e0cefd","#a480f2","#d4b0f9","#c580ed","#d199f1"]
|
||||
}
|
||||
|
||||
export const QUICKBOARD_FG_COLORS = {
|
||||
"def": ["#000000","#ffffff"],
|
||||
"alt": ["#000000","#ffffff"],
|
||||
"cld": ["#000000","#ffffff"],
|
||||
"hot": ["#000000","#ffffff"],
|
||||
"ert": ["#000000","#ffffff"],
|
||||
"clr": ["#000000","#ffffff"],
|
||||
"ptl": ["#000000","#ffffff"],
|
||||
"pur": ["#000000","#ffffff"]
|
||||
}
|
||||
|
||||
export function getBackgroundColor(index, theme="def") {
|
||||
let idx = index % QUICKBOARD_BG_COLORS[theme].length;
|
||||
return QUICKBOARD_BG_COLORS[theme][idx];
|
||||
}
|
||||
|
||||
export function getForegroundColor(index, theme="def") {
|
||||
let idx = index % QUICKBOARD_FG_COLORS[theme].length;
|
||||
return QUICKBOARD_FG_COLORS[theme][idx];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
import { Component, useRef, useState, useExternalListener } from "@odoo/owl";
|
||||
|
||||
export class QbColorList extends Component {
|
||||
static template = "quickboard.QbColorList";
|
||||
static defaultProps = {
|
||||
forceExpanded: false,
|
||||
isExpanded: false,
|
||||
};
|
||||
static props = {
|
||||
canToggle: { type: Boolean, optional: true },
|
||||
colors: Array,
|
||||
forceExpanded: { type: Boolean, optional: true },
|
||||
isExpanded: { type: Boolean, optional: true },
|
||||
onColorSelected: Function,
|
||||
selectedColor: { type: Number, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.colorlistRef = useRef("colorlist");
|
||||
this.state = useState({ isExpanded: this.props.isExpanded });
|
||||
useExternalListener(window, "click", this.onOutsideClick);
|
||||
}
|
||||
|
||||
get colors() {
|
||||
return this.props.colors;
|
||||
}
|
||||
|
||||
onColorSelected(id) {
|
||||
const idx = this.props.colors.indexOf(id);
|
||||
this.props.onColorSelected(idx);
|
||||
if (!this.props.forceExpanded) {
|
||||
this.state.isExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
onOutsideClick(ev) {
|
||||
if (this.colorlistRef.el.contains(ev.target) || this.props.forceExpanded) {
|
||||
return;
|
||||
}
|
||||
this.state.isExpanded = false;
|
||||
}
|
||||
|
||||
onToggle(ev) {
|
||||
if (this.props.canToggle) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.state.isExpanded = !this.state.isExpanded;
|
||||
this.colorlistRef.el.firstElementChild.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="quickboard.QbColorList">
|
||||
<div class="o_colorlist d-flex flex-wrap align-items-center mw-100 gap-2" aria-atomic="true" t-ref="colorlist">
|
||||
<t t-if="!props.forceExpanded and !state.isExpanded">
|
||||
<button t-on-click="onToggle"
|
||||
role="menuitem"
|
||||
t-att-data-color="colors[props.selectedColor]"
|
||||
t-attf-style="background-color: {{ colors[props.selectedColor] }};"
|
||||
t-attf-class="btn p-0 rounded-0 o_colorlist_toggler"/>
|
||||
</t>
|
||||
<t t-else="" t-foreach="props.colors" t-as="colorId" t-key="colorId">
|
||||
<button t-on-click.prevent.stop="() => this.onColorSelected(colorId)"
|
||||
role="menuitem"
|
||||
t-att-data-color="colorId"
|
||||
t-attf-style="background-color: {{ colorId }};"
|
||||
t-attf-class="btn p-0 rounded-0 {{ colorId === props.selectedColor ? 'o_colorlist_selected' : '' }}"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,28 @@
|
||||
.quickboard {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.grid-stack-item-content {
|
||||
background-color: whitesmoke;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.quickboard-item-basic-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quickboard-item-basic-value {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.quickboard-item-basic-icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.quickboard-item-chart-title, .quickboard-item-list-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quickboard-item-chart-icon, .quickboard-item-list-icon {
|
||||
font-size: 1em !important;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useRef, useEffect, useState, onPatched } from "@odoo/owl";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
import { SelectMenu } from "@web/core/select_menu/select_menu";
|
||||
import { deserializeDateTime, serializeDateTime } from "@web/core/l10n/dates";
|
||||
import { QuickboardItem } from "./quickboard_item";
|
||||
import { QUICKBOARD_BG_COLORS } from "../core/colors"
|
||||
|
||||
class Quickboard extends Component {
|
||||
static template = "quickboard";
|
||||
static components = { SelectMenu, DateTimeInput, QuickboardItem };
|
||||
static props = {
|
||||
...standardActionServiceProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
|
||||
let theme = "def";
|
||||
if (user.settings.quickboard_theme) {
|
||||
theme = user.settings.quickboard_theme;
|
||||
}
|
||||
|
||||
let startDate = luxon.DateTime.local().startOf("month");
|
||||
if (user.settings.quickboard_start_date) {
|
||||
startDate = deserializeDateTime(
|
||||
user.settings.quickboard_start_date
|
||||
);
|
||||
}
|
||||
let endDate = luxon.DateTime.now();
|
||||
if (user.settings.quickboard_end_date) {
|
||||
endDate = deserializeDateTime(user.settings.quickboard_end_date);
|
||||
}
|
||||
|
||||
this.gridRef = useRef("grid-stack");
|
||||
this.state = useState({
|
||||
"theme": theme,
|
||||
"startDate": startDate,
|
||||
"endDate": endDate,
|
||||
"items": [],
|
||||
});
|
||||
|
||||
this.quickboard = useState(useService("quickboard"));
|
||||
this.quickboard.getQuickboardItemDefs(
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
);
|
||||
|
||||
useEffect(
|
||||
(isReady) => {
|
||||
self = this;
|
||||
let items = Object.entries(this.quickboard.items)
|
||||
.filter(([k, v]) => !isNaN(k))
|
||||
.map(([k, v]) => Object.assign({}, v));
|
||||
this.state.items = items;
|
||||
},
|
||||
() => [this.quickboard.isReady]
|
||||
);
|
||||
|
||||
onPatched(() => {
|
||||
this.gridRef.current =
|
||||
this.gridRef.current ||
|
||||
GridStack.init({
|
||||
float: true,
|
||||
columnOpts: {
|
||||
breakpoints: [{ w: 768, c: 1 }],
|
||||
},
|
||||
cellHeight: "10rem",
|
||||
});
|
||||
if (this.gridRef.current) {
|
||||
const grid = this.gridRef.current;
|
||||
grid.batchUpdate();
|
||||
grid.removeAll(false);
|
||||
|
||||
for (let i = 0; i < this.state.items.length; i++) {
|
||||
const element = document.querySelector(
|
||||
`#grid-stack-item-${this.state.items[i]["id"]}`
|
||||
);
|
||||
if (element) {
|
||||
grid.makeWidget(element);
|
||||
}
|
||||
}
|
||||
|
||||
grid.batchUpdate(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.busService.addChannel("quickboard");
|
||||
this.busService.subscribe("quickboard_updated", ({}) => {
|
||||
this.onQuickboardUpdated();
|
||||
});
|
||||
|
||||
this.setupNoData();
|
||||
}
|
||||
|
||||
onQuickboardUpdated() {
|
||||
this.applyFilter();
|
||||
let grid = this.gridRef.current;
|
||||
grid.compact();
|
||||
}
|
||||
|
||||
onStartDateChanged(date) {
|
||||
this.state.startDate = date;
|
||||
}
|
||||
|
||||
onEndDateChanged(date) {
|
||||
this.state.endDate = date;
|
||||
}
|
||||
|
||||
saveQuickboard(ev) {
|
||||
let serializedData = this.gridRef.current.save(false);
|
||||
this.quickboard.saveLayout(serializedData);
|
||||
}
|
||||
|
||||
compact(ev) {
|
||||
let grid = this.gridRef.current;
|
||||
grid.compact();
|
||||
}
|
||||
|
||||
async generateQuickboard(ev) {
|
||||
const cell_width = this.gridRef.current.cellWidth();
|
||||
// DO NOT REMOVE: Without this the getCellHeight will return weird number
|
||||
const h_0 = this.gridRef.current.cellHeight().el.clientHeight;
|
||||
const cell_height = this.gridRef.current.getCellHeight();
|
||||
const screen_width = screen.width;
|
||||
const screen_height = screen.height;
|
||||
|
||||
this.action.doAction(
|
||||
{
|
||||
type: "ir.actions.act_window",
|
||||
name: "Generate Quickboard",
|
||||
res_model: "quickboard.generator",
|
||||
views: [[false, "form"]],
|
||||
view_mode: "form",
|
||||
target: "new",
|
||||
context: {
|
||||
dialog_size: "medium",
|
||||
cell_width: cell_width,
|
||||
cell_height: cell_height,
|
||||
screen_width: screen_width,
|
||||
screen_height: screen_height,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addItem(ev) {
|
||||
this.action.doAction(
|
||||
{
|
||||
type: "ir.actions.act_window",
|
||||
name: "New",
|
||||
res_model: "quickboard.item",
|
||||
views: [[false, "form"]],
|
||||
view_mode: "form",
|
||||
target: "new",
|
||||
context: {
|
||||
dialog_size: "medium",
|
||||
quick_add: true
|
||||
},
|
||||
},
|
||||
{
|
||||
onClose: () => {
|
||||
this.applyFilter();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setupNoData() {
|
||||
Chart.register({
|
||||
id: "NoData",
|
||||
afterDraw: function (chart) {
|
||||
if (
|
||||
chart.data.datasets
|
||||
.map((d) => d.data.length)
|
||||
.reduce((p, a) => p + a, 0) === 0
|
||||
) {
|
||||
const ctx = chart.ctx;
|
||||
const width = chart.width;
|
||||
const height = chart.height;
|
||||
chart.clear();
|
||||
|
||||
ctx.save();
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
ctx.fillText("No data to display.", width / 2, height / 2);
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async applyFilter(ev) {
|
||||
await user.setUserSettings(
|
||||
"quickboard_start_date",
|
||||
serializeDateTime(this.state.startDate)
|
||||
);
|
||||
await user.setUserSettings(
|
||||
"quickboard_end_date",
|
||||
serializeDateTime(this.state.endDate)
|
||||
);
|
||||
await this.quickboard.getQuickboardItemDefs();
|
||||
}
|
||||
|
||||
async onSelectTheme(val) {
|
||||
this.state.theme = val;
|
||||
await user.setUserSettings("quickboard_theme", val);
|
||||
this.quickboard.getQuickboardItemDefs();
|
||||
}
|
||||
|
||||
getThemeSelectionItem(label, theme){
|
||||
return {
|
||||
value: theme,
|
||||
label: label,
|
||||
colors: QUICKBOARD_BG_COLORS[theme]
|
||||
}
|
||||
}
|
||||
|
||||
get themes() {
|
||||
return [
|
||||
this.getThemeSelectionItem("Default", "def"),
|
||||
this.getThemeSelectionItem("Alternative", "alt"),
|
||||
this.getThemeSelectionItem("Cold", "cld"),
|
||||
this.getThemeSelectionItem("Hot", "hot"),
|
||||
this.getThemeSelectionItem("Earth", "ert"),
|
||||
this.getThemeSelectionItem("Colorful", "clr"),
|
||||
this.getThemeSelectionItem("Pastel", "ptl"),
|
||||
this.getThemeSelectionItem("Pink Purple", "pur"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("lazy_components").add("Quickboard", Quickboard);
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="quickboard" owl="1">
|
||||
<div class="quickboard h-100 overflow-auto" style="min-height:-webkit-fill-available;">
|
||||
<div class="container-fluid flex-column flex-md-row bg-light d-flex align-items-center justify-content-between">
|
||||
<div class="btn-toolbar w-100 w-md-auto my-2 d-flex" role="toolbar">
|
||||
<div class="btn-group me-1 flex-grow-1" role="group">
|
||||
<button id="save" type="button" class="btn btn-primary" t-on-click="(ev) => this.saveQuickboard(ev)">
|
||||
<i class="fa fa-floppy-o me-2 d-none d-md-inline" />Save
|
||||
</button>
|
||||
<button id="compant" type="button" class="btn btn-secondary" t-on-click="(ev) => this.compact(ev)">
|
||||
<i class="fa fa-th me-2 d-none d-md-inline" />Arrange
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group ms-1 flex-grow-1" role="group">
|
||||
<button id="generate" type="button" class="btn btn-secondary" t-on-click="(ev) => this.addItem(ev)">
|
||||
<i class="fa fa-plus me-2 d-none d-md-inline" />New
|
||||
</button>
|
||||
<button id="generate" type="button" class="btn btn-secondary" t-on-click="(ev) => this.generateQuickboard(ev)">
|
||||
<i class="fa fa-magic me-2 d-none d-md-inline" />Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex my-auto mb-2">
|
||||
<div class="input-group input-group-sm ms-1 me-1">
|
||||
<div class="input-group-text d-none d-md-block"><span class="align-middle">Theme</span></div>
|
||||
<div class="flex-fill" style="width: 5rem;">
|
||||
<SelectMenu
|
||||
choices="themes"
|
||||
value="this.state.theme"
|
||||
onSelect.bind="this.onSelectTheme"
|
||||
required="true"
|
||||
searchable="false"
|
||||
togglerClass="'fw-normal'"
|
||||
class="'theme-menu'">
|
||||
<t t-set-slot="choice" t-slot-scope="choice">
|
||||
<div class="d-flex">
|
||||
<t t-foreach="choice.data.colors" t-as="color" t-key="color">
|
||||
<div class="border border-2 rounded-circle" t-attf-style="background-color: {{ color }}; width: 1.5em; height: 1.5em">
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</SelectMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group input-group-sm ms-1 me-1">
|
||||
<div class="input-group-text d-none d-md-block"><span class="align-middle">Start Date</span></div>
|
||||
<div class="form-control">
|
||||
<DateTimeInput type="'date'" value="state.startDate" placeholder="'Start Date'"
|
||||
onChange="(d) => this.onStartDateChanged(d)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group input-group-sm ms-1 me-1">
|
||||
<div class="input-group-text d-none d-md-block"><span class="align-middle">End Date</span></div>
|
||||
<div class="form-control">
|
||||
<DateTimeInput type="'date'" value="state.endDate" placeholder="'End Date'"
|
||||
onChange="(d) => this.onEndDateChanged(d)"/>
|
||||
</div>
|
||||
</div>
|
||||
<button id="applyFilter" type="button" class="btn btn-primary" t-on-click="(ev) => this.applyFilter(ev)">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-stack " t-ref="grid-stack">
|
||||
<div t-foreach="state.items" t-as="item" t-key="item.id"
|
||||
t-attf-gs-id="{{item.id}}"
|
||||
t-attf-gs-x="{{item.x_pos}}"
|
||||
t-attf-gs-y="{{item.y_pos}}"
|
||||
t-attf-gs-w="{{item.width}}"
|
||||
t-attf-gs-h="{{item.height}}"
|
||||
t-attf-id="grid-stack-item-{{item.id}}">
|
||||
<div class="grid-stack-item-content rounded">
|
||||
<QuickboardItem itemType="item.type"
|
||||
itemId="item.id"
|
||||
action="action"
|
||||
theme="state.theme"
|
||||
startDate="state.startDate"
|
||||
endDate="state.endDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,36 @@
|
||||
/** @odoo-module **/
|
||||
import { Component } from "@odoo/owl";
|
||||
import { QuickboardItemBasic } from "./quickboard_item_basic";
|
||||
import { QuickboardItemChart } from "./quickboard_item_chart";
|
||||
import { QuickboardItemList } from "./quickboard_item_list";
|
||||
import { standardQuickboardItemProps } from "./standard_quickboard_item_props"
|
||||
|
||||
export class QuickboardItem extends Component {
|
||||
static template = "quickboard.QuickboardItem"
|
||||
static props = {
|
||||
...standardQuickboardItemProps,
|
||||
itemType: { type: String },
|
||||
}
|
||||
|
||||
get _itemComponent(){
|
||||
if (this.props.itemType === "basic"){
|
||||
return QuickboardItemBasic;
|
||||
} else if (this.props.itemType === "chart"){
|
||||
return QuickboardItemChart
|
||||
} else if (this.props.itemType === "list"){
|
||||
return QuickboardItemList
|
||||
}
|
||||
|
||||
return Component;
|
||||
}
|
||||
|
||||
get _itemProps(){
|
||||
return {
|
||||
"action": this.props.action,
|
||||
"itemId": this.props.itemId,
|
||||
"theme": this.props.theme,
|
||||
"startDate": this.props.startDate,
|
||||
"endDate": this.props.endDate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="quickboard.QuickboardItem" owl="1">
|
||||
<t t-component="_itemComponent" t-props="_itemProps"/>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,73 @@
|
||||
/** @odoo-module **/
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { user } from "@web/core/user";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardQuickboardItemProps } from "./standard_quickboard_item_props"
|
||||
|
||||
export class QuickboardItemBase extends Component {
|
||||
static props = {
|
||||
...standardQuickboardItemProps
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.action = this.props.action;
|
||||
this.itemId = this.props.itemId;
|
||||
|
||||
this.quickboard = useState(useService("quickboard"));
|
||||
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.busService.subscribe("quickboard_item_updated", ({ id }) => {
|
||||
this.onMessage(id)
|
||||
});
|
||||
}
|
||||
|
||||
onMessage(id) {
|
||||
console.log(id);
|
||||
}
|
||||
|
||||
showItemConfig(ev) {
|
||||
this._showItemConfig();
|
||||
}
|
||||
|
||||
_getSpinnerOpt(){
|
||||
var opts = {
|
||||
"lines": 10, // The number of lines to draw
|
||||
"length": 0, // The length of each line
|
||||
"width": 2, // The line thickness
|
||||
"radius": 4, // The radius of the inner circle
|
||||
"scale": 4, // Scales overall size of the spinner
|
||||
"corners": 1, // Corner roundness (0..1)
|
||||
"speed": 0.7, // Rounds per second
|
||||
"rotate": 0, // The rotation offset
|
||||
"animation": 'spinner-line-fade-more', // The CSS animation name for the lines
|
||||
"direction": 1, // 1: clockwise, -1: counterclockwise
|
||||
"color": '#7a008a', // CSS color or array of colors
|
||||
"fadeColor": 'transparent', // CSS color or array of colors
|
||||
"top": '51%', // Top position relative to parent
|
||||
"left": '50%', // Left position relative to parent
|
||||
"shadow": '0 0 1px transparent', // Box-shadow for the lines
|
||||
"zIndex": 2000000000, // The z-index (defaults to 2e9)
|
||||
"className": 'spinner', // The CSS class to assign to the spinner
|
||||
"position": 'absolute', // Element positioning
|
||||
};
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
_showItemConfig(){
|
||||
var self = this;
|
||||
this.action.doAction({
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Quickboard Item',
|
||||
'res_model': 'quickboard.item',
|
||||
'res_id': self.itemId,
|
||||
'views': [[false, 'form']],
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'dialog_size': 'medium',
|
||||
'quick_edit': true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/** @odoo-module **/
|
||||
import { useState, onMounted, useRef } from "@odoo/owl";
|
||||
import { parseFloat, parseInteger, parseMonetary } from "@web/views/fields/parsers";
|
||||
import { formatFloat, formatInteger, formatMonetary } from "@web/views/fields/formatters";;
|
||||
import { QuickboardItemBase } from "./quickboard_item_base";
|
||||
import { getBackgroundColor, getForegroundColor } from "../core/colors";
|
||||
|
||||
export class QuickboardItemBasic extends QuickboardItemBase {
|
||||
static template = "quickboard.QuickboardItemBasic";
|
||||
|
||||
setup(){
|
||||
super.setup();
|
||||
|
||||
this.gsItemRef = useRef("grid-stack-item");
|
||||
this.containerRef = useRef("container");
|
||||
this.spinner = new Spin.Spinner(this._getSpinnerOpt());
|
||||
|
||||
this.state = useState({
|
||||
"title": "",
|
||||
"icon": "",
|
||||
"valueFieldType": "",
|
||||
"aggregateValue": "",
|
||||
"value": "",
|
||||
"aggregateFunction": "",
|
||||
"textColor": "",
|
||||
"backgroundColor": "",
|
||||
|
||||
"theme": this.props.theme,
|
||||
"startDate": this.props.startDate,
|
||||
"endDate": this.props.endDate,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
var target = this.gsItemRef.el;
|
||||
this.spinner.spin(target);
|
||||
await this.loadData(
|
||||
this.props.itemId,
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
).then(() => {this.spinner.stop()});
|
||||
})
|
||||
}
|
||||
|
||||
async onMessage(id) {
|
||||
if (id == this.itemId){
|
||||
var target = this.gsItemRef.el;
|
||||
if (this.containerRef.el){
|
||||
this.containerRef.el.classList.add("d-none");
|
||||
}
|
||||
this.spinner.spin(target);
|
||||
await this.loadData(
|
||||
this.props.itemId,
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
).then(() => {
|
||||
if (this.containerRef.el){
|
||||
this.containerRef.el.classList.remove("d-none");
|
||||
}
|
||||
this.spinner.stop()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadData(itemId, startDate, endDate) {
|
||||
const res = await this.quickboard.getQuickboardItem(itemId, startDate, endDate)
|
||||
this.state.title = res.name;
|
||||
this.state.icon = res.icon;
|
||||
this.state.valueFieldType = res.value_field_type;
|
||||
this.state.aggregateValue = res.aggregate_value;
|
||||
this.state.value = this.getFormattedValue();
|
||||
this.state.aggregateFunction = this.aggregate_function;
|
||||
|
||||
this.state.textColor = getForegroundColor(res.text_color, this.state.theme);
|
||||
this.state.backgroundColor = getBackgroundColor(res.background_color, this.state.theme);
|
||||
}
|
||||
|
||||
getFormattedValue(){
|
||||
let val;
|
||||
let val_formatted;
|
||||
|
||||
switch (this.state.valueFieldType){
|
||||
case "integer":
|
||||
val = parseInteger(String(this.state.aggregateValue));
|
||||
val_formatted = formatInteger(val);
|
||||
break;
|
||||
case "float":
|
||||
val = parseFloat(String(this.state.aggregateValue));
|
||||
val_formatted = formatFloat(val);
|
||||
break;
|
||||
case "monetary":
|
||||
val = parseMonetary(String(this.state.aggregateValue));
|
||||
val_formatted = formatMonetary(val);
|
||||
break;
|
||||
default:
|
||||
if (Number.isSafeInteger(this.state.aggregateValue))
|
||||
val_formatted = formatInteger(this.state.aggregateValue);
|
||||
else
|
||||
val_formatted = formatFloat(this.state.aggregateValue);
|
||||
}
|
||||
return val_formatted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="quickboard.QuickboardItemBasic" owl="1">
|
||||
<div t-ref="grid-stack-item" class="grid-stack-item h-100 overflow-hidden d-flex flex-column">
|
||||
<div t-ref="container" class="d-flex flex-column flex-fill p-1">
|
||||
<div class="d-flex p-1 flex-grow-1"
|
||||
t-att-style="'color:' + state.textColor + '; background-color: ' + state.backgroundColor">
|
||||
<div class="quickboard-item-basic-icon ps-2">
|
||||
<span><i t-att-class="'fa ' + state.icon"/></span>
|
||||
</div>
|
||||
<div class="d-flex flex-column text-end flex-grow-1 align-self-end h-100">
|
||||
<span class="quickboard-item-chart-icon me-1 flex-fill">
|
||||
<i class="fa fa-cog" t-on-click="(ev) => this.showItemConfig(ev)"/>
|
||||
</span>
|
||||
<div class="quickboard-item-basic-value flex-wrap w-100 pe-1">
|
||||
<span><t t-out="this.state.value"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quickboard-item-basic-title text-center"><t t-out="this.state.title"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,263 @@
|
||||
/** @odoo-module **/
|
||||
import { parseDate, parseDateTime } from "@web/core/l10n/dates";
|
||||
import { useState, useRef, onMounted } from "@odoo/owl";
|
||||
import { getBackgroundColor } from "../core/colors";
|
||||
|
||||
import { QuickboardItemBase } from "./quickboard_item_base";
|
||||
|
||||
export class QuickboardItemChart extends QuickboardItemBase {
|
||||
static template = "quickboard.QuickboardItemChart";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.gsItemRef = useRef("grid-stack-item");
|
||||
this.spinner = new Spin.Spinner(this._getSpinnerOpt());
|
||||
|
||||
this.state = useState({
|
||||
"title": "",
|
||||
"icon": "",
|
||||
"chartType": "",
|
||||
"data": "",
|
||||
"valueFieldName": "",
|
||||
"valueFieldType": "",
|
||||
"dimensionFieldName": "",
|
||||
"dimensionFieldType": "",
|
||||
"datetimeGranularity": "",
|
||||
"aggregateFunction": "",
|
||||
"datetimeGranularity": "",
|
||||
|
||||
"theme": this.props.theme,
|
||||
"startDate": this.props.startDate,
|
||||
"endDate": this.props.endDate,
|
||||
});
|
||||
this.chartCanvasRef = useRef("chartCanvas");
|
||||
|
||||
onMounted(async () => {
|
||||
var target = this.gsItemRef.el;
|
||||
this.spinner.spin(target);
|
||||
await this.loadData(
|
||||
this.props.itemId,
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
).then(() => {
|
||||
this.spinner.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async onMessage(id) {
|
||||
if (id == this.itemId) {
|
||||
var target = this.gsItemRef.el;
|
||||
this.spinner.spin(target);
|
||||
if (this.chartCanvasRef.el) {
|
||||
this.chartCanvasRef.el.style.display = "none";
|
||||
}
|
||||
await this.loadData(
|
||||
this.props.itemId,
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
).then(() => {
|
||||
this.spinner.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadData(itemId, startDate, endDate) {
|
||||
var target = this.gsItemRef.el;
|
||||
this.spinner.spin(target);
|
||||
|
||||
const res = await this.quickboard.getQuickboardItem(
|
||||
itemId,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
this.state.title = res.name;
|
||||
this.state.icon = res.icon;
|
||||
this.state.chartType = res.chart_type;
|
||||
this.state.data = res.data;
|
||||
this.state.valueFieldName = res.value_field_name;
|
||||
this.state.valueFieldType = res.value_field_type;
|
||||
this.state.dimensionFieldName = res.dimension_field_name;
|
||||
this.state.dimensionFieldType = res.dimension_field_type;
|
||||
this.state.groupFieldName = res.group_field_name;
|
||||
this.state.groupFieldType = res.group_field_type;
|
||||
this.state.aggregateFunction = res.aggregate_function;
|
||||
this.state.datetimeGranularity = res.datetime_granularity;
|
||||
|
||||
this.renderChart(
|
||||
this.state.chartType,
|
||||
this.state.data,
|
||||
this.state.valueFieldName,
|
||||
this.state.aggregateFunction,
|
||||
this.state.dimensionFieldType,
|
||||
this.state.datetimeGranularity
|
||||
);
|
||||
}
|
||||
|
||||
_getCircularChartConfig(chartType, data, valueFieldName, aggregateFunction, dimensionFieldType, datetimeGranularity){
|
||||
if (chartType === "polar") {
|
||||
chartType = "polarArea";
|
||||
}
|
||||
|
||||
const chartData = [];
|
||||
let labels = [];
|
||||
let dataset_color = [];
|
||||
data.forEach((element, index) => {
|
||||
const lbl = Object.entries(element["dataset"])
|
||||
.filter(([k, v]) => !isNaN(k))
|
||||
.map(([k, v]) => v[0]);
|
||||
|
||||
labels = Array.from(new Set(labels.concat(lbl)))
|
||||
});
|
||||
|
||||
dataset_color = labels.map((_, index) => getBackgroundColor(index, this.state.theme));
|
||||
|
||||
data.forEach((element, index) => {
|
||||
const dt = Object.entries(element["dataset"])
|
||||
.filter(([k, v]) => !isNaN(k))
|
||||
.map(([k, v]) => v[1]);
|
||||
|
||||
chartData.push({
|
||||
label: element.label,
|
||||
data: dt,
|
||||
backgroundColor: dataset_color
|
||||
});
|
||||
});
|
||||
|
||||
let chartConfig = {
|
||||
type: chartType,
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: chartData,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom"
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return chartConfig;
|
||||
}
|
||||
|
||||
_getXYChartConfig(chartType, data, valueFieldName, aggregateFunction, dimensionFieldType, datetimeGranularity){
|
||||
const chartData = [];
|
||||
data.forEach((element, index) => {
|
||||
const dt = Object.entries(element["dataset"])
|
||||
.filter(([k, v]) => !isNaN(k))
|
||||
.map(([k, v]) => {
|
||||
let x_val;
|
||||
let y_val;
|
||||
let dateTimeDimension = ["date", "datetime"].includes(dimensionFieldType)
|
||||
if (chartType === 'horizontal-bar'){
|
||||
x_val = v[1];
|
||||
y_val = dateTimeDimension ? parseDateTime(v[0]) : v[0]
|
||||
} else {
|
||||
x_val = dateTimeDimension ? parseDateTime(v[0]) : v[0];
|
||||
y_val = v[1]
|
||||
}
|
||||
|
||||
return Object.assign({}, {
|
||||
x: x_val,
|
||||
y: y_val
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const dataset_color = getBackgroundColor(index, this.state.theme);
|
||||
|
||||
chartData.push({
|
||||
label: element.label,
|
||||
data: dt,
|
||||
backgroundColor: dataset_color,
|
||||
borderColor: dataset_color,
|
||||
borderWidth: 3,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
});
|
||||
});
|
||||
|
||||
let x_axis_option = {};
|
||||
let y_axis_option = {}
|
||||
if (["date", "datetime"].includes(dimensionFieldType)) {
|
||||
if (chartType === 'horizontal-bar') {
|
||||
chartType = 'bar';
|
||||
y_axis_option = {
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
y: {
|
||||
type: "time",
|
||||
time: {
|
||||
unit: datetimeGranularity,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
x_axis_option = {
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
unit: datetimeGranularity,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (chartType === 'horizontal-bar') {
|
||||
chartType = 'bar';
|
||||
y_axis_option = {
|
||||
indexAxis: 'y',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let chartConfig = {
|
||||
type: chartType,
|
||||
data: {
|
||||
datasets: chartData
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom"
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(chartConfig.options, x_axis_option);
|
||||
Object.assign(chartConfig.options, y_axis_option);
|
||||
|
||||
return chartConfig;
|
||||
}
|
||||
|
||||
renderChart(chartType, chartData, valueFieldName, aggregateFunction, dimensionFieldType, datetimeGranularity) {
|
||||
let data = Object.entries(chartData)
|
||||
.filter(([k, v]) => !isNaN(k))
|
||||
.map(([k, v]) => Object.assign({}, v));
|
||||
|
||||
let chartConfig = {}
|
||||
if (["doughnut", "pie", "polar"].includes(chartType)) {
|
||||
chartConfig = this._getCircularChartConfig(chartType, data, valueFieldName, aggregateFunction, dimensionFieldType, datetimeGranularity)
|
||||
} else {
|
||||
chartConfig = this._getXYChartConfig(chartType, data, valueFieldName, aggregateFunction, dimensionFieldType, datetimeGranularity)
|
||||
}
|
||||
|
||||
if (this.chartCanvasRef.el) {
|
||||
const ctx = this.chartCanvasRef.el.getContext("2d");
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
this.chart = new Chart(ctx, chartConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="quickboard.QuickboardItemChart" owl="1">
|
||||
<div t-ref="grid-stack-item" class="grid-stack-item h-100 p-2 pb-5 overflow-hidden">
|
||||
<div class="quickboard-item-chart-title d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="quickboard-item-chart-icon mx-1"><i t-att-class="'fa ' + this.state.icon"/></span>
|
||||
<span><t t-out="state.title"/></span>
|
||||
</div>
|
||||
<span class="quickboard-item-cog me-1"><i class="fa fa-cog" t-on-click="(ev) => this.showItemConfig(ev)"/></span>
|
||||
</div>
|
||||
<div class="pt-4 px-3 w-100 h-100">
|
||||
<div class="h-100 w-100">
|
||||
<canvas t-ref="chartCanvas" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,112 @@
|
||||
/** @odoo-module **/
|
||||
import { useState, useRef, onMounted } from "@odoo/owl";
|
||||
|
||||
import { parseFloat, parseInteger, parseMonetary } from "@web/views/fields/parsers";
|
||||
import { formatFloat, formatInteger, formatMonetary } from "@web/views/fields/formatters";;
|
||||
|
||||
import { QuickboardItemBase } from "./quickboard_item_base";
|
||||
|
||||
export class QuickboardItemList extends QuickboardItemBase {
|
||||
static template = "quickboard.QuickboardItemList";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.gsItemRef = useRef("grid-stack-item");
|
||||
this.containerRef = useRef("container");
|
||||
this.spinner = new Spin.Spinner(this._getSpinnerOpt());
|
||||
|
||||
this.state = useState({
|
||||
"title": "",
|
||||
"icon": "",
|
||||
"data": "",
|
||||
"valueFieldName": "",
|
||||
"valueFieldType": "",
|
||||
"aggregateFunction": "",
|
||||
|
||||
"dimensionFieldName": "",
|
||||
"dimensionFieldType": "",
|
||||
|
||||
"startDate": this.props.startDate,
|
||||
"endDate": this.props.endDate,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
var target = this.gsItemRef.el;
|
||||
this.spinner.spin(target);
|
||||
await this.loadData(
|
||||
this.props.itemId,
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
).then(() => {
|
||||
this.spinner.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async onMessage(id) {
|
||||
if (id == this.itemId) {
|
||||
var target = this.gsItemRef.el;
|
||||
if (this.containerRef.el){
|
||||
this.containerRef.el.classList.add("d-none");
|
||||
}
|
||||
this.spinner.spin(target);
|
||||
await this.loadData(
|
||||
this.props.itemId,
|
||||
this.state.startDate,
|
||||
this.state.endDate
|
||||
).then(() => {
|
||||
if (this.containerRef.el){
|
||||
this.containerRef.el.classList.remove("d-none");
|
||||
}
|
||||
this.spinner.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadData(itemId, startDate, endDate) {
|
||||
var target = this.gsItemRef.el;
|
||||
this.spinner.spin(target);
|
||||
|
||||
const res = await this.quickboard.getQuickboardItem(
|
||||
itemId,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
this.state.title = res.name;
|
||||
this.state.icon = res.icon;
|
||||
this.state.data = res.data;
|
||||
this.state.valueFieldName = res.value_field_name;
|
||||
this.state.valueFieldType = res.value_field_type;
|
||||
this.state.dimensionFieldName = res.dimension_field_name;
|
||||
this.state.dimensionFieldType = res.dimension_field_type;
|
||||
this.state.aggregateFunction = res.aggregate_function;
|
||||
|
||||
}
|
||||
|
||||
formatValue(value){
|
||||
let val;
|
||||
let val_formatted;
|
||||
|
||||
switch (this.state.valueFieldType){
|
||||
case "integer":
|
||||
val = parseInteger(String(value));
|
||||
val_formatted = formatInteger(val);
|
||||
break;
|
||||
case "float":
|
||||
val = parseFloat(String(value));
|
||||
val_formatted = formatFloat(val);
|
||||
break;
|
||||
case "monetary":
|
||||
val = parseMonetary(String(value));
|
||||
val_formatted = formatMonetary(val);
|
||||
break;
|
||||
default:
|
||||
if (Number.isSafeInteger(value))
|
||||
val_formatted = formatInteger(value);
|
||||
else
|
||||
val_formatted = formatFloat(value);
|
||||
}
|
||||
return val_formatted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="quickboard.QuickboardItemList" owl="1">
|
||||
<div t-ref="grid-stack-item" class="grid-stack-item h-100 p-2 pb-5 overflow-hidden">
|
||||
<div class="quickboard-item-list-title d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="quickboard-item-list-icon mx-1"><i t-att-class="'fa ' + this.state.icon"/></span>
|
||||
<span><t t-out="state.title"/></span>
|
||||
</div>
|
||||
<span class="quickboard-item-cog me-1"><i class="fa fa-cog" t-on-click="(ev) => this.showItemConfig(ev)"/></span>
|
||||
</div>
|
||||
<div t-ref="container" class="d-flex flex-column flex-fill p-1">
|
||||
<div class="p-3 w-100 h-100">
|
||||
<div class="h-100 w-100 d-flex">
|
||||
<t t-if="state.data.length > 0" >
|
||||
<table class="table table-sm table-hover table-striped align-self-start">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><t t-out="state.dimensionFieldName"/></th>
|
||||
<th scope="col" class="text-end"><t t-out="state.valueFieldName"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.data" t-as="item" t-key="item.seq">
|
||||
<td><t t-out="item['x']"/></td>
|
||||
<td class="text-end"><t t-esc="formatValue(item['y'])"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="flex-fill text-center align-self-center">
|
||||
No data to display.
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,58 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { reactive } from "@odoo/owl";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
const quickboardService = {
|
||||
start(env, services) {
|
||||
const quickboard = reactive({
|
||||
items: {},
|
||||
isReady: false
|
||||
});
|
||||
|
||||
async function getQuickboardItemDefs() {
|
||||
quickboard.isReady = false;
|
||||
quickboard.items = {};
|
||||
const updates = await rpc("/quickboard/item_defs",{});
|
||||
Object.assign(quickboard.items, updates);
|
||||
quickboard.isReady = true;
|
||||
};
|
||||
|
||||
async function getQuickboardItem(itemId, startDate, endDate) {
|
||||
return await rpc("/quickboard/item",{
|
||||
item_id: itemId,
|
||||
start_date: startDate.toSQLDate(),
|
||||
end_date: endDate.toSQLDate()
|
||||
});
|
||||
};
|
||||
|
||||
async function saveLayout(layout){
|
||||
await rpc("/quickboard/save_layout",{
|
||||
"layout": layout
|
||||
});
|
||||
};
|
||||
|
||||
async function saveFilter(startDate, endDate) {
|
||||
return await rpc("/quickboard/save_filter",{
|
||||
start_date: startDate.toSQLDate(),
|
||||
end_date: endDate.toSQLDate()
|
||||
});
|
||||
};
|
||||
|
||||
async function saveTheme(theme) {
|
||||
return await rpc("/quickboard/save_theme",{
|
||||
theme: theme
|
||||
});
|
||||
};
|
||||
|
||||
quickboard.saveTheme = saveTheme;
|
||||
quickboard.saveFilter = saveFilter;
|
||||
quickboard.getQuickboardItemDefs = getQuickboardItemDefs;
|
||||
quickboard.getQuickboardItem = getQuickboardItem;
|
||||
quickboard.saveLayout = saveLayout;
|
||||
return quickboard;
|
||||
}
|
||||
};
|
||||
|
||||
registry.category("services").add("quickboard", quickboardService);
|
||||
@@ -0,0 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
export const standardQuickboardItemProps = {
|
||||
action: { type: Object },
|
||||
itemId: { type: Number },
|
||||
theme: { type: String },
|
||||
startDate: { type: luxon.DateTime },
|
||||
endDate: { type: luxon.DateTime },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { LazyComponent } from "@web/core/assets";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
|
||||
class QuickboardLoader extends Component {
|
||||
static components = { LazyComponent };
|
||||
static template = xml`
|
||||
<LazyComponent bundle="'quickboard.assets'" Component="'Quickboard'" props="props"/>
|
||||
`;
|
||||
static props = {
|
||||
...standardActionServiceProps,
|
||||
props: { type: Object, optional: true },
|
||||
Component: { type: Function, optional: true },
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("actions").add("quickboard", QuickboardLoader);
|
||||
@@ -0,0 +1,63 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { QbColorList } from "../../../core/qb_color_list/qb_color_list";
|
||||
|
||||
import { user } from "@web/core/user";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
import { QUICKBOARD_BG_COLORS, QUICKBOARD_FG_COLORS } from "../../../core/colors";
|
||||
|
||||
export class QbColorPickerField extends Component {
|
||||
static template = "quickboard.QbColorPickerField";
|
||||
static components = {
|
||||
QbColorList,
|
||||
};
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
canToggle: { type: Boolean },
|
||||
mode: { type: String },
|
||||
};
|
||||
|
||||
currentColorPalette() {
|
||||
let theme = user.settings.quickboard_theme;
|
||||
if (this.props.mode === "foreground"){
|
||||
return QUICKBOARD_FG_COLORS[theme];
|
||||
} else {
|
||||
return QUICKBOARD_BG_COLORS[theme];
|
||||
}
|
||||
}
|
||||
|
||||
get isExpanded() {
|
||||
return !this.props.canToggle && !this.props.readonly;
|
||||
}
|
||||
|
||||
switchColor(colorIndex) {
|
||||
this.props.record.update({ [this.props.name]: colorIndex });
|
||||
}
|
||||
}
|
||||
|
||||
export const qbColorPickerField = {
|
||||
component: QbColorPickerField,
|
||||
supportedTypes: ["integer"],
|
||||
supportedOptions: [
|
||||
{
|
||||
label: _t("Mode"),
|
||||
name: "mode",
|
||||
type: "selection",
|
||||
choices: [
|
||||
{ label: "Foreground", value: "fg" },
|
||||
{ label: "Background", value: "bg" },
|
||||
],
|
||||
default: "bg",
|
||||
},
|
||||
],
|
||||
extractProps: ({ options, viewType }) => ({
|
||||
canToggle: viewType !== "list",
|
||||
mode: options.mode,
|
||||
}),
|
||||
};
|
||||
|
||||
registry.category("fields").add("qb_color_picker", qbColorPickerField);
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="quickboard.QbColorPickerField">
|
||||
<QbColorList canToggle="props.canToggle"
|
||||
colors="currentColorPalette()"
|
||||
forceExpanded="isExpanded"
|
||||
onColorSelected.bind="switchColor"
|
||||
selectedColor="props.record.data[props.name] || 0"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { SelectMenu } from "@web/core/select_menu/select_menu";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
import { fa_icons } from "./fa_icons";
|
||||
|
||||
export class QbIconPickerField extends Component {
|
||||
static template = "quickboard.QbIconPickerField";
|
||||
static components = {
|
||||
SelectMenu,
|
||||
};
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
async onSelectIcon(val) {
|
||||
this.props.record.update({ [this.props.name]: val });
|
||||
}
|
||||
|
||||
get icons() {
|
||||
return fa_icons;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const qbIconPickerField = {
|
||||
component: QbIconPickerField,
|
||||
supportedTypes: ["char"],
|
||||
};
|
||||
|
||||
registry.category("fields").add("qb_icon_picker", qbIconPickerField);
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="quickboard.QbIconPickerField">
|
||||
<SelectMenu
|
||||
choices="icons"
|
||||
value="props.record.data[props.name] || ''"
|
||||
onSelect.bind="this.onSelectIcon"
|
||||
required="false"
|
||||
searchable="true"
|
||||
togglerClass="'fw-normal'"
|
||||
class="'icon-select'">
|
||||
<t t-set-slot="choice" t-slot-scope="choice">
|
||||
<i t-attf-class="{{ 'fa ' + choice.data.value + ' me-1' }}"/><t t-esc="choice.data.label" />
|
||||
</t>
|
||||
</SelectMenu>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user