Skip to content

Commit 669af1d

Browse files
committed
Add org view page
1 parent 1c7ee69 commit 669af1d

12 files changed

+208
-76
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { BsSliders } from 'react-icons/bs';
2+
import IconButton from './components/IconButton';
3+
import { Link } from 'react-router-dom';
4+
import { OrganizationDto } from 'shared-types';
5+
6+
export interface OrganizationSettingsSectionProps {
7+
organization: OrganizationDto;
8+
}
9+
10+
function OrganizationSettingsSection({ organization }: OrganizationSettingsSectionProps) {
11+
return (
12+
<div>
13+
<h2 className="mb-4 text-2xl">Settings</h2>
14+
<p className="mb-4 text-muted">Modify organization settings</p>
15+
<Link to={`../settings/${organization.id}`}>
16+
<IconButton icon={BsSliders}>Organization settings</IconButton>
17+
</Link>
18+
</div>
19+
);
20+
}
21+
export default OrganizationSettingsSection;

apps/client/src/components/InventoryAddForm.tsx

+26-52
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { useForm } from 'react-hook-form';
44
import toast from 'react-hot-toast';
55
import { BsArrowLeftRight } from 'react-icons/bs';
66
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
7-
import { AddInventoryItemDto, OrganizationDto } from 'shared-types';
7+
import {
8+
AddInventoryItemDto,
9+
CreateWarehouseDto,
10+
CreateWarehouseInOrgDto,
11+
OrganizationDto,
12+
WarehouseDto,
13+
} from 'shared-types';
814
import useProductsDetails from '../hooks/useProductsDetails';
915
import { Utils } from '../utils';
1016
import Button from './Button';
@@ -13,8 +19,8 @@ import FormError from './Form/FormError';
1319
import FormInput from './Form/FormInput';
1420

1521
type Inputs = {
16-
quantity: number;
17-
location: string;
22+
name: string;
23+
address: string;
1824
};
1925

2026
function InventoryAddForm() {
@@ -23,78 +29,47 @@ function InventoryAddForm() {
2329
const [loading, setLoading] = useState(false);
2430
const [error, setError] = useState<string | null>(null);
2531
const navigate = useNavigate();
26-
const [searchParams, setSearchParams] = useSearchParams();
27-
28-
const productId = searchParams.get('productId')!;
29-
const { product, error: productFetchError } = useProductsDetails(productId);
30-
31-
useEffect(() => {
32-
if (productId && productFetchError != undefined) {
33-
toast.error('Provided product ID is invalid, please input product manual');
34-
setSearchParams(undefined);
35-
}
36-
}, [productFetchError, productId, setSearchParams]);
3732

3833
function onSubmit(inputs: Inputs) {
3934
setLoading(true);
4035
setError(null);
4136

42-
const dto: AddInventoryItemDto = {
43-
warehouseId: appContext.currentWarehouse.id,
44-
productId: product.id,
45-
...inputs,
37+
const dto: CreateWarehouseInOrgDto = {
38+
organizationId: appContext.organization.id,
39+
warehouse: inputs,
4640
};
4741

48-
Utils.postFetcher<OrganizationDto>(`/api/inventory/`, dto)
42+
Utils.postFetcher<WarehouseDto>(`/api/organizations/warehouses/`, dto)
4943
.then(() => navigate('..'))
50-
.then(() => toast.success('Successfully created item'))
44+
.then(() => toast.success('Successfully created warehouse'))
5145
.catch((err) => setError(Utils.requestErrorToString(err)))
5246
.finally(() => setLoading(false));
5347
}
5448

5549
return (
5650
<form onSubmit={handleSubmit(onSubmit)}>
5751
<FormInput
58-
label="Warehouse"
52+
label="Organization"
5953
readOnly
6054
required
61-
value={appContext.currentWarehouse.name}
55+
value={appContext.organization.name}
6256
/>
6357

6458
<FormInput
65-
label="Product"
66-
disabled
67-
value={product?.name}
68-
noEndMargin
59+
label="Name"
60+
placeholder="US West Main"
61+
type="number"
6962
minLength={2}
7063
maxLength={32}
71-
required
64+
{...register('name')}
7265
/>
73-
<Link
74-
className={classNames('link-primary mb-1 ms-1 mt-3 flex items-center gap-2', {
75-
'animate-bounce': product == undefined,
76-
})}
77-
to={
78-
Utils.dashboardUrl(appContext.organization.id, appContext.currentWarehouse.id) +
79-
`/products`
80-
}
81-
>
82-
<BsArrowLeftRight /> {product ? 'Change' : 'Select'} product
83-
</Link>
8466

8567
<FormInput
86-
label="Quantity"
87-
hint="If you know current item quantity you can add it here"
88-
placeholder="0"
89-
type="number"
90-
{...register('quantity', { setValueAs: (v) => (v == null ? 0 : +v) })}
91-
/>
92-
93-
<FormInput
94-
label="Location"
95-
hint="Where is this item located in warehouse"
96-
placeholder="shelf / aisle / bin number"
97-
{...register('location')}
68+
label="Address"
69+
placeholder="18 Milton Street"
70+
minLength={2}
71+
maxLength={32}
72+
{...register('address')}
9873
/>
9974

10075
<FormError>{error}</FormError>
@@ -103,9 +78,8 @@ function InventoryAddForm() {
10378
role="submit"
10479
className="mt-4"
10580
loading={loading}
106-
disabled={product == undefined}
10781
>
108-
Add product to inventory
82+
Create warehouse
10983
</Button>
11084
</form>
11185
);

apps/client/src/components/OrganizationActions.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface OrganizationSettingsButtonProps {
99

1010
function OrganizationSettingsButton({ organization }: OrganizationSettingsButtonProps) {
1111
return (
12-
<Link to={organization.id + '/settings'}>
12+
<Link to={`/dashboard/view/${organization.id}`}>
1313
<ActionButton icon={BsSliders} />
1414
</Link>
1515
);

apps/client/src/components/OrganizationCard.tsx

+2-14
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,13 @@ import { OrganizationDto } from 'shared-types';
33
import OrganizationSettingsButton from './OrganizationActions';
44
import WarehouseInfoRow from './WarehouseInfoRow';
55
import ExpandableList from './Helpers/ExpandableList';
6+
import WarehousesList from './WarehousesList';
67

78
export interface OrganizationCardProps {
89
organization: OrganizationDto;
910
}
1011

1112
function OrganizationCard({ organization }: OrganizationCardProps) {
12-
const warehouseElements = organization.warehouses.map((warehouse, i) => (
13-
<WarehouseInfoRow
14-
organizationId={organization.id}
15-
warehouse={warehouse}
16-
key={i}
17-
/>
18-
));
19-
20-
const needExpander = warehouseElements.length > 3;
21-
2213
return (
2314
<div className="mt-12">
2415
<div className="flex justify-between border-b border-gray-300 px-8 py-4">
@@ -30,10 +21,7 @@ function OrganizationCard({ organization }: OrganizationCardProps) {
3021
<OrganizationSettingsButton organization={organization} />
3122
</span>
3223
</div>
33-
<div className="my-4">
34-
{warehouseElements.slice(0, needExpander ? 3 : 4)}
35-
{needExpander && <ExpandableList>{warehouseElements.slice(3)}</ExpandableList>}
36-
</div>
24+
<WarehousesList organization={organization} />
3725
</div>
3826
);
3927
}

apps/client/src/components/Pages/InventoryViewPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ function InventoryViewPage() {
4343

4444
<EntityInfoTable
4545
properties={{
46-
'internal ID': <code>{inventoryItem?.id}</code>,
4746
name: inventoryItem?.name,
47+
'internal ID': <code>{inventoryItem?.id}</code>,
4848
quantity: `${inventoryItem?.quantity} ${inventoryItem?.unit}`,
4949
location: inventoryItem?.location,
5050
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { BsBuildingGear, BsChevronLeft, BsPencil, BsTrash } from 'react-icons/bs';
2+
import { Link, useNavigate, useParams } from 'react-router-dom';
3+
import OrganizationSettingsSection from '../../OrganizationSettingsSection';
4+
import useOrganizationDetails from '../../hooks/useOrganisationDetails';
5+
import { Utils } from '../../utils';
6+
import Button from '../Button';
7+
import Container from '../Container';
8+
import EntityActionsRow from '../Entity/EntityActionsRow';
9+
import EntityInfoTable from '../Entity/EntityInfoTable';
10+
import HeaderWithHint from '../HeaderWithHint';
11+
import IconButton from '../IconButton';
12+
import DashboardLayout from '../Layout/DasboardLayout';
13+
import Loader from '../Loader';
14+
import { SecondaryNavbar } from '../SecondaryNavbar';
15+
import WarehousesList from '../WarehousesList';
16+
17+
function OrganizationViewPage() {
18+
const { id } = useParams();
19+
const { organization, isLoading, error } = useOrganizationDetails(id);
20+
const navigate = useNavigate();
21+
22+
const dashboardUrl = Utils.dashboardUrl(organization?.id, organization?.warehouses[0].id);
23+
24+
return (
25+
<DashboardLayout>
26+
<SecondaryNavbar
27+
icon={BsBuildingGear}
28+
title={organization?.name}
29+
actions={
30+
<Link to="/dashboard/select">
31+
<Button className="flex items-center gap-3">
32+
<BsChevronLeft size={20} />
33+
Go back
34+
</Button>
35+
</Link>
36+
}
37+
/>
38+
<Loader
39+
isLoading={isLoading}
40+
isError={error != undefined}
41+
>
42+
<Container>
43+
<div className="flex flex-col gap-14">
44+
<div>
45+
<HeaderWithHint
46+
hint="organization"
47+
className="mb-4 mt-8"
48+
>
49+
{organization?.name}
50+
</HeaderWithHint>
51+
<p className="mb-8 text-muted">
52+
This page offers insights into the specified StockedUp organization, representing
53+
the highest organizational units within the StockedUp system. Think of organizations
54+
as a centralized hub for managing all company resources.
55+
</p>
56+
57+
<EntityInfoTable
58+
properties={{
59+
name: organization?.name,
60+
'internal ID': <code>{organization?.id}</code>,
61+
currency: organization?.currency,
62+
warehouses: organization?.warehouses.length,
63+
'total value': Utils.humanizeCurrency(
64+
organization?.stats.totalValue,
65+
organization?.currency,
66+
),
67+
}}
68+
/>
69+
</div>
70+
71+
<OrganizationSettingsSection organization={organization} />
72+
73+
<div>
74+
<div className="flex items-baseline gap-2">
75+
<h2 className="mb-4 text-2xl">Warehouses</h2>
76+
<Link
77+
to={`${dashboardUrl}/warehouses/create`}
78+
className="link-primary"
79+
>
80+
[Create]
81+
</Link>
82+
</div>
83+
84+
<p className="mb-4 text-muted">
85+
This list comprises all warehouses created within the organization. Warehouses store
86+
information about stock quantity and location for products. It is recommended to
87+
create a StockedUp warehouse corresponding to each physical warehouse.
88+
</p>
89+
<WarehousesList organization={organization} />
90+
</div>
91+
</div>
92+
93+
<EntityActionsRow>
94+
<IconButton
95+
icon={BsPencil}
96+
onClick={() => navigate(`../edit/${organization.id}`)}
97+
>
98+
Edit
99+
</IconButton>
100+
<IconButton
101+
icon={BsTrash}
102+
onClick={() => navigate(`../delete/${organization.id}`)}
103+
>
104+
Delete
105+
</IconButton>
106+
</EntityActionsRow>
107+
</Container>
108+
</Loader>
109+
</DashboardLayout>
110+
);
111+
}
112+
113+
export default OrganizationViewPage;

apps/client/src/components/Pages/ProductViewPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ function ProductViewPage() {
3636

3737
<EntityInfoTable
3838
properties={{
39-
'internal ID': <code>{product?.id}</code>,
4039
name: product?.name,
40+
'internal ID': <code>{product?.id}</code>,
4141
'buy price': Utils.humanizeCurrency(
4242
product?.buyPrice,
4343
appContext.organization.currency,

apps/client/src/components/Pages/WarehouseViewPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ function WarehouseViewPage() {
2828
</div>
2929
<EntityInfoTable
3030
properties={{
31-
'internal ID': <code>{warehouse.id}</code>,
3231
name: warehouse.name,
32+
'internal ID': <code>{warehouse.id}</code>,
3333
organization: appContext.organization.name,
3434
address: warehouse.address,
3535
}}

apps/client/src/components/Router.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import RegisterForm from './RegisterForm';
2525
import RegisterSelect from './RegisterSelect';
2626
import { ProtectedRoute, PublicRoute } from './SpecialRoutes';
2727
import InventoryViewPage from './Pages/InventoryViewPage';
28+
import OrganizationViewPage from './Pages/OrganizationViewPage';
2829

2930
function Router() {
3031
const router = createBrowserRouter([
@@ -90,6 +91,10 @@ function Router() {
9091
path: 'create',
9192
element: <OrganizationCreatePage />,
9293
},
94+
{
95+
path: 'view/:id',
96+
element: <OrganizationViewPage />,
97+
},
9398
{
9499
path: ':organization/:warehouse',
95100
element: <DashboardPage />,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { OrganizationDto } from 'shared-types';
2+
import WarehouseInfoRow from './WarehouseInfoRow';
3+
import ExpandableList from './Helpers/ExpandableList';
4+
5+
export interface WarehousesListProps {
6+
organization: OrganizationDto;
7+
}
8+
9+
function WarehousesList({ organization }: WarehousesListProps) {
10+
const warehouseElements = organization.warehouses.map((warehouse, i) => (
11+
<WarehouseInfoRow
12+
organizationId={organization.id}
13+
warehouse={warehouse}
14+
key={i}
15+
/>
16+
));
17+
18+
const needExpander = warehouseElements.length > 3;
19+
20+
return (
21+
<div className="my-4">
22+
{warehouseElements.slice(0, needExpander ? 3 : 4)}
23+
{needExpander && <ExpandableList>{warehouseElements.slice(3)}</ExpandableList>}
24+
</div>
25+
);
26+
}
27+
export default WarehousesList;

0 commit comments

Comments
 (0)