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:
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
+8
-8
@@ -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';
|
||||
|
||||
@@ -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: {}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 />}>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user