diff --git a/src/frontend/src/components/previews/models/PartPreview.tsx b/src/frontend/src/components/previews/models/PartPreview.tsx index d253cac760..b6cfa9f1c7 100644 --- a/src/frontend/src/components/previews/models/PartPreview.tsx +++ b/src/frontend/src/components/previews/models/PartPreview.tsx @@ -1,217 +1,6 @@ -import { - ApiEndpoints, - ModelType, - TagsList, - apiUrl, - formatDecimal -} from '@lib/index'; -import { t } from '@lingui/core/macro'; -import { Group, Stack, Text } from '@mantine/core'; -import { useQuery } from '@tanstack/react-query'; -import { useMemo } from 'react'; -import { useApi } from '../../../contexts/ApiContext'; -import { formatDate, formatPriceRange } from '../../../defaults/formatters'; -import { useInstance } from '../../../hooks/UseInstance'; -import { ApiImage } from '../../images/ApiImage'; -import { RenderRemoteInstance } from '../../render/Instance'; -import { RenderUser } from '../../render/User'; -import { AttributeGrid, type AttributeRow } from '../AttributeGrid'; +import { PartDetailsPanel } from '../../../pages/part/PartDetailsPanel'; import type { PreviewType } from '../PreviewType'; -function PartPreviewContent({ instance }: Readonly<{ instance: any }>) { - const api = useApi(); - - const { instance: requirements } = useInstance({ - endpoint: ApiEndpoints.part_requirements, - pk: instance?.pk, - hasPrimaryKey: true, - disabled: !instance?.pk, - defaultValue: {} - }); - - const { data: parameters = [] } = useQuery({ - queryKey: ['part-preview-parameters', instance?.pk], - enabled: !!instance?.pk, - queryFn: () => - api - .get(apiUrl(ApiEndpoints.parameter_list), { - params: { - model_type: ModelType.part, - model_id: instance.pk, - limit: 100 - } - }) - .then((res) => res.data?.results ?? []) - }); - - const units = instance?.units ? ` ${instance.units}` : ''; - - const stockItems: AttributeRow[] = useMemo(() => { - const totalStock = - requirements?.total_stock ?? instance?.total_in_stock ?? 0; - const availableStock = - requirements?.unallocated_stock ?? instance?.unallocated_stock ?? 0; - const minStock = instance?.minimum_stock ?? 0; - const ordering = requirements?.ordering ?? 0; - const canBuild = requirements?.can_build ?? 0; - const building = requirements?.building ?? 0; - - return !instance?.virtual - ? [ - { - label: t`In Stock`, - value: formatDecimal(totalStock) + units - }, - availableStock !== totalStock - ? { - label: t`Available`, - value: formatDecimal(availableStock) + units - } - : null, - minStock > 0 - ? { label: t`Minimum`, value: formatDecimal(minStock) + units } - : null, - instance?.purchaseable && ordering > 0 - ? { label: t`On Order`, value: formatDecimal(ordering) + units } - : null, - instance?.assembly && canBuild >= 0 - ? { label: t`Can Build`, value: formatDecimal(canBuild) } - : null, - building > 0 - ? { label: t`In Production`, value: formatDecimal(building) } - : null - ].filter((x): x is NonNullable => x !== null) - : []; - }, [requirements, instance, units]); - - const parameterItems: AttributeRow[] = parameters.map((param: any) => ({ - label: param.template_detail?.name, - value: - param.data + - (param.template_detail?.units ? ` ${param.template_detail.units}` : '') - })); - - const infoItems: AttributeRow[] = useMemo(() => { - const priceRange = formatPriceRange( - instance?.pricing_min, - instance?.pricing_max - ); - - return [ - { - label: t`Category`, - value: instance?.category ? ( - - ) : null - }, - { - label: t`Revision Of`, - value: instance?.revision_of ? ( - - ) : null - }, - { - label: t`Variant Of`, - value: instance?.variant_of ? ( - - ) : null - }, - { - label: t`Price Range`, - value: - instance?.pricing_min != null || instance?.pricing_max != null - ? priceRange - : null - }, - { - label: t`Creation Date`, - value: instance?.creation_date - ? formatDate(instance.creation_date) - : null - }, - { - label: t`Created By`, - value: instance?.creation_user_detail - ? RenderUser({ instance: instance.creation_user_detail }) - : null - } - ]; - }, [instance]); - - return ( - - - - - - {instance?.full_name || instance?.name} - - {instance?.description && ( - - {instance.description} - - )} - {instance?.IPN && ( - - - {t`IPN`}: - - - {instance.IPN} - - - )} - {instance?.revision && ( - - - {t`Revision`}: - - - {instance.revision} - - - )} - {instance?.category_name && ( - - - {t`Category`}: - - {instance?.category_name} - - )} - - - - {instance?.tags?.length > 0 && } - - - {!instance?.virtual && ( - - )} - - - ); -} - export function PartPreviewComponent({ instance, modelId @@ -221,6 +10,6 @@ export function PartPreviewComponent({ }>): PreviewType { return { title: instance?.full_name || instance?.name || `Part #${modelId}`, - preview: + preview: }; } diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 38194069b6..2590a540b3 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -3,7 +3,6 @@ import { ActionIcon, Alert, Center, - Grid, Group, Loader, Paper, @@ -41,7 +40,6 @@ import { type ReactNode, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import Select from 'react-select'; -import TagsList from '@lib/components/TagsList'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; @@ -52,13 +50,7 @@ import type { PanelType } from '@lib/types/Panel'; import AdminButton from '../../components/buttons/AdminButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import StarredToggleButton from '../../components/buttons/StarredToggleButton'; -import { - type DetailsField, - DetailsTable -} from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; -import { DetailsImage } from '../../components/details/DetailsImage'; -import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { Thumbnail } from '../../components/images/Thumbnail'; import { ActionDropdown, @@ -77,7 +69,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup'; import { RenderPart } from '../../components/render/Part'; import OrderPartsWizard from '../../components/wizards/OrderPartsWizard'; import { useApi } from '../../contexts/ApiContext'; -import { formatDecimal, formatPriceRange } from '../../defaults/formatters'; +import { formatDecimal } from '../../defaults/formatters'; import { usePartFields } from '../../forms/PartForms'; import { useFindSerialNumberForm } from '../../forms/StockForms'; import { @@ -106,6 +98,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import { TransferOrderTable } from '../../tables/stock/TransferOrderTable'; import PartAllocationPanel from './PartAllocationPanel'; +import { PartDetailsPanel } from './PartDetailsPanel'; import PartPricingPanel from './PartPricingPanel'; import PartStockHistoryDetail from './PartStockHistoryDetail'; import PartSupplierDetail from './PartSupplierDetail'; @@ -175,14 +168,6 @@ export default function PartDetail() { refetchOnMount: true }); - const { instance: serials } = useInstance({ - endpoint: ApiEndpoints.part_serial_numbers, - pk: id, - hasPrimaryKey: true, - refetchOnMount: false, - defaultValue: {} - }); - const { instance: part, refreshInstance, @@ -275,398 +260,22 @@ export default function PartDetail() { return partRevisionOptions.length > 0 && revisionsEnabled; }, [partRevisionOptions, revisionsEnabled]); - const detailsPanel = useMemo(() => { - if (instanceQuery.isFetching) { - return ; - } - - const data = { ...part }; - - const fetching = - partRequirementsQuery.isFetching || instanceQuery.isFetching; - - // Copy part requirements data into the main part data - data.total_in_stock = - partRequirements?.total_stock ?? part?.total_in_stock ?? 0; - data.unallocated = - partRequirements?.unallocated_stock ?? part?.unallocated_stock ?? 0; - data.ordering = partRequirements?.ordering ?? part?.ordering ?? 0; - - data.required = - (partRequirements?.required_for_build_orders ?? - part?.required_for_build_orders ?? - 0) + - (partRequirements?.required_for_sales_orders ?? - part?.required_for_sales_orders ?? - 0); - - data.allocated = - (partRequirements?.allocated_to_build_orders ?? - part?.allocated_to_build_orders ?? - 0) + - (partRequirements?.allocated_to_sales_orders ?? - part?.allocated_to_sales_orders ?? - 0); - - // Extract requirements data - data.can_build = partRequirements?.can_build ?? 0; - - // Provide latest serial number info - if (!!serials.latest) { - data.latest_serial_number = serials.latest; - } - - // Top left - core part information - const tl: DetailsField[] = [ - { - type: 'string', - name: 'name', - label: t`Name`, - icon: 'part', - copy: true - }, - { - type: 'string', - name: 'IPN', - label: t`IPN`, - copy: true, - hidden: !part.IPN - }, - { - type: 'string', - name: 'description', - label: t`Description`, - copy: true - }, - { - type: 'link', - name: 'variant_of', - label: t`Variant of`, - model: ModelType.part, - model_field: 'full_name', - hidden: !part.variant_of - }, - { - type: 'link', - name: 'revision_of', - label: t`Revision of`, - model: ModelType.part, - model_field: 'full_name', - hidden: !part.revision_of - }, - { - type: 'string', - name: 'revision', - label: t`Revision`, - hidden: !part.revision, - copy: true - }, - { - type: 'link', - name: 'category', - label: t`Category`, - model: ModelType.partcategory - }, - { - type: 'link', - name: 'default_location', - label: t`Default Location`, - model: ModelType.stocklocation, - hidden: !part.default_location - }, - { - type: 'link', - name: 'category_default_location', - label: t`Category Default Location`, - model: ModelType.stocklocation, - hidden: part.default_location || !part.category_default_location - }, - { - type: 'string', - name: 'units', - label: t`Units`, - copy: true, - hidden: !part.units - }, - { - type: 'string', - name: 'keywords', - label: t`Keywords`, - copy: true, - hidden: !part.keywords - }, - { - type: 'link', - name: 'link', - label: t`Link`, - external: true, - copy: true, - hidden: !part.link - } - ]; - - // Top right - stock availability information - const tr: DetailsField[] = [ - { - type: 'number', - name: 'total_in_stock', - unit: part.units, - label: t`In Stock`, - hidden: part.virtual - }, - { - type: 'progressbar', - name: 'unallocated_stock', - total: data.total_in_stock, - progress: data.unallocated, - label: t`Available Stock`, - hidden: part.virtual || data.total_in_stock == data.unallocated - }, - { - type: 'number', - name: 'ordering', - label: t`On order`, - unit: part.units, - hidden: !part.purchaseable || part.ordering <= 0 - }, - { - type: 'number', - name: 'required', - label: t`Required for Orders`, - unit: part.units, - hidden: data.required <= 0, - icon: 'stocktake' - }, - { - type: 'progressbar', - name: 'allocated_to_build_orders', - icon: 'manufacturers', - total: partRequirements.required_for_build_orders, - progress: partRequirements.allocated_to_build_orders, - label: t`Allocated to Build Orders`, - hidden: - fetching || - (partRequirements.required_for_build_orders <= 0 && - partRequirements.allocated_to_build_orders <= 0) - }, - { - type: 'progressbar', - icon: 'sales_orders', - name: 'allocated_to_sales_orders', - total: partRequirements.required_for_sales_orders, - progress: partRequirements.allocated_to_sales_orders, - label: t`Allocated to Sales Orders`, - hidden: - fetching || - (partRequirements.required_for_sales_orders <= 0 && - partRequirements.allocated_to_sales_orders <= 0) - }, - { - type: 'progressbar', - name: 'building', - label: t`In Production`, - progress: partRequirements.building, - total: partRequirements.scheduled_to_build, - hidden: - fetching || - (!partRequirements.building && !partRequirements.scheduled_to_build) - }, - { - type: 'number', - name: 'can_build', - unit: part.units, - label: t`Can Build`, - hidden: !part.assembly || fetching - }, - { - type: 'number', - name: 'minimum_stock', - unit: part.units, - label: t`Minimum Stock`, - hidden: part.minimum_stock <= 0 - }, - { - type: 'number', - name: 'maximum_stock', - unit: part.units, - label: t`Maximum Stock`, - hidden: part.maximum_stock <= 0 - } - ]; - - // Bottom left - part attributes - const bl: DetailsField[] = [ - { - type: 'boolean', - name: 'active', - label: t`Active` - }, - { - type: 'boolean', - name: 'locked', - label: t`Locked` - }, - { - type: 'boolean', - icon: 'template', - name: 'is_template', - label: t`Template Part` - }, - { - type: 'boolean', - name: 'assembly', - label: t`Assembled Part` - }, - { - type: 'boolean', - name: 'component', - label: t`Component Part` - }, - { - type: 'boolean', - name: 'testable', - label: t`Testable Part`, - icon: 'test' - }, - { - type: 'boolean', - name: 'trackable', - label: t`Trackable Part` - }, - { - type: 'boolean', - name: 'purchaseable', - label: t`Purchaseable Part` - }, - { - type: 'boolean', - name: 'salable', - icon: 'saleable', - label: t`Saleable Part` - }, - { - type: 'boolean', - name: 'virtual', - label: t`Virtual Part` - }, - { - type: 'boolean', - name: 'starred', - label: t`Subscribed`, - icon: 'bell' - } - ]; - - // Bottom right - other part information - const br: DetailsField[] = [ - { - type: 'string', - name: 'creation_date', - label: t`Creation Date` - }, - { - type: 'string', - name: 'creation_user', - label: t`Created By`, - badge: 'user', - icon: 'user', - hidden: !part.creation_user - }, - { - type: 'string', - name: 'responsible', - label: t`Responsible`, - badge: 'owner', - hidden: !part.responsible - }, - { - name: 'default_expiry', - label: t`Default Expiry`, - hidden: !part.default_expiry, - icon: 'calendar', - type: 'string', - value_formatter: () => { - return `${part.default_expiry} ${t`days`}`; - } - } - ]; - - // Add in price range data - if (part.pricing_min || part.pricing_max) { - br.push({ - type: 'string', - name: 'pricing', - label: t`Price Range`, - value_formatter: () => { - return formatPriceRange(part.pricing_min, part.pricing_max); - } - }); - } - - br.push({ - type: 'string', - name: 'latest_serial_number', - label: t`Latest Serial Number`, - hidden: !part.trackable || !data.latest_serial_number, - icon: 'serial' - }); - - return part ? ( - + const revisionSelector = useMemo(() => { + if (!enableRevisionSelection) return null; + return ( + - - - - - - - - {enableRevisionSelection && ( - - - - - - - {t`Select Part Revision`} - - - - - )} + + + + + {t`Select Part Revision`} + + - - - - - ) : ( - + ); - }, [ - globalSettings, - part, - id, - serials, - instanceQuery.isFetching, - instanceQuery.data, - enableRevisionSelection, - partRevisionOptions, - partRequirementsQuery.isFetching, - partRequirements - ]); + }, [enableRevisionSelection, part, partRevisionOptions]); // Part data panels (recalculate when part data changes) const partPanels: PanelType[] = useMemo(() => { @@ -675,7 +284,14 @@ export default function PartDetail() { name: 'details', label: t`Part Details`, icon: , - content: detailsPanel + content: ( + + ) }, { name: 'stock', @@ -903,8 +519,9 @@ export default function PartDetail() { user, globalSettings, userSettings, - detailsPanel, - bomInformation + bomInformation, + revisionSelector, + refreshInstance ]); const breadcrumbs = useMemo(() => { diff --git a/src/frontend/src/pages/part/PartDetailsPanel.tsx b/src/frontend/src/pages/part/PartDetailsPanel.tsx new file mode 100644 index 0000000000..320513dbfa --- /dev/null +++ b/src/frontend/src/pages/part/PartDetailsPanel.tsx @@ -0,0 +1,360 @@ +import { t } from '@lingui/core/macro'; +import { Grid, Skeleton, Stack } from '@mantine/core'; +import { type ReactNode, useMemo } from 'react'; + +import TagsList from '@lib/components/TagsList'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; +import { UserRoles } from '@lib/enums/Roles'; +import { apiUrl } from '@lib/functions/Api'; + +import { + type DetailsField, + DetailsTable +} from '../../components/details/Details'; +import { DetailsImage } from '../../components/details/DetailsImage'; +import { ItemDetailsGrid } from '../../components/details/ItemDetails'; +import { formatPriceRange } from '../../defaults/formatters'; +import { useInstance } from '../../hooks/UseInstance'; + +export function PartDetailsPanel({ + instance, + allowImageEdit = false, + refreshInstance, + additionalContent +}: Readonly<{ + instance: any; + allowImageEdit?: boolean; + refreshInstance?: () => void; + additionalContent?: ReactNode; +}>) { + const { instance: requirements, instanceQuery: requirementsQuery } = + useInstance({ + endpoint: ApiEndpoints.part_requirements, + pk: instance?.pk, + hasPrimaryKey: true, + disabled: !instance?.pk, + defaultValue: {} + }); + + const { instance: serials } = useInstance({ + endpoint: ApiEndpoints.part_serial_numbers, + pk: instance?.pk, + hasPrimaryKey: true, + disabled: !instance?.pk, + defaultValue: {} + }); + + const data = useMemo(() => { + if (!instance) return {}; + const d = { ...instance }; + d.total_in_stock = + requirements?.total_stock ?? instance?.total_in_stock ?? 0; + d.unallocated = + requirements?.unallocated_stock ?? instance?.unallocated_stock ?? 0; + d.ordering = requirements?.ordering ?? instance?.ordering ?? 0; + d.required = + (requirements?.required_for_build_orders ?? + instance?.required_for_build_orders ?? + 0) + + (requirements?.required_for_sales_orders ?? + instance?.required_for_sales_orders ?? + 0); + d.allocated = + (requirements?.allocated_to_build_orders ?? + instance?.allocated_to_build_orders ?? + 0) + + (requirements?.allocated_to_sales_orders ?? + instance?.allocated_to_sales_orders ?? + 0); + d.can_build = requirements?.can_build ?? 0; + if (serials?.latest) d.latest_serial_number = serials.latest; + return d; + }, [instance, requirements, serials]); + + const fetching = requirementsQuery?.isFetching ?? false; + + const tl: DetailsField[] = [ + { type: 'string', name: 'name', label: t`Name`, icon: 'part', copy: true }, + { + type: 'string', + name: 'IPN', + label: t`IPN`, + copy: true, + hidden: !instance?.IPN + }, + { type: 'string', name: 'description', label: t`Description`, copy: true }, + { + type: 'link', + name: 'variant_of', + label: t`Variant of`, + model: ModelType.part, + model_field: 'full_name', + hidden: !instance?.variant_of + }, + { + type: 'link', + name: 'revision_of', + label: t`Revision of`, + model: ModelType.part, + model_field: 'full_name', + hidden: !instance?.revision_of + }, + { + type: 'string', + name: 'revision', + label: t`Revision`, + hidden: !instance?.revision, + copy: true + }, + { + type: 'link', + name: 'category', + label: t`Category`, + model: ModelType.partcategory + }, + { + type: 'link', + name: 'default_location', + label: t`Default Location`, + model: ModelType.stocklocation, + hidden: !instance?.default_location + }, + { + type: 'link', + name: 'category_default_location', + label: t`Category Default Location`, + model: ModelType.stocklocation, + hidden: instance?.default_location || !instance?.category_default_location + }, + { + type: 'string', + name: 'units', + label: t`Units`, + copy: true, + hidden: !instance?.units + }, + { + type: 'string', + name: 'keywords', + label: t`Keywords`, + copy: true, + hidden: !instance?.keywords + }, + { + type: 'link', + name: 'link', + label: t`Link`, + external: true, + copy: true, + hidden: !instance?.link + } + ]; + + const tr: DetailsField[] = [ + { + type: 'number', + name: 'total_in_stock', + unit: instance?.units, + label: t`In Stock`, + hidden: instance?.virtual + }, + { + type: 'progressbar', + name: 'unallocated_stock', + total: data.total_in_stock, + progress: data.unallocated, + label: t`Available Stock`, + hidden: instance?.virtual || data.total_in_stock == data.unallocated + }, + { + type: 'number', + name: 'ordering', + label: t`On Order`, + unit: instance?.units, + hidden: !instance?.purchaseable || instance?.ordering <= 0 + }, + { + type: 'number', + name: 'required', + label: t`Required for Orders`, + unit: instance?.units, + hidden: data.required <= 0, + icon: 'stocktake' + }, + { + type: 'progressbar', + name: 'allocated_to_build_orders', + icon: 'manufacturers', + total: requirements?.required_for_build_orders, + progress: requirements?.allocated_to_build_orders, + label: t`Allocated to Build Orders`, + hidden: + fetching || + (requirements?.required_for_build_orders <= 0 && + requirements?.allocated_to_build_orders <= 0) + }, + { + type: 'progressbar', + icon: 'sales_orders', + name: 'allocated_to_sales_orders', + total: requirements?.required_for_sales_orders, + progress: requirements?.allocated_to_sales_orders, + label: t`Allocated to Sales Orders`, + hidden: + fetching || + (requirements?.required_for_sales_orders <= 0 && + requirements?.allocated_to_sales_orders <= 0) + }, + { + type: 'progressbar', + name: 'building', + label: t`In Production`, + progress: requirements?.building, + total: requirements?.scheduled_to_build, + hidden: + fetching || + (!requirements?.building && !requirements?.scheduled_to_build) + }, + { + type: 'number', + name: 'can_build', + unit: instance?.units, + label: t`Can Build`, + hidden: !instance?.assembly || fetching + }, + { + type: 'number', + name: 'minimum_stock', + unit: instance?.units, + label: t`Minimum Stock`, + hidden: instance?.minimum_stock <= 0 + }, + { + type: 'number', + name: 'maximum_stock', + unit: instance?.units, + label: t`Maximum Stock`, + hidden: instance?.maximum_stock <= 0 + } + ]; + + const bl: DetailsField[] = [ + { type: 'boolean', name: 'active', label: t`Active` }, + { type: 'boolean', name: 'locked', label: t`Locked` }, + { + type: 'boolean', + icon: 'template', + name: 'is_template', + label: t`Template Part` + }, + { type: 'boolean', name: 'assembly', label: t`Assembled Part` }, + { type: 'boolean', name: 'component', label: t`Component Part` }, + { + type: 'boolean', + name: 'testable', + label: t`Testable Part`, + icon: 'test' + }, + { type: 'boolean', name: 'trackable', label: t`Trackable Part` }, + { type: 'boolean', name: 'purchaseable', label: t`Purchaseable Part` }, + { + type: 'boolean', + name: 'salable', + icon: 'saleable', + label: t`Saleable Part` + }, + { type: 'boolean', name: 'virtual', label: t`Virtual Part` }, + { type: 'boolean', name: 'starred', label: t`Subscribed`, icon: 'bell' } + ]; + + const br: DetailsField[] = useMemo(() => { + const fields: DetailsField[] = [ + { + type: 'string', + name: 'creation_date', + label: t`Creation Date` + }, + { + type: 'string', + name: 'creation_user', + label: t`Created By`, + badge: 'user', + icon: 'user', + hidden: !instance?.creation_user + }, + { + type: 'string', + name: 'responsible', + label: t`Responsible`, + badge: 'owner', + hidden: !instance?.responsible + }, + { + name: 'default_expiry', + label: t`Default Expiry`, + hidden: !instance?.default_expiry, + icon: 'calendar', + type: 'string', + value_formatter: () => `${instance?.default_expiry} ${t`days`}` + } + ]; + + if (instance?.pricing_min || instance?.pricing_max) { + fields.push({ + type: 'string', + name: 'pricing', + label: t`Price Range`, + value_formatter: () => + formatPriceRange(instance?.pricing_min, instance?.pricing_max) + }); + } + + fields.push({ + type: 'string', + name: 'latest_serial_number', + label: t`Latest Serial Number`, + hidden: !instance?.trackable || !data.latest_serial_number, + icon: 'serial' + }); + + return fields; + }, [instance, data.latest_serial_number]); + + if (!instance?.pk) return ; + + return ( + + + + + + + + + + {additionalContent} + + + + + + ); +}