diff --git a/assets/icons/productskuicon.svg b/assets/icons/productskuicon.svg
new file mode 100644
index 0000000..6a0ff42
--- /dev/null
+++ b/assets/icons/productskuicon.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/src/components/Dashboard/Management/ManagementSidebar.jsx b/src/components/Dashboard/Management/ManagementSidebar.jsx
index 2d42229..e315e40 100644
--- a/src/components/Dashboard/Management/ManagementSidebar.jsx
+++ b/src/components/Dashboard/Management/ManagementSidebar.jsx
@@ -3,6 +3,7 @@ import DashboardSidebar from '../common/DashboardSidebar'
import FilamentIcon from '../../Icons/FilamentIcon'
import PartIcon from '../../Icons/PartIcon'
import ProductIcon from '../../Icons/ProductIcon'
+import ProductSkuIcon from '../../Icons/ProductSkuIcon'
import VendorIcon from '../../Icons/VendorIcon'
import MaterialIcon from '../../Icons/MaterialIcon'
import NoteTypeIcon from '../../Icons/NoteTypeIcon'
@@ -42,6 +43,12 @@ const items = [
label: 'Products',
path: '/dashboard/management/products'
},
+ {
+ key: 'productSkus',
+ icon: ,
+ label: 'Product SKUs',
+ path: '/dashboard/management/productskus'
+ },
{
key: 'vendors',
icon: ,
@@ -176,6 +183,7 @@ const routeKeyMap = {
'/dashboard/management/users': 'users',
'/dashboard/management/apppasswords': 'appPasswords',
'/dashboard/management/products': 'products',
+ '/dashboard/management/productskus': 'productSkus',
'/dashboard/management/vendors': 'vendors',
'/dashboard/management/couriers': 'couriers',
'/dashboard/management/courierservices': 'courierServices',
diff --git a/src/components/Dashboard/Management/ProductSkus.jsx b/src/components/Dashboard/Management/ProductSkus.jsx
new file mode 100644
index 0000000..0b1bbf9
--- /dev/null
+++ b/src/components/Dashboard/Management/ProductSkus.jsx
@@ -0,0 +1,104 @@
+import { useState, useRef } from 'react'
+
+import { Button, Flex, Space, Modal, Dropdown } from 'antd'
+
+import NewProductSku from './ProductSkus/NewProductSku'
+import PlusIcon from '../../Icons/PlusIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import useColumnVisibility from '../hooks/useColumnVisibility'
+import ObjectTable from '../common/ObjectTable'
+import ListIcon from '../../Icons/ListIcon'
+import GridIcon from '../../Icons/GridIcon'
+import useViewMode from '../hooks/useViewMode'
+import ColumnViewButton from '../common/ColumnViewButton'
+import ExportListButton from '../common/ExportListButton'
+
+const ProductSkus = () => {
+ const tableRef = useRef()
+
+ const [newProductSkuOpen, setNewProductSkuOpen] = useState(false)
+
+ const [viewMode, setViewMode] = useViewMode('productSkus')
+
+ const [columnVisibility, setColumnVisibility] =
+ useColumnVisibility('productSku')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Product SKU',
+ key: 'newProductSku',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newProductSku') {
+ setNewProductSkuOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
+
+
+ {
+ setNewProductSkuOpen(false)
+ }}
+ destroyOnHidden={true}
+ >
+ {
+ setNewProductSkuOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={newProductSkuOpen}
+ />
+
+ >
+ )
+}
+
+export default ProductSkus
diff --git a/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx b/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx
new file mode 100644
index 0000000..6608694
--- /dev/null
+++ b/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewProductSku = ({ onOk, reset, defaultValues }) => {
+ return (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Optional',
+ key: 'optional',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+ {
+ const result = await handleSubmit()
+ if (result) {
+ onOk()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewProductSku.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewProductSku
diff --git a/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx b/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx
new file mode 100644
index 0000000..81333ac
--- /dev/null
+++ b/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx
@@ -0,0 +1,194 @@
+import { useRef, useState } from 'react'
+import { useLocation } from 'react-router-dom'
+import { Space, Flex, Card } from 'antd'
+import useCollapseState from '../../hooks/useCollapseState'
+import NotesPanel from '../../common/NotesPanel'
+import InfoCollapse from '../../common/InfoCollapse'
+import ObjectInfo from '../../common/ObjectInfo'
+import ViewButton from '../../common/ViewButton'
+import ObjectForm from '../../common/ObjectForm'
+import EditButtons from '../../common/EditButtons'
+import LockIndicator from '../../common/LockIndicator.jsx'
+import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import ActionHandler from '../../common/ActionHandler.jsx'
+import ObjectActions from '../../common/ObjectActions.jsx'
+import ObjectTable from '../../common/ObjectTable.jsx'
+import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
+import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
+import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
+import ScrollBox from '../../common/ScrollBox.jsx'
+
+const ProductSkuInfo = () => {
+ const location = useLocation()
+ const objectFormRef = useRef(null)
+ const actionHandlerRef = useRef(null)
+ const productSkuId = new URLSearchParams(location.search).get('productSkuId')
+ const [collapseState, updateCollapseState] = useCollapseState('ProductSkuInfo', {
+ info: true,
+ notes: true,
+ auditLogs: true
+ })
+ const [objectFormState, setEditFormState] = useState({
+ isEditing: false,
+ editLoading: false,
+ formValid: false,
+ lock: null,
+ loading: false,
+ objectData: {}
+ })
+
+ const actions = {
+ reload: () => {
+ objectFormRef?.current?.fetchObject?.()
+ return true
+ },
+ edit: () => {
+ objectFormRef?.current?.startEditing?.()
+ return false
+ },
+ cancelEdit: () => {
+ objectFormRef?.current?.cancelEditing?.()
+ return true
+ },
+ finishEdit: () => {
+ objectFormRef?.current?.handleUpdate?.()
+ return true
+ },
+ delete: () => {
+ objectFormRef?.current?.handleDelete?.()
+ return true
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ actionHandlerRef.current.callAction('finishEdit')
+ }}
+ cancelEditing={() => {
+ actionHandlerRef.current.callAction('cancelEdit')
+ }}
+ startEditing={() => {
+ actionHandlerRef.current.callAction('edit')
+ }}
+ editLoading={objectFormState.editLoading}
+ formValid={objectFormState.formValid}
+ disabled={objectFormState.lock?.locked || objectFormState.loading}
+ loading={objectFormState.editLoading}
+ />
+
+
+
+
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) => updateCollapseState('info', expanded)}
+ collapseKey='info'
+ >
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ )}
+
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) => updateCollapseState('notes', expanded)}
+ collapseKey='notes'
+ >
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ )
+}
+
+export default ProductSkuInfo
diff --git a/src/components/Dashboard/Management/Products/ProductInfo.jsx b/src/components/Dashboard/Management/Products/ProductInfo.jsx
index 172bfa8..c0b8336 100644
--- a/src/components/Dashboard/Management/Products/ProductInfo.jsx
+++ b/src/components/Dashboard/Management/Products/ProductInfo.jsx
@@ -1,4 +1,5 @@
import { useRef, useState } from 'react'
+import { Modal } from 'antd'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import useCollapseState from '../../hooks/useCollapseState'
@@ -22,6 +23,8 @@ import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import PartIcon from '../../../Icons/PartIcon.jsx'
+import ProductSkuIcon from '../../../Icons/ProductSkuIcon.jsx'
+import NewProductSku from '../ProductSkus/NewProductSku'
const ProductInfo = () => {
const location = useLocation()
@@ -31,9 +34,12 @@ const ProductInfo = () => {
const [collapseState, updateCollapseState] = useCollapseState('ProductInfo', {
info: true,
parts: true,
+ productSkus: true,
notes: true,
auditLogs: true
})
+ const [newProductSkuOpen, setNewProductSkuOpen] = useState(false)
+ const productSkusTableRef = useRef()
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
@@ -48,6 +54,10 @@ const ProductInfo = () => {
objectFormRef?.current?.fetchObject?.()
return true
},
+ newProductSku: () => {
+ setNewProductSkuOpen(true)
+ return false
+ },
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
@@ -83,6 +93,7 @@ const ProductInfo = () => {
items={[
{ key: 'info', label: 'Product Information' },
{ key: 'parts', label: 'Product Parts' },
+ { key: 'productSkus', label: 'Product SKUs' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@@ -172,13 +183,54 @@ const ProductInfo = () => {
isEditing={isEditing}
objectData={objectData}
loading={loading}
+ size='medium'
/>
+ }
+ active={collapseState.productSkus}
+ onToggle={(expanded) =>
+ updateCollapseState('productSkus', expanded)
+ }
+ collapseKey='productSkus'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
)}
+ setNewProductSkuOpen(false)}
+ destroyOnClose
+ >
+ {
+ setNewProductSkuOpen(false)
+ productSkusTableRef.current?.reload?.()
+ }}
+ reset={newProductSkuOpen}
+ defaultValues={{
+ product: productId ? { _id: productId } : undefined
+ }}
+ />
+
+
}
diff --git a/src/components/Icons/ProductSkuIcon.jsx b/src/components/Icons/ProductSkuIcon.jsx
new file mode 100644
index 0000000..c92aa34
--- /dev/null
+++ b/src/components/Icons/ProductSkuIcon.jsx
@@ -0,0 +1,8 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/productskuicon.svg?react'
+
+const ProductSkuIcon = (props) => (
+
+)
+
+export default ProductSkuIcon
diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js
index 00ba537..d50809a 100644
--- a/src/database/ObjectModels.js
+++ b/src/database/ObjectModels.js
@@ -5,6 +5,7 @@ import { Spool } from './models/Spool'
import { GCodeFile } from './models/GCodeFile'
import { Job } from './models/Job'
import { Product } from './models/Product'
+import { ProductSku } from './models/ProductSku'
import { Part } from './models/Part.js'
import { Vendor } from './models/Vendor'
import { Courier } from './models/Courier'
@@ -45,6 +46,7 @@ export const objectModels = [
GCodeFile,
Job,
Product,
+ ProductSku,
Part,
Vendor,
Courier,
@@ -86,6 +88,7 @@ export {
GCodeFile,
Job,
Product,
+ ProductSku,
Part,
Vendor,
Courier,
diff --git a/src/database/models/Client.js b/src/database/models/Client.js
index f6563a8..a850697 100644
--- a/src/database/models/Client.js
+++ b/src/database/models/Client.js
@@ -158,56 +158,14 @@ export const Client = {
{
name: 'tags',
label: 'Tags',
- type: 'array',
+ type: 'tags',
readOnly: false,
required: false
},
{
- name: 'address.building',
- label: 'Building',
- type: 'text',
- readOnly: false,
- required: false
- },
- {
- name: 'address.addressLine1',
- label: 'Address Line 1',
- type: 'text',
- readOnly: false,
- required: false
- },
- {
- name: 'address.addressLine2',
- label: 'Address Line 2',
- type: 'text',
- readOnly: false,
- required: false
- },
- {
- name: 'address.city',
- label: 'City',
- type: 'text',
- readOnly: false,
- required: false
- },
- {
- name: 'address.state',
- label: 'State',
- type: 'text',
- readOnly: false,
- required: false
- },
- {
- name: 'address.postcode',
- label: 'Postcode',
- type: 'text',
- readOnly: false,
- required: false
- },
- {
- name: 'address.country',
- label: 'Country',
- type: 'country',
+ name: 'address',
+ label: 'Address',
+ type: 'address',
readOnly: false,
required: false
}
diff --git a/src/database/models/Product.js b/src/database/models/Product.js
index 7f7099c..0b8138c 100644
--- a/src/database/models/Product.js
+++ b/src/database/models/Product.js
@@ -4,6 +4,7 @@ import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
+import PlusIcon from '../../components/Icons/PlusIcon'
export const Product = {
name: 'product',
@@ -56,9 +57,31 @@ export const Product = {
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
+ },
+ {
+ type: 'divider'
+ },
+ {
+ name: 'newProductSku',
+ label: 'New Product SKU',
+ type: 'button',
+ icon: PlusIcon,
+ url: (_id) =>
+ `/dashboard/management/products/info?productId=${_id}&action=newProductSku`,
+ visible: (objectData) => {
+ return !(objectData?._isEditing && objectData?._isEditing == true)
+ }
}
],
- columns: ['_reference', 'name', 'tags', 'vendor', 'price', 'createdAt', 'updatedAt'],
+ columns: [
+ '_reference',
+ 'name',
+ 'tags',
+ 'vendor',
+ 'price',
+ 'createdAt',
+ 'updatedAt'
+ ],
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor'],
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
properties: [
diff --git a/src/database/models/ProductSku.js b/src/database/models/ProductSku.js
new file mode 100644
index 0000000..4e3138e
--- /dev/null
+++ b/src/database/models/ProductSku.js
@@ -0,0 +1,123 @@
+import ProductSkuIcon from '../../components/Icons/ProductSkuIcon'
+import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
+import EditIcon from '../../components/Icons/EditIcon'
+import CheckIcon from '../../components/Icons/CheckIcon'
+import XMarkIcon from '../../components/Icons/XMarkIcon'
+import BinIcon from '../../components/Icons/BinIcon'
+import ReloadIcon from '../../components/Icons/ReloadIcon'
+
+export const ProductSku = {
+ name: 'productSku',
+ label: 'Product SKU',
+ prefix: 'SKU',
+ icon: ProductSkuIcon,
+ actions: [
+ {
+ name: 'info',
+ label: 'Info',
+ default: true,
+ row: true,
+ icon: InfoCircleIcon,
+ url: (_id) => `/dashboard/management/productskus/info?productSkuId=${_id}`
+ },
+ {
+ name: 'reload',
+ label: 'Reload',
+ icon: ReloadIcon,
+ url: (_id) =>
+ `/dashboard/management/productskus/info?productSkuId=${_id}&action=reload`
+ },
+ {
+ name: 'edit',
+ label: 'Edit',
+ row: true,
+ icon: EditIcon,
+ url: (_id) =>
+ `/dashboard/management/productskus/info?productSkuId=${_id}&action=edit`,
+ visible: (objectData) => {
+ return !(objectData?._isEditing && objectData?._isEditing == true)
+ }
+ },
+ {
+ name: 'finishEdit',
+ label: 'Save Edits',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/management/productskus/info?productSkuId=${_id}&action=finishEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ {
+ name: 'cancelEdit',
+ label: 'Cancel Edits',
+ icon: XMarkIcon,
+ url: (_id) =>
+ `/dashboard/management/productskus/info?productSkuId=${_id}&action=cancelEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ { type: 'divider' },
+ {
+ name: 'delete',
+ label: 'Delete',
+ icon: BinIcon,
+ danger: true,
+ url: (_id) =>
+ `/dashboard/management/productskus/info?productSkuId=${_id}&action=delete`
+ }
+ ],
+ url: (id) => `/dashboard/management/productskus/info?productSkuId=${id}`,
+ columns: ['_reference', 'sku', 'product', 'name', 'createdAt', 'updatedAt'],
+ filters: ['_id', 'sku', 'product', 'product._id', 'name'],
+ sorters: ['sku', 'product', 'name', 'createdAt', 'updatedAt'],
+ properties: [
+ {
+ name: '_id',
+ label: 'ID',
+ type: 'id',
+ objectType: 'productSku',
+ showCopy: true,
+ readOnly: true
+ },
+ {
+ name: 'createdAt',
+ label: 'Created At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'name',
+ label: 'Name',
+ required: true,
+ type: 'text'
+ },
+ {
+ name: 'updatedAt',
+ label: 'Updated At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'product',
+ label: 'Product',
+ type: 'object',
+ objectType: 'product',
+ required: true,
+ showHyperlink: true
+ },
+ {
+ name: 'sku',
+ label: 'SKU',
+ required: true,
+ type: 'text'
+ },
+ {
+ name: 'description',
+ label: 'Description',
+ required: false,
+ type: 'text'
+ }
+ ]
+}
diff --git a/src/routes/ManagementRoutes.jsx b/src/routes/ManagementRoutes.jsx
index 3cc6b0e..b92fcde 100644
--- a/src/routes/ManagementRoutes.jsx
+++ b/src/routes/ManagementRoutes.jsx
@@ -7,6 +7,8 @@ const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx'))
const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx'))
const Products = lazy(() => import('../components/Dashboard/Management/Products.jsx'))
const ProductInfo = lazy(() => import('../components/Dashboard/Management/Products/ProductInfo.jsx'))
+const ProductSkus = lazy(() => import('../components/Dashboard/Management/ProductSkus.jsx'))
+const ProductSkuInfo = lazy(() => import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx'))
const Vendors = lazy(() => import('../components/Dashboard/Management/Vendors'))
const VendorInfo = lazy(() => import('../components/Dashboard/Management/Vendors/VendorInfo'))
const Materials = lazy(() => import('../components/Dashboard/Management/Materials'))
@@ -60,6 +62,12 @@ const ManagementRoutes = [
path='management/products/info'
element={}
/>,
+ } />,
+ }
+ />,
} />,
} />,