import Icon, { SyncOutlined } from "@ant-design/icons"; import { useMutation, useQuery } from "@apollo/client/react"; import { Button, Dropdown, Space } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; import { useEffect, useMemo, useState } from "react"; import { Responsive, WidthProvider } from "react-grid-layout/legacy"; import { useTranslation } from "react-i18next"; import { MdClose } from "react-icons/md"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { QUERY_USER_DASHBOARD_LAYOUT, UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; import { QUERY_DASHBOARD_BODYSHOP } from "../../graphql/bodyshop.queries"; import { selectCurrentUser } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import { GenerateDashboardData } from "./dashboard-grid.utils"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import componentList from "./componentList.js"; import createDashboardQuery from "./createDashboardQuery.js"; import "./dashboard-grid.styles.scss"; const ResponsiveReactGridLayout = WidthProvider(Responsive); const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser }); const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export function DashboardGridComponent({ currentUser }) { const { t } = useTranslation(); const notification = useNotification(); // Constants for layout defaults const DEFAULT_COLS = 12; const DEFAULT_Y_POSITION = 1000; const GRID_BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }; const GRID_COLS = { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }; // Fetch dashboard layout data const { data: layoutData } = useQuery(QUERY_USER_DASHBOARD_LAYOUT, { variables: { email: currentUser.email }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", skip: !currentUser?.email }); // Fetch minimal bodyshop data for components const { loading, error, data: bodyshopData } = useQuery(QUERY_DASHBOARD_BODYSHOP, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); // Memoize layout state initialization const initialState = useMemo(() => { const persisted = layoutData?.users?.[0]?.dashboardlayout; if (persisted) { const { items = [], layout = [], layouts = {}, cols = DEFAULT_COLS } = persisted; return { items: Array.isArray(items) ? items : [], layout: Array.isArray(layout) ? layout : [], layouts: typeof layouts === "object" && !Array.isArray(layouts) ? layouts : {}, cols }; } return { items: [], layout: [], layouts: {}, cols: DEFAULT_COLS }; }, [layoutData]); const [state, setState] = useState(initialState); // Update state when layout data changes useEffect(() => { if (layoutData?.users?.[0]?.dashboardlayout) { const { items = [], layout = [], layouts = {}, cols = DEFAULT_COLS } = layoutData.users[0].dashboardlayout; setState({ items: Array.isArray(items) ? items : [], layout: Array.isArray(layout) ? layout : [], layouts: typeof layouts === "object" && !Array.isArray(layouts) ? layouts : {}, cols }); } }, [layoutData]); // Get bodyshop data for components const bodyshop = bodyshopData?.dashboard_bodyshops?.[0]; // DRY helper function to update layout in database and cache const updateLayoutAndCache = async (updatedLayout, errorContext = "updating layout") => { try { const { data: result } = await updateLayout({ variables: { email: currentUser.email, layout: updatedLayout } }); const { errors = [] } = result?.update_users?.returning?.[0] || {}; if (errors.length) { const errorMessages = errors.map(({ message }) => message || String(error)); notification.error({ title: t("dashboard.errors.updatinglayout", { message: errorMessages.join("; ") }) }); return false; } return true; } catch (err) { console.error(`Dashboard ${errorContext} failed`, err); notification.error({ title: t("dashboard.errors.updatinglayout", { message: err?.message || String(err) }) }); return false; } }; // Memoize the query document so Apollo doesn't treat each render as a brand-new query causing continuous re-fetches const dashboardQueryDoc = useMemo(() => createDashboardQuery(state.items), [state.items]); const { loading: dashboardLoading, error: dashboardError, data: dashboardQueryData, refetch } = useQuery(dashboardQueryDoc, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); const dashboardData = useMemo(() => GenerateDashboardData(dashboardQueryData), [dashboardQueryData]); // Memoize existing layout keys to prevent unnecessary recalculations const existingLayoutKeys = useMemo(() => state.items.map(({ i }) => i), [state.items]); // Memoize menu items to prevent unnecessary recalculations const menuItems = useMemo( () => Object.entries(componentList).map(([key, { label }]) => ({ key, label, value: key, disabled: existingLayoutKeys.includes(key) })), [existingLayoutKeys] ); if (loading || dashboardLoading) return ; if (error || dashboardError) return ; const handleLayoutChange = async (layout, layouts) => { logImEXEvent("dashboard_change_layout"); setState((prev) => ({ ...prev, layout, layouts })); await updateLayoutAndCache({ ...state, layout, layouts }, "layout change"); }; const handleRemoveComponent = async (key) => { logImEXEvent("dashboard_remove_component", { name: key }); const updatedState = { ...state, items: state.items.filter((item) => item.i !== key) }; setState(updatedState); await updateLayoutAndCache(updatedState, "component removal"); }; const handleAddComponent = async ({ key }) => { logImEXEvent("dashboard_add_component", { key }); const { minW = 1, minH = 1, w: baseW = 2, h: baseH = 2 } = componentList[key] || {}; const nextItems = [ ...state.items, { i: key, x: (state.items.length * 2) % (state.cols || DEFAULT_COLS), y: DEFAULT_Y_POSITION, w: Math.max(baseW, minW), h: Math.max(baseH, minH) } ]; const updatedState = { ...state, items: nextItems }; setState(updatedState); await updateLayoutAndCache(updatedState, "component addition"); }; const menu = { items: menuItems, onClick: handleAddComponent }; return (
} /> {state.items.map((item) => { const { component: TheComponent, minW = 1, minH = 1, w: specW, h: specH } = componentList[item.i] || {}; const safeItem = { ...item, w: Math.max(item.w || specW || minW, minW), h: Math.max(item.h || specH || minH, minH) }; return (
handleRemoveComponent(safeItem.i)} /> {TheComponent && }
); })}
); } export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent);