React UI improvements (#5756)

* Add CompanyDetail page

* Add "edit company" button (and associated form)

* Implement dropdown actions for company page

* Update actions for PartDetail

* Adds overlay while modal is loading

- Lets the user know that something is at least happening in the background

* Update panels

* Implement separate pages for different company types

- Mostly for better breadcrumbs

* Placeholder actions for build detail

* Add stock table to company page

* typescript linting

* Fix unused variables

* remove dodgy improt
This commit is contained in:
Oliver 2023-10-20 00:23:58 +11:00 committed by GitHub
parent ae063d2722
commit e57b69be48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 689 additions and 93 deletions

View File

@ -1,11 +1,5 @@
import { t } from '@lingui/macro';
import {
Alert,
Divider,
LoadingOverlay,
ScrollArea,
Text
} from '@mantine/core';
import { Alert, Divider, LoadingOverlay, Text } from '@mantine/core';
import { Button, Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { modals } from '@mantine/modals';
@ -277,24 +271,22 @@ export function ApiForm({
</Alert>
)}
{preFormElement}
<ScrollArea>
<Stack spacing="xs">
{Object.entries(props.fields ?? {}).map(
([fieldName, field]) =>
!field.hidden && (
<ApiFormField
key={fieldName}
field={field}
fieldName={fieldName}
formProps={props}
form={form}
error={form.errors[fieldName] ?? null}
definitions={fieldDefinitions}
/>
)
)}
</Stack>
</ScrollArea>
<Stack spacing="xs">
{Object.entries(props.fields ?? {}).map(
([fieldName, field]) =>
!field.hidden && (
<ApiFormField
key={fieldName}
field={field}
fieldName={fieldName}
formProps={props}
form={form}
error={form.errors[fieldName] ?? null}
definitions={fieldDefinitions}
/>
)
)}
</Stack>
{postFormElement}
</Stack>
<Divider />

View File

@ -19,7 +19,7 @@ export function Thumbnail({
return (
<ApiImage
src={src}
src={src || '/static/img/blank_image.png'}
alt={alt}
width={size}
fit="contain"

View File

@ -0,0 +1,65 @@
import { ActionIcon, Menu, Tooltip } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { notYetImplemented } from '../../functions/notifications';
export type ActionDropdownItem = {
icon: ReactNode;
name: string;
tooltip?: string;
disabled?: boolean;
onClick?: () => void;
};
/**
* A simple Menu component which renders a set of actions.
*
* If no "active" actions are provided, the menu will not be rendered
*/
export function ActionDropdown({
icon,
tooltip,
actions
}: {
icon: ReactNode;
tooltip?: string;
actions: ActionDropdownItem[];
}) {
const hasActions = useMemo(() => {
return actions.some((action) => !action.disabled);
}, [actions]);
return hasActions ? (
<Menu position="bottom-end">
<Menu.Target>
<Tooltip label={tooltip}>
<ActionIcon size="lg" radius="sm" variant="outline">
{icon}
</ActionIcon>
</Tooltip>
</Menu.Target>
<Menu.Dropdown>
{actions.map((action, index) =>
action.disabled ? null : (
<Tooltip label={action.tooltip}>
<Menu.Item
icon={action.icon}
key={index}
onClick={() => {
if (action.onClick != undefined) {
action.onClick();
} else {
notYetImplemented();
}
}}
disabled={action.disabled}
>
{action.name}
</Menu.Item>
</Tooltip>
)
)}
</Menu.Dropdown>
</Menu>
) : null;
}

View File

@ -1,7 +1,8 @@
import { Container, Flex, Space } from '@mantine/core';
import { Container, Flex, LoadingOverlay, Space } from '@mantine/core';
import { Navigate, Outlet } from 'react-router-dom';
import { InvenTreeStyle } from '../../globalStyle';
import { useModalState } from '../../states/ModalState';
import { useSessionState } from '../../states/SessionState';
import { Footer } from './Footer';
import { Header } from './Header';
@ -19,9 +20,12 @@ export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
export default function LayoutComponent() {
const { classes } = InvenTreeStyle();
const modalState = useModalState();
return (
<ProtectedRoute>
<Flex direction="column" mih="100vh">
<LoadingOverlay visible={modalState.loading} />
<Header />
<Container className={classes.layoutContent} size="100%">
<Outlet />

View File

@ -41,7 +41,11 @@ export function PageDetail({
</Stack>
</Group>
<Space />
{actions && <Group position="right">{actions}</Group>}
{actions && (
<Group spacing={5} position="right">
{actions}
</Group>
)}
</Group>
</Stack>
</Paper>

View File

@ -224,20 +224,22 @@ export function AttachmentTable({
return (
<Stack spacing="xs">
<InvenTreeTable
url={url}
tableKey={tableKey}
columns={tableColumns}
props={{
noRecordsText: t`No attachments found`,
enableSelection: true,
customActionGroups: customActionGroups,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk
}
}}
/>
{pk && pk > 0 && (
<InvenTreeTable
url={url}
tableKey={tableKey}
columns={tableColumns}
props={{
noRecordsText: t`No attachments found`,
enableSelection: true,
customActionGroups: customActionGroups,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk
}
}}
/>
)}
{allowEdit && validPk && (
<Dropzone onDrop={uploadFiles}>
<Dropzone.Idle>

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
@ -11,9 +12,17 @@ import { InvenTreeTable } from '../InvenTreeTable';
* A table which displays a list of company records,
* based on the provided filter parameters
*/
export function CompanyTable({ params }: { params?: any }) {
export function CompanyTable({
params,
path
}: {
params?: any;
path?: string;
}) {
const { tableKey } = useTableRefresh('company');
const navigate = useNavigate();
const columns = useMemo(() => {
return [
{
@ -24,7 +33,7 @@ export function CompanyTable({ params }: { params?: any }) {
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail
src={record.thumbnail ?? record.image}
src={record.thumbnail ?? record.image ?? ''}
alt={record.name}
size={24}
/>
@ -56,6 +65,10 @@ export function CompanyTable({ params }: { params?: any }) {
props={{
params: {
...params
},
onRowClick: (row: any) => {
let base = path ?? 'company';
navigate(`/${base}/${row.pk}`);
}
}}
/>

View File

@ -7,6 +7,7 @@ import { api } from '../App';
import { ApiForm, ApiFormProps } from '../components/forms/ApiForm';
import { ApiFormFieldType } from '../components/forms/fields/ApiFormField';
import { apiUrl } from '../states/ApiState';
import { useModalState } from '../states/ModalState';
import { invalidResponse, permissionDenied } from './notifications';
import { generateUniqueId } from './uid';
@ -97,6 +98,10 @@ export function openModalApiForm(props: ApiFormProps) {
let url = constructFormUrl(props);
// let modalState = useModalState();
useModalState.getState().lock();
// Make OPTIONS request first
api
.options(url)
@ -119,6 +124,7 @@ export function openModalApiForm(props: ApiFormProps) {
modals.open({
title: props.title,
modalId: modalId,
size: 'xl',
onClose: () => {
props.onClose ? props.onClose() : null;
},
@ -126,8 +132,12 @@ export function openModalApiForm(props: ApiFormProps) {
<ApiForm modalId={modalId} props={props} fieldDefinitions={fields} />
)
});
useModalState.getState().unlock();
})
.catch((error) => {
useModalState.getState().unlock();
console.log('Error:', error);
if (error.response) {
invalidResponse(error.response.status);

View File

@ -0,0 +1,57 @@
import { t } from '@lingui/macro';
import {
IconAt,
IconCurrencyDollar,
IconGlobe,
IconPhone
} from '@tabler/icons-react';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { openEditApiForm } from '../forms';
/**
* Field set for editing a company instance
*/
export function companyFields(): ApiFormFieldSet {
return {
name: {},
description: {},
website: {
icon: <IconGlobe />
},
currency: {
icon: <IconCurrencyDollar />
},
phone: {
icon: <IconPhone />
},
email: {
icon: <IconAt />
},
is_supplier: {},
is_manufacturer: {},
is_customer: {}
};
}
/**
* Edit a company instance
*/
export function editCompany({
pk,
callback
}: {
pk: number;
callback?: () => void;
}) {
openEditApiForm({
name: 'company-edit',
title: t`Edit Company`,
url: ApiPaths.company_list,
pk: pk,
fields: companyFields(),
successMessage: t`Company updated`,
onFormSuccess: callback
});
}

View File

@ -3,16 +3,26 @@ import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
import {
IconClipboardCheck,
IconClipboardList,
IconCopy,
IconDots,
IconEdit,
IconFileTypePdf,
IconInfoCircle,
IconLink,
IconList,
IconListCheck,
IconNotes,
IconPaperclip,
IconSitemap
IconPrinter,
IconQrcode,
IconSitemap,
IconTrash,
IconUnlink
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import {
PlaceholderPanel,
PlaceholderPill
@ -25,6 +35,7 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
/**
* Detail page for a single Build Order
@ -44,6 +55,8 @@ export default function BuildDetail() {
}
});
const user = useUserState();
const buildPanels: PanelType[] = useMemo(() => {
return [
{
@ -130,22 +143,78 @@ export default function BuildDetail() {
];
}, [build]);
const buildActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions
return [
<ActionDropdown
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={[
{
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View part barcode`
},
{
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode to part`,
disabled: build?.barcode_hash
},
{
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode from part`,
disabled: !build?.barcode_hash
}
]}
/>,
<ActionDropdown
tooltip={t`Reporting Actions`}
icon={<IconPrinter />}
actions={[
{
icon: <IconFileTypePdf />,
name: t`Report`,
tooltip: t`Print build report`
}
]}
/>,
<ActionDropdown
tooltip={t`Build Order Actions`}
icon={<IconDots />}
actions={[
{
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: t`Edit build order`
},
{
icon: <IconCopy color="green" />,
name: t`Duplicate`,
tooltip: t`Duplicate build order`
},
{
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: t`Delete build order`
}
]}
/>
];
}, [id, build, user]);
return (
<>
<Stack spacing="xs">
<PageDetail
title={t`Build Order`}
subtitle={build.reference}
detail={
<Alert color="teal" title="Build order detail goes here">
<Text>TODO: Build details</Text>
</Alert>
}
breadcrumbs={[
{ name: t`Build Orders`, url: '/build' },
{ name: build.reference, url: `/build/${build.pk}` }
]}
actions={[<PlaceholderPill key="1" />]}
actions={buildActions}
/>
<LoadingOverlay visible={instanceQuery.isFetching} />
<PanelGroup pageKey="build" panels={buildPanels} />

View File

@ -0,0 +1,226 @@
import { t } from '@lingui/macro';
import { Group, LoadingOverlay, Stack, Text } from '@mantine/core';
import {
IconBuildingFactory2,
IconBuildingWarehouse,
IconDots,
IconEdit,
IconInfoCircle,
IconMap2,
IconNotes,
IconPackageExport,
IconPackages,
IconPaperclip,
IconShoppingCart,
IconTrash,
IconTruckDelivery,
IconTruckReturn,
IconUsersGroup
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { Thumbnail } from '../../components/images/Thumbnail';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import { Breadcrumb } from '../../components/nav/BreadcrumbList';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/nav/PanelGroup';
import { PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { PurchaseOrderTable } from '../../components/tables/purchasing/PurchaseOrderTable';
import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable';
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editCompany } from '../../functions/forms/CompanyForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
export type CompanyDetailProps = {
title: string;
breadcrumbs: Breadcrumb[];
};
/**
* Detail view for a single company instance
*/
export default function CompanyDetail(props: CompanyDetailProps) {
const { id } = useParams();
const user = useUserState();
const {
instance: company,
refreshInstance,
instanceQuery
} = useInstance({
endpoint: ApiPaths.company_list,
pk: id,
params: {},
refetchOnMount: true
});
const companyPanels: PanelType[] = useMemo(() => {
return [
{
name: 'details',
label: t`Details`,
icon: <IconInfoCircle />
},
{
name: 'manufactured-parts',
label: t`Manufactured Parts`,
icon: <IconBuildingFactory2 />,
hidden: !company?.is_manufacturer
},
{
name: 'supplied-parts',
label: t`Supplied Parts`,
icon: <IconBuildingWarehouse />,
hidden: !company?.is_supplier
},
{
name: 'purchase-orders',
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
hidden: !company?.is_supplier,
content: company?.pk && (
<PurchaseOrderTable params={{ supplier: company.pk }} />
)
},
{
name: 'stock-items',
label: t`Stock Items`,
icon: <IconPackages />,
hidden: !company?.is_manufacturer && !company?.is_supplier,
content: company?.pk && (
<StockItemTable params={{ company: company.pk }} />
)
},
{
name: 'sales-orders',
label: t`Sales Orders`,
icon: <IconTruckDelivery />,
hidden: !company?.is_customer,
content: company?.pk && (
<SalesOrderTable params={{ customer: company.pk }} />
)
},
{
name: 'return-orders',
label: t`Return Orders`,
icon: <IconTruckReturn />,
hidden: !company?.is_customer,
content: company.pk && (
<ReturnOrderTable params={{ customer: company.pk }} />
)
},
{
name: 'assigned-stock',
label: t`Assigned Stock`,
icon: <IconPackageExport />,
hidden: !company?.is_customer
},
{
name: 'contacts',
label: t`Contacts`,
icon: <IconUsersGroup />
},
{
name: 'addresses',
label: t`Addresses`,
icon: <IconMap2 />
},
{
name: 'attachments',
label: t`Attachments`,
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiPaths.company_attachment_list}
model="company"
pk={company.pk ?? -1}
/>
)
},
{
name: 'notes',
label: t`Notes`,
icon: <IconNotes />,
content: (
<NotesEditor
url={apiUrl(ApiPaths.company_list, company.pk)}
data={company?.notes ?? ''}
allowEdit={true}
/>
)
}
];
}, [id, company]);
const companyDetail = useMemo(() => {
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail
src={String(company.image || '')}
size={128}
alt={company?.name}
/>
<Stack spacing="xs">
<Text size="lg" weight={500}>
{company.name}
</Text>
<Text size="sm">{company.description}</Text>
</Stack>
</Group>
);
}, [id, company]);
const companyActions = useMemo(() => {
// TODO: Finer fidelity on these permissions, perhaps?
let canEdit = user.checkUserRole('purchase_order', 'change');
let canDelete = user.checkUserRole('purchase_order', 'delete');
return [
<ActionDropdown
tooltip={t`Company Actions`}
icon={<IconDots />}
actions={[
{
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: t`Edit company`,
disabled: !canEdit,
onClick: () => {
if (company?.pk) {
editCompany({
pk: company?.pk,
callback: refreshInstance
});
}
}
},
{
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: t`Delete company`,
disabled: !canDelete
}
]}
/>
];
}, [id, company, user]);
return (
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
detail={companyDetail}
actions={companyActions}
breadcrumbs={props.breadcrumbs}
/>
<PanelGroup pageKey="company" panels={companyPanels} />
</Stack>
);
}

View File

@ -0,0 +1,12 @@
import { t } from '@lingui/macro';
import CompanyDetail from './CompanyDetail';
export default function CustomerDetail() {
return (
<CompanyDetail
title={t`Customer`}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/>
);
}

View File

@ -0,0 +1,12 @@
import { t } from '@lingui/macro';
import CompanyDetail from './CompanyDetail';
export default function ManufacturerDetail() {
return (
<CompanyDetail
title={t`Manufacturer`}
breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]}
/>
);
}

View File

@ -0,0 +1,12 @@
import { t } from '@lingui/macro';
import CompanyDetail from './CompanyDetail';
export default function SupplierDetail() {
return (
<CompanyDetail
title={t`Supplier`}
breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]}
/>
);
}

View File

@ -1,34 +1,37 @@
import { t } from '@lingui/macro';
import {
Alert,
Button,
Group,
LoadingOverlay,
Stack,
Text
} from '@mantine/core';
import { Group, LoadingOverlay, Stack, Text } from '@mantine/core';
import {
IconBuilding,
IconCalendarStats,
IconClipboardList,
IconCopy,
IconCurrencyDollar,
IconDots,
IconEdit,
IconInfoCircle,
IconLayersLinked,
IconLink,
IconList,
IconListTree,
IconNotes,
IconPackages,
IconPaperclip,
IconQrcode,
IconShoppingCart,
IconStack2,
IconTestPipe,
IconTools,
IconTransfer,
IconTrash,
IconTruckDelivery,
IconUnlink,
IconVersions
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { ApiImage } from '../../components/images/ApiImage';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
@ -40,6 +43,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editPart } from '../../functions/forms/PartForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
/**
* Detail view for a single Part instance
@ -47,6 +51,8 @@ import { ApiPaths, apiUrl } from '../../states/ApiState';
export default function PartDetail() {
const { id } = useParams();
const user = useUserState();
const {
instance: part,
refreshInstance,
@ -66,8 +72,7 @@ export default function PartDetail() {
{
name: 'details',
label: t`Details`,
icon: <IconInfoCircle />,
content: <PlaceholderPanel />
icon: <IconInfoCircle />
},
{
name: 'parameters',
@ -98,55 +103,57 @@ export default function PartDetail() {
name: 'bom',
label: t`Bill of Materials`,
icon: <IconListTree />,
hidden: !part.assembly,
content: <PlaceholderPanel />
hidden: !part.assembly
},
{
name: 'builds',
label: t`Build Orders`,
icon: <IconTools />,
hidden: !part.assembly && !part.component,
content: <PlaceholderPanel />
hidden: !part.assembly && !part.component
},
{
name: 'used_in',
label: t`Used In`,
icon: <IconStack2 />,
hidden: !part.component,
content: <PlaceholderPanel />
hidden: !part.component
},
{
name: 'pricing',
label: t`Pricing`,
icon: <IconCurrencyDollar />,
content: <PlaceholderPanel />
icon: <IconCurrencyDollar />
},
{
name: 'suppliers',
label: t`Suppliers`,
icon: <IconBuilding />,
hidden: !part.purchaseable,
content: <PlaceholderPanel />
hidden: !part.purchaseable
},
{
name: 'purchase_orders',
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
content: <PlaceholderPanel />,
hidden: !part.purchaseable
},
{
name: 'sales_orders',
label: t`Sales Orders`,
icon: <IconTruckDelivery />,
content: <PlaceholderPanel />,
hidden: !part.salable
},
{
name: 'scheduling',
label: t`Scheduling`,
icon: <IconCalendarStats />
},
{
name: 'stocktake',
label: t`Stocktake`,
icon: <IconClipboardList />
},
{
name: 'test_templates',
label: t`Test Templates`,
icon: <IconTestPipe />,
content: <PlaceholderPanel />,
hidden: !part.trackable
},
{
@ -212,6 +219,79 @@ export default function PartDetail() {
);
}, [part, id]);
const partActions = useMemo(() => {
// TODO: Disable actions based on user permissions
return [
<ActionDropdown
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={[
{
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View part barcode`
},
{
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode to part`,
disabled: part?.barcode_hash
},
{
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode from part`,
disabled: !part?.barcode_hash
}
]}
/>,
<ActionDropdown
tooltip={t`Stock Actions`}
icon={<IconPackages />}
actions={[
{
icon: <IconClipboardList color="blue" />,
name: t`Count Stock`,
tooltip: t`Count part stock`
},
{
icon: <IconTransfer color="blue" />,
name: t`Transfer Stock`,
tooltip: t`Transfer part stock`
}
]}
/>,
<ActionDropdown
tooltip={t`Part Actions`}
icon={<IconDots />}
actions={[
{
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: t`Edit part`,
onClick: () => {
part.pk &&
editPart({
part_id: part.pk,
callback: refreshInstance
});
}
},
{
icon: <IconCopy color="green" />,
name: t`Duplicate`,
tooltip: t`Duplicate part`
},
{
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: t`Delete part`
}
]}
/>
];
}, [id, part, user]);
return (
<>
<Stack spacing="xs">
@ -219,21 +299,7 @@ export default function PartDetail() {
<PageDetail
detail={partDetail}
breadcrumbs={breadcrumbs}
actions={[
<Button
variant="outline"
color="blue"
onClick={() =>
part.pk &&
editPart({
part_id: part.pk,
callback: refreshInstance
})
}
>
Edit Part
</Button>
]}
actions={partActions}
/>
<PanelGroup pageKey="part" panels={partPanels} />
</Stack>

View File

@ -26,13 +26,23 @@ export default function PurchasingIndex() {
name: 'suppliers',
label: t`Suppliers`,
icon: <IconBuildingStore />,
content: <CompanyTable params={{ is_supplier: true }} />
content: (
<CompanyTable
path="purchasing/supplier"
params={{ is_supplier: true }}
/>
)
},
{
name: 'manufacturer',
label: t`Manufacturers`,
icon: <IconBuildingFactory2 />,
content: <CompanyTable params={{ is_manufacturer: true }} />
content: (
<CompanyTable
path="purchasing/manufacturer"
params={{ is_manufacturer: true }}
/>
)
}
];
}, []);

View File

@ -32,7 +32,9 @@ export default function PurchasingIndex() {
name: 'suppliers',
label: t`Customers`,
icon: <IconBuildingStore />,
content: <CompanyTable params={{ is_customer: true }} />
content: (
<CompanyTable path="sales/customer" params={{ is_customer: true }} />
)
}
];
}, []);

View File

@ -12,6 +12,22 @@ export const Playground = Loadable(
lazy(() => import('./pages/Index/Playground'))
);
export const CompanyDetail = Loadable(
lazy(() => import('./pages/company/CompanyDetail'))
);
export const CustomerDetail = Loadable(
lazy(() => import('./pages/company/CustomerDetail'))
);
export const SupplierDetail = Loadable(
lazy(() => import('./pages/company/SupplierDetail'))
);
export const ManufacturerDetail = Loadable(
lazy(() => import('./pages/company/ManufacturerDetail'))
);
export const CategoryDetail = Loadable(
lazy(() => import('./pages/part/CategoryDetail'))
);
@ -109,9 +125,13 @@ export const routes = (
</Route>
<Route path="purchasing/">
<Route index element={<PurchasingIndex />} />
<Route path="supplier/:id/" element={<SupplierDetail />} />
<Route path="manufacturer/:id/" element={<ManufacturerDetail />} />
</Route>
<Route path="company/:id/" element={<CompanyDetail />} />
<Route path="sales/">
<Route index element={<SalesIndex />} />
<Route path="customer/:id/" element={<CustomerDetail />} />
</Route>
<Route path="/profile/:tabValue" element={<Profile />} />
</Route>

View File

@ -55,6 +55,7 @@ export enum ApiPaths {
// Company URLs
company_list = 'api-company-list',
company_attachment_list = 'api-company-attachment-list',
supplier_part_list = 'api-supplier-part-list',
// Stock Item URLs
@ -137,6 +138,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'part/attachment/';
case ApiPaths.company_list:
return 'company/';
case ApiPaths.company_attachment_list:
return 'company/attachment/';
case ApiPaths.supplier_part_list:
return 'company/part/';
case ApiPaths.stock_item_list:

View File

@ -0,0 +1,16 @@
import { create } from 'zustand';
interface ModalStateProps {
loading: boolean;
lock: () => void;
unlock: () => void;
}
/**
* Global state manager for modal forms.
*/
export const useModalState = create<ModalStateProps>((set) => ({
loading: false,
lock: () => set(() => ({ loading: true })),
unlock: () => set(() => ({ loading: false }))
}));

View File

@ -10,6 +10,7 @@ interface UserStateProps {
username: () => string;
setUser: (newUser: UserProps) => void;
fetchUserState: () => void;
checkUserRole: (role: string, permission: string) => boolean;
}
/**