Source

client/ui/data/dashboard.js

/** 
 * 
 * The **Dashboard** derives from [DataComponent](kiss.ui.DataComponent.html).
 * 
 * A dashboard is a group of charts that are displayed together in a single screen.
 * 
 * @param {object} config
 * @param {string} config.name - The dashboard title. We use the "name" property instead of "title" to be consistent with all view names.
 * @param {Collection} config.collection - The data source collection
 * @param {object} [config.record] - Record to persist the view configuration into the db
 * @param {boolean} [config.canEditView] - true if the user can edit the view setup. Default is true.
 * @param {boolean} [config.useCDN] - Set to false to use the local version of ChartJS. Default is true.
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-dashboard class="a-dashboard">
 *      <div class="dashboard-header">
 *          <div class="dashboard-title">
 *              <!-- Dashboard title -->
 *          </div>
 *          <div class="dashboard-toolbar">
 *              <!-- Dashboard toolbar items -->
 *          </div>
 *      </div>
 *      <div class="dashboard-container">
 *          <!-- Embedded charts -->
 *      </div>
 * </a-dashboard>
 * ```
 */
kiss.ui.Dashboard = class Dashboard extends kiss.ui.DataComponent {
	/**
	 * Its a Custom Web Component. Do not use the constructor directly with the **new** keyword.
	 * Instead, use one of the following methods:
	 * 
	 * Create the Web Component and call its **init** method:
	 * ```
	 * const myDashboard = document.createElement("a-dashboard").init(config)
	 * ```
	 * 
	 * Or use the shorthand for it:
	 * ```
	 * const myDashboard = createDashboard({
	 *   id: "my-dashboard",
	 *   collection: kiss.app.collections["opportunity"]
	 * })
	 * 
	 * myDashboard.render()
	 * ```
	 */
	constructor() {
		super()
	}

	/**
	 * Generates a Dashboard from a JSON config
	 * 
	 * @ignore
	 * @param {object} config - JSON config
	 * @returns {HTMLElement}
	 */
	init(config) {
		// This component must be resized with its parent container
		config.autoSize = true

		// Init the parent DataComponent
		super.init(config)

		// Options
		this.color = config.color || "#00aaee"
		this.canEditView = (config.canEditView === false) ? false : true
		this.useCDN = (config.useCDN === false) ? false : true
		this.CHART_TOP_SPACE = 13 // Busy space at the top of each chart, in rem

		// Build dashboard skeletton markup
		let id = this.id
		this.innerHTML = /*html*/
            `<div class="dashboard-container">
                <div class="dashboard-header">
                    <div class="dashboard-title">
                        ${this.name || ""}
                    </div>
                    <div style="flex: 1"></div>
                    <div id="dashboard-toolbar:${id}" class="dashboard-toolbar">
                        <div id="setup:${id}"></div>
                    </div>
                </div>
                <div id="dashboard-groups:${id}" class="dashboard-groups"></div>
            </div>`.removeExtraSpaces()

		// Set dashboard components
		this.header = this.querySelector(".dashboard-header")
		this.headerTitle = this.querySelector(".dashboard-title")
		this.toolbar = this.querySelector(".dashboard-toolbar")
		this.dashboardGroups = this.querySelector(".dashboard-groups")

		this._initClickEvents()
		this._initSubscriptions()

		return this
	}

	/**
	 * Load the dashboard
	 * 
	 * @ignore
	 */
	async load() {
		await this.collection.find()
		this._render()
	}

	/**
	 * Render the dashboard
	 * 
	 * @private
	 * @ignore
	 * @returns this
	 */
	_render() {
		let isFirstGroup = false
		this.dashboardGroups.innerHTML = ""
		let items = this.record.config || []

		// Init an empty group if no items are found
		if (items.length == 0) {
			isFirstGroup = true
			const groupId = "dashboard-group:" + uid()
			const viewId = uid()

			items.push({
				id: groupId,
				class: "dashboard-group",
				height: `calc(calc(calc(100vh - ${this.CHART_TOP_SPACE}rem) * 0.5) - 1px)`, // 50% of the container's available height
				items: [{
					id: viewId,
					type: "chartview",
					chartType: "bar",
					dashboard: true,
					name: txtTitleCase("untitled"),
					color: this.color,
					collection: this.collection.clone("memory"),
					useCDN: this.useCDN
				}]
			})
		}

		// Hide the setup button of each chart if the user cannot update the dashboard
		if (!this.canEditView) {
			items.forEach(group => {
				group.items.forEach(item => {
					if (item.type == "chartview") {
						item.showActions = false
					}
				})
			})
		}

		// Constrain CDN parameter of each chart to reflect the dashboard
		items.forEach(group => {
			group.items.forEach(item => {
				if (item.type == "chartview") {
					item.useCDN = this.useCDN
				}
			})
		})

		// Inject the new group
		this.dashboardGroups.appendChild(
			createBlock({
				id: "dashboard-groups-container:" + this.id,
				width: "100%",
				flex: 1,
				overflow: "auto",
				items
			}
			).render())

		// Add buttons to manage the dashboard (= add a chart to a group)
		this._manageDashboardButtons()

		// Enable drag and drop
		this._enableDragAndDrop()

		// Adjust buttons color
		this.setColor(this.color)

		// Show the quick tips for the 1st created group
		if (isFirstGroup) this._showQuickTips()

		return this
	}

	/**
	 * Set the dashboard title
	 * 
	 * @param {string} newTitle 
	 */
	setTitle(newTitle) {
		this.headerTitle.innerHTML = newTitle
	}    

	/**
	 * Set the color of the dashboard (mainly buttons at the moment)
	 * 
	 * @param {string} color 
	 * @returns this
	 */
	setColor(color) {
		const charts = this._getCharts()
		for (const chart of charts) {
			chart.setColor(color)
		}
		return this
	}

	/**
	 * Add a group of charts to the dashboard
	 * 
	 * @async
	 * @returns this
	 */
	async addGroup() {
		const newGroupId = uid()
		const newGroup = createBlock({
			id: "dashboard-group:" + newGroupId,
			class: "dashboard-group",
			items: []
		}).render()

		const groupsContainer = this._getGroupsContainer()
		groupsContainer.appendChild(newGroup)
		await this.saveConfig()

		const escapedId = CSS.escape("dashboard-group:" + newGroupId)
		kiss.tools.waitForElement("#" + escapedId).then(() => {
			$("dashboard-group:" + newGroupId).scrollIntoView({
				behavior: "smooth"
			})
		})

		this.addChart("dashboard-group:" + newGroupId)
		this._manageDashboardButtons()
		this._enableDragAndDrop()

		return this
	}

	/**
	 * Delete a group of charts from the dashboard
	 * 
	 * @param {string} groupId - The group ID to delete
	 */
	async deleteGroup(groupId) {
		createDialog({
			title: txtTitleCase("delete this row"),
			icon: "fas fa-trash",
			headerBackgroundColor: "var(--red)",
			message: txtTitleCase("#delete row"),
			action: async () => {
				const groupsContainer = this._getGroupsContainer()
				groupsContainer.deleteItem(groupId)
				await this.saveConfig()
				this._render()
			}
		})
	}

	/**
	 * Move a group up in the dashboard
	 * 
	 * @param {string} groupId 
	 * @returns this
	 */
	moveGroupUp(groupId) {
		const group = $(groupId)
		if (!group) return
    
		const previousGroup = group.previousElementSibling
		if (!previousGroup || !previousGroup.classList.contains("dashboard-group")) return // Already at the top
    
		previousGroup.before(group)
		this.saveConfig()
		return this
	}

	/**
	 * Move a group down in the dashboard
	 * 
	 * @param {string} groupId 
	 * @returns this
	 */
	moveGroupDown(groupId) {
		const group = $(groupId)
		if (!group) return
    
		const nextGroup = group.nextElementSibling
		if (!nextGroup || !nextGroup.classList.contains("dashboard-group")) return // Already at the bottom
    
		nextGroup.after(group)
		this.saveConfig()
		return this
	}

	/**
	 * Checks whether a group can be moved up or down.
	 *
	 * @param {string} groupId - The ID of the group to check.
	 * @returns {object} - An object with `up` and `down` properties indicating the possible moves.
	 */
	checkGroupMove(groupId) {
		const group = $(groupId)
		if (!group) return false

		const previousGroup = group.previousElementSibling
		const nextGroup = group.nextElementSibling

		return {
			up: !!(previousGroup && previousGroup.classList.contains("dashboard-group")),
			down: !!(nextGroup && nextGroup.classList.contains("dashboard-group"))
		}
	}

	/**
	 * Get the groups of the dashboard
	 * 
	 * @returns {HTMLElement[]} - The groups of charts (1 group = 1 row in the dashboard)
	 */
	getGroups() {
		return Array.from(this.querySelectorAll(".dashboard-group"))
	}

	/**
	 * Get the number of groups in the dashboard
	 * 
	 * @returns {number} - The number of groups
	 */
	getGroupCount() {
		return this.getGroups().length
	}

	/**
	 * Get the charts of a group
	 * 
	 * @param {string} groupId 
	 * @returns {HTMLElement[]} - The charts of the group
	 */
	getGroupCharts(groupId) {
		return Array.from($(groupId).querySelectorAll(".a-chartview"))
	}

	/**
	 * Get a chart from its ID
	 * 
	 * @param {string} chartId
	 * @returns {HTMLElement} - The chart element
	 */
	getChart(chartId) {
		const dashboardGroups = $("dashboard-groups:" + this.id)
		let chart = null

		for (const dashboardGroup of dashboardGroups.firstChild.items) {
			chart = dashboardGroup.items.find(item => item.id == chartId)
			if (chart) break
		}
		return chart
	}

	/**
	 * Add a chart to a group of charts
	 * 
	 * @async
	 * @param {string} groupId 
	 * @returns this
	 */
	async addChart(groupId) {
		const viewId = uid()
		const dashboardGroup = $(groupId)

		dashboardGroup.insertItem({
			id: viewId,
			type: "chartview",
			chartType: "bar",
			dashboard: true,
			name: txtTitleCase("untitled"),
			color: this.color,
			collection: this.collection.clone("memory"),
			useCDN: this.useCDN
		}, dashboardGroup.items.length - 1)

		this._manageDashboardButtons()
		this._enableDragAndDrop()
        
		await this.saveConfig()
		return this
	}

	/**
	 * Move a chart to another position in the dashboard
	 * 
	 * @param {string} chartId 
	 * @param {string} targetChartId
	 * @returns this
	 */
	moveChart(chartId, targetChartId) {
		const sourceChart = $(chartId)
		const targetChart = $(targetChartId)
		const sourceGroup = $(chartId).closest(".dashboard-group")
		const targetGroup = $(targetChartId).closest(".dashboard-group")
        
		// Case 1: same group
		if (sourceGroup.id == targetGroup.id) {
			const sourceIndex = this.getChartIndexInGroup(chartId)
			const targetIndex = this.getChartIndexInGroup(targetChartId)

			if (sourceIndex < targetIndex) {
				targetChart.after(sourceChart)
			} else {
				targetChart.before(sourceChart)
			}

			this.saveConfig()
			return this
		}

		// Case 2: different groups
		const chartsInTargetGroup = targetGroup.querySelectorAll(".a-chartview").length

		if (chartsInTargetGroup >= 5) {
			return createNotification(txtTitleCase("this row is full"))
		}

		const targetIndex = [...targetGroup.children].indexOf(targetChart)

		if (targetIndex !== -1) {
			targetChart.before(sourceChart)
		} else {
			targetGroup.appendChild(sourceChart)
		}

		// Recompute the size of the charts
		sourceChart.updateSize()
		this._updateGroupLayout(sourceGroup)

		// If the source group is empty, remove it
		if (sourceGroup.children.length == 1) {
			sourceGroup.deepDelete()
		}

		this.saveConfig()
		return this
	}    

	/**
	 * Remove a chart from the dashboard
	 * 
	 * @async
	 * @param {string} chartId - The chart ID to remove
	 * @returns this
	 */
	async deleteChart(chartId) {
		const dashboardGroup = $(chartId).closest(".dashboard-group")
		dashboardGroup.deleteItem(chartId)
		this._manageDashboardButtons()
		await this.saveConfig()
		return this
	}

	/**
	 * Get the index of a chart in a group
	 * 
	 * @param {string} chartId 
	 * @returns {number} - The chart index in the group
	 */
	getChartIndexInGroup(chartId) {
		const sourceChart = $(chartId)
		const sourceGroup = $(chartId).closest(".dashboard-group")
		return [...sourceGroup.children].indexOf(sourceChart)
	}    

	/**
	 * Get the current dashboard configuration
	 * 
	 * @returns {object[]} - The dashboard configuration, as an array of groups of charts
	 */
	getConfig() {
		const dashboardGroupsContainer = $("dashboard-groups:" + this.id)
		let dashboardGroups = []

		const dashboardGroupElements = Array.from(dashboardGroupsContainer.firstChild.children)
		dashboardGroupElements.forEach(dashboardGroup => {
			let group = {
				id: dashboardGroup.id,
				height: dashboardGroup.config.height,
				class: "dashboard-group"
			}

			let items = []
			const groupItems = [...dashboardGroup.children]
			groupItems.forEach(item => {
				if (item.type == "chartview") {
					let newItem = {
						type: "chartview",
						dashboard: true,
						id: item.id,
						name: item.name,
						modelId: item.model.id,
						chartType: item.chartType,
						sort: item.sort,
						filter: item.filter,
						isTimeSeries: item.isTimeSeries,
						categoryField: item.categoryField,
						timeField: item.timeField,
						timePeriod: item.timePeriod,
						operationType: item.operationType,
						summaryOperation: item.summaryOperation,
						valueField: item.valueField,
						startAtZero: item.startAtZero,
						showLegend: item.showLegend,
						legendPosition: item.legendPosition,
						showValues: item.showValues,
						showLabels: item.showLabels,
						centerLabels: item.centerLabels,
						labelColor: item.labelColor,
						precision: item.precision,
						unit: item.unit,
						useCDN: this.useCDN
					}
					items.push(newItem)
				}
			})
			group.items = items
			dashboardGroups.push(group)
		})
		return dashboardGroups
	}
    
	/**
	 * Save the current dashboard configuration
	 * 
	 * @async
	 * @returns this
	 */
	async saveConfig() {
		const config = this.getConfig()
		await this.record.update({
			config
		})
		return this
	}
    
	/**
	 * Update the layout of the dashboard
	 * 
	 * @returns this
	 */
	updateLayout() {
		return this
	}

	/**
	 * Print the dashboard - Work in progress
	 */
	async print() {
		// const charts = document.querySelectorAll("a-chart")
		// charts.forEach((chartElement) => {
		//     const chartInstance = chartElement.chart
		//     if (chartInstance) {
		//         const img = document.createElement("img")
		//         img.src = chartInstance.canvas.toDataURL()
		//         img.style.display = "block"
		//         img.style.width = "100%"
		//         img.style.maxWidth = "fit-content"
		//         img.style.height = "100%"
		//         img.style.maxHeight = "fit-content"
		//         chartElement.replaceChild(img, chartInstance.canvas)
		//     }
		// })
		// setTimeout(() => window.print(), 500)
	}

	/**
	 * Initialize click events
	 * 
	 * @private
	 * @ignore
	 */
	_initClickEvents() {
		if (!this.canEditView) return

		// this.onclick = (e) => {
		//     const target = e.target
		//     log("---------")
		//     log(target)
		//     const chartButton = target.closest(".a-button")

		//     if (!chartButton && target.closest(".dashboard-group")) {
		//         const groupId = target.closest(".dashboard-group").id
		//         this._showGroupSetup(groupId, e)
		//     }
		// }
	}

	/**
	 * Initialize subscriptions to PubSub
	 * 
	 * @private
	 * @ignore
	 * @returns this
	 */
	_initSubscriptions() {
		// React to database mutations
		this.subscriptions = this.subscriptions.concat([
			subscribe("EVT_DB_UPDATE:VIEW", (msgData) => {
				if (msgData.id != this.id) return
				this._updateTitle(msgData)

				// Update the component ACL
				if (msgData.data.hasOwnProperty("authenticatedCanRead") && this.record) this.record.authenticatedCanRead = msgData.data.authenticatedCanRead
				if (msgData.data.hasOwnProperty("accessRead") && this.record) this.record.accessRead = msgData.data.accessRead
				if (msgData.data.hasOwnProperty("authenticatedCanReadDetails") && this.record) this.record.authenticatedCanReadDetails = msgData.data.authenticatedCanReadDetails
				if (msgData.data.hasOwnProperty("accessReadDetails") && this.record) this.record.accessReadDetails = msgData.data.accessReadDetails
			}),

			// React to events coming from individual charts
			subscribe("EVT_DASHBOARD_SETUP", (chartId) => {
				if (!this.isConnected) return

				const chartIds = this._getChartIds()
				if (chartIds.length == 0) return

				if (chartIds.includes(chartId)) {
					this.saveConfig()
				}
			}),

			// React when one of the charts is deleted
			subscribe("EVT_DASHBOARD_CHART_DELETED", (chartId) => {
				if (!this.isConnected) return

				const chartIds = this._getChartIds()
				if (chartIds.length == 0) return

				if (chartIds.includes(chartId)) {
					this.deleteChart(chartId)
				}
			}),

			// React when one of the charts is clicked
			subscribe("EVT_CHART_CLICKED", (msg) => {
				if (!this.isConnected) return

				const chartIds = this._getChartIds()
				if (chartIds.length == 0) return

				if (chartIds.includes(msg.chartId)) {
					const group = this._getChartGroup(msg.chartId)
					this._showGroupSetup(group.id, msg.event)
				}
			})
		])

		return this
	}

	/**
	 * Initialize the charts drag and drop
	 * 
	 * @private
	 * @ignore
	 */
	_enableDragAndDrop() {
		if (!this.canEditView) return
        
		// Drag and drop helpers
		const getCharts = () => this.querySelectorAll(".a-chartview")
		const resetCharts = () => getCharts().forEach(chart => chart.classList.remove("chartview-highlight"))
		const resetChart = (chart) => chart.classList.remove("chartview-highlight")
		const highlightChart = (chart) => chart.classList.add("chartview-highlight")

		// Drag and drop events
		const dndEvents = {
			ondragstart: (event) => {
				const chart = event.target.closest(".a-chartview")
				kiss.context.chartId = chart.id
			},

			ondragover: (event) => {
				event.preventDefault()
				resetCharts()
				const chart = event.target.closest(".a-chartview")
				if (!chart) return
				highlightChart(chart)
			},

			ondrop: (event) => {
				event.preventDefault()
				resetCharts()
				const chart = event.target.closest(".a-chartview")
				if (!chart) return

				const chartId = kiss.context.chartId
				this.moveChart(chartId, chart.id)
			},

			ondragleave: (event) => {
				const chart = event.target.closest(".a-chartview")
				if (!chart) return
				resetChart(chart)
			}
		}

		this.querySelectorAll(".a-chartview").forEach(chart => {
			chart.draggable = true
			chart.ondragstart = dndEvents.ondragstart
			Object.assign(chart, dndEvents)
		})
	}

	/**
	 * Show the menu to setup a group of charts
	 * 
	 * @param {string} groupId 
	 * @param {object} event - The click event
	 */
	_showGroupSetup(groupId, event) {
		const _this = this
		const group = $(groupId)
		const groupMoves = this.checkGroupMove(groupId)
		const itemHeight = "3rem"

		createMenu({
			defaultConfig: {
				height: "1rem"
			},
			items: [
				// SIZE
				`<h3>${txtTitleCase("row height")}</h3>`,
				"-",
				{
					text: "20%",
					icon: "fas fa-circle",
					iconSize: "0.2rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.2) - 1px)`
						await _this.saveConfig()
					}
				},                
				{
					text: "25%",
					icon: "fas fa-circle",
					iconSize: "0.5rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.25) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "30%",
					icon: "fas fa-circle",
					iconSize: "0.60rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.29) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "33%",
					icon: "fas fa-circle",
					iconSize: "0.66rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.33) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "40%",
					icon: "fas fa-circle",
					iconSize: "0.8rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.40) - 1px)`
						await _this.saveConfig()
					}
				},                
				{
					text: "50%",
					icon: "fas fa-circle",
					iconSize: "1rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.5) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "60%",
					icon: "fas fa-circle",
					iconSize: "1.2rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.60) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "66%",
					icon: "fas fa-circle",
					iconSize: "1.3rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.66) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "70%",
					icon: "fas fa-circle",
					iconSize: "1.4rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.70) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "75%",
					icon: "fas fa-circle",
					iconSize: "1.5rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.75) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "80%",
					icon: "fas fa-circle",
					iconSize: "1.6rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.80) - 1px)`
						await _this.saveConfig()
					}
				},
				{
					text: "100%",
					icon: "fas fa-circle",
					iconSize: "2rem",
					height: itemHeight,
					action: async function() {
						group.config.height = group.style.height = `calc(100vh - ${_this.CHART_TOP_SPACE}rem)`
						await _this.saveConfig()
					}
				},
				"-",
				// MOVE UP
				{
					hidden: groupMoves.up ? false : true,
					text: txtTitleCase("move up"),
					icon: "fas fa-arrow-up",
					action: async () => this.moveGroupUp(groupId)
				},
				// MOVE DOWN
				{
					hidden: groupMoves.down ? false : true,
					text: txtTitleCase("move down"),
					icon: "fas fa-arrow-down",
					action: async () => this.moveGroupDown(groupId)
				},
				// ADD A ROW OF CHARTS
				{
					text: txtTitleCase("add row"),
					icon: "fas fa-plus",
					action: async () => this.addGroup()
				},
				// DELETE
				"-",
				{
					text: txtTitleCase("delete this row"),
					icon: "fas fa-trash",
					iconColor: "var(--red)",
					action: async () => this.deleteGroup(groupId)
				}
			]
		}).render().showAt(event.clientX - 20, event.clientY - 20)
	}

	/**
	 * Show quick tips for the 1st created chart
	 * 
	 * @private
	 * @ignore
	 */
	_showQuickTips() {
		const dashboardGroup = this.getGroups()[0]
		setTimeout(() => {
			const buttonSetup = $("actions:" + dashboardGroup.items[0].id)
			const buttonAdd = dashboardGroup.items[dashboardGroup.items.length - 1]

			kiss.tools.highlightElements([
				{
					element: buttonSetup,
					text: txtTitleCase("#help setup chart"),
					position: "left"
				},
				{
					element: buttonAdd,
					text: txtTitleCase("#help add chart"),
					position: "left"
				}
			])
		}, 1000)
	}

	/**
	 * Manage the dashboard buttons
	 * 
	 * @private
	 * @ignore
	 */
	_manageDashboardButtons() {
		if (!this.canEditView) return

		setTimeout(() => {
			const dashboardGroupsContainer = $("dashboard-groups:" + this.id)
			const dashboardGroupElements = Array.from(dashboardGroupsContainer.firstChild.children)

			// Remove all buttons from the dashboard
			dashboardGroupElements.forEach(dashboardGroup => {
				dashboardGroup.items.forEach((item, index) => {
					if (item.type == "button") {
						dashboardGroup.deleteItem(item.id)
					}
				})
			})

			// Add a button to add a chart to each group
			dashboardGroupElements.forEach(dashboardGroup => {
				if (dashboardGroup.items.length > 3) return

				let buttonConfig = {
					type: "button",
					icon: "fas fa-plus",
					width: "3.2rem",
					height: "3.2rem",
					margin: "0 0 0 -2.5rem",
					tip: txtTitleCase("add chart"),
					action: () => this.addChart(dashboardGroup.id)
				}

				dashboardGroup.addItem(buttonConfig)
			})
		}, 0)
	}
 
	/**
	 * Update the dashboard title
	 * 
	 * @private
	 * @ignore
	 */
	_updateTitle(msgData) {
		if (!this.record) return
		if (msgData.id == this.record.id && msgData.data.name) {
			this.setTitle(msgData.data.name)
		}
	}

	/**
	 * Get the charts of the dashboard
	 * 
	 * @returns {HTMLElement[]} - The charts
	 */
	_getCharts() {
		return Array.from(this.querySelectorAll(".a-chartview"))
	}

	/**
	 * Get the chart IDs from the dashboard
	 * 
	 * @private
	 * @ignore
	 * @returns {string[]} - The chart IDs
	 */
	_getChartIds() {
		return this._getCharts().map(chart => chart.id)
	}

	/**
	 * Get the group of a chart
	 * 
	 * @private
	 * @ignore
	 * @param {string} chartId 
	 * @returns {HTMLElement} - The group element
	 */
	_getChartGroup(chartId) {
		return $(chartId).closest(".dashboard-group")
	}

	/**
	 * Get the index of a group in the dashboard
	 * 
	 * @private
	 * @ignore
	 * @param {string} groupId 
	 * @returns {number} - The group index, or -1 if not found
	 */
	_getGroupIndex(groupId) {
		const dashboardGroups = $("dashboard-groups:" + this.id)
		return dashboardGroups.firstChild.items.findIndex(group => group.id == groupId)
	}

	/**
	 * Get the groups container
	 * 
	 * @private
	 * @ignore
	 * @returns {HTMLElement} - The groups container
	 */
	_getGroupsContainer() {
		return $("dashboard-groups-container:" + this.id)
	}

	/**
	 * Refresh the sizes of the charts in a group
	 * 
	 * @private
	 * @ignore
	 * @param {HTMLElement} group - The group element
	 */
	_updateGroupLayout(group) {
		group.querySelectorAll(".a-chartview").forEach(chart => chart.updateSize())
	}    
}

// Create a Custom Element and add a shortcut to create it
customElements.define("a-dashboard", kiss.ui.Dashboard)

/**
 * Shorthand to create a new Dashboard. See [kiss.ui.Dashboard](kiss.ui.Dashboard.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createDashboard = (config) => document.createElement("a-dashboard").init(config)