- Introduced DashboardOverviewPage and DashboardOverviewFlexColumns components to streamline the layout and management of dashboard sections, improving overall organization and usability. - Replaced individual collapsible sections in Finance, Inventory, Production, and Sales overviews with a unified structure, enhancing maintainability and consistency across the dashboard. - Added ActionsButton component for improved action handling and keyboard shortcuts, enhancing user interaction capabilities. - Updated styles in App.css to support new drag-and-drop functionality and layout features, ensuring a cohesive visual experience. - Refactored existing components to utilize the new structure, improving code clarity and reducing redundancy.
214 lines
5.9 KiB
JavaScript
214 lines
5.9 KiB
JavaScript
import { useCallback, useContext } from 'react'
|
|
import { ApiServerContext } from '../context/ApiServerContext'
|
|
|
|
export const mergeOrder = (saved = [], defaults = []) => {
|
|
const defaultSet = new Set(defaults)
|
|
const kept = saved.filter((key) => defaultSet.has(key))
|
|
const keptSet = new Set(kept)
|
|
const appended = defaults.filter((key) => !keptSet.has(key))
|
|
return [...kept, ...appended]
|
|
}
|
|
|
|
export const COLUMN_PROPORTION_STEP = 5
|
|
const MIN_COLUMN_PROPORTION = 5
|
|
|
|
export const normalizeColumnProportions = (count, saved) => {
|
|
if (count <= 0) return []
|
|
if (count === 1) return [100]
|
|
|
|
if (Array.isArray(saved) && saved.length === count) {
|
|
return saved.map((value) => {
|
|
const numeric = Number(value)
|
|
if (!Number.isFinite(numeric)) return MIN_COLUMN_PROPORTION
|
|
return Math.max(
|
|
MIN_COLUMN_PROPORTION,
|
|
Math.round(numeric / COLUMN_PROPORTION_STEP) * COLUMN_PROPORTION_STEP
|
|
)
|
|
})
|
|
}
|
|
|
|
const base =
|
|
Math.floor(100 / count / COLUMN_PROPORTION_STEP) * COLUMN_PROPORTION_STEP
|
|
const result = Array(count).fill(Math.max(MIN_COLUMN_PROPORTION, base))
|
|
let remainder = 100 - result.reduce((sum, value) => sum + value, 0)
|
|
let i = 0
|
|
while (remainder >= COLUMN_PROPORTION_STEP) {
|
|
result[i % count] += COLUMN_PROPORTION_STEP
|
|
remainder -= COLUMN_PROPORTION_STEP
|
|
i += 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
const asObject = (value) =>
|
|
value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
|
|
const asStringArray = (value) =>
|
|
Array.isArray(value) ? value.filter((key) => typeof key === 'string') : []
|
|
|
|
const collectKnownLeaves = (defaultOrder, defaultFlexColumns) => {
|
|
const leaves = new Set()
|
|
for (const key of defaultOrder) {
|
|
if (defaultFlexColumns[key]) {
|
|
for (const child of defaultFlexColumns[key]) {
|
|
leaves.add(child)
|
|
}
|
|
} else {
|
|
leaves.add(key)
|
|
}
|
|
}
|
|
return leaves
|
|
}
|
|
|
|
export const mergeLayout = (saved, defaultLayout) => {
|
|
const defaultOrder = asStringArray(defaultLayout?.sectionOrder)
|
|
const defaultFlexColumns = Object.fromEntries(
|
|
Object.entries(asObject(defaultLayout?.flexColumns)).map(([id, items]) => [
|
|
id,
|
|
asStringArray(items)
|
|
])
|
|
)
|
|
const savedOrder = asStringArray(saved?.sectionOrder)
|
|
const savedFlexColumns = Object.fromEntries(
|
|
Object.entries(asObject(saved?.flexColumns)).map(([id, items]) => [
|
|
id,
|
|
asStringArray(items)
|
|
])
|
|
)
|
|
const savedStats =
|
|
saved?.statsOrder &&
|
|
typeof saved.statsOrder === 'object' &&
|
|
!Array.isArray(saved.statsOrder)
|
|
? saved.statsOrder
|
|
: {}
|
|
const savedVisibility =
|
|
saved?.statsVisibility &&
|
|
typeof saved.statsVisibility === 'object' &&
|
|
!Array.isArray(saved.statsVisibility)
|
|
? saved.statsVisibility
|
|
: {}
|
|
|
|
const knownLeaves = collectKnownLeaves(defaultOrder, defaultFlexColumns)
|
|
const defaultGroupIds = new Set(Object.keys(defaultFlexColumns))
|
|
const userGroupIds = Object.keys(savedFlexColumns).filter(
|
|
(id) => !defaultGroupIds.has(id) && !knownLeaves.has(id)
|
|
)
|
|
const userGroupIdSet = new Set(userGroupIds)
|
|
const isGroupId = (key) => defaultGroupIds.has(key) || userGroupIdSet.has(key)
|
|
|
|
const placedLeaves = new Set()
|
|
const takeLeaves = (keys) => {
|
|
const result = []
|
|
for (const key of keys || []) {
|
|
if (knownLeaves.has(key) && !placedLeaves.has(key)) {
|
|
placedLeaves.add(key)
|
|
result.push(key)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
const sectionOrder = []
|
|
const flexColumns = {}
|
|
|
|
const addGroup = (id, childKeys) => {
|
|
flexColumns[id] = childKeys
|
|
if (!sectionOrder.includes(id)) {
|
|
sectionOrder.push(id)
|
|
}
|
|
}
|
|
|
|
const hasSavedStructure =
|
|
savedOrder.length > 0 || Object.keys(savedFlexColumns).length > 0
|
|
|
|
if (hasSavedStructure) {
|
|
for (const key of savedOrder) {
|
|
if (knownLeaves.has(key) && !placedLeaves.has(key)) {
|
|
placedLeaves.add(key)
|
|
sectionOrder.push(key)
|
|
} else if (isGroupId(key)) {
|
|
addGroup(
|
|
key,
|
|
takeLeaves(savedFlexColumns[key] ?? defaultFlexColumns[key] ?? [])
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const key of defaultOrder) {
|
|
if (knownLeaves.has(key) && !placedLeaves.has(key)) {
|
|
placedLeaves.add(key)
|
|
sectionOrder.push(key)
|
|
} else if (defaultGroupIds.has(key)) {
|
|
const remaining = takeLeaves(
|
|
mergeOrder(savedFlexColumns[key] || [], defaultFlexColumns[key])
|
|
)
|
|
if (sectionOrder.includes(key)) {
|
|
flexColumns[key] = [...(flexColumns[key] || []), ...remaining]
|
|
} else if (remaining.length > 0 || !hasSavedStructure) {
|
|
addGroup(key, remaining)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const id of userGroupIds) {
|
|
if (!sectionOrder.includes(id)) {
|
|
addGroup(id, takeLeaves(savedFlexColumns[id]))
|
|
}
|
|
}
|
|
|
|
for (const leaf of knownLeaves) {
|
|
if (!placedLeaves.has(leaf)) {
|
|
placedLeaves.add(leaf)
|
|
sectionOrder.push(leaf)
|
|
}
|
|
}
|
|
|
|
const flexColumnProportions = Object.fromEntries(
|
|
Object.entries(asObject(saved?.flexColumnProportions)).map(
|
|
([id, value]) => [
|
|
id,
|
|
Array.isArray(value)
|
|
? value.filter(
|
|
(item) => typeof item === 'number' && Number.isFinite(item)
|
|
)
|
|
: []
|
|
]
|
|
)
|
|
)
|
|
|
|
return {
|
|
sectionOrder,
|
|
flexColumns,
|
|
statsOrder: savedStats,
|
|
statsVisibility: savedVisibility,
|
|
sectionHeights: asObject(saved?.sectionHeights),
|
|
flexColumnProportions
|
|
}
|
|
}
|
|
|
|
const usePageLayout = (
|
|
pageName,
|
|
defaultLayout = {
|
|
sectionOrder: [],
|
|
flexColumns: {},
|
|
statsOrder: {},
|
|
statsVisibility: {}
|
|
}
|
|
) => {
|
|
const { userSettings, updateUserSettings } = useContext(ApiServerContext)
|
|
const saved = userSettings.pageLayout?.[pageName]
|
|
const layout = mergeLayout(saved, defaultLayout)
|
|
|
|
const updatePageLayout = useCallback(
|
|
(value) => {
|
|
updateUserSettings('pageLayout', pageName, value)
|
|
},
|
|
[pageName, updateUserSettings]
|
|
)
|
|
|
|
return [layout, updatePageLayout]
|
|
}
|
|
|
|
export default usePageLayout
|