2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-22 01:06:50 +00:00

[UI] Calendar tooltips (#11980)

* Allow tooltips for calendar entries

* Render tooltip for purchase order calendar

* Calendar tooltip renderers

* Add tooltip for build calendar

* Add key to filter list

* Display responsible owner
This commit is contained in:
Oliver
2026-05-22 00:17:11 +10:00
committed by GitHub
parent 3f3433685d
commit e392348654
8 changed files with 166 additions and 14 deletions
@@ -1,4 +1,8 @@
import type { CalendarOptions, DatesSetArg } from '@fullcalendar/core'; import type {
CalendarOptions,
DatesSetArg,
EventContentArg
} from '@fullcalendar/core';
import allLocales from '@fullcalendar/core/locales-all'; import allLocales from '@fullcalendar/core/locales-all';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
@@ -15,6 +19,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
HoverCard,
Indicator, Indicator,
LoadingOverlay, LoadingOverlay,
Popover, Popover,
@@ -29,7 +34,13 @@ import {
IconDownload, IconDownload,
IconFilter IconFilter
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useState
} from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { import {
defaultLocale, defaultLocale,
@@ -44,6 +55,7 @@ export interface InvenTreeCalendarProps extends CalendarOptions {
enableDownload?: boolean; enableDownload?: boolean;
enableFilters?: boolean; enableFilters?: boolean;
enableSearch?: boolean; enableSearch?: boolean;
eventTooltipContent?: (event: EventContentArg) => ReactNode;
filters?: TableFilter[]; filters?: TableFilter[];
isLoading?: boolean; isLoading?: boolean;
state: CalendarState; state: CalendarState;
@@ -53,6 +65,7 @@ export default function Calendar({
enableDownload, enableDownload,
enableFilters = false, enableFilters = false,
enableSearch, enableSearch,
eventTooltipContent,
isLoading, isLoading,
filters, filters,
state, state,
@@ -112,6 +125,36 @@ export default function Calendar({
[calendarProps.datesSet, state.ref, state.setMonthName] [calendarProps.datesSet, state.ref, state.setMonthName]
); );
const wrappedEventContent = useCallback(
(arg: EventContentArg) => {
const inner =
typeof calendarProps.eventContent === 'function'
? calendarProps.eventContent(arg, null)
: (calendarProps.eventContent ?? null);
if (!eventTooltipContent) return inner;
const tooltip = eventTooltipContent(arg);
if (!tooltip) return inner;
return (
<HoverCard
openDelay={300}
closeDelay={100}
shadow='md'
position='top-start'
>
<HoverCard.Target>
<div style={{ width: '100%', overflow: 'hidden' }}>{inner}</div>
</HoverCard.Target>
<HoverCard.Dropdown>{tooltip}</HoverCard.Dropdown>
</HoverCard>
);
},
[calendarProps.eventContent, eventTooltipContent]
);
return ( return (
<> <>
{state.exportModal.modal} {state.exportModal.modal}
@@ -217,6 +260,7 @@ export default function Calendar({
footerToolbar={false} footerToolbar={false}
{...calendarProps} {...calendarProps}
datesSet={datesSet} datesSet={datesSet}
eventContent={wrappedEventContent}
/> />
</Box> </Box>
</Stack> </Stack>
@@ -46,12 +46,14 @@ export default function OrderCalendar({
model, model,
role, role,
params, params,
filters filters,
tooltip
}: { }: {
model: ModelType; model: ModelType;
role: UserRoles; role: UserRoles;
params: Record<string, any>; params: Record<string, any>;
filters?: TableFilter[]; filters?: TableFilter[];
tooltip?: (event: EventContentArg) => React.ReactNode;
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
@@ -106,6 +108,7 @@ export default function OrderCalendar({
const end: string = order.target_date || start; const end: string = order.target_date || start;
return { return {
order: order,
id: order.pk, id: order.pk,
title: order.reference, title: order.reference,
description: order.description, description: order.description,
@@ -209,6 +212,7 @@ export default function OrderCalendar({
state={calendarState} state={calendarState}
filters={calendarFilters} filters={calendarFilters}
editable={true} editable={true}
eventTooltipContent={tooltip}
eventContent={renderOrder} eventContent={renderOrder}
eventClick={onClickOrder} eventClick={onClickOrder}
eventChange={onEditOrder} eventChange={onEditOrder}
@@ -0,0 +1,61 @@
import type { EventContentArg } from '@fullcalendar/core';
import type { ModelType } from '@lib/enums/ModelType';
import { resolveItem } from '@lib/functions/Conversion';
import { t } from '@lingui/core/macro';
import { Badge, Divider, Group, Stack, Text } from '@mantine/core';
import { formatDate } from '../../defaults/formatters';
import { RenderInstance } from '../render/Instance';
import { RenderOwner } from '../render/User';
export default function OrderCalendarToolTip({
event,
instanceLookup,
modelType
}: {
event: EventContentArg;
instanceLookup: string;
modelType: ModelType;
}) {
// Extract the order instance from the event
const order = event?.event?._def?.extendedProps?.order;
const instance = resolveItem(order, instanceLookup);
if (!order) return null;
return (
<Stack gap='xs'>
<RenderInstance model={modelType} instance={instance} />
<Divider />
<Group gap='xs'>
<Text size='sm' fw='bold'>
{order.reference}
</Text>
<Text size='xs'>{order.description || order.title}</Text>
</Group>
{order.start_date && (
<Group gap='xs'>
<Text size='sm' fw='bold'>{t`Start Date`}</Text>
<Text size='xs'>{formatDate(order.start_date)}</Text>
</Group>
)}
{order.target_date && (
<Group gap='xs'>
<Text size='sm' fw='bold'>{t`Target Date`}</Text>
<Text size='xs'>{formatDate(order.target_date)}</Text>
{order.overdue && (
<Badge size='sm' color='red'>
{t`Overdue`}
</Badge>
)}
</Group>
)}
{order.responsible && (
<Group gap='xs'>
<Text size='sm' fw='bold'>{t`Responsible`}</Text>
<RenderOwner instance={order.responsible_detail} />
</Group>
)}
</Stack>
);
}
@@ -50,11 +50,11 @@ export function RenderCompany(
return ( return (
<RenderInlineModel <RenderInlineModel
{...props} {...props}
image={instance.thumbnail || instance.image} image={instance?.thumbnail || instance?.image}
primary={instance.name} primary={instance?.name}
suffix={instance.description} suffix={instance?.description}
url={ url={
props.link ? getDetailUrl(ModelType.company, instance.pk) : undefined props.link ? getDetailUrl(ModelType.company, instance?.pk) : undefined
} }
/> />
); );
+13 -2
View File
@@ -6,14 +6,16 @@ import {
IconTable, IconTable,
IconTools IconTools
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { EventContentArg } from '@fullcalendar/core';
import { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles'; import { UserRoles } from '@lib/enums/Roles';
import type { TableFilter } from '@lib/types/Filters'; import type { TableFilter } from '@lib/types/Filters';
import type { PanelType } from '@lib/types/Panel'; import type { PanelType } from '@lib/types/Panel';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import OrderCalendar from '../../components/calendar/OrderCalendar'; import OrderCalendar from '../../components/calendar/OrderCalendar';
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
import PermissionDenied from '../../components/errors/PermissionDenied'; import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup'; import { PanelGroup } from '../../components/panels/PanelGroup';
@@ -34,12 +36,21 @@ function BuildOrderCalendar() {
}); });
}, [globalSettings]); }, [globalSettings]);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.part,
instanceLookup: 'part_detail'
});
}, []);
return ( return (
<OrderCalendar <OrderCalendar
model={ModelType.build} model={ModelType.build}
role={UserRoles.build} role={UserRoles.build}
params={{ outstanding: true }} params={{ outstanding: true, part_detail: true }}
filters={calendarFilters} filters={calendarFilters}
tooltip={renderTooltip}
/> />
); );
} }
@@ -10,13 +10,15 @@ import {
IconShoppingCart, IconShoppingCart,
IconTable IconTable
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { EventContentArg } from '@fullcalendar/core';
import { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles'; import { UserRoles } from '@lib/enums/Roles';
import type { TableFilter } from '@lib/index'; import type { TableFilter } from '@lib/index';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import OrderCalendar from '../../components/calendar/OrderCalendar'; import OrderCalendar from '../../components/calendar/OrderCalendar';
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
import PermissionDenied from '../../components/errors/PermissionDenied'; import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup'; import { PanelGroup } from '../../components/panels/PanelGroup';
@@ -37,12 +39,21 @@ function PurchaseOrderCalendar() {
return PurchaseOrderFilters({ includeDateFilters: false }); return PurchaseOrderFilters({ includeDateFilters: false });
}, []); }, []);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.company,
instanceLookup: 'supplier_detail'
});
}, []);
return ( return (
<OrderCalendar <OrderCalendar
model={ModelType.purchaseorder} model={ModelType.purchaseorder}
role={UserRoles.purchase_order} role={UserRoles.purchase_order}
params={{ outstanding: true }} params={{ outstanding: true, supplier_detail: true }}
filters={calendarFilters} filters={calendarFilters}
tooltip={renderTooltip}
/> />
); );
} }
+23 -3
View File
@@ -9,13 +9,15 @@ import {
IconTruckDelivery, IconTruckDelivery,
IconTruckReturn IconTruckReturn
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { EventContentArg } from '@fullcalendar/core';
import { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles'; import { UserRoles } from '@lib/enums/Roles';
import type { TableFilter } from '@lib/index'; import type { TableFilter } from '@lib/index';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import OrderCalendar from '../../components/calendar/OrderCalendar'; import OrderCalendar from '../../components/calendar/OrderCalendar';
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
import PermissionDenied from '../../components/errors/PermissionDenied'; import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup'; import { PanelGroup } from '../../components/panels/PanelGroup';
@@ -35,12 +37,21 @@ function SalesOrderCalendar() {
return SalesOrderFilters({ includeDateFilters: false }); return SalesOrderFilters({ includeDateFilters: false });
}, []); }, []);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.company,
instanceLookup: 'customer_detail'
});
}, []);
return ( return (
<OrderCalendar <OrderCalendar
model={ModelType.salesorder} model={ModelType.salesorder}
role={UserRoles.sales_order} role={UserRoles.sales_order}
params={{ outstanding: true }} params={{ outstanding: true, customer_detail: true }}
filters={calendarFilters} filters={calendarFilters}
tooltip={renderTooltip}
/> />
); );
} }
@@ -50,12 +61,21 @@ const ReturnOrderCalendar = () => {
return SalesOrderFilters({ includeDateFilters: false }); return SalesOrderFilters({ includeDateFilters: false });
}, []); }, []);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.company,
instanceLookup: 'customer_detail'
});
}, []);
return ( return (
<OrderCalendar <OrderCalendar
model={ModelType.returnorder} model={ModelType.returnorder}
role={UserRoles.return_order} role={UserRoles.return_order}
params={{ outstanding: true }} params={{ outstanding: true, customer_detail: true }}
filters={calendarFilters} filters={calendarFilters}
tooltip={renderTooltip}
/> />
); );
}; };
@@ -276,6 +276,7 @@ export default function InvenTreeTableHeader({
<Divider /> <Divider />
{tableState.filterSet.activeFilters?.map((filter) => ( {tableState.filterSet.activeFilters?.map((filter) => (
<FilterPreview <FilterPreview
key={filter.name}
filter={filter} filter={filter}
filters={tableProps.tableFilters} filters={tableProps.tableFilters}
/> />