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 dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
@@ -15,6 +19,7 @@ import {
Box,
Button,
Group,
HoverCard,
Indicator,
LoadingOverlay,
Popover,
@@ -29,7 +34,13 @@ import {
IconDownload,
IconFilter
} 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 {
defaultLocale,
@@ -44,6 +55,7 @@ export interface InvenTreeCalendarProps extends CalendarOptions {
enableDownload?: boolean;
enableFilters?: boolean;
enableSearch?: boolean;
eventTooltipContent?: (event: EventContentArg) => ReactNode;
filters?: TableFilter[];
isLoading?: boolean;
state: CalendarState;
@@ -53,6 +65,7 @@ export default function Calendar({
enableDownload,
enableFilters = false,
enableSearch,
eventTooltipContent,
isLoading,
filters,
state,
@@ -112,6 +125,36 @@ export default function Calendar({
[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 (
<>
{state.exportModal.modal}
@@ -217,6 +260,7 @@ export default function Calendar({
footerToolbar={false}
{...calendarProps}
datesSet={datesSet}
eventContent={wrappedEventContent}
/>
</Box>
</Stack>
@@ -46,12 +46,14 @@ export default function OrderCalendar({
model,
role,
params,
filters
filters,
tooltip
}: {
model: ModelType;
role: UserRoles;
params: Record<string, any>;
filters?: TableFilter[];
tooltip?: (event: EventContentArg) => React.ReactNode;
}) {
const navigate = useNavigate();
const user = useUserState();
@@ -106,6 +108,7 @@ export default function OrderCalendar({
const end: string = order.target_date || start;
return {
order: order,
id: order.pk,
title: order.reference,
description: order.description,
@@ -209,6 +212,7 @@ export default function OrderCalendar({
state={calendarState}
filters={calendarFilters}
editable={true}
eventTooltipContent={tooltip}
eventContent={renderOrder}
eventClick={onClickOrder}
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 (
<RenderInlineModel
{...props}
image={instance.thumbnail || instance.image}
primary={instance.name}
suffix={instance.description}
image={instance?.thumbnail || instance?.image}
primary={instance?.name}
suffix={instance?.description}
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,
IconTools
} 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 { UserRoles } from '@lib/enums/Roles';
import type { TableFilter } from '@lib/types/Filters';
import type { PanelType } from '@lib/types/Panel';
import { useLocalStorage } from '@mantine/hooks';
import OrderCalendar from '../../components/calendar/OrderCalendar';
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup';
@@ -34,12 +36,21 @@ function BuildOrderCalendar() {
});
}, [globalSettings]);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.part,
instanceLookup: 'part_detail'
});
}, []);
return (
<OrderCalendar
model={ModelType.build}
role={UserRoles.build}
params={{ outstanding: true }}
params={{ outstanding: true, part_detail: true }}
filters={calendarFilters}
tooltip={renderTooltip}
/>
);
}
@@ -10,13 +10,15 @@ import {
IconShoppingCart,
IconTable
} 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 { UserRoles } from '@lib/enums/Roles';
import type { TableFilter } from '@lib/index';
import { useLocalStorage } from '@mantine/hooks';
import OrderCalendar from '../../components/calendar/OrderCalendar';
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup';
@@ -37,12 +39,21 @@ function PurchaseOrderCalendar() {
return PurchaseOrderFilters({ includeDateFilters: false });
}, []);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.company,
instanceLookup: 'supplier_detail'
});
}, []);
return (
<OrderCalendar
model={ModelType.purchaseorder}
role={UserRoles.purchase_order}
params={{ outstanding: true }}
params={{ outstanding: true, supplier_detail: true }}
filters={calendarFilters}
tooltip={renderTooltip}
/>
);
}
+23 -3
View File
@@ -9,13 +9,15 @@ import {
IconTruckDelivery,
IconTruckReturn
} 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 { UserRoles } from '@lib/enums/Roles';
import type { TableFilter } from '@lib/index';
import { useLocalStorage } from '@mantine/hooks';
import OrderCalendar from '../../components/calendar/OrderCalendar';
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
import PermissionDenied from '../../components/errors/PermissionDenied';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/panels/PanelGroup';
@@ -35,12 +37,21 @@ function SalesOrderCalendar() {
return SalesOrderFilters({ includeDateFilters: false });
}, []);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.company,
instanceLookup: 'customer_detail'
});
}, []);
return (
<OrderCalendar
model={ModelType.salesorder}
role={UserRoles.sales_order}
params={{ outstanding: true }}
params={{ outstanding: true, customer_detail: true }}
filters={calendarFilters}
tooltip={renderTooltip}
/>
);
}
@@ -50,12 +61,21 @@ const ReturnOrderCalendar = () => {
return SalesOrderFilters({ includeDateFilters: false });
}, []);
const renderTooltip = useCallback((event: EventContentArg) => {
return OrderCalendarToolTip({
event: event,
modelType: ModelType.company,
instanceLookup: 'customer_detail'
});
}, []);
return (
<OrderCalendar
model={ModelType.returnorder}
role={UserRoles.return_order}
params={{ outstanding: true }}
params={{ outstanding: true, customer_detail: true }}
filters={calendarFilters}
tooltip={renderTooltip}
/>
);
};
@@ -276,6 +276,7 @@ export default function InvenTreeTableHeader({
<Divider />
{tableState.filterSet.activeFilters?.map((filter) => (
<FilterPreview
key={filter.name}
filter={filter}
filters={tableProps.tableFilters}
/>