2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-21 16:56:47 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue5729

This commit is contained in:
Matthias Mair
2023-10-19 07:36:24 +02:00
166 changed files with 765 additions and 1081 deletions
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { api } from '../App';
import { ApiPaths, apiUrl } from '../states/ApiState';
import { StatisticItem } from './items/DashboardItem';
import { ErrorItem } from './items/ErrorItem';
@@ -15,13 +16,13 @@ export function DashboardItemProxy({
}: {
id: string;
text: string;
url: string;
url: ApiPaths;
params: any;
autoupdate: boolean;
}) {
function fetchData() {
return api
.get(`${url}/?search=&offset=0&limit=25`, { params: params })
.get(`${apiUrl(url)}?search=&offset=0&limit=25`, { params: params })
.then((res) => res.data);
}
const { isLoading, error, data, isFetching } = useQuery({
+12 -6
View File
@@ -136,13 +136,19 @@ export function ApiForm({
useEffect(() => {
// Provide initial form data
Object.entries(props.fields ?? {}).forEach(([fieldName, field]) => {
if (field.value !== undefined) {
// fieldDefinition is supplied by the API, and can serve as a backup
let fieldDefinition = fieldDefinitions[fieldName] ?? {};
let v =
field.value ??
field.default ??
fieldDefinition.value ??
fieldDefinition.default ??
undefined;
if (v !== undefined) {
form.setValues({
[fieldName]: field.value
});
} else if (field.default !== undefined) {
form.setValues({
[fieldName]: field.default
[fieldName]: v
});
}
});
@@ -3,6 +3,7 @@ import {
Anchor,
Button,
Group,
Loader,
Paper,
PasswordInput,
Stack,
@@ -13,6 +14,7 @@ import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { doClassicLogin, doSimpleLogin } from '../../functions/auth';
@@ -25,12 +27,18 @@ export function AuthenticationForm() {
const [classicLoginMode, setMode] = useDisclosure(true);
const navigate = useNavigate();
const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false);
function handleLogin() {
setIsLoggingIn(true);
if (classicLoginMode === true) {
doClassicLogin(
classicForm.values.username,
classicForm.values.password
).then((ret) => {
setIsLoggingIn(false);
if (ret === false) {
notifications.show({
title: t`Login failed`,
@@ -49,6 +57,8 @@ export function AuthenticationForm() {
});
} else {
doSimpleLogin(simpleForm.values.email).then((ret) => {
setIsLoggingIn(false);
if (ret?.status === 'ok') {
notifications.show({
title: t`Mail delivery successful`,
@@ -126,11 +136,17 @@ export function AuthenticationForm() {
<Trans>I will use username and password</Trans>
)}
</Anchor>
<Button type="submit" onClick={handleLogin}>
{classicLoginMode ? (
<Trans>Log in</Trans>
<Button type="submit" disabled={isLoggingIn} onClick={handleLogin}>
{isLoggingIn ? (
<Loader size="sm" />
) : (
<Trans>Send mail</Trans>
<>
{classicLoginMode ? (
<Trans>Log In</Trans>
) : (
<Trans>Send Email</Trans>
)}
</>
)}
</Button>
</Group>
@@ -68,6 +68,7 @@ export type ApiFormFieldType = {
choices?: any[];
hidden?: boolean;
disabled?: boolean;
read_only?: boolean;
placeholder?: string;
description?: string;
preFieldContent?: JSX.Element | (() => JSX.Element);
@@ -115,6 +116,10 @@ export function constructField({
break;
}
// Clear out the 'read_only' attribute
def.disabled = def.disabled ?? def.read_only ?? false;
delete def['read_only'];
return def;
}
@@ -190,16 +195,26 @@ export function ApiFormField({
// Coerce the value to a numerical value
const numericalValue: number | undefined = useMemo(() => {
let val = 0;
switch (definition.field_type) {
case 'integer':
return parseInt(value);
val = parseInt(value) ?? 0;
break;
case 'decimal':
case 'float':
case 'number':
return parseFloat(value);
val = parseFloat(value) ?? 0;
break;
default:
return undefined;
break;
}
if (isNaN(val) || !isFinite(val)) {
val = 0;
}
return val;
}, [value]);
// Construct the individual field
@@ -34,8 +34,6 @@ export function ChoiceField({
definitions: definitions
});
form.setValues({ [fieldName]: def.value ?? def.default });
return def;
}, [fieldName, field, definitions]);
@@ -37,16 +37,18 @@ export function RelatedModelField({
// Extract field definition from provided data
// Where user has provided specific data, override the API definition
const definition: ApiFormFieldType = useMemo(
() =>
constructField({
form: form,
field: field,
fieldName: fieldName,
definitions: definitions
}),
[form.values, field, definitions]
);
const definition: ApiFormFieldType = useMemo(() => {
let def = constructField({
form: form,
field: field,
fieldName: fieldName,
definitions: definitions
});
// Remove the 'read_only' attribute (causes issues with Mantine)
delete def['read_only'];
return def;
}, [form.values, field, definitions]);
// Keep track of the primary key value for this field
const [pk, setPk] = useState<number | null>(null);
@@ -170,8 +172,20 @@ export function RelatedModelField({
}
}
/* Construct a "cut-down" version of the definition,
* which does not include any attributes that the lower components do not recognize
*/
const fieldDefinition = useMemo(() => {
return {
...definition,
onValueChange: undefined,
adjustFilters: undefined,
read_only: undefined
};
}, [definition]);
return (
<Input.Wrapper {...definition} error={error}>
<Input.Wrapper {...fieldDefinition} error={error}>
<Select
id={fieldId}
value={pk != null && data.find((item) => item.value == pk)}
@@ -347,7 +347,18 @@ export function InvenTreeTable({
setMissingRecordsText(
tableProps.noRecordsText ?? t`No records found`
);
return response.data;
// Extract returned data (accounting for pagination) and ensure it is a list
let results = response.data?.results ?? response.data ?? [];
if (!Array.isArray(results)) {
setMissingRecordsText(t`Server returned incorrect data type`);
results = [];
}
setRecordCount(response.data?.count ?? results.length);
return results;
case 400:
setMissingRecordsText(t`Bad request`);
break;
@@ -389,6 +400,8 @@ export function InvenTreeTable({
refetchOnMount: true
});
const [recordCount, setRecordCount] = useState<number>(0);
/*
* Reload the table whenever the refetch changes
* this allows us to programmatically refresh the table
@@ -487,7 +500,7 @@ export function InvenTreeTable({
loaderVariant="dots"
idAccessor={tableProps.idAccessor}
minHeight={200}
totalRecords={data?.count ?? data?.length ?? 0}
totalRecords={recordCount}
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
page={page}
onPageChange={setPage}
@@ -501,7 +514,7 @@ export function InvenTreeTable({
}
fetching={isFetching}
noRecordsText={missingRecordsText}
records={data?.results ?? data ?? []}
records={data}
columns={dataColumns}
onRowClick={tableProps.onRowClick}
/>
@@ -6,18 +6,18 @@ import { notifications } from '@mantine/notifications';
import { IconExternalLink, IconFileUpload } from '@tabler/icons-react';
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { api } from '../../../App';
import {
addAttachment,
deleteAttachment,
editAttachment
} from '../../functions/forms/AttachmentForms';
import { useTableRefresh } from '../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { AttachmentLink } from '../items/AttachmentLink';
import { TableColumn } from './Column';
import { InvenTreeTable } from './InvenTreeTable';
import { RowAction } from './RowActions';
} from '../../../functions/forms/AttachmentForms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AttachmentLink } from '../../items/AttachmentLink';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
/**
* Define set of columns to display for the attachment table
@@ -0,0 +1,63 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
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 }) {
const { tableKey } = useTableRefresh('company');
const columns = useMemo(() => {
return [
{
accessor: 'name',
title: t`Company Name`,
sortable: true,
render: (record: any) => {
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail
src={record.thumbnail ?? record.image}
alt={record.name}
size={24}
/>
<Text>{record.name}</Text>
</Group>
);
}
},
{
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
},
{
accessor: 'website',
title: t`Website`,
sortable: false,
switchable: true
}
];
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.company_list)}
tableKey={tableKey}
columns={columns}
props={{
params: {
...params
}
}}
/>
);
}
@@ -0,0 +1,101 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { InvenTreeTable } from '../InvenTreeTable';
export function PurchaseOrderTable({ params }: { params?: any }) {
const { tableKey } = useTableRefresh('purchase-order');
// TODO: Custom filters
// TODO: Row actions
// TODO: Table actions (e.g. create new purchase order)
const tableColumns = useMemo(() => {
return [
{
accessor: 'reference',
title: t`Reference`,
sortable: true,
switchable: false
},
{
accessor: 'description',
title: t`Description`,
switchable: true
},
{
accessor: 'supplier__name',
title: t`Supplier`,
sortable: true,
render: function (record: any) {
let supplier = record.supplier_detail ?? {};
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail src={supplier?.image} alt={supplier.name} />
<Text>{supplier?.name}</Text>
</Group>
);
}
},
{
accessor: 'supplier_reference',
title: t`Supplier Reference`,
switchable: true
},
{
accessor: 'project_code',
title: t`Project Code`,
switchable: true
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
switchable: true
// TODO: Custom formatter
},
{
accessor: 'creation_date',
title: t`Created`,
switchable: true
// TODO: Custom formatter
},
{
accessor: 'target_date',
title: t`Target Date`,
switchable: true
// TODO: Custom formatter
},
{
accessor: 'line_items',
title: t`Line Items`,
sortable: true,
switchable: true
}
// TODO: total_price
// TODO: responsible
];
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.purchase_order_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
params: {
...params,
supplier_detail: true
}
}}
/>
);
}
@@ -0,0 +1,85 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) {
const { tableKey } = useTableRefresh('return-orders');
// TODO: Custom filters
// TODO: Row actions
// TODO: Table actions (e.g. create new return order)
const tableColumns = useMemo(() => {
return [
{
accessor: 'reference',
title: t`Return Order`,
sortable: true
},
{
accessor: 'description',
title: t`Description`,
switchable: true
},
{
accessor: 'customer__name',
title: t`Customer`,
sortable: true,
render: function (record: any) {
let customer = record.customer_detail ?? {};
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail src={customer?.image} alt={customer.name} />
<Text>{customer?.name}</Text>
</Group>
);
}
},
{
accessor: 'customer_reference',
title: t`Customer Reference`,
switchable: true
},
{
accessor: 'project_code',
title: t`Project Code`,
switchable: true
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
switchable: true
// TODO: Custom formatter
}
// TODO: Creation date
// TODO: Target date
// TODO: Line items
// TODO: Responsible
// TODO: Total cost
];
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.return_order_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
params: {
...params,
customer_detail: true
}
}}
/>
);
}
@@ -0,0 +1,87 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { InvenTreeTable } from '../InvenTreeTable';
export function SalesOrderTable({ params }: { params?: any }) {
const { tableKey } = useTableRefresh('sales-order');
// TODO: Custom filters
// TODO: Row actions
// TODO: Table actions (e.g. create new sales order)
const tableColumns = useMemo(() => {
return [
{
accessor: 'reference',
title: t`Sales Order`,
sortable: true,
switchable: false
},
{
accessor: 'description',
title: t`Description`,
switchable: true
},
{
accessor: 'customer__name',
title: t`Customer`,
sortable: true,
render: function (record: any) {
let customer = record.customer_detail ?? {};
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail src={customer?.image} alt={customer.name} />
<Text>{customer?.name}</Text>
</Group>
);
}
},
{
accessor: 'customer_reference',
title: t`Customer Reference`,
switchable: true
},
{
accessor: 'project_code',
title: t`Project Code`,
switchable: true
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
switchable: true
// TODO: Custom formatter
}
// TODO: Creation date
// TODO: Target date
// TODO: Shipment date
// TODO: Line items
// TODO: Total price
];
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.sales_order_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
params: {
...params,
customer_detail: true
}
}}
/>
);
}
@@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { ActionIcon, Text, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconCirclePlus } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
@@ -9,7 +8,6 @@ import {
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column';
+26 -19
View File
@@ -1,125 +1,132 @@
import { t } from '@lingui/macro';
import { ApiPaths, apiUrl } from '../states/ApiState';
import { ApiPaths } from '../states/ApiState';
export const dashboardItems = [
interface DashboardItems {
id: string;
text: string;
icon: string;
url: ApiPaths;
params: any;
}
export const dashboardItems: DashboardItems[] = [
{
id: 'starred-parts',
text: t`Subscribed Parts`,
icon: 'fa-bell',
url: apiUrl(ApiPaths.part_list),
url: ApiPaths.part_list,
params: { starred: true }
},
{
id: 'starred-categories',
text: t`Subscribed Categories`,
icon: 'fa-bell',
url: apiUrl(ApiPaths.category_list),
url: ApiPaths.category_list,
params: { starred: true }
},
{
id: 'latest-parts',
text: t`Latest Parts`,
icon: 'fa-newspaper',
url: apiUrl(ApiPaths.part_list),
url: ApiPaths.part_list,
params: { ordering: '-creation_date', limit: 10 }
},
{
id: 'bom-validation',
text: t`BOM Waiting Validation`,
icon: 'fa-times-circle',
url: apiUrl(ApiPaths.part_list),
url: ApiPaths.part_list,
params: { bom_valid: false }
},
{
id: 'recently-updated-stock',
text: t`Recently Updated`,
icon: 'fa-clock',
url: apiUrl(ApiPaths.stock_item_list),
url: ApiPaths.stock_item_list,
params: { part_detail: true, ordering: '-updated', limit: 10 }
},
{
id: 'low-stock',
text: t`Low Stock`,
icon: 'fa-flag',
url: apiUrl(ApiPaths.part_list),
url: ApiPaths.part_list,
params: { low_stock: true }
},
{
id: 'depleted-stock',
text: t`Depleted Stock`,
icon: 'fa-times',
url: apiUrl(ApiPaths.part_list),
url: ApiPaths.part_list,
params: { depleted_stock: true }
},
{
id: 'stock-to-build',
text: t`Required for Build Orders`,
icon: 'fa-bullhorn',
url: apiUrl(ApiPaths.part_list),
url: ApiPaths.part_list,
params: { stock_to_build: true }
},
{
id: 'expired-stock',
text: t`Expired Stock`,
icon: 'fa-calendar-times',
url: apiUrl(ApiPaths.stock_item_list),
url: ApiPaths.stock_item_list,
params: { expired: true }
},
{
id: 'stale-stock',
text: t`Stale Stock`,
icon: 'fa-stopwatch',
url: apiUrl(ApiPaths.stock_item_list),
url: ApiPaths.stock_item_list,
params: { stale: true, expired: true }
},
{
id: 'build-pending',
text: t`Build Orders In Progress`,
icon: 'fa-cogs',
url: apiUrl(ApiPaths.build_order_list),
url: ApiPaths.build_order_list,
params: { active: true }
},
{
id: 'build-overdue',
text: t`Overdue Build Orders`,
icon: 'fa-calendar-times',
url: apiUrl(ApiPaths.build_order_list),
url: ApiPaths.build_order_list,
params: { overdue: true }
},
{
id: 'po-outstanding',
text: t`Outstanding Purchase Orders`,
icon: 'fa-sign-in-alt',
url: apiUrl(ApiPaths.purchase_order_list),
url: ApiPaths.purchase_order_list,
params: { supplier_detail: true, outstanding: true }
},
{
id: 'po-overdue',
text: t`Overdue Purchase Orders`,
icon: 'fa-calendar-times',
url: apiUrl(ApiPaths.purchase_order_list),
url: ApiPaths.purchase_order_list,
params: { supplier_detail: true, overdue: true }
},
{
id: 'so-outstanding',
text: t`Outstanding Sales Orders`,
icon: 'fa-sign-out-alt',
url: apiUrl(ApiPaths.sales_order_list),
url: ApiPaths.sales_order_list,
params: { customer_detail: true, outstanding: true }
},
{
id: 'so-overdue',
text: t`Overdue Sales Orders`,
icon: 'fa-calendar-times',
url: apiUrl(ApiPaths.sales_order_list),
url: ApiPaths.sales_order_list,
params: { customer_detail: true, overdue: true }
},
{
id: 'news',
text: t`Current News`,
icon: 'fa-newspaper',
url: 'news',
url: ApiPaths.news,
params: {}
}
];
+3 -1
View File
@@ -25,7 +25,9 @@ export const navTabs = [
{ text: <Trans>Dashboard</Trans>, name: 'dashboard' },
{ text: <Trans>Parts</Trans>, name: 'part' },
{ text: <Trans>Stock</Trans>, name: 'stock' },
{ text: <Trans>Build</Trans>, name: 'build' }
{ text: <Trans>Build</Trans>, name: 'build' },
{ text: <Trans>Purchasing</Trans>, name: 'purchasing' },
{ text: <Trans>Sales</Trans>, name: 'sales' }
];
if (IS_DEV_OR_DEMO) {
navTabs.push({ text: <Trans>Playground</Trans>, name: 'playground' });
+13 -7
View File
@@ -20,7 +20,8 @@ export const doClassicLogin = async (username: string, password: string) => {
const token = await axios
.get(apiUrl(ApiPaths.user_token), {
auth: { username, password },
baseURL: host.toString()
baseURL: host.toString(),
timeout: 5000
})
.then((response) => response.data.token)
.catch((error) => {
@@ -62,7 +63,7 @@ export const doSimpleLogin = async (email: string) => {
email: email
})
.then((response) => response.data)
.catch((error) => {
.catch((_error) => {
return false;
});
return mail;
@@ -107,9 +108,14 @@ export function handleReset(navigate: any, values: { email: string }) {
});
}
export function checkLoginState(navigate: any) {
/**
* Check login state, and redirect the user as required
*/
export function checkLoginState(navigate: any, redirect?: string) {
api
.get(apiUrl(ApiPaths.user_token))
.get(apiUrl(ApiPaths.user_token), {
timeout: 5000
})
.then((val) => {
if (val.status === 200 && val.data.token) {
doTokenLogin(val.data.token);
@@ -120,13 +126,13 @@ export function checkLoginState(navigate: any) {
color: 'green',
icon: <IconCheck size="1rem" />
});
navigate('/home');
navigate(redirect ?? '/home');
} else {
navigate('/login');
}
})
.catch(() => {
.catch((error) => {
console.error('Error fetching login information:', error);
navigate('/login');
});
}
+5 -1
View File
@@ -69,8 +69,12 @@ export function extractAvailableFields(
name: fieldName,
field_type: field.type,
description: field.help_text,
value: field.value ?? field.default
value: field.value ?? field.default,
disabled: field.read_only ?? false
};
// Remove the 'read_only' field - plays havoc with react components
delete fields['read_only'];
}
return fields;
+15 -4
View File
@@ -1,5 +1,5 @@
import { Trans } from '@lingui/macro';
import { Text } from '@mantine/core';
import { Card, Container, Group, Loader, Stack, Text } from '@mantine/core';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -14,9 +14,20 @@ export default function Logged_In() {
return (
<>
<Text>
<Trans>Checking if you are already logged in</Trans>
</Text>
<Container>
<Stack align="center">
<Card shadow="sm" padding="lg" radius="md">
<Stack>
<Text size="lg">
<Trans>Checking if you are already logged in</Trans>
</Text>
<Group position="center">
<Loader />
</Group>
</Stack>
</Card>
</Stack>
</Container>
</>
);
}
+1 -1
View File
@@ -19,8 +19,8 @@ import {
} 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 { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance';
+2 -2
View File
@@ -31,7 +31,7 @@ import { ApiImage } from '../../components/images/ApiImage';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/AttachmentTable';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
@@ -180,7 +180,7 @@ export default function PartDetail() {
)
}
];
}, [part]);
}, [id, part]);
const breadcrumbs = useMemo(
() => [
@@ -0,0 +1,48 @@
import { t } from '@lingui/macro';
import { Stack } from '@mantine/core';
import {
IconBuildingFactory2,
IconBuildingStore,
IconShoppingCart
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/nav/PanelGroup';
import { CompanyTable } from '../../components/tables/general/CompanyTable';
import { PurchaseOrderTable } from '../../components/tables/purchasing/PurchaseOrderTable';
export default function PurchasingIndex() {
const panels = useMemo(() => {
return [
{
name: 'purchaseorders',
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
content: <PurchaseOrderTable />
// TODO: Add optional "calendar" display here...
},
{
name: 'suppliers',
label: t`Suppliers`,
icon: <IconBuildingStore />,
content: <CompanyTable params={{ is_supplier: true }} />
},
{
name: 'manufacturer',
label: t`Manufacturers`,
icon: <IconBuildingFactory2 />,
content: <CompanyTable params={{ is_manufacturer: true }} />
}
];
}, []);
return (
<>
<Stack>
<PageDetail title={t`Purchasing`} />
<PanelGroup pageKey="purchasing-index" panels={panels} />
</Stack>
</>
);
}
@@ -0,0 +1,48 @@
import { t } from '@lingui/macro';
import { Stack } from '@mantine/core';
import {
IconBuildingStore,
IconTruckDelivery,
IconTruckReturn
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/nav/PanelGroup';
import { CompanyTable } from '../../components/tables/general/CompanyTable';
import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable';
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
export default function PurchasingIndex() {
const panels = useMemo(() => {
return [
{
name: 'salesorders',
label: t`Sales Orders`,
icon: <IconTruckDelivery />,
content: <SalesOrderTable />
},
{
name: 'returnorders',
label: t`Return Orders`,
icon: <IconTruckReturn />,
content: <ReturnOrderTable />
},
{
name: 'suppliers',
label: t`Customers`,
icon: <IconBuildingStore />,
content: <CompanyTable params={{ is_customer: true }} />
}
];
}, []);
return (
<>
<Stack>
<PageDetail title={t`Sales`} />
<PanelGroup pageKey="sales-index" panels={panels} />
</Stack>
</>
);
}
+1 -1
View File
@@ -15,7 +15,7 @@ import { useParams } from 'react-router-dom';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/AttachmentTable';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
+14
View File
@@ -34,6 +34,14 @@ export const BuildDetail = Loadable(
lazy(() => import('./pages/build/BuildDetail'))
);
export const PurchasingIndex = Loadable(
lazy(() => import('./pages/purchasing/PurchasingIndex'))
);
export const SalesIndex = Loadable(
lazy(() => import('./pages/sales/SalesIndex'))
);
export const Scan = Loadable(lazy(() => import('./pages/Index/Scan')));
export const Dashboard = Loadable(
@@ -99,6 +107,12 @@ export const routes = (
<Route index element={<BuildIndex />} />
<Route path=":id/" element={<BuildDetail />} />
</Route>
<Route path="purchasing/">
<Route index element={<PurchasingIndex />} />
</Route>
<Route path="sales/">
<Route index element={<SalesIndex />} />
</Route>
<Route path="/profile/:tabValue" element={<Profile />} />
</Route>
<Route path="/" errorElement={<ErrorPage />}>
+8
View File
@@ -39,6 +39,7 @@ export enum ApiPaths {
notifications_list = 'api-notifications-list',
barcode = 'api-barcode',
news = 'news',
// Build order URLs
build_order_list = 'api-build-list',
@@ -67,6 +68,9 @@ export enum ApiPaths {
// Sales Order URLs
sales_order_list = 'api-sales-order-list',
// Return Order URLs
return_order_list = 'api-return-order-list',
// Plugin URLs
plugin_list = 'api-plugin-list',
@@ -113,6 +117,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'notifications/';
case ApiPaths.barcode:
return 'barcode/';
case ApiPaths.news:
return 'news/';
case ApiPaths.build_order_list:
return 'build/';
case ApiPaths.build_order_attachment_list:
@@ -143,6 +149,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'order/po/';
case ApiPaths.sales_order_list:
return 'order/so/';
case ApiPaths.return_order_list:
return 'order/ro/';
case ApiPaths.plugin_list:
return 'plugins/';
case ApiPaths.project_code_list:
+8 -3
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { api } from '../App';
import { doClassicLogout } from '../functions/auth';
import { ApiPaths, apiUrl } from './ApiState';
import { UserProps } from './states';
@@ -19,17 +20,19 @@ export const useUserState = create<UserStateProps>((set, get) => ({
username: () => {
const user: UserProps = get().user as UserProps;
if (user.first_name || user.last_name) {
if (user?.first_name || user?.last_name) {
return `${user.first_name} ${user.last_name}`.trim();
} else {
return user.username;
return user?.username ?? '';
}
},
setUser: (newUser: UserProps) => set({ user: newUser }),
fetchUserState: async () => {
// Fetch user data
await api
.get(apiUrl(ApiPaths.user_me))
.get(apiUrl(ApiPaths.user_me), {
timeout: 5000
})
.then((response) => {
const user: UserProps = {
first_name: response.data?.first_name ?? '',
@@ -41,6 +44,8 @@ export const useUserState = create<UserStateProps>((set, get) => ({
})
.catch((error) => {
console.error('Error fetching user data:', error);
// Redirect to login page
doClassicLogout();
});
// Fetch role data