diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx
index 50dc588f01..3370f368eb 100644
--- a/src/frontend/src/components/forms/ApiForm.tsx
+++ b/src/frontend/src/components/forms/ApiForm.tsx
@@ -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({
)}
{preFormElement}
-
-
- {Object.entries(props.fields ?? {}).map(
- ([fieldName, field]) =>
- !field.hidden && (
-
- )
- )}
-
-
+
+ {Object.entries(props.fields ?? {}).map(
+ ([fieldName, field]) =>
+ !field.hidden && (
+
+ )
+ )}
+
{postFormElement}
diff --git a/src/frontend/src/components/images/Thumbnail.tsx b/src/frontend/src/components/images/Thumbnail.tsx
index 2e7524014c..3798d1ae10 100644
--- a/src/frontend/src/components/images/Thumbnail.tsx
+++ b/src/frontend/src/components/images/Thumbnail.tsx
@@ -19,7 +19,7 @@ export function Thumbnail({
return (
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 ? (
+
+ ) : null;
+}
diff --git a/src/frontend/src/components/nav/Layout.tsx b/src/frontend/src/components/nav/Layout.tsx
index fdcbe6c578..d685a2c6ad 100644
--- a/src/frontend/src/components/nav/Layout.tsx
+++ b/src/frontend/src/components/nav/Layout.tsx
@@ -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 (
+
diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx
index be9257f618..6ecd565fcf 100644
--- a/src/frontend/src/components/nav/PageDetail.tsx
+++ b/src/frontend/src/components/nav/PageDetail.tsx
@@ -41,7 +41,11 @@ export function PageDetail({
- {actions && {actions}}
+ {actions && (
+
+ {actions}
+
+ )}
diff --git a/src/frontend/src/components/tables/general/AttachmentTable.tsx b/src/frontend/src/components/tables/general/AttachmentTable.tsx
index 86907aa81f..391be18191 100644
--- a/src/frontend/src/components/tables/general/AttachmentTable.tsx
+++ b/src/frontend/src/components/tables/general/AttachmentTable.tsx
@@ -224,20 +224,22 @@ export function AttachmentTable({
return (
-
+ {pk && pk > 0 && (
+
+ )}
{allowEdit && validPk && (
diff --git a/src/frontend/src/components/tables/general/CompanyTable.tsx b/src/frontend/src/components/tables/general/CompanyTable.tsx
index 5880c9fe8c..2a7cfe0582 100644
--- a/src/frontend/src/components/tables/general/CompanyTable.tsx
+++ b/src/frontend/src/components/tables/general/CompanyTable.tsx
@@ -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 (
@@ -56,6 +65,10 @@ export function CompanyTable({ params }: { params?: any }) {
props={{
params: {
...params
+ },
+ onRowClick: (row: any) => {
+ let base = path ?? 'company';
+ navigate(`/${base}/${row.pk}`);
}
}}
/>
diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx
index a93376fdec..eb46c51145 100644
--- a/src/frontend/src/functions/forms.tsx
+++ b/src/frontend/src/functions/forms.tsx
@@ -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) {
)
});
+
+ useModalState.getState().unlock();
})
.catch((error) => {
+ useModalState.getState().unlock();
+
console.log('Error:', error);
if (error.response) {
invalidResponse(error.response.status);
diff --git a/src/frontend/src/functions/forms/CompanyForms.tsx b/src/frontend/src/functions/forms/CompanyForms.tsx
new file mode 100644
index 0000000000..84a87554f1
--- /dev/null
+++ b/src/frontend/src/functions/forms/CompanyForms.tsx
@@ -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:
+ },
+ currency: {
+ icon:
+ },
+ phone: {
+ icon:
+ },
+ email: {
+ icon:
+ },
+ 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
+ });
+}
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index b77416aed4..72e60b57eb 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -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 [
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`View`,
+ tooltip: t`View part barcode`
+ },
+ {
+ icon: ,
+ name: t`Link Barcode`,
+ tooltip: t`Link custom barcode to part`,
+ disabled: build?.barcode_hash
+ },
+ {
+ icon: ,
+ name: t`Unlink Barcode`,
+ tooltip: t`Unlink custom barcode from part`,
+ disabled: !build?.barcode_hash
+ }
+ ]}
+ />,
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`Report`,
+ tooltip: t`Print build report`
+ }
+ ]}
+ />,
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`Edit`,
+ tooltip: t`Edit build order`
+ },
+ {
+ icon: ,
+ name: t`Duplicate`,
+ tooltip: t`Duplicate build order`
+ },
+ {
+ icon: ,
+ name: t`Delete`,
+ tooltip: t`Delete build order`
+ }
+ ]}
+ />
+ ];
+ }, [id, build, user]);
+
return (
<>
- TODO: Build details
-
- }
breadcrumbs={[
{ name: t`Build Orders`, url: '/build' },
{ name: build.reference, url: `/build/${build.pk}` }
]}
- actions={[]}
+ actions={buildActions}
/>
diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx
new file mode 100644
index 0000000000..f080fbb6e2
--- /dev/null
+++ b/src/frontend/src/pages/company/CompanyDetail.tsx
@@ -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:
+ },
+ {
+ name: 'manufactured-parts',
+ label: t`Manufactured Parts`,
+ icon: ,
+ hidden: !company?.is_manufacturer
+ },
+ {
+ name: 'supplied-parts',
+ label: t`Supplied Parts`,
+ icon: ,
+ hidden: !company?.is_supplier
+ },
+ {
+ name: 'purchase-orders',
+ label: t`Purchase Orders`,
+ icon: ,
+ hidden: !company?.is_supplier,
+ content: company?.pk && (
+
+ )
+ },
+ {
+ name: 'stock-items',
+ label: t`Stock Items`,
+ icon: ,
+ hidden: !company?.is_manufacturer && !company?.is_supplier,
+ content: company?.pk && (
+
+ )
+ },
+ {
+ name: 'sales-orders',
+ label: t`Sales Orders`,
+ icon: ,
+ hidden: !company?.is_customer,
+ content: company?.pk && (
+
+ )
+ },
+ {
+ name: 'return-orders',
+ label: t`Return Orders`,
+ icon: ,
+ hidden: !company?.is_customer,
+ content: company.pk && (
+
+ )
+ },
+ {
+ name: 'assigned-stock',
+ label: t`Assigned Stock`,
+ icon: ,
+ hidden: !company?.is_customer
+ },
+ {
+ name: 'contacts',
+ label: t`Contacts`,
+ icon:
+ },
+ {
+ name: 'addresses',
+ label: t`Addresses`,
+ icon:
+ },
+ {
+ name: 'attachments',
+ label: t`Attachments`,
+ icon: ,
+ content: (
+
+ )
+ },
+ {
+ name: 'notes',
+ label: t`Notes`,
+ icon: ,
+ content: (
+
+ )
+ }
+ ];
+ }, [id, company]);
+
+ const companyDetail = useMemo(() => {
+ return (
+
+
+
+
+ {company.name}
+
+ {company.description}
+
+
+ );
+ }, [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 [
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`Edit`,
+ tooltip: t`Edit company`,
+ disabled: !canEdit,
+ onClick: () => {
+ if (company?.pk) {
+ editCompany({
+ pk: company?.pk,
+ callback: refreshInstance
+ });
+ }
+ }
+ },
+ {
+ icon: ,
+ name: t`Delete`,
+ tooltip: t`Delete company`,
+ disabled: !canDelete
+ }
+ ]}
+ />
+ ];
+ }, [id, company, user]);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/frontend/src/pages/company/CustomerDetail.tsx b/src/frontend/src/pages/company/CustomerDetail.tsx
new file mode 100644
index 0000000000..2725180dbe
--- /dev/null
+++ b/src/frontend/src/pages/company/CustomerDetail.tsx
@@ -0,0 +1,12 @@
+import { t } from '@lingui/macro';
+
+import CompanyDetail from './CompanyDetail';
+
+export default function CustomerDetail() {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/pages/company/ManufacturerDetail.tsx b/src/frontend/src/pages/company/ManufacturerDetail.tsx
new file mode 100644
index 0000000000..aa04b0405c
--- /dev/null
+++ b/src/frontend/src/pages/company/ManufacturerDetail.tsx
@@ -0,0 +1,12 @@
+import { t } from '@lingui/macro';
+
+import CompanyDetail from './CompanyDetail';
+
+export default function ManufacturerDetail() {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/pages/company/SupplierDetail.tsx b/src/frontend/src/pages/company/SupplierDetail.tsx
new file mode 100644
index 0000000000..5be35dda8e
--- /dev/null
+++ b/src/frontend/src/pages/company/SupplierDetail.tsx
@@ -0,0 +1,12 @@
+import { t } from '@lingui/macro';
+
+import CompanyDetail from './CompanyDetail';
+
+export default function SupplierDetail() {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 7b722191f4..3179a65ae7 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -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: ,
- content:
+ icon:
},
{
name: 'parameters',
@@ -98,55 +103,57 @@ export default function PartDetail() {
name: 'bom',
label: t`Bill of Materials`,
icon: ,
- hidden: !part.assembly,
- content:
+ hidden: !part.assembly
},
{
name: 'builds',
label: t`Build Orders`,
icon: ,
- hidden: !part.assembly && !part.component,
- content:
+ hidden: !part.assembly && !part.component
},
{
name: 'used_in',
label: t`Used In`,
icon: ,
- hidden: !part.component,
- content:
+ hidden: !part.component
},
{
name: 'pricing',
label: t`Pricing`,
- icon: ,
- content:
+ icon:
},
{
name: 'suppliers',
label: t`Suppliers`,
icon: ,
- hidden: !part.purchaseable,
- content:
+ hidden: !part.purchaseable
},
{
name: 'purchase_orders',
label: t`Purchase Orders`,
icon: ,
- content: ,
hidden: !part.purchaseable
},
{
name: 'sales_orders',
label: t`Sales Orders`,
icon: ,
- content: ,
hidden: !part.salable
},
+ {
+ name: 'scheduling',
+ label: t`Scheduling`,
+ icon:
+ },
+ {
+ name: 'stocktake',
+ label: t`Stocktake`,
+ icon:
+ },
{
name: 'test_templates',
label: t`Test Templates`,
icon: ,
- content: ,
hidden: !part.trackable
},
{
@@ -212,6 +219,79 @@ export default function PartDetail() {
);
}, [part, id]);
+ const partActions = useMemo(() => {
+ // TODO: Disable actions based on user permissions
+ return [
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`View`,
+ tooltip: t`View part barcode`
+ },
+ {
+ icon: ,
+ name: t`Link Barcode`,
+ tooltip: t`Link custom barcode to part`,
+ disabled: part?.barcode_hash
+ },
+ {
+ icon: ,
+ name: t`Unlink Barcode`,
+ tooltip: t`Unlink custom barcode from part`,
+ disabled: !part?.barcode_hash
+ }
+ ]}
+ />,
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`Count Stock`,
+ tooltip: t`Count part stock`
+ },
+ {
+ icon: ,
+ name: t`Transfer Stock`,
+ tooltip: t`Transfer part stock`
+ }
+ ]}
+ />,
+ }
+ actions={[
+ {
+ icon: ,
+ name: t`Edit`,
+ tooltip: t`Edit part`,
+ onClick: () => {
+ part.pk &&
+ editPart({
+ part_id: part.pk,
+ callback: refreshInstance
+ });
+ }
+ },
+ {
+ icon: ,
+ name: t`Duplicate`,
+ tooltip: t`Duplicate part`
+ },
+ {
+ icon: ,
+ name: t`Delete`,
+ tooltip: t`Delete part`
+ }
+ ]}
+ />
+ ];
+ }, [id, part, user]);
+
return (
<>
@@ -219,21 +299,7 @@ export default function PartDetail() {
- part.pk &&
- editPart({
- part_id: part.pk,
- callback: refreshInstance
- })
- }
- >
- Edit Part
-
- ]}
+ actions={partActions}
/>
diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx
index 4e2c7969d0..4209d76a1e 100644
--- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx
+++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx
@@ -26,13 +26,23 @@ export default function PurchasingIndex() {
name: 'suppliers',
label: t`Suppliers`,
icon: ,
- content:
+ content: (
+
+ )
},
{
name: 'manufacturer',
label: t`Manufacturers`,
icon: ,
- content:
+ content: (
+
+ )
}
];
}, []);
diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx
index 9ed47387f7..0bfe2a5b74 100644
--- a/src/frontend/src/pages/sales/SalesIndex.tsx
+++ b/src/frontend/src/pages/sales/SalesIndex.tsx
@@ -32,7 +32,9 @@ export default function PurchasingIndex() {
name: 'suppliers',
label: t`Customers`,
icon: ,
- content:
+ content: (
+
+ )
}
];
}, []);
diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx
index 33c1e65119..f67a85ece8 100644
--- a/src/frontend/src/router.tsx
+++ b/src/frontend/src/router.tsx
@@ -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 = (
} />
+ } />
+ } />
+ } />
} />
+ } />
} />
diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx
index d5069b1d3b..47fe1bbd5f 100644
--- a/src/frontend/src/states/ApiState.tsx
+++ b/src/frontend/src/states/ApiState.tsx
@@ -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:
diff --git a/src/frontend/src/states/ModalState.tsx b/src/frontend/src/states/ModalState.tsx
new file mode 100644
index 0000000000..6c6e585102
--- /dev/null
+++ b/src/frontend/src/states/ModalState.tsx
@@ -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((set) => ({
+ loading: false,
+ lock: () => set(() => ({ loading: true })),
+ unlock: () => set(() => ({ loading: false }))
+}));
diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx
index d8edeee061..05ae161660 100644
--- a/src/frontend/src/states/UserState.tsx
+++ b/src/frontend/src/states/UserState.tsx
@@ -10,6 +10,7 @@ interface UserStateProps {
username: () => string;
setUser: (newUser: UserProps) => void;
fetchUserState: () => void;
+ checkUserRole: (role: string, permission: string) => boolean;
}
/**