diff --git a/src/frontend/src/components/items/Placeholder.tsx b/src/frontend/src/components/items/Placeholder.tsx index 0a6d47cba4..e1da638c39 100644 --- a/src/frontend/src/components/items/Placeholder.tsx +++ b/src/frontend/src/components/items/Placeholder.tsx @@ -1,6 +1,10 @@ import { Trans, t } from '@lingui/macro'; -import { Badge, Tooltip } from '@mantine/core'; +import { Alert, Badge, Stack, Text, Tooltip } from '@mantine/core'; +import { IconInfoCircle } from '@tabler/icons-react'; +/** + * Small badge to indicate that a feature is a placeholder. + */ export function PlaceholderPill() { return ( ); } + +/** + * Placeholder panel for use in a PanelGroup. + */ +export function PlaceholderPanel() { + return ( + + } + > + This panel has not yet been implemented + + + ); +} diff --git a/src/frontend/src/components/nav/BreadcrumbList.tsx b/src/frontend/src/components/nav/BreadcrumbList.tsx new file mode 100644 index 0000000000..0619933a04 --- /dev/null +++ b/src/frontend/src/components/nav/BreadcrumbList.tsx @@ -0,0 +1,28 @@ +import { Anchor, Breadcrumbs, Paper, Text } from '@mantine/core'; +import { useNavigate } from 'react-router-dom'; + +export type Breadcrumb = { + name: string; + url: string; +}; + +/** + * Construct a breadcrumb list, with integrated navigation. + */ +export function BreadcrumbList({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) { + const navigate = useNavigate(); + + return ( + + + {breadcrumbs.map((breadcrumb, index) => { + return ( + breadcrumb.url && navigate(breadcrumb.url)}> + {breadcrumb.name} + + ); + })} + + + ); +} diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx new file mode 100644 index 0000000000..87e9329bf8 --- /dev/null +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -0,0 +1,47 @@ +import { Group, Paper, Space, Stack, Text } from '@mantine/core'; +import { ReactNode } from 'react'; + +import { Breadcrumb, BreadcrumbList } from './BreadcrumbList'; + +/** + * Construct a "standard" page detail for common display between pages. + * + * @param breadcrumbs - The breadcrumbs to display (optional) + * @param + */ +export function PageDetail({ + title, + subtitle, + detail, + breadcrumbs, + actions +}: { + title: string; + subtitle?: string; + detail?: ReactNode; + breadcrumbs?: Breadcrumb[]; + actions?: ReactNode[]; +}) { + return ( + + {breadcrumbs && breadcrumbs.length > 0 && ( + + + + )} + + + + + {title} + {subtitle && {subtitle}} + + + {actions && {actions}} + + {detail} + + + + ); +} diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx index 3c1bc560b6..b9dc3b66f3 100644 --- a/src/frontend/src/components/nav/PanelGroup.tsx +++ b/src/frontend/src/components/nav/PanelGroup.tsx @@ -54,40 +54,41 @@ export function PanelGroup({ } return ( - - + + + + {panels.map( + (panel, idx) => + !panel.hidden && ( + + ) + )} + {panels.map( (panel, idx) => !panel.hidden && ( - - ) - )} - - {panels.map( - (panel, idx) => - !panel.hidden && ( - - + {panel.label} {panel.content} - - - ) - )} - + + ) + )} + + ); } diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx index 5606aa0326..d00dda1a16 100644 --- a/src/frontend/src/components/tables/InvenTreeTable.tsx +++ b/src/frontend/src/components/tables/InvenTreeTable.tsx @@ -79,6 +79,8 @@ function saveActiveFilters(tableKey: string, filters: TableFilter[]) { /** * Table Component which extends DataTable with custom InvenTree functionality + * + * TODO: Refactor table props into a single type */ export function InvenTreeTable({ url, @@ -99,6 +101,7 @@ export function InvenTreeTable({ customActionGroups = [], customFilters = [], rowActions, + onRowClick, refreshId }: { url: string; @@ -119,6 +122,7 @@ export function InvenTreeTable({ customActionGroups?: any[]; customFilters?: TableFilter[]; rowActions?: (record: any) => RowAction[]; + onRowClick?: (record: any, index: number, event: any) => void; refreshId?: string; }) { // Check if any columns are switchable (can be hidden) @@ -507,6 +511,7 @@ export function InvenTreeTable({ noRecordsText={missingRecordsText} records={data?.results ?? data ?? []} columns={dataColumns} + onRowClick={onRowClick} /> diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index a645970a0c..d4d9f36343 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -1,6 +1,7 @@ import { t } from '@lingui/macro'; import { Progress } from '@mantine/core'; import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { ThumbnailHoverCard } from '../../items/Thumbnail'; import { TableColumn } from '../Column'; @@ -50,12 +51,6 @@ function buildOrderTableColumns(): TableColumn[] { // TODO: Hide this if project code is not enabled // TODO: Custom render function here }, - { - accessor: 'priority', - title: t`Priority`, - sortable: true, - switchable: true - }, { accessor: 'quantity', sortable: true, @@ -87,16 +82,44 @@ function buildOrderTableColumns(): TableColumn[] { switchable: true // TODO: Custom render function here (status label) }, + { + accessor: 'priority', + title: t`Priority`, + sortable: true, + switchable: true + }, { accessor: 'creation_date', sortable: true, title: t`Created`, switchable: true + }, + { + accessor: 'target_date', + sortable: true, + title: t`Target Date`, + switchable: true + }, + { + accessor: 'completion_date', + sortable: true, + title: t`Completed`, + switchable: true + }, + { + accessor: 'issued_by', + sortable: true, + title: t`Issued By`, + switchable: true + // TODO: custom render function + }, + { + accessor: 'responsible', + sortable: true, + title: t`Responsible`, + switchable: true + // TODO: custom render function } - // TODO: issued_by - // TODO: responsible - // TODO: target_date - // TODO: completion_date ]; } @@ -116,9 +139,11 @@ function buildOrderTableParams(params: any): any { */ export function BuildOrderTable({ params = {} }: { params?: any }) { // Add required query parameters - let tableParams = useMemo(() => buildOrderTableParams(params), [params]); - let tableColumns = useMemo(() => buildOrderTableColumns(), []); - let tableFilters = useMemo(() => buildOrderTableFilters(), []); + const tableParams = useMemo(() => buildOrderTableParams(params), [params]); + const tableColumns = useMemo(() => buildOrderTableColumns(), []); + const tableFilters = useMemo(() => buildOrderTableFilters(), []); + + const navigate = useNavigate(); tableParams.part_detail = true; @@ -130,6 +155,7 @@ export function BuildOrderTable({ params = {} }: { params?: any }) { params={tableParams} columns={tableColumns} customFilters={tableFilters} + onRowClick={(row) => navigate(`/build/${row.pk}`)} /> ); } diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx index dae52dea19..f9dbba62a3 100644 --- a/src/frontend/src/components/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx @@ -79,17 +79,6 @@ function stockItemTableColumns(): TableColumn[] { ]; } -/** - * Return a set of parameters for the stock item table - */ -function stockItemTableParams(params: any): any { - return { - ...params, - part_detail: true, - location_detail: true - }; -} - /** * Construct a list of available filters for the stock item table */ @@ -113,7 +102,14 @@ function stockItemTableFilters(): TableFilter[] { * Load a table of stock items */ export function StockItemTable({ params = {} }: { params?: any }) { - let tableParams = useMemo(() => stockItemTableParams(params), []); + let tableParams = useMemo(() => { + return { + part_detail: true, + location_detail: true, + ...params + }; + }, [params]); + let tableColumns = useMemo(() => stockItemTableColumns(), []); let tableFilters = useMemo(() => stockItemTableFilters(), []); diff --git a/src/frontend/src/pages/Index/Build.tsx b/src/frontend/src/pages/Index/Build.tsx deleted file mode 100644 index faacc8ecfc..0000000000 --- a/src/frontend/src/pages/Index/Build.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { Group } from '@mantine/core'; - -import { PlaceholderPill } from '../../components/items/Placeholder'; -import { StylishText } from '../../components/items/StylishText'; -import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; - -export default function Build() { - return ( - <> - - - Build Orders - - - - - - ); -} diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx new file mode 100644 index 0000000000..67064419ad --- /dev/null +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -0,0 +1,166 @@ +import { t } from '@lingui/macro'; +import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core'; +import { + IconClipboardCheck, + IconClipboardList, + IconInfoCircle, + IconList, + IconListCheck, + IconListTree, + IconNotes, + IconPaperclip, + IconSitemap +} from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import { api } from '../../App'; +import { + PlaceholderPanel, + PlaceholderPill +} from '../../components/items/Placeholder'; +import { PageDetail } from '../../components/nav/PageDetail'; +import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; +import { AttachmentTable } from '../../components/tables/AttachmentTable'; +import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; +import { StockItemTable } from '../../components/tables/stock/StockItemTable'; +import { NotesEditor } from '../../components/widgets/MarkdownEditor'; + +/** + * Detail page for a single Build Order + */ +export default function BuildDetail() { + const { id } = useParams(); + + // Build data + const [build, setBuild] = useState({}); + + // Query hook for fetching build data + const buildQuery = useQuery(['build', id ?? -1], async () => { + let url = `/build/${id}/`; + + return api + .get(url, { + params: { + part_detail: true + } + }) + .then((response) => { + setBuild(response.data); + }) + .catch((error) => { + console.error(error); + setBuild({}); + }); + }); + + const buildPanels: PanelType[] = useMemo(() => { + return [ + { + name: 'details', + label: t`Build Details`, + icon: , + content: + }, + { + name: 'allocate-stock', + label: t`Allocate Stock`, + icon: , + content: + // TODO: Hide if build is complete + }, + { + name: 'incomplete-outputs', + label: t`Incomplete Outputs`, + icon: , + content: + // TODO: Hide if build is complete + }, + { + name: 'complete-outputs', + label: t`Completed Outputs`, + icon: , + content: ( + + ) + }, + { + name: 'consumed-stock', + label: t`Consumed Stock`, + icon: , + content: ( + + ) + }, + { + name: 'child-orders', + label: t`Child Build Orders`, + icon: , + content: ( + + ) + }, + { + name: 'attachments', + label: t`Attachments`, + icon: , + content: ( + + ) + }, + { + name: 'notes', + label: t`Notes`, + icon: , + content: ( + + ) + } + ]; + }, [build]); + + return ( + <> + + + TODO: Build details + + } + breadcrumbs={[ + { name: t`Build Orders`, url: '/build' }, + { name: build.reference, url: `/build/${build.pk}` } + ]} + actions={[]} + /> + + + + + ); +} diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx new file mode 100644 index 0000000000..652cc6c160 --- /dev/null +++ b/src/frontend/src/pages/build/BuildIndex.tsx @@ -0,0 +1,27 @@ +import { t } from '@lingui/macro'; +import { Button, Stack, Text } from '@mantine/core'; + +import { PageDetail } from '../../components/nav/PageDetail'; +import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; +import { notYetImplemented } from '../../functions/notifications'; + +/** + * Build Order index page + */ +export default function BuildIndex() { + return ( + <> + + notYetImplemented()}> + {t`New Build Order`} + + ]} + /> + + + + ); +} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index de9ac74887..6c6169fbb5 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,5 +1,6 @@ import { t } from '@lingui/macro'; import { + Alert, Button, Group, LoadingOverlay, @@ -29,6 +30,11 @@ import { useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { api } from '../../App'; +import { + PlaceholderPanel, + PlaceholderPill +} from '../../components/items/Placeholder'; +import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { AttachmentTable } from '../../components/tables/AttachmentTable'; import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; @@ -52,7 +58,7 @@ export default function PartDetail() { name: 'details', label: t`Details`, icon: , - content: part details go here + content: }, { name: 'stock', @@ -65,61 +71,61 @@ export default function PartDetail() { label: t`Variants`, icon: , hidden: !part.is_template, - content: part variants go here + content: }, { name: 'bom', label: t`Bill of Materials`, icon: , hidden: !part.assembly, - content: part.assembly && part BOM goes here + content: }, { name: 'builds', label: t`Build Orders`, icon: , hidden: !part.assembly && !part.component, - content: part builds go here + content: }, { name: 'used_in', label: t`Used In`, icon: , hidden: !part.component, - content: part used in goes here + content: }, { name: 'pricing', label: t`Pricing`, icon: , - content: part pricing goes here + content: }, { name: 'suppliers', label: t`Suppliers`, icon: , - content: part suppliers go here, - hidden: !part.purchaseable + hidden: !part.purchaseable, + content: }, { name: 'purchase_orders', label: t`Purchase Orders`, icon: , - content: part purchase orders go here, + content: , hidden: !part.purchaseable }, { name: 'sales_orders', label: t`Sales Orders`, icon: , - content: part sales orders go here, + content: , hidden: !part.salable }, { name: 'test_templates', label: t`Test Templates`, icon: , - content: part test templates go here, + content: , hidden: !part.trackable }, { @@ -195,31 +201,38 @@ export default function PartDetail() { return ( <> + + TODO: Part details + + } + breadcrumbs={[ + { name: t`Parts`, url: '/part' }, + { name: '...', url: '' }, + { name: part.full_name, url: `/part/${part.pk}` } + ]} + actions={[ + + ]} + /> - - - Part Detail - {part.name} - {part.description} - - - In Stock: {part.total_in_stock} - - diff --git a/src/frontend/src/pages/part/PartIndex.tsx b/src/frontend/src/pages/part/PartIndex.tsx index a72d7200dd..078cad45f4 100644 --- a/src/frontend/src/pages/part/PartIndex.tsx +++ b/src/frontend/src/pages/part/PartIndex.tsx @@ -9,6 +9,7 @@ import { useMemo } from 'react'; import { PlaceholderPill } from '../../components/items/Placeholder'; import { StylishText } from '../../components/items/StylishText'; +import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PartListTable } from '../../components/tables/part/PartTable'; @@ -41,10 +42,18 @@ export default function PartIndex() { return ( <> - - - Parts - + + diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index 171be315d1..d26d53c21c 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -12,8 +12,19 @@ export const Playground = Loadable( lazy(() => import('./pages/Index/Playground')) ); export const PartIndex = Loadable(lazy(() => import('./pages/part/PartIndex'))); +export const PartDetail = Loadable( + lazy(() => import('./pages/part/PartDetail')) +); + export const Stock = Loadable(lazy(() => import('./pages/Index/Stock'))); -export const Build = Loadable(lazy(() => import('./pages/Index/Build'))); + +export const BuildIndex = Loadable( + lazy(() => import('./pages/build/BuildIndex')) +); +export const BuildDetail = Loadable( + lazy(() => import('./pages/build/BuildDetail')) +); + export const Scan = Loadable(lazy(() => import('./pages/Index/Scan'))); export const Dashboard = Loadable( @@ -29,10 +40,6 @@ export const Profile = Loadable( lazy(() => import('./pages/Index/Profile/Profile')) ); -export const PartDetail = Loadable( - lazy(() => import('./pages/part/PartDetail')) -); - export const NotFound = Loadable(lazy(() => import('./pages/NotFound'))); export const Login = Loadable(lazy(() => import('./pages/Auth/Login'))); export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); @@ -92,7 +99,11 @@ export const router = createBrowserRouter( }, { path: 'build/', - element: + element: + }, + { + path: 'build/:id', + element: }, { path: '/profile/:tabValue',