Compare commits

..

27 Commits

Author SHA1 Message Date
84e98aa626 Add Payments Routes to Finance Module
- Introduced new routes for Payments and PaymentInfo components in the Finance module.
- Enhanced lazy loading for improved performance and user experience in the finance dashboard.
2025-12-28 02:10:47 +00:00
9d78473a2a Update SalesOrder model to refine state validation for invoice actions
- Enhanced the disabled logic for invoice actions by introducing a list of allowed states: 'delivered', 'sent', 'confirmed', 'shipped', 'partiallyDelivered', and 'partiallyShipped'.
- Simplified the return statement for better readability and maintainability.
2025-12-28 02:10:40 +00:00
b2b1cd4fe0 Enhance Invoice Model with New Payment and Acknowledgment Features
- Added a "New Payment" button to the Invoice model, allowing users to initiate payments based on invoice state.
- Introduced an "Acknowledge" button for invoices in the "sent" state, enabling acknowledgment actions.
- Updated the invoice grouping and filtering criteria to include "orderType" and "to/from" fields.
- Adjusted column definitions to improve layout and visibility of new fields, including "acknowledgedAt" and "paidAt".
- Enhanced the invoice summary calculations to include acknowledged counts and totals.
2025-12-28 02:10:31 +00:00
d52b89de43 Add New Invoice Modal to SalesOrderInfo Component
- Introduced a new modal for creating invoices within the SalesOrderInfo component.
- Implemented state management for the new invoice modal, allowing users to open and close it.
- Integrated NewInvoice component with default values for sales order processing.
2025-12-28 02:10:16 +00:00
b4a78f3c91 Add divider to InventorySidebar for improved item separation 2025-12-28 02:10:09 +00:00
4debbe13b8 Add canCollapse prop to InventoryOverview and SalesOverview components for layout control 2025-12-28 02:09:55 +00:00
2fd4870d21 Enhance ObjectProperty component to support size prop for improved layout control
- Added `size` prop to ObjectProperty component for better customization of input elements.
- Updated relevant instances in the component to utilize the new `size` prop, ensuring consistent styling across different usages.
2025-12-28 02:09:43 +00:00
210ae5975a Update InvoiceInfo component to include PaymentIcon import for enhanced payment visibility 2025-12-28 02:09:33 +00:00
6fc952be4d Add Payment Management Features
- Introduced Payment model and associated components for managing payments within the finance dashboard.
- Added PaymentIcon for visual representation in the sidebar and other components.
- Implemented Payments overview, including statistics and a new modal for creating payments.
- Enhanced InvoiceInfo component to include payment details and actions for acknowledging and posting payments.
- Updated database models to integrate payment functionalities, ensuring comprehensive financial tracking.
2025-12-28 02:09:01 +00:00
455c223ec0 Update SalesOrderInfo component to change syncAmount from 'itemCost' to 'itemPrice' for accurate order processing 2025-12-28 01:10:33 +00:00
6b983ca873 Enhance Shipment model and NewShipment component with invoicing fields
- Added new invoicing fields: invoicedAmount, invoicedAmountWithTax, invoicedAmountRemaining, and invoicedAmountWithTaxRemaining to the Shipment model.
- Updated NewShipment component to include the new invoicing fields in its initial state configuration for better shipment tracking and management.
2025-12-28 01:10:26 +00:00
4da63e6a32 Enhance PurchaseOrder components with new invoice modal and state management
- Added a new modal for creating invoices within the PurchaseOrderInfo component.
- Introduced state management for the new invoice modal, allowing users to open and close it.
- Updated PurchaseOrder model to include additional allowed states for invoice creation.
- Added a new field for 'completedAt' in the NewPurchaseOrder component for better order tracking.
2025-12-28 01:10:10 +00:00
de74fb0668 Enhance OrderItem model and components with new invoicing fields
- Added new fields for invoicing, including invoicedAmount, invoicedAmountWithTax, invoicedQuantity, and their remaining counterparts in the OrderItem model.
- Updated NewOrderItem and OrderItemInfo components to include visibility settings for the new invoicing fields.
- Adjusted label widths in OrderItemInfo for better layout consistency.
2025-12-28 01:09:57 +00:00
bace57b436 Enhance Invoice management features with new PostInvoice functionality
- Added PostInvoice component for posting invoices with confirmation dialog.
- Updated InvoiceInfo component to include new invoice order items and shipments sections.
- Modified NewInvoice component to set default issued and due dates.
- Refactored Invoice model to include new fields for issuedAt, dueAt, invoiceOrderItems, and invoiceShipments.
- Updated action names from 'send' to 'post' for clarity in the invoice workflow.
2025-12-28 01:09:36 +00:00
0bf16d844e Refactor TimeDisplay component to improve time difference calculation
- Updated the time difference logic to handle both past and future dates more accurately.
- Simplified the calculation process by introducing base and compare dates.
- Enhanced the output format to include "in" for future dates and improved readability of time strings.
2025-12-28 01:09:11 +00:00
cefe77bc0e Enhance ObjectProperty component to support conditional input rendering
- Added `useFormItem` prop to control input rendering behavior.
- Refactored input rendering logic to streamline component structure and improve maintainability.
- Updated various input types to utilize the new `inputProps` for consistent handling of properties.
2025-12-28 01:09:02 +00:00
aa6fe3c839 Enhance ObjectForm component with custom merge logic for array handling
- Introduced a custom mergeWith function to handle array replacements during state updates.
- Updated initial form data setting to include computed values while maintaining editing state.
- Refactored update event handler to utilize the new merge logic for improved data management.
2025-12-28 01:08:54 +00:00
4bd9acdc11 Refactor ObjectChildTable component to enhance editing functionality
- Removed unused formListName prop and related logic for cleaner code.
- Introduced resolveChangeValue function to streamline value handling during edits.
- Updated cell rendering to include a new handleCellChange function for better state management.
- Simplified the component structure by eliminating unnecessary Form.List integration.
2025-12-28 01:08:46 +00:00
b38af41929 Add form validation to NewObjectForm component
- Implemented validation for form fields in the NewObjectForm component.
- Added logic to set form validity state based on validation results.
2025-12-28 01:08:35 +00:00
2fbafc5396 Refactor database models to replace '_id' with '_reference' in columns
- Updated multiple models to change the column identifier from '_id' to '_reference' for consistency across the database schema.
- Adjusted relevant filters and properties to align with the new column naming convention.
2025-12-27 21:22:59 +00:00
9fb884638f Add ActionsIcon component and corresponding SVG icon
- Introduced a new ActionsIcon component that utilizes an SVG for rendering.
- Updated ObjectTable to display the ActionsIcon in the actions column when applicable.
- Added the SVG file for the ActionsIcon to the assets/icons directory.
2025-12-27 21:22:44 +00:00
e2a81a8503 Update Invoice and PurchaseOrder models to replace 'customer' with 'client' and change 'processing' color to 'purple' 2025-12-27 20:52:11 +00:00
ceaf4715dc Add invoices and stock events sections to PurchaseOrderInfo component
- Introduced new icons for invoices and stock events.
- Added collapsible sections for displaying invoices and stock events related to purchase orders.
- Updated state management to handle the visibility of the new sections.
2025-12-27 20:52:02 +00:00
0dfb22b5cf Update StateTag component to include 'confirmed' status with purple color coding 2025-12-27 20:51:53 +00:00
ca7ab55d1e Add sales module with client and sales order management features
- Introduced new SVG icons for client and sales order.
- Implemented SalesRoutes for navigation.
- Created components for managing clients and sales orders, including overview, client info, and order details.
- Added functionality for creating, editing, and canceling sales orders.
- Integrated sales statistics and actions within the dashboard layout.
2025-12-27 20:46:45 +00:00
8f65154691 Fixed vendor selection. 2025-12-27 20:00:18 +00:00
cd924780aa Updated icons. 2025-12-27 19:59:27 +00:00
89 changed files with 3964 additions and 504 deletions

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.889695,0,0,0.889695,3,2.383378)">
<path d="M0,33.052C0,45.309 6.996,56.09 16.234,61.178C20.659,63.638 24.162,57.594 19.732,55.013C12.395,50.857 7.182,42.577 7.182,33.052C7.182,18.948 18.465,7.622 32.569,7.622C46.673,7.622 58.03,18.948 58.03,33.052C58.03,35.325 59.805,36.728 61.717,36.728C63.565,36.728 65.191,35.303 65.191,33.052C65.191,15.177 50.444,0.462 32.569,0.462C14.726,0.462 0,15.177 0,33.052Z" style="fill-rule:nonzero;"/>
<path d="M13.589,33.042C13.589,39.892 17.302,45.715 21.556,48.454C25.359,50.937 28.902,45.774 25.59,43.286C22.321,41.043 20.176,37.305 20.176,33.042C20.176,26.121 25.701,20.596 32.579,20.596C39.458,20.596 44.753,26.1 45.025,32.804C45.127,34.798 46.618,36.042 48.354,36.042C50.099,36.042 51.612,34.718 51.612,32.764C51.612,22.599 43.022,14.009 32.579,14.009C22.178,14.009 13.589,22.599 13.589,33.042Z" style="fill-rule:nonzero;"/>
<path d="M47.066,65.874C48.64,65.243 49.395,63.472 48.712,61.887L44.195,51.408L50.357,51.285C51.793,51.304 52.485,49.84 51.462,48.796L34.618,31.611C33.647,30.639 32.221,31.207 32.19,32.582L31.859,56.934C31.817,58.42 33.513,58.935 34.483,57.946L38.738,53.572L43.036,64.259C43.647,65.781 45.481,66.537 47.066,65.874Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.993756,0,0,0.993756,4.70027,3)">
<path d="M21.917,58.364L7.977,58.364C2.598,58.364 0,56.673 0,53.004C0,45.534 8.171,35.818 21.434,33.487L21.434,38.985C11.428,41.086 5.926,48.099 5.926,52.158C5.926,52.741 6.224,52.965 6.958,52.965L21.434,52.965L21.434,54.927C21.434,56.202 21.603,57.347 21.917,58.364ZM22.29,28.195C17.315,25.985 13.813,20.695 13.813,14.529C13.813,6.508 19.979,0 27.481,0C35.04,0 41.156,6.405 41.156,14.478C41.156,17.712 40.199,20.706 38.573,23.144L30.955,23.144C30.844,23.144 30.734,23.146 30.625,23.148C33.464,21.732 35.494,18.445 35.494,14.478C35.494,9.257 31.873,5.399 27.481,5.399C23.126,5.399 19.474,9.334 19.474,14.524C19.474,19.579 22.791,23.478 26.82,23.866C25.711,24.303 24.771,24.923 23.999,25.691C23.298,26.389 22.72,27.223 22.29,28.195Z"/>
<g transform="matrix(0.589451,0,0,0.589451,24.45243,26.163338)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.570497,0,0,0.570497,16.522802,13.725853)">
<path d="M6.499,56.236L41.124,56.236C44.294,56.236 46.809,54.281 46.809,50.825C46.809,47.443 44.391,45.464 41.124,45.464L18.117,45.464L18.117,45.062C22.049,43.001 23.774,38.335 23.774,33.596C23.774,32.751 23.669,32.057 23.534,31.399L37.509,31.399C39.584,31.399 41.06,30.032 41.06,28.125C41.06,26.241 39.584,24.905 37.509,24.905L22.134,24.905C21.788,23.604 21.265,21.585 21.265,19.395C21.265,13.243 26.441,10.758 32.681,10.758C34.774,10.758 36.433,10.942 37.882,11.247C38.957,11.394 40.282,11.543 41.544,11.543C44.007,11.543 46.13,10.334 46.13,7.354C46.13,5.297 45.211,3.871 43.447,2.745C39.934,0.628 34.458,0.378 30.367,0.378C18.274,0.378 7.903,5.899 7.903,17.414C7.903,19.396 8.212,21.38 9.112,24.905L3.574,24.905C1.5,24.905 0,26.241 0,28.125C0,30.071 1.514,31.399 3.574,31.399L10.467,31.399C10.73,32.486 10.797,33.332 10.797,34.157C10.797,38.814 8.74,42.769 5.065,44.806C3.057,46.118 0.808,47.931 0.808,50.875C0.808,54.298 3.219,56.236 6.499,56.236Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><path d="M29.953,40.788l10.342,0c1.761,0 3.18,1.258 3.18,3.055c0,1.788 -1.349,3.03 -3.18,3.03l-18.004,0c-1.785,0 -3.086,-1.234 -3.086,-3.075c0,-1.581 0.953,-2.598 2.336,-3.193l0.024,-0.01c2.054,-0.833 3.153,-2.551 3.153,-4.757c0,-0.425 -0.052,-0.856 -0.133,-1.305l-2.8,0c-1.588,0 -2.701,-1.131 -2.701,-2.591c0,-1.408 1.105,-2.56 2.701,-2.56l1.544,-0c-0.168,-0.87 -0.242,-1.672 -0.242,-2.485c0,-6.288 4.756,-9.887 11.404,-9.887c2.234,0 3.578,0.148 5.274,0.766c1.53,0.469 2.646,1.469 2.646,3.106c0,0.878 -0.321,1.553 -0.828,2.023c-0.48,0.446 -1.159,0.725 -2.011,0.725c-0.479,0 -1.089,-0.115 -1.715,-0.248l-0.045,-0.01c-0.697,-0.176 -1.631,-0.301 -2.775,-0.301c-2.932,0 -5.078,1.33 -5.078,3.98c0,0.734 0.074,1.394 0.272,2.332l7.292,0c1.585,0 2.701,1.163 2.701,2.56c0,1.449 -1.124,2.591 -2.701,2.591l-6.16,0c0.024,0.361 0.034,0.743 0.034,1.147c0,1.832 -0.479,3.671 -1.444,5.108Z"/><path d="M12.892,61l38.215,0c6.616,0 9.892,-3.245 9.892,-9.735l0,-38.499c0,-6.49 -3.276,-9.766 -9.892,-9.766l-38.215,-0c-6.584,0 -9.892,3.276 -9.892,9.766l0,38.499c0,6.49 3.308,9.735 9.892,9.735Zm0.063,-5.072c-3.15,0 -4.883,-1.67 -4.883,-4.946l0,-37.932c0,-3.276 1.733,-4.978 4.883,-4.978l38.089,0c3.119,0 4.883,1.701 4.883,4.978l0,37.932c0,3.276 -1.764,4.946 -4.883,4.946l-38.089,0Z" style="fill-rule:nonzero;"/></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<g transform="matrix(0.5364,0,0,0.5364,20.017716,17.05902)">
<path d="M4.547,55.679L40.65,55.679C42.9,55.679 44.677,54.281 44.677,51.912C44.677,49.519 42.886,48.112 40.65,48.112L13.379,48.112L13.379,47.835C18.007,45.956 20.276,40.62 20.276,35.255C20.276,33.462 20.031,31.987 19.701,30.619L35.224,30.619C36.908,30.619 38.161,29.503 38.161,27.888C38.161,26.283 36.908,25.198 35.224,25.198L18.371,25.198C17.843,23.27 17.28,20.86 17.28,18.015C17.28,10.985 22.845,7.524 30.131,7.524C32.516,7.524 34.83,7.82 36.656,8.348C37.578,8.537 38.512,8.672 39.384,8.672C41.303,8.672 42.716,7.658 42.716,5.542C42.716,3.792 41.713,2.742 40.158,1.909C36.798,0.28 32.549,0.03 28.876,0.03C17.257,0.03 8.014,5.885 8.014,17.428C8.014,19.73 8.435,21.979 9.266,25.198L2.947,25.198C1.263,25.198 0,26.283 0,27.888C0,29.555 1.305,30.619 2.947,30.619L10.523,30.619C11.107,32.695 11.299,34.196 11.299,35.634C11.299,41.002 8.545,45.64 3.477,47.51C1.747,48.347 0.209,49.631 0.209,51.878C0.209,54.27 1.978,55.679 4.547,55.679Z" style="fill-rule:nonzero;"/>
</g>
<path d="M12.892,61L51.107,61C57.723,61 60.999,57.755 60.999,51.265L60.999,12.766C60.999,6.276 57.723,3 51.107,3L12.892,3C6.308,3 3,6.276 3,12.766L3,51.265C3,57.755 6.308,61 12.892,61ZM12.955,55.928C9.805,55.928 8.072,54.258 8.072,50.982L8.072,13.05C8.072,9.774 9.805,8.072 12.955,8.072L51.044,8.072C54.163,8.072 55.927,9.773 55.927,13.05L55.927,50.982C55.927,54.258 54.163,55.928 51.044,55.928L12.955,55.928Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,14 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.58577,0,0,0.58577,29,29)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.664312,0,0,0.664312,5.121464,12.135807)">
<path d="M37.594,52.969C39.062,52.969 40.625,52.281 41.875,51L61.844,31.094C62.938,30 63.656,28.188 63.656,26.5C63.656,24.812 62.938,23 61.844,21.906L41.875,1.969C40.625,0.688 39.062,0 37.594,0C33.812,0 31.406,2.562 31.406,5.906C31.406,7.875 32.312,9.25 33.5,10.406L40.5,17.344L50.219,26.5L40.5,35.656L33.5,42.562C32.312,43.688 31.406,45.094 31.406,47.062C31.406,50.406 33.812,52.969 37.594,52.969ZM1.485,32.781L37.75,32.781L51.969,32.094C55.531,31.938 57.906,29.844 57.906,26.5C57.906,23.156 55.531,21.062 51.969,20.906L37.75,20.219L1.485,20.219C-2.515,20.219 -5.14,22.719 -5.14,26.5C-5.14,30.281 -2.515,32.781 1.485,32.781Z" style="fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(0.739137,0,0,0.739137,-0,2.611674)">
<path d="M35.853,52.047L25.734,52.047C20.578,52.047 17.797,48.969 17.047,43.844L12.484,12.719L3.906,12.719C1.812,12.719 0,10.906 0,8.766C0,6.625 1.812,4.828 3.906,4.828L14.172,4.828C18.125,4.828 19.609,6.437 20.047,9.625L20.402,12.109L69.062,12.109C71.922,12.109 73.438,13.75 73.438,16.031C73.438,16.5 73.344,17.062 73.281,17.516L71.172,31.781C71.146,31.963 71.118,32.143 71.087,32.319L63.084,32.319C63.272,32.028 63.394,31.657 63.453,31.219L65.078,19.094L21.4,19.094L23.397,33.062L43.157,33.063C41.542,33.617 40.191,34.464 39.103,35.547C37.914,36.73 37.007,38.224 36.457,40.031L24.393,40.031L24.844,43.188C25,44.312 25.656,45.094 26.719,45.094L35.853,45.094L35.853,52.047ZM28.422,68.437C25.078,68.437 22.359,65.766 22.359,62.391C22.359,59.063 25.078,56.359 28.422,56.359C31.781,56.359 34.453,59.063 34.453,62.391C34.453,65.766 31.781,68.437 28.422,68.437Z"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><rect x="9.624" y="3.845" width="44.752" height="56.309" style="fill-opacity:0;"/><path d="M26.257,60.138l-8.186,-0c-5.571,0 -8.446,-2.897 -8.446,-8.515l0,-39.242c0,-5.597 2.897,-8.536 8.446,-8.536l12.536,0c3.245,0 5.052,0.512 7.225,2.733l13.66,13.874c1.791,1.825 2.456,3.244 2.655,5.548l-18.685,0c-1.881,0 -3.481,0.368 -4.804,1.026c-0.718,-0.905 -1.073,-2.175 -1.073,-3.822l0,-14.263l-11.086,0c-2.532,0 -3.779,1.356 -3.779,3.758l0,38.608c0,2.423 1.247,3.736 3.758,3.736l7.523,0l0,2.541c0,0.919 0.089,1.77 0.256,2.554Zm9.331,-35.907l12.829,0l-14.339,-14.583l0,13.051c0,1.073 0.432,1.531 1.51,1.531Z"/><path d="M40.85,55.833l11.571,0c1.059,0 1.9,-0.653 1.9,-1.808c0,-1.13 -0.808,-1.792 -1.9,-1.792l-7.688,0l0,-0.134c1.314,-0.689 1.89,-2.248 1.89,-3.831c0,-0.282 -0.035,-0.515 -0.08,-0.734l4.67,0c0.693,0 1.187,-0.457 1.187,-1.094c0,-0.629 -0.493,-1.076 -1.187,-1.076l-5.138,0c-0.116,-0.435 -0.29,-1.109 -0.29,-1.841c0,-2.056 1.729,-2.886 3.815,-2.886c0.699,0 1.254,0.061 1.738,0.164c0.359,0.049 0.802,0.099 1.224,0.099c0.823,0 1.533,-0.404 1.533,-1.4c0,-0.687 -0.307,-1.164 -0.897,-1.54c-1.174,-0.707 -3.004,-0.791 -4.371,-0.791c-4.041,0 -7.507,1.845 -7.507,5.693c0,0.662 0.103,1.325 0.404,2.503l-1.851,0c-0.693,0 -1.194,0.446 -1.194,1.076c0,0.65 0.506,1.094 1.194,1.094l2.303,0c0.088,0.363 0.11,0.646 0.11,0.921c0,1.556 -0.687,2.878 -1.915,3.559c-0.671,0.438 -1.423,1.044 -1.423,2.028c0,1.144 0.806,1.792 1.902,1.792Z" style="fill-rule:nonzero;"/><g><path d="M35.462,64l22.077,0c4.182,0 6.462,-2.26 6.462,-6.416l0,-22.159c0,-4.146 -2.279,-6.425 -6.462,-6.425l-22.077,0c-4.174,0 -6.462,2.279 -6.462,6.425l0,22.159c0,4.155 2.288,6.416 6.462,6.416Zm0.513,-4.722c-1.473,0 -2.251,-0.714 -2.251,-2.27l0,-20.996c0,-1.556 0.778,-2.279 2.251,-2.279l21.051,0c1.464,0 2.252,0.723 2.252,2.279l0,20.996c0,1.556 -0.787,2.27 -2.252,2.27l-21.051,0Z" style="fill-rule:nonzero;"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.58577,0,0,0.58577,29.000019,28.999974)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.570497,0,0,0.570497,16.522802,13.725853)">
<path d="M6.499,56.236L41.124,56.236C44.294,56.236 46.809,54.281 46.809,50.825C46.809,47.443 44.391,45.464 41.124,45.464L18.117,45.464L18.117,45.062C22.049,43.001 23.774,38.335 23.774,33.596C23.774,32.751 23.669,32.057 23.534,31.399L37.509,31.399C39.584,31.399 41.06,30.032 41.06,28.125C41.06,26.241 39.584,24.905 37.509,24.905L22.134,24.905C21.788,23.604 21.265,21.585 21.265,19.395C21.265,13.243 26.441,10.758 32.681,10.758C34.774,10.758 36.433,10.942 37.882,11.247C38.957,11.394 40.282,11.543 41.544,11.543C44.007,11.543 46.13,10.334 46.13,7.354C46.13,5.297 45.211,3.871 43.447,2.745C39.934,0.628 34.458,0.378 30.367,0.378C18.274,0.378 7.903,5.899 7.903,17.414C7.903,19.396 8.212,21.38 9.112,24.905L3.574,24.905C1.5,24.905 0,26.241 0,28.125C0,30.071 1.514,31.399 3.574,31.399L10.467,31.399C10.73,32.486 10.797,33.332 10.797,34.157C10.797,38.814 8.74,42.769 5.065,44.806C3.057,46.118 0.808,47.931 0.808,50.875C0.808,54.298 3.219,56.236 6.499,56.236Z" style="fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(1,0,0,1,11.133834,5.394156)">
<rect x="0" y="0" width="74.451" height="58.962" style="fill-opacity:0;"/>
<g transform="matrix(0.700206,0,0,0.700206,-5.133834,5.956054)">
<path d="M28.563,58.962L10.588,58.962C3.679,58.962 0,55.326 0,48.481L0,10.522C0,3.667 3.679,0.02 10.588,0.02L63.676,0.02C70.606,0.02 74.264,3.667 74.264,10.522L74.264,20.933C74.047,20.926 73.827,20.922 73.604,20.922L67.305,20.922L67.305,11.251C67.305,8.378 65.813,6.979 63.097,6.979L11.167,6.979C8.429,6.979 6.959,8.378 6.959,11.251L6.959,47.743C6.959,50.615 8.429,52.003 11.167,52.003L28.563,52.003L28.563,58.962ZM28.857,31.097L16.175,31.097C14.981,31.097 14.107,30.204 14.107,29.051C14.107,27.929 14.981,27.055 16.175,27.055L30.296,27.055C29.634,28.235 29.144,29.582 28.857,31.097ZM16.175,19.207C14.981,19.207 14.107,18.313 14.107,17.138C14.107,16.016 14.981,15.164 16.175,15.164L58.13,15.164C59.295,15.164 60.157,16.016 60.157,17.138C60.157,18.313 59.295,19.207 58.13,19.207L16.175,19.207Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,10 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.822372,0,0,0.822372,2,2.325593)">
<path d="M25.615,51.541L61.925,51.541C63.571,51.541 65.041,50.302 65.041,48.462C65.041,46.631 63.571,45.412 61.925,45.412L26.434,45.412C25.11,45.412 24.295,44.46 24.093,43.051L19.177,9.045C18.785,6.142 17.522,4.675 13.848,4.675L3.326,4.675C1.539,4.675 0,6.215 0,8.032C0,9.86 1.539,11.411 3.326,11.411L12.627,11.411L17.348,43.696C18.064,48.571 20.686,51.541 25.615,51.541ZM20.362,39.849L62.285,39.849C67.216,39.849 69.857,36.878 70.563,31.952L72.815,16.861C72.877,16.425 72.96,15.886 72.96,15.485C72.96,13.488 71.586,12.07 69.142,12.07L17.504,12.07L17.525,18.229L65.71,18.229L63.868,31.389C63.678,32.83 62.925,33.711 61.558,33.711L20.319,33.711L20.362,39.849ZM28.223,67.493C31.383,67.493 33.901,64.986 33.901,61.805C33.901,58.664 31.383,56.126 28.223,56.126C25.061,56.126 22.513,58.664 22.513,61.805C22.513,64.986 25.061,67.493 28.223,67.493ZM56.875,67.493C60.047,67.493 62.575,64.986 62.575,61.805C62.575,58.664 60.047,56.126 56.875,56.126C53.735,56.126 51.175,58.664 51.175,61.805C51.175,64.986 53.735,67.493 56.875,67.493Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.754667,0,0,0.754667,0,25.090254)">
<path d="M3.178,17.892L14.469,11.058C16.041,10.11 16.004,8.217 14.469,7.275L3.178,0.415C1.563,-0.576 0,0.29 0,2.137L0,16.19C0,18.069 1.569,18.872 3.178,17.892ZM25.758,14.461L80.772,14.461C83.437,14.461 84.806,13.117 84.806,10.453L84.806,7.792C84.806,5.153 83.437,3.784 80.772,3.784L25.758,3.784C23.094,3.784 21.725,5.153 21.725,7.792L21.725,10.453C21.725,13.117 23.094,14.461 25.758,14.461Z" style="fill-rule:nonzero;"/>
<g transform="matrix(1.26871,0,0,1.26871,0,-2.460294)">
<path d="M3.178,17.892L14.469,11.058C16.041,10.11 16.004,8.217 14.469,7.275L3.178,0.415C1.563,-0.576 0,0.29 0,2.137L0,16.19C0,18.069 1.569,18.872 3.178,17.892Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.842778,0,0,0.842778,34.449623,-16.022037)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<path d="M11.031,59.75C3.906,59.75 0,55.891 0,48.797L0,10.969C0,3.891 3.906,0 11.031,0L48.719,0C55.859,0 59.75,3.891 59.75,10.969L59.75,48.797C59.75,55.891 55.859,59.75 48.719,59.75L11.031,59.75ZM11.906,51.688L47.844,51.688C50.344,51.688 51.688,50.469 51.688,47.813L51.688,11.969C51.688,9.313 50.344,8.078 47.844,8.078L11.906,8.078C9.391,8.078 8.063,9.313 8.063,11.969L8.063,47.813C8.063,50.469 9.391,51.688 11.906,51.688Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.81149,0,0,0.81149,2,10.044134)">
<path d="M14.864,42.322L22.52,42.322C24.364,42.322 25.582,41.078 25.582,39.328L25.582,33.547C25.582,31.766 24.364,30.547 22.52,30.547L14.864,30.547C13.02,30.547 11.801,31.766 11.801,33.547L11.801,39.328C11.801,41.078 13.02,42.322 14.864,42.322ZM3.51,20.305L70.459,20.305L70.459,12.998L3.51,12.998L3.51,20.305ZM10.251,54.093L63.687,54.093C70.463,54.093 73.938,50.638 73.938,43.969L73.938,10.169C73.938,3.501 70.463,0.02 63.687,0.02L10.251,0.02C3.506,0.02 0,3.501 0,10.169L0,43.969C0,50.638 3.506,54.093 10.251,54.093ZM10.606,47.97C7.701,47.97 6.122,46.471 6.122,43.429L6.122,10.709C6.122,7.667 7.701,6.142 10.606,6.142L63.332,6.142C66.186,6.142 67.816,7.667 67.816,10.709L67.816,43.429C67.816,46.471 66.186,47.97 63.332,47.97L10.606,47.97Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.822372,0,0,0.822372,2,2.325529)">
<path d="M25.615,51.541L61.925,51.541C63.571,51.541 65.041,50.302 65.041,48.462C65.041,46.631 63.571,45.412 61.925,45.412L26.434,45.412C25.11,45.412 24.295,44.46 24.093,43.051L19.177,9.045C18.785,6.142 17.522,4.675 13.848,4.675L3.326,4.675C1.539,4.675 0,6.215 0,8.032C0,9.86 1.539,11.411 3.326,11.411L12.627,11.411L17.348,43.696C18.064,48.571 20.686,51.541 25.615,51.541ZM20.362,39.849L62.285,39.849C67.216,39.849 69.857,36.878 70.563,31.952L72.815,16.861C72.877,16.425 72.96,15.886 72.96,15.485C72.96,13.488 71.586,12.07 69.142,12.07L17.504,12.07L17.525,18.229L65.71,18.229L63.868,31.389C63.678,32.83 62.925,33.711 61.558,33.711L20.319,33.711L20.362,39.849ZM28.223,67.493C31.383,67.493 33.901,64.986 33.901,61.805C33.901,58.664 31.383,56.126 28.223,56.126C25.061,56.126 22.513,58.664 22.513,61.805C22.513,64.986 25.061,67.493 28.223,67.493ZM56.875,67.493C60.047,67.493 62.575,64.986 62.575,61.805C62.575,58.664 60.047,56.126 56.875,56.126C53.735,56.126 51.175,58.664 51.175,61.805C51.175,64.986 53.735,67.493 56.875,67.493Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -32,6 +32,7 @@ import {
ProductionRoutes,
InventoryRoutes,
FinanceRoutes,
SalesRoutes,
ManagementRoutes,
DeveloperRoutes
} from './routes'
@ -98,6 +99,7 @@ const AppContent = () => {
{ProductionRoutes}
{InventoryRoutes}
{FinanceRoutes}
{SalesRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>

View File

@ -13,7 +13,8 @@ const FinanceOverview = () => {
const [collapseState, updateCollapseState] = useCollapseState(
'FinanceOverview',
{
invoiceStats: true
invoiceStats: true,
paymentStats: true
}
)
@ -41,6 +42,7 @@ const FinanceOverview = () => {
}
className='no-t-padding-collapse'
collapseKey='invoiceStats'
canCollapse={false}
>
<Flex
justify='flex-start'
@ -51,6 +53,26 @@ const FinanceOverview = () => {
<StatsDisplay objectType='invoice' />
</Flex>
</InfoCollapse>
<InfoCollapse
title='Payment Statistics'
icon={null}
active={collapseState.paymentStats}
onToggle={(isActive) =>
updateCollapseState('paymentStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='paymentStats'
canCollapse={false}
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='payment' />
</Flex>
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
@ -58,4 +80,3 @@ const FinanceOverview = () => {
}
export default FinanceOverview

View File

@ -1,6 +1,7 @@
import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import InvoiceIcon from '../../Icons/InvoiceIcon'
import PaymentIcon from '../../Icons/PaymentIcon'
import FinanceIcon from '../../Icons/FinanceIcon'
const items = [
@ -16,12 +17,19 @@ const items = [
label: 'Invoices',
icon: <InvoiceIcon />,
path: '/dashboard/finance/invoices'
},
{
key: 'payments',
label: 'Payments',
icon: <PaymentIcon />,
path: '/dashboard/finance/payments'
}
]
const routeKeyMap = {
'/dashboard/finance/overview': 'overview',
'/dashboard/finance/invoices': 'invoices'
'/dashboard/finance/invoices': 'invoices',
'/dashboard/finance/payments': 'payments'
}
const FinanceSidebar = (props) => {
@ -43,4 +51,3 @@ const FinanceSidebar = (props) => {
}
export default FinanceSidebar

View File

@ -0,0 +1,47 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const AcknowledgeInvoice = ({ onOk, objectData }) => {
const [acknowledgeLoading, setAcknowledgeLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleAcknowledge = async () => {
setAcknowledgeLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Invoice',
'acknowledge'
)
if (result) {
message.success('Invoice acknowledged successfully')
onOk(result)
}
} catch (error) {
console.error('Error acknowledging invoice:', error)
} finally {
setAcknowledgeLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to acknowledge this invoice?'}
description={`Acknowledging invoice ${objectData?.name || objectData?._reference || objectData?._id} will update its status to acknowledged.`}
onOk={handleAcknowledge}
okText='Acknowledge'
okLoading={acknowledgeLoading}
/>
)
}
AcknowledgeInvoice.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default AcknowledgeInvoice

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
@ -8,9 +8,11 @@ import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import PaymentIcon from '../../../Icons/PaymentIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
@ -21,7 +23,15 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelByName } from '../../../../database/ObjectModels.js'
import {
getModelByName,
getModelProperty
} from '../../../../database/ObjectModels.js'
import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx'
import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
import PostInvoice from './PostInvoice.jsx'
import AcknowledgeInvoice from './AcknowledgeInvoice.jsx'
import NewPayment from '../Payments/NewPayment.jsx'
const log = loglevel.getLogger('InvoiceInfo')
log.setLevel(config.logLevel)
@ -31,14 +41,14 @@ const InvoiceInfo = () => {
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const invoiceId = new URLSearchParams(location.search).get('invoiceId')
const [collapseState, updateCollapseState] = useCollapseState(
'InvoiceInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [collapseState, updateCollapseState] = useCollapseState('InvoiceInfo', {
info: true,
invoiceOrderItems: true,
invoiceShipments: true,
payments: true,
notes: true,
auditLogs: true
})
const [objectFormState, setEditFormState] = useState({
isEditing: false,
@ -48,6 +58,9 @@ const InvoiceInfo = () => {
loading: false,
objectData: {}
})
const [postInvoiceOpen, setPostInvoiceOpen] = useState(false)
const [acknowledgeInvoiceOpen, setAcknowledgeInvoiceOpen] = useState(false)
const [newPaymentOpen, setNewPaymentOpen] = useState(false)
const actions = {
reload: () => {
@ -69,12 +82,25 @@ const InvoiceInfo = () => {
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
},
post: () => {
setPostInvoiceOpen(true)
return true
},
acknowledge: () => {
setAcknowledgeInvoiceOpen(true)
return true
},
newPayment: () => {
setNewPaymentOpen(true)
return true
}
}
const editDisabled = getModelByName('invoice')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
const editDisabled =
getModelByName('invoice')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
return (
<>
@ -99,6 +125,9 @@ const InvoiceInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Invoice Information' },
{ key: 'invoiceOrderItems', label: 'Invoice Order Items' },
{ key: 'invoiceShipments', label: 'Invoice Shipments' },
{ key: 'payments', label: 'Payments' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -171,12 +200,67 @@ const InvoiceInfo = () => {
type='invoice'
labelWidth='225px'
objectData={objectData}
visibleProperties={{
invoiceOrderItems: false,
invoiceShipments: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Invoice Order Items'
icon={<OrderItemIcon />}
active={collapseState.invoiceOrderItems}
onToggle={(expanded) =>
updateCollapseState('invoiceOrderItems', expanded)
}
collapseKey='invoiceOrderItems'
>
<ObjectProperty
{...getModelProperty('invoice', 'invoiceOrderItems')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
<InfoCollapse
title='Invoice Shipments'
icon={<ShipmentIcon />}
active={collapseState.invoiceShipments}
onToggle={(expanded) =>
updateCollapseState('invoiceShipments', expanded)
}
collapseKey='invoiceShipments'
>
<ObjectProperty
{...getModelProperty('invoice', 'invoiceShipments')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Payments'
icon={<PaymentIcon />}
active={collapseState.payments}
onToggle={(expanded) => updateCollapseState('payments', expanded)}
collapseKey='payments'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='payment'
masterFilter={{ 'invoice._id': invoiceId }}
visibleColumns={{ invoice: false }}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
@ -210,9 +294,66 @@ const InvoiceInfo = () => {
</Flex>
</ScrollBox>
</Flex>
<Modal
open={postInvoiceOpen}
onCancel={() => {
setPostInvoiceOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostInvoice
onOk={() => {
setPostInvoiceOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={acknowledgeInvoiceOpen}
onCancel={() => {
setAcknowledgeInvoiceOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<AcknowledgeInvoice
onOk={() => {
setAcknowledgeInvoiceOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={newPaymentOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewPaymentOpen(false)
}}
destroyOnHidden={true}
>
<NewPayment
onOk={() => {
setNewPaymentOpen(false)
actions.reload()
}}
reset={newPaymentOpen}
defaultValues={{
invoice: { ...objectFormState.objectData },
amount: objectFormState.objectData?.grandTotalAmount
}}
/>
</Modal>
</>
)
}
export default InvoiceInfo

View File

@ -1,4 +1,5 @@
import PropTypes from 'prop-types'
import dayjs from 'dayjs'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
@ -10,7 +11,8 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
reset={reset}
defaultValues={{
state: { type: 'draft' },
invoiceType: 'sales',
issuedAt: new Date(),
dueAt: dayjs().add(3, 'day').toDate(),
...defaultValues
}}
>
@ -30,27 +32,10 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
visibleProperties={{
orderType: true,
order: true,
vendor: true,
invoiceDate: true,
dueDate: true
}}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='invoice'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
relatedOrderType: true,
relatedOrder: true
to: true,
from: true,
issuedAt: true,
dueAt: true
}}
/>
)
@ -66,6 +51,8 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
visibleProperties={{
_id: false,
createdAt: false,
invoiceOrderItems: false,
invoiceShipments: false,
updatedAt: false,
_reference: false,
totalAmount: false,

View File

@ -0,0 +1,42 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const PostInvoice = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(objectData._id, 'Invoice', 'post')
if (result) {
message.success('Invoice posted successfully')
onOk(result)
}
} catch (error) {
console.error('Error posting invoice:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to post this invoice?'}
description={`Posting invoice ${objectData?.name || objectData?._reference || objectData?._id} will set it to sent status.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostInvoice.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostInvoice

View File

@ -0,0 +1,99 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Dropdown, Modal } from 'antd'
import NewPayment from './Payments/NewPayment'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const Payments = () => {
const [newPaymentOpen, setNewPaymentOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('payments')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('payments')
const actionItems = {
items: [
{
label: 'New Payment',
key: 'newPayment',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newPayment') {
setNewPaymentOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='payment'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='payment'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newPaymentOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewPaymentOpen(false)
}}
destroyOnHidden={true}
>
<NewPayment
onOk={() => {
setNewPaymentOpen(false)
tableRef.current?.reload()
}}
reset={newPaymentOpen}
/>
</Modal>
</>
)
}
export default Payments

View File

@ -0,0 +1,89 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewPayment = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type={'payment'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
paymentDate: new Date(),
amount: 0,
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='payment'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
invoice: true,
vendor: true,
client: true,
paymentDate: true,
amount: true,
paymentMethod: true
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='payment'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
_reference: false,
postedAt: false,
cancelledAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Payment'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewPayment.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewPayment

View File

@ -0,0 +1,242 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.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 ScrollBox from '../../common/ScrollBox.jsx'
import {
getModelByName
} from '../../../../database/ObjectModels.js'
import PostPayment from './PostPayment.jsx'
const log = loglevel.getLogger('PaymentInfo')
log.setLevel(config.logLevel)
const PaymentInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const paymentId = new URLSearchParams(location.search).get('paymentId')
const [collapseState, updateCollapseState] = useCollapseState('PaymentInfo', {
info: true,
notes: true,
auditLogs: true
})
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const [postPaymentOpen, setPostPaymentOpen] = useState(false)
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
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
},
post: () => {
setPostPaymentOpen(true)
return true
}
}
const editDisabled =
getModelByName('payment')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='payment'
id={paymentId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Payment Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='payment'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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 ||
editDisabled
}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={paymentId}
type='payment'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Payment Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='payment'
labelWidth='225px'
objectData={objectData}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={paymentId} type='payment' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': paymentId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={postPaymentOpen}
onCancel={() => {
setPostPaymentOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostPayment
onOk={() => {
setPostPaymentOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default PaymentInfo

View File

@ -0,0 +1,43 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const PostPayment = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(objectData._id, 'Payment', 'post')
if (result) {
message.success('Payment posted successfully')
onOk(result)
}
} catch (error) {
console.error('Error posting payment:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to post this payment?'}
description={`Posting payment ${objectData?.name || objectData?._reference || objectData?._id} will set it to posted status.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostPayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostPayment

View File

@ -42,6 +42,7 @@ const InventoryOverview = () => {
}
className='no-t-padding-collapse'
collapseKey='inventoryStats'
canCollapse={false}
>
<Flex
justify='flex-start'

View File

@ -43,6 +43,7 @@ const items = [
icon: <PurchaseOrderIcon />,
path: '/dashboard/inventory/purchaseorders'
},
{ type: 'divider' },
{
key: 'orderitems',
label: 'Order Items',

View File

@ -33,7 +33,15 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
itemAmount: false,
totalAmount: false,
totalAmountWithTax: false,
quantity: false
quantity: false,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false
}}
/>
)
@ -71,7 +79,16 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
required={false}
objectData={objectData}
visibleProperties={{
shipment: true
shipment: true,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false,
syncAmount: false
}}
/>
)
@ -88,7 +105,15 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
_id: false,
createdAt: false,
updatedAt: false,
_reference: false
_reference: false,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false
}}
isEditing={false}
objectData={objectData}

View File

@ -155,7 +155,7 @@ const OrderItemInfo = () => {
isEditing={isEditing}
type='orderItem'
objectData={objectData}
labelWidth='200px'
labelWidth='275px'
/>
</InfoCollapse>
</Flex>

View File

@ -53,7 +53,8 @@ const NewPurchaseOrder = ({ onOk, reset, defaultValues }) => {
acknowledgedAt: false,
shippingAmount: false,
shippingAmountWithTax: false,
grandTotalAmount: false
grandTotalAmount: false,
completedAt: false
}}
isEditing={false}
objectData={objectData}

View File

@ -28,7 +28,10 @@ import PostPurchaseOrder from './PostPurchaseOrder.jsx'
import AcknowledgePurchaseOrder from './AcknowledgePurchaseOrder.jsx'
import CancelPurchaseOrder from './CancelPurchaseOrder.jsx'
import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
import InvoiceIcon from '../../../Icons/InvoiceIcon.jsx'
import StockEventIcon from '../../../Icons/StockEventIcon.jsx'
import { getModelByName } from '../../../../database/ObjectModels.js'
import NewInvoice from '../../Finance/Invoices/NewInvoice.jsx'
const log = loglevel.getLogger('PurchaseOrderInfo')
log.setLevel(config.logLevel)
@ -45,6 +48,7 @@ const PurchaseOrderInfo = () => {
const [acknowledgePurchaseOrderOpen, setAcknowledgePurchaseOrderOpen] =
useState(false)
const [cancelPurchaseOrderOpen, setCancelPurchaseOrderOpen] = useState(false)
const [newInvoiceOpen, setNewInvoiceOpen] = useState(false)
const purchaseOrderId = new URLSearchParams(location.search).get(
'purchaseOrderId'
)
@ -53,7 +57,9 @@ const PurchaseOrderInfo = () => {
{
info: true,
notes: true,
auditLogs: true
auditLogs: true,
invoices: true,
stockEvents: true
}
)
@ -98,6 +104,10 @@ const PurchaseOrderInfo = () => {
setNewShipmentOpen(true)
return true
},
newInvoice: () => {
setNewInvoiceOpen(true)
return true
},
post: () => {
setPostPurchaseOrderOpen(true)
return true
@ -141,6 +151,8 @@ const PurchaseOrderInfo = () => {
{ key: 'info', label: 'Purchase Order Information' },
{ key: 'orderItems', label: 'Order Items' },
{ key: 'shipments', label: 'Shipments' },
{ key: 'invoices', label: 'Invoices' },
{ key: 'stockEvents', label: 'Stock Events' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -256,6 +268,48 @@ const PurchaseOrderInfo = () => {
ref={shipmentsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Invoices'
icon={<InvoiceIcon />}
active={collapseState.invoices}
onToggle={(expanded) =>
updateCollapseState('invoices', expanded)
}
collapseKey='invoices'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='invoice'
masterFilter={{
'order._id': purchaseOrderId,
orderType: 'purchaseOrder'
}}
visibleColumns={{ order: false }}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Stock Events'
icon={<StockEventIcon />}
active={collapseState.stockEvents}
onToggle={(expanded) =>
updateCollapseState('stockEvents', expanded)
}
collapseKey='stockEvents'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='stockEvent'
masterFilter={{
'owner._id': purchaseOrderId
}}
/>
)}
</InfoCollapse>
</Flex>
)}
</ObjectForm>
@ -334,6 +388,26 @@ const PurchaseOrderInfo = () => {
}}
/>
</Modal>
<Modal
open={newInvoiceOpen}
onCancel={() => {
setNewInvoiceOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewInvoice
onOk={() => {
setNewInvoiceOpen(false)
}}
reset={newInvoiceOpen}
defaultValues={{
orderType: 'purchaseOrder',
order: { ...objectFormState.objectData }
}}
/>
</Modal>
<Modal
open={postPurchaseOrderOpen}
onCancel={() => {

View File

@ -85,7 +85,11 @@ const NewShipment = ({ onOk, reset, defaultValues }) => {
shippedAt: false,
expectedAt: false,
deliveredAt: false,
taxRecord: false
taxRecord: false,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false
}}
isEditing={false}
objectData={objectData}

View File

@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom'
import ProductionSidebar from './Production/ProductionSidebar'
import InventorySidebar from './Inventory/InventorySidebar'
import FinanceSidebar from './Finance/FinanceSidebar'
import SalesSidebar from './Sales/SalesSidebar'
import ManagementSidebar from './Management/ManagementSidebar'
import DashboardNavigation from './common/DashboardNavigation'
import DashboardBreadcrumb from './common/DashboardBreadcrumb'
@ -19,6 +20,7 @@ const DashboardLayout = ({ children }) => {
const isProduction = location.pathname.startsWith('/dashboard/production')
const isInventory = location.pathname.startsWith('/dashboard/inventory')
const isFinance = location.pathname.startsWith('/dashboard/finance')
const isSales = location.pathname.startsWith('/dashboard/sales')
const isManagement = location.pathname.startsWith('/dashboard/management')
const isDeveloper = location.pathname.startsWith('/dashboard/developer')
@ -38,6 +40,8 @@ const DashboardLayout = ({ children }) => {
<InventorySidebar />
) : isFinance ? (
<FinanceSidebar />
) : isSales ? (
<SalesSidebar />
) : isManagement ? (
<ManagementSidebar />
) : isDeveloper ? (

View File

@ -0,0 +1,95 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewClient from './Clients/NewClient'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const Clients = () => {
const [newClientOpen, setNewClientOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('client')
const [columnVisibility, setColumnVisibility] = useColumnVisibility('client')
const actionItems = {
items: [
{
label: 'New Client',
key: 'newClient',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newClient') {
setNewClientOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='client'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='client'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newClientOpen}
onCancel={() => setNewClientOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewClient
onOk={() => {
setNewClientOpen(false)
tableRef.current?.reload()
}}
reset={!newClientOpen}
/>
</Modal>
</>
)
}
export default Clients

View File

@ -0,0 +1,194 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config'
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 InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../common/LockIndicator.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 ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('ClientInfo')
log.setLevel(config.logLevel)
const ClientInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const clientId = new URLSearchParams(location.search).get('clientId')
const [collapseState, updateCollapseState] = useCollapseState('ClientInfo', {
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?.handleFetchObject?.()
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 (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='client'
id={clientId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Client Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='client'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Client Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={clientId}
type='client'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='client'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={clientId} type='client' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': clientId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default ClientInfo

View File

@ -0,0 +1,87 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewClient = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'client'}
defaultValues={{ active: true, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='client'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='client'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='client'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Client'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewClient.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewClient

View File

@ -0,0 +1,99 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewSalesOrder from './SalesOrders/NewSalesOrder'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const SalesOrders = () => {
const [newSalesOrderOpen, setNewSalesOrderOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('salesOrders')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('salesOrders')
const actionItems = {
items: [
{
label: 'New Sales Order',
key: 'newSalesOrder',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newSalesOrder') {
setNewSalesOrderOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='salesOrder'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='salesOrder'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newSalesOrderOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewSalesOrderOpen(false)
}}
destroyOnHidden={true}
>
<NewSalesOrder
onOk={() => {
setNewSalesOrderOpen(false)
tableRef.current?.reload()
}}
reset={newSalesOrderOpen}
/>
</Modal>
</>
)
}
export default SalesOrders

View File

@ -0,0 +1,47 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const CancelSalesOrder = ({ onOk, objectData }) => {
const [cancelLoading, setCancelLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleCancel = async () => {
setCancelLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'SalesOrder',
'cancel'
)
if (result) {
message.success('Sales order cancelled successfully')
onOk(result)
}
} catch (error) {
console.error('Error cancelling sales order:', error)
} finally {
setCancelLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to cancel this sales order?'}
description={`Cancelling sales order ${objectData?.name || objectData?._reference || objectData?._id} will update its status to cancelled.`}
onOk={handleCancel}
okText='Cancel'
okLoading={cancelLoading}
/>
)
}
CancelSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelSalesOrder

View File

@ -0,0 +1,47 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const ConfirmSalesOrder = ({ onOk, objectData }) => {
const [confirmLoading, setConfirmLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleConfirm = async () => {
setConfirmLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'SalesOrder',
'confirm'
)
if (result) {
message.success('Sales order confirmed successfully')
onOk(result)
}
} catch (error) {
console.error('Error confirming sales order:', error)
} finally {
setConfirmLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to confirm this sales order?'}
description={`Confirming sales order ${objectData?.name || objectData?._reference || objectData?._id} will update its status to confirmed.`}
onOk={handleConfirm}
okText='Confirm'
okLoading={confirmLoading}
/>
)
}
ConfirmSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default ConfirmSalesOrder

View File

@ -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 NewSalesOrder = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type={'salesOrder'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='salesOrder'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
_reference: false,
items: false,
cost: false
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='salesOrder'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
_reference: false,
totalAmount: false,
totalAmountWithTax: false,
totalTaxAmount: false,
postedAt: false,
confirmedAt: false,
shippingAmount: false,
shippingAmountWithTax: false,
grandTotalAmount: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Sales Order'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewSalesOrder

View File

@ -0,0 +1,47 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const PostSalesOrder = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'SalesOrder',
'post'
)
if (result) {
message.success('Sales order posted successfully')
onOk(result)
}
} catch (error) {
console.error('Error posting sales order:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to post this sales order?'}
description={`Posting sales order ${objectData?.name || objectData?._reference || objectData?._id} will finalize it and update inventory levels where applicable.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostSalesOrder

View File

@ -0,0 +1,466 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.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 ScrollBox from '../../common/ScrollBox.jsx'
import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx'
import NewOrderItem from '../../Inventory/OrderItems/NewOrderItem.jsx'
import NewShipment from '../../Inventory/Shipments/NewShipment.jsx'
import PostSalesOrder from './PostSalesOrder.jsx'
import ConfirmSalesOrder from './ConfirmSalesOrder.jsx'
import CancelSalesOrder from './CancelSalesOrder.jsx'
import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
import InvoiceIcon from '../../../Icons/InvoiceIcon.jsx'
import StockEventIcon from '../../../Icons/StockEventIcon.jsx'
import { getModelByName } from '../../../../database/ObjectModels.js'
import NewInvoice from '../../Finance/Invoices/NewInvoice.jsx'
const log = loglevel.getLogger('SalesOrderInfo')
log.setLevel(config.logLevel)
const SalesOrderInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const orderItemsTableRef = useRef(null)
const shipmentsTableRef = useRef(null)
const actionHandlerRef = useRef(null)
const [newOrderItemOpen, setNewOrderItemOpen] = useState(false)
const [newShipmentOpen, setNewShipmentOpen] = useState(false)
const [postSalesOrderOpen, setPostSalesOrderOpen] = useState(false)
const [confirmSalesOrderOpen, setConfirmSalesOrderOpen] = useState(false)
const [cancelSalesOrderOpen, setCancelSalesOrderOpen] = useState(false)
const [newInvoiceOpen, setNewInvoiceOpen] = useState(false)
const salesOrderId = new URLSearchParams(location.search).get('salesOrderId')
const [collapseState, updateCollapseState] = useCollapseState(
'SalesOrderInfo',
{
info: true,
notes: true,
auditLogs: true,
invoices: true,
stockEvents: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
orderItemsTableRef?.current?.startEditing?.()
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
orderItemsTableRef?.current?.cancelEditing?.()
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
orderItemsTableRef?.current?.handleUpdate?.()
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
},
newOrderItem: () => {
setNewOrderItemOpen(true)
return true
},
newShipment: () => {
setNewShipmentOpen(true)
return true
},
newInvoice: () => {
setNewInvoiceOpen(true)
return true
},
post: () => {
setPostSalesOrderOpen(true)
return true
},
confirm: () => {
setConfirmSalesOrderOpen(true)
return true
},
cancel: () => {
setCancelSalesOrderOpen(true)
return true
}
}
const editDisabled = getModelByName('salesOrder')
.actions.find((action) => action.name === 'edit')
.disabled(objectFormState.objectData)
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='salesOrder'
id={salesOrderId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Sales Order Information' },
{ key: 'orderItems', label: 'Order Items' },
{ key: 'shipments', label: 'Shipments' },
{ key: 'invoices', label: 'Invoices' },
{ key: 'stockEvents', label: 'Stock Events' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='salesOrder'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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 ||
editDisabled
}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={salesOrderId}
type='salesOrder'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Sales Order Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='salesOrder'
labelWidth='225px'
objectData={objectData}
visibleProperties={{
items: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Sales Order Items'
icon={<OrderItemsIcon />}
active={collapseState.orderItems}
onToggle={(expanded) =>
updateCollapseState('orderItems', expanded)
}
collapseKey='orderItems'
>
<ObjectTable
type='orderItem'
masterFilter={{
'order._id': salesOrderId,
orderType: 'salesOrder'
}}
visibleColumns={{ order: false }}
ref={orderItemsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Shipments'
icon={<ShipmentIcon />}
active={collapseState.shipments}
onToggle={(expanded) =>
updateCollapseState('shipments', expanded)
}
collapseKey='shipments'
>
<ObjectTable
type='shipment'
masterFilter={{
'order._id': salesOrderId,
orderType: 'salesOrder'
}}
visibleColumns={{ order: false }}
ref={shipmentsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Invoices'
icon={<InvoiceIcon />}
active={collapseState.invoices}
onToggle={(expanded) =>
updateCollapseState('invoices', expanded)
}
collapseKey='invoices'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='invoice'
masterFilter={{
'order._id': salesOrderId,
orderType: 'salesOrder'
}}
visibleColumns={{ order: false }}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Stock Events'
icon={<StockEventIcon />}
active={collapseState.stockEvents}
onToggle={(expanded) =>
updateCollapseState('stockEvents', expanded)
}
collapseKey='stockEvents'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='stockEvent'
masterFilter={{
'owner._id': salesOrderId
}}
/>
)}
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={salesOrderId} type='salesOrder' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': salesOrderId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={newOrderItemOpen}
onCancel={() => {
setNewOrderItemOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewOrderItem
onOk={() => {
setNewOrderItemOpen(false)
}}
reset={newOrderItemOpen}
defaultValues={{
order: { _id: salesOrderId },
orderType: 'salesOrder',
syncAmount: 'itemPrice'
}}
/>
</Modal>
<Modal
open={newShipmentOpen}
onCancel={() => {
setNewShipmentOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewShipment
onOk={() => {
setNewShipmentOpen(false)
}}
reset={newShipmentOpen}
defaultValues={{
orderType: 'salesOrder',
order: { _id: salesOrderId }
}}
/>
</Modal>
<Modal
open={newInvoiceOpen}
onCancel={() => {
setNewInvoiceOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewInvoice
onOk={() => {
setNewInvoiceOpen(false)
}}
reset={newInvoiceOpen}
defaultValues={{
orderType: 'salesOrder',
order: { ...objectFormState.objectData }
}}
/>
</Modal>
<Modal
open={postSalesOrderOpen}
onCancel={() => {
setPostSalesOrderOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostSalesOrder
onOk={() => {
setPostSalesOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={confirmSalesOrderOpen}
onCancel={() => {
setConfirmSalesOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<ConfirmSalesOrder
onOk={() => {
setConfirmSalesOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={cancelSalesOrderOpen}
onCancel={() => {
setCancelSalesOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<CancelSalesOrder
onOk={() => {
setCancelSalesOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default SalesOrderInfo

View File

@ -0,0 +1,62 @@
import { useContext } from 'react'
import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState'
import StatsDisplay from '../common/StatsDisplay'
import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox'
import { ApiServerContext } from '../context/ApiServerContext'
const SalesOverview = () => {
const { connected } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState(
'SalesOverview',
{
clientStats: true,
salesOrderStats: true
}
)
if (!connected) {
return null
}
return (
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<ScrollBox>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Sales Order Statistics'
icon={null}
active={collapseState.salesOrderStats}
onToggle={(isActive) =>
updateCollapseState('salesOrderStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='salesOrderStats'
canCollapse={false}
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='salesOrder' />
</Flex>
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
)
}
export default SalesOverview

View File

@ -0,0 +1,54 @@
import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import ClientIcon from '../../Icons/ClientIcon'
import SalesIcon from '../../Icons/SalesIcon'
import SalesOrderIcon from '../../Icons/SalesOrderIcon'
const items = [
{
key: 'overview',
label: 'Overview',
icon: <SalesIcon />,
path: '/dashboard/sales/overview'
},
{ type: 'divider' },
{
key: 'clients',
label: 'Clients',
icon: <ClientIcon />,
path: '/dashboard/sales/clients'
},
{
key: 'salesorders',
label: 'Sales Orders',
icon: <SalesOrderIcon />,
path: '/dashboard/sales/salesorders'
}
]
const routeKeyMap = {
'/dashboard/sales/overview': 'overview',
'/dashboard/sales/clients': 'clients',
'/dashboard/sales/salesorders': 'salesorders'
}
const SalesSidebar = (props) => {
const location = useLocation()
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'overview'
})()
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
}
export default SalesSidebar

View File

@ -11,6 +11,7 @@ const breadcrumbNameMap = {
management: 'Management',
developer: 'Developer',
finance: 'Finance',
sales: 'Sales',
overview: 'Overview',
info: 'Info',
design: 'Design',

View File

@ -31,6 +31,7 @@ import MenuIcon from '../../Icons/MenuIcon'
import ProductionIcon from '../../Icons/ProductionIcon'
import InventoryIcon from '../../Icons/InventoryIcon'
import FinanceIcon from '../../Icons/FinanceIcon'
import SalesIcon from '../../Icons/SalesIcon'
import PersonIcon from '../../Icons/PersonIcon'
import CloudIcon from '../../Icons/CloudIcon'
import BellIcon from '../../Icons/BellIcon'
@ -71,6 +72,11 @@ const DashboardNavigation = () => {
label: 'Inventory',
icon: <InventoryIcon />
},
{
key: 'sales',
label: 'Sales',
icon: <SalesIcon />
},
{
key: 'finance',
label: 'Finance',
@ -141,6 +147,8 @@ const DashboardNavigation = () => {
navigate('/dashboard/inventory/overview')
} else if (key === 'finance') {
navigate('/dashboard/finance/overview')
} else if (key === 'sales') {
navigate('/dashboard/sales/overview')
} else if (key === 'management') {
navigate('/dashboard/management/filaments')
}

View File

@ -141,6 +141,10 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, defaultValues, computedValuesObject)
form.setFieldsValue(initialFormData)
form
.validateFields({ validateOnly: true })
.then(() => setFormValid(true))
.catch(() => setFormValid(false))
setObjectData((prev) => merge({}, prev, initialFormData))
}
}, [form, defaultValues, calculateComputedValues, model])

View File

@ -1,6 +1,6 @@
import { useMemo, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import { Table, Skeleton, Card, Button, Flex, Form, Typography } from 'antd'
import { Table, Skeleton, Card, Button, Flex, Typography } from 'antd'
import PlusIcon from '../../Icons/PlusIcon'
import ObjectProperty from './ObjectProperty'
import { LoadingOutlined } from '@ant-design/icons'
@ -21,6 +21,14 @@ const getDefaultWidth = (type) => {
return DEFAULT_COLUMN_WIDTHS[type] || 200
}
const resolveChangeValue = (val, type) => {
if (type === 'bool') return val
if (val?.target && typeof val.target === 'object') {
return val.target.value
}
return val
}
const createSkeletonRows = (rowCount, keyPrefix, keyName) => {
return Array.from({ length: rowCount }).map((_, index) => {
const skeletonKey = `${keyPrefix}-${index}`
@ -51,7 +59,6 @@ const ObjectChildTable = ({
additionalColumns = [],
emptyText = 'No items',
isEditing = false,
formListName,
value = [],
rollups = [],
onChange,
@ -103,34 +110,44 @@ const ObjectChildTable = ({
return value ?? []
}, [value])
// When used with antd Form.List, grab the form instance so we can read
// the latest row values and pass them into ObjectProperty as objectData.
// Assumes this component is rendered within a Form context when editing.
const formInstance = Form.useFormInstance()
const listNamePath = useMemo(() => {
if (!formListName) return null
return Array.isArray(formListName) ? formListName : [formListName]
}, [formListName])
const tableColumns = useMemo(() => {
const propertyColumns = resolvedProperties.map((property) => ({
title: property.label || property.name,
dataIndex: property.name,
key: property.name,
width: property.columnWidth || getDefaultWidth(property.type),
render: (_text, record) => {
render: (_text, record, index) => {
if (record?.isSkeleton) {
return (
<Skeleton.Input active size='small' style={{ width: '100%' }} />
)
}
const handleCellChange = (newVal) => {
const resolved = resolveChangeValue(newVal, property.type)
const currentItems = Array.isArray(itemsSource)
? [...itemsSource]
: []
const updatedItem = {
...currentItems[index],
[property.name]: resolved
}
currentItems[index] = updatedItem
if (typeof onChange === 'function') {
onChange(currentItems)
}
}
return (
<ObjectProperty
{...property}
longId={false}
objectData={record}
isEditing={isEditing}
useFormItem={false}
name={undefined}
value={record[property.name]}
onChange={handleCellChange}
/>
)
}
@ -416,122 +433,6 @@ const ObjectChildTable = ({
</Flex>
)
// When editing and a Form.List name is provided, bind rows via Form.List
// instead of the manual value/onChange mechanism.
if (isEditing === true && formListName) {
return (
<Form.List name={formListName}>
{(fields, { add, remove }) => {
const listDataSource = fields.map((field, index) => ({
_field: field,
_index: index,
key: field.key
}))
const listColumns = resolvedProperties.map((property) => ({
title: property.label || property.name,
dataIndex: property.name,
key: property.name,
width: property.columnWidth || getDefaultWidth(property.type),
render: (_text, record) => {
const field = record?._field
if (!field) return null
// Resolve the most up-to-date row data for this index from the form
let rowObjectData = undefined
if (formInstance && listNamePath) {
const namePath = [...listNamePath, field.name]
rowObjectData = formInstance.getFieldValue(namePath)
}
return (
<ObjectProperty
{...property}
// Bind directly to this list item + property via NamePath
name={[field.name, property.name]}
longId={false}
isEditing={true}
objectData={rowObjectData}
/>
)
}
}))
const deleteColumn = {
title: '',
key: 'delete',
width: 10,
fixed: 'right',
render: (_text, record) => {
const field = record?._field
const index = record?._index
if (!field || index == null) return null
return (
<Button
type='text'
danger
size='small'
icon={<BinIcon />}
onClick={(e) => {
e.stopPropagation()
if (typeof remove === 'function') {
remove(index)
}
}}
/>
)
}
}
const listTable = (
<Flex vertical>
<div ref={mainTableWrapperRef}>
<Table
dataSource={listDataSource}
columns={[...listColumns, ...additionalColumns, deleteColumn]}
pagination={false}
size={size}
loading={loading}
rowKey={(record) => record.key ?? record._index}
scroll={scrollConfig}
locale={{ emptyText }}
className={hasRollups ? 'child-table-rollups' : 'child-table'}
style={{ maxWidth, minWidth: 0 }}
{...tableProps}
/>
</div>
{rollupTable}
</Flex>
)
const handleAddListItem = () => {
const newItem = {}
resolvedProperties.forEach((property) => {
if (property?.name) {
newItem[property.name] = null
}
})
add(newItem)
}
return (
<Card style={{ minWidth: 0 }}>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
<Button>Actions</Button>
<Button
type='primary'
icon={<PlusIcon />}
onClick={handleAddListItem}
/>
</Flex>
{listTable}
</Flex>
</Card>
)
}}
</Form.List>
)
}
if (isEditing === true) {
return (
<Card>
@ -571,7 +472,6 @@ ObjectChildTable.propTypes = {
additionalColumns: PropTypes.arrayOf(PropTypes.object),
emptyText: PropTypes.node,
isEditing: PropTypes.bool,
formListName: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
value: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
maxWidth: PropTypes.string,

View File

@ -14,10 +14,17 @@ import { useMessageContext } from '../context/MessageContext'
import PropTypes from 'prop-types'
import DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge'
import mergeWith from 'lodash/mergeWith'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
import { useNavigate } from 'react-router-dom'
const arrayReplaceCustomizer = (objValue, srcValue) => {
if (Array.isArray(srcValue)) {
return srcValue
}
}
const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => {
const { namePath, value } = entry || {}
@ -285,14 +292,14 @@ const ObjectForm = forwardRef(
const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent)
onStateChangeRef.current({ lock: lockEvent })
setObjectData({ ...data, _isEditing: isEditingRef.current })
serverObjectData.current = data
// Calculate and set computed values on initial load
const computedEntries = calculateComputedValues(data, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, data, computedValuesObject)
setObjectData({ ...initialFormData, _isEditing: isEditingRef.current })
form.setFieldsValue(initialFormData)
setFetchLoading(false)
onStateChangeRef.current({ loading: false })
@ -318,7 +325,9 @@ const ObjectForm = forwardRef(
// Update event handler
const updateObjectEventHandler = useCallback((value) => {
setObjectData((prev) => merge({}, prev, value))
setObjectData((prev) =>
mergeWith({}, prev, value, arrayReplaceCustomizer)
)
}, [])
// Update event handler
@ -540,15 +549,22 @@ const ObjectForm = forwardRef(
}
const computedValuesObject = buildObjectFromEntries(computedEntries)
const mergedFormValues = merge(
const mergedFormValues = mergeWith(
{},
allFormValues,
computedValuesObject
arrayReplaceCustomizer
)
merge(mergedFormValues, computedValuesObject)
mergedFormValues._isEditing = isEditingRef.current
setObjectData((prev) => {
return merge({}, prev, mergedFormValues)
return mergeWith(
{},
prev,
mergedFormValues,
arrayReplaceCustomizer
)
})
}}
>

View File

@ -61,6 +61,7 @@ const MATERIAL_OPTIONS = [
const ObjectProperty = ({
type = 'text',
prefix,
size,
suffix,
value,
min,
@ -85,6 +86,7 @@ const ObjectProperty = ({
minimal = false,
previewOpen = false,
showPreview = true,
useFormItem = true,
options = [],
roundNumber = false,
fixedNumber = false,
@ -410,6 +412,7 @@ const ObjectProperty = ({
maxWidth={maxWidth}
loading={loading}
rollups={rollups}
size={size}
/>
)
}
@ -613,254 +616,219 @@ const ObjectProperty = ({
mergedFormItemProps.onChange = onChange
}
switch (type) {
case 'netGross':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
const inputProps = useFormItem
? {}
: {
value,
onChange,
disabled
}
const renderInput = () => {
switch (type) {
case 'netGross':
return (
<NetGrossInput
difference={difference}
prefix={prefix}
suffix={suffix}
{...inputProps}
/>
</Form.Item>
)
case 'secret':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'secret':
return (
<Input.Password
placeholder={label}
disabled={disabled}
{...mergedFormItemProps}
iconRender={(visible) =>
visible ? <EyeSlashIcon /> : <EyeIcon />
}
{...mergedFormItemProps}
{...inputProps}
/>
</Form.Item>
)
case 'wsprotocol':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'wsprotocol':
return (
<Select
defaultValue='ws'
disabled={disabled}
options={[
{ value: 'ws', label: 'Websocket' },
{ value: 'wss', label: 'Websocket Secure' }
]}
{...inputProps}
/>
</Form.Item>
)
case 'select':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'select':
return (
<CustomSelect
placeholder={'Select a ' + label.toLowerCase() + '...'}
disabled={disabled}
options={Array.isArray(options) ? options : []}
{...inputProps}
/>
</Form.Item>
)
case 'priceMode':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'priceMode':
return (
<Select
defaultValue='margin'
disabled={disabled}
options={[
{ value: 'margin', label: 'Margin %' },
{ value: 'amount', label: '£ Amount' }
]}
{...inputProps}
/>
</Form.Item>
)
case 'bool':
return (
<Form.Item
name={formItemName}
{...mergedFormItemProps}
valuePropName='checked'
>
<Switch disabled={disabled} />
</Form.Item>
)
case 'dateTime':
return (
<Form.Item
name={formItemName}
{...mergedFormItemProps}
getValueProps={(v) => ({ value: v ? dayjs(v) : null })}
>
)
case 'bool':
return (
<Switch
{...inputProps}
{...(useFormItem ? {} : { checked: value })}
/>
)
case 'dateTime':
return (
<DatePicker
showTime
style={{ width: '100%' }}
disabled={disabled}
{...inputProps}
{...(useFormItem ? {} : { value: value ? dayjs(value) : null })}
/>
</Form.Item>
)
case 'country':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<CountrySelect disabled={disabled} />
</Form.Item>
)
case 'color':
return (
<Form.Item
name={formItemName}
{...mergedFormItemProps}
valuePropName='value'
getValueFromEvent={(v) => v}
>
<ColorSelector required={required} disabled={disabled} />
</Form.Item>
)
case 'weight':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'country':
return <CountrySelect {...inputProps} />
case 'color':
return (
<ColorSelector
required={required}
{...inputProps}
{...(useFormItem ? {} : { value })}
/>
)
case 'weight':
return (
<InputNumber
suffix='g'
style={{ width: '100%' }}
placeholder={label}
disabled={disabled}
{...inputProps}
/>
</Form.Item>
)
case 'number':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'number':
return (
<InputNumber
placeholder={label}
disabled={disabled}
prefix={prefix}
suffix={suffix}
min={min}
max={max}
step={step}
{...mergedFormItemProps}
style={{ width: '100%' }}
{...inputProps}
/>
</Form.Item>
)
case 'text':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<Input
placeholder={label}
{...mergedFormItemProps}
disabled={disabled}
/>
</Form.Item>
)
case 'codeBlock':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'text':
return <Input placeholder={label} {...inputProps} />
case 'codeBlock':
return (
<CodeBlockEditor
code={value}
language={language}
disabled={disabled}
height={height}
minimal={minimal}
{...inputProps}
/>
</Form.Item>
)
case 'markdown':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<MarkdownInput value={value} />
</Form.Item>
)
case 'material':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'markdown':
return <MarkdownInput {...inputProps} />
case 'material':
return (
<Select
options={MATERIAL_OPTIONS}
placeholder={label}
disabled={disabled}
{...inputProps}
/>
</Form.Item>
)
case 'id':
// id is not editable, just show view mode
if (value) {
return <IdDisplay id={value} type={objectType} {...rest} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
case 'object':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
case 'id':
// id is not editable, just show view mode
if (value) {
return <IdDisplay id={value} type={objectType} {...rest} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
case 'object':
return (
<ObjectSelect
type={objectType}
disabled={disabled}
masterFilter={masterFilter}
{...inputProps}
/>
</Form.Item>
)
case 'objectType':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<ObjectTypeSelect disabled={disabled} masterFilter={masterFilter} />
</Form.Item>
)
case 'objectList':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<ObjectSelect type={objectType} multiple disabled={disabled} />
</Form.Item>
)
case 'tags':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<TagsInput />
</Form.Item>
)
case 'file':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'objectType':
return (
<ObjectTypeSelect masterFilter={masterFilter} {...inputProps} />
)
case 'objectList':
return <ObjectSelect type={objectType} multiple {...inputProps} />
case 'tags':
return <TagsInput {...inputProps} />
case 'file':
return (
<FileUpload
value={value}
multiple={false}
defaultPreviewOpen={previewOpen}
showPreview={showPreview}
showInfo={showHyperlink}
{...inputProps}
/>
</Form.Item>
)
case 'fileList':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
)
case 'fileList':
return (
<FileUpload
value={value}
multiple={true}
defaultPreviewOpen={previewOpen}
showPreview={showPreview}
showInfo={showHyperlink}
{...inputProps}
/>
</Form.Item>
)
case 'objectChildren': {
return (
<ObjectChildTable
value={value}
properties={properties}
objectData={objectData}
isEditing={true}
formListName={formItemName}
rollups={rollups}
/>
)
)
case 'objectChildren': {
return (
<ObjectChildTable
properties={properties}
objectData={objectData}
isEditing={true}
rollups={rollups}
size={size}
{...inputProps}
/>
)
}
default:
return <Input placeholder={label} {...inputProps} />
}
default:
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<Input placeholder={label} {...mergedFormItemProps} />
</Form.Item>
)
}
if (!useFormItem) {
return renderInput()
}
return (
<Form.Item
name={formItemName}
{...mergedFormItemProps}
{...(type === 'bool' ? { valuePropName: 'checked' } : {})}
{...(type === 'color'
? { valuePropName: 'value', getValueFromEvent: (v) => v }
: {})}
{...(type === 'dateTime'
? { getValueProps: (v) => ({ value: v ? dayjs(v) : null }) }
: {})}
>
{renderInput()}
</Form.Item>
)
}
const property = renderProperty()
@ -892,6 +860,7 @@ ObjectProperty.propTypes = {
height: PropTypes.string,
previewOpen: PropTypes.bool,
showPreview: PropTypes.bool,
useFormItem: PropTypes.bool,
showHyperlink: PropTypes.bool,
options: PropTypes.array,
showSince: PropTypes.bool,

View File

@ -41,6 +41,7 @@ import { useNavigate } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import ActionsIcon from '../../Icons/ActionsIcon'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@ -758,11 +759,11 @@ const ObjectTable = forwardRef(
}
})
if (rowActions.length > 0) {
if (rowActions.length > 0 && tableData.some((item) => !item.isSkeleton)) {
columnsWithSkeleton.push({
title: (
<Flex gap='small' align='center' justify='center'>
{'Actions'}
<ActionsIcon />
</Flex>
),
key: 'actions',

View File

@ -101,9 +101,13 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
text = 'Sent'
break
case 'acknowledged':
status = 'processing'
status = 'purple'
text = 'Acknowledged'
break
case 'confirmed':
status = 'purple'
text = 'Confirmed'
break
case 'ordered':
status = 'cyan'
text = 'Ordered'

View File

@ -8,25 +8,33 @@ const { Text } = Typography
const formatTimeDifference = (dateTime) => {
const now = dayjs()
const diff = dayjs(dateTime)
const target = dayjs(dateTime)
const isFuture = target.isAfter(now)
const years = now.diff(diff, 'year')
const months = now.diff(diff.add(years, 'year'), 'month')
const weeks = now.diff(diff.add(years, 'year').add(months, 'month'), 'week')
const days = now.diff(
diff.add(years, 'year').add(months, 'month').add(weeks, 'week'),
// If future, calculate from target to now; if past, from now to target
const baseDate = isFuture ? target : now
const compareDate = isFuture ? now : target
const years = baseDate.diff(compareDate, 'year')
const months = baseDate.diff(compareDate.add(years, 'year'), 'month')
const weeks = baseDate.diff(
compareDate.add(years, 'year').add(months, 'month'),
'week'
)
const days = baseDate.diff(
compareDate.add(years, 'year').add(months, 'month').add(weeks, 'week'),
'day'
)
const hours = now.diff(
diff
const hours = baseDate.diff(
compareDate
.add(years, 'year')
.add(months, 'month')
.add(weeks, 'week')
.add(days, 'day'),
'hour'
)
const minutes = now.diff(
diff
const minutes = baseDate.diff(
compareDate
.add(years, 'year')
.add(months, 'month')
.add(weeks, 'week')
@ -34,8 +42,8 @@ const formatTimeDifference = (dateTime) => {
.add(hours, 'hour'),
'minute'
)
const seconds = now.diff(
diff
const seconds = baseDate.diff(
compareDate
.add(years, 'year')
.add(months, 'month')
.add(weeks, 'week')
@ -45,21 +53,24 @@ const formatTimeDifference = (dateTime) => {
'second'
)
let timeStr = ''
if (years > 0) {
return `${years}y`
timeStr = `${years} ${years === 1 ? 'year' : 'years'}`
} else if (months > 0) {
return `${months}mo`
timeStr = `${months} ${months === 1 ? 'month' : 'months'}`
} else if (weeks > 0) {
return `${weeks}w`
timeStr = `${weeks} ${weeks === 1 ? 'week' : 'weeks'}`
} else if (days > 0) {
return `${days}d`
timeStr = `${days} ${days === 1 ? 'day' : 'days'}`
} else if (hours > 0) {
return `${hours}h`
timeStr = `${hours} ${hours === 1 ? 'hour' : 'hours'}`
} else if (minutes > 0) {
return `${minutes}m`
timeStr = `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`
} else {
return `${seconds}s`
timeStr = `${seconds} ${seconds === 1 ? 'second' : 'seconds'}`
}
return isFuture ? `in ${timeStr} time` : `${timeStr} ago`
}
const TimeDisplay = ({
@ -98,7 +109,7 @@ const TimeDisplay = ({
return (
<Flex align={'center'} gap={'small'}>
<Text type={type}>{formattedDate}</Text>
{showSince ? <Tag style={{ margin: 0 }}>{timeAgo + ' ago'}</Tag> : null}
{showSince ? <Tag style={{ margin: 0 }}>{timeAgo}</Tag> : null}
</Flex>
)
}

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/actionsicon.svg?react'
const ActionsIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ActionsIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/clienticon.svg?react'
const ClientIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ClientIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/paymenticon.svg?react'
const PaymentIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PaymentIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/salesicon.svg?react'
const SalesIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default SalesIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/salesordericon.svg?react'
const SalesOrderIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default SalesOrderIcon

View File

@ -31,6 +31,9 @@ import { DocumentJob } from './models/DocumentJob.js'
import { TaxRate } from './models/TaxRate.js'
import { TaxRecord } from './models/TaxRecord.js'
import { Invoice } from './models/Invoice.js'
import { Payment } from './models/Payment.js'
import { Client } from './models/Client.js'
import { SalesOrder } from './models/SalesOrder.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
@ -66,7 +69,10 @@ export const objectModels = [
DocumentJob,
TaxRate,
TaxRecord,
Invoice
Invoice,
Payment,
Client,
SalesOrder
]
// Re-export individual models for direct access
@ -103,7 +109,10 @@ export {
DocumentJob,
TaxRate,
TaxRecord,
Invoice
Invoice,
Payment,
Client,
SalesOrder
}
export function getModelByName(name, ignoreCase = false) {

View File

@ -7,7 +7,7 @@ export const AuditLog = {
icon: AuditLogIcon,
actions: [],
columns: [
'_id',
'_reference',
'owner',
'parent',
'operation',

View File

@ -0,0 +1,215 @@
import ClientIcon from '../../components/Icons/ClientIcon'
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 ReloadIcon from '../../components/Icons/ReloadIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const Client = {
name: 'client',
label: 'Client',
prefix: 'CLI',
icon: ClientIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/sales/clients/info?clientId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/sales/clients/info?clientId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) => `/dashboard/sales/clients/info?clientId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/clients/info?clientId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/sales/clients/info?clientId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/clients/info?clientId=${_id}&action=delete`
}
],
columns: [
'name',
'_reference',
'country',
'email',
'phone',
'active',
'createdAt',
'updatedAt'
],
filters: [
'name',
'_id',
'country',
'email',
'phone',
'active',
'createdAt',
'updatedAt'
],
sorters: [
'name',
'country',
'email',
'phone',
'active',
'createdAt',
'updatedAt',
'_id'
],
group: [],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'client',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'active',
label: 'Active',
type: 'bool',
readOnly: false,
required: true
},
{
name: 'country',
label: 'Country',
type: 'country',
readOnly: false,
required: false
},
{
name: 'email',
label: 'Email',
columnWidth: 300,
type: 'email',
readOnly: false,
required: false
},
{
name: 'phone',
label: 'Phone',
type: 'phone',
readOnly: false,
required: false
},
{
name: 'tags',
label: 'Tags',
type: 'array',
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',
readOnly: false,
required: false
}
]
}

View File

@ -68,7 +68,7 @@ export const Courier = {
`/dashboard/management/couriers/info?courierId=${_id}&action=delete`
}
],
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
columns: ['name', '_reference', 'country', 'email', 'website', 'createdAt'],
filters: ['name', '_id', 'country', 'email'],
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
group: ['country'],

View File

@ -69,7 +69,14 @@ export const CourierService = {
`/dashboard/management/courierservices/info?courierServiceId=${_id}&action=delete`
}
],
columns: ['name', '_id', 'courier', 'tracked', 'deliveryTime', 'active'],
columns: [
'name',
'_reference',
'courier',
'tracked',
'deliveryTime',
'active'
],
filters: ['name', '_id', 'courier', 'active', 'deliveryTime', 'tracked'],
sorters: [
'name',

View File

@ -61,7 +61,7 @@ export const DocumentJob = {
}
}
],
columns: ['name', '_id', 'state', 'createdAt', 'updatedAt'],
columns: ['name', '_reference', 'state', 'createdAt', 'updatedAt'],
filters: ['name', '_id', 'state'],
sorters: ['name', 'state', 'createdAt', 'updatedAt'],
properties: [

View File

@ -60,7 +60,15 @@ export const DocumentPrinter = {
}
}
],
columns: ['name', '_id', 'state', 'host', 'tags', 'connectedAt', 'updatedAt'],
columns: [
'name',
'_reference',
'state',
'host',
'tags',
'connectedAt',
'updatedAt'
],
filters: ['name', '_id'],
sorters: ['name', 'documentSize', 'connectedAt', 'updatedAt'],
properties: [

View File

@ -62,7 +62,7 @@ export const DocumentSize = {
],
columns: [
'name',
'_id',
'_reference',
'width',
'height',
'infiniteHeight',

View File

@ -71,7 +71,7 @@ export const DocumentTemplate = {
],
columns: [
'name',
'_id',
'_reference',
'active',
'global',
'objectType',

View File

@ -59,7 +59,7 @@ export const Filament = {
}
],
columns: [
'_id',
'_reference',
'name',
'type',
'color',

View File

@ -18,7 +18,7 @@ export const FilamentStock = {
}
],
columns: [
'_id',
'_reference',
'state',
'currentWeight',
'startingWeight',

View File

@ -78,7 +78,7 @@ export const File = {
}
],
url: (id) => `/dashboard/management/files/info?fileId=${id}`,
columns: ['name', '_id', 'type', 'size', 'temp', 'createdAt'],
columns: ['name', '_reference', 'type', 'size', 'temp', 'createdAt'],
filters: ['name', '_id', 'type', 'temp'],
sorters: ['name', 'type', 'size', 'createdAt', 'temp'],
group: ['type'],

View File

@ -70,7 +70,7 @@ export const GCodeFile = {
columns: [
'name',
'_id',
'_reference',
'filament',
'gcodeFileInfo.estimatedPrintingTimeNormalMode',
'gcodeFileInfo.sparseInfillDensity',

View File

@ -67,7 +67,7 @@ export const Host = {
}
}
],
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
columns: ['name', '_reference', 'state', 'tags', 'connectedAt'],
filters: ['name', '_id', 'state', 'tags'],
sorters: ['name', 'state', 'connectedAt'],
group: ['tags'],

View File

@ -4,6 +4,7 @@ import CheckIcon from '../../components/Icons/CheckIcon'
import EditIcon from '../../components/Icons/EditIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
export const Invoice = {
name: 'invoice',
@ -72,28 +73,38 @@ export const Invoice = {
},
{ type: 'divider' },
{
name: 'send',
label: 'Send',
name: 'newPayment',
label: 'New Payment',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=newPayment`,
disabled: (objectData) => {
const allowedStates = ['acknowledged', 'partiallyPaid', 'overdue']
return !allowedStates.includes(objectData?.state?.type)
}
},
{ type: 'divider' },
{
name: 'post',
label: 'Post',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=send`,
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
{
name: 'markPaid',
label: 'Mark Paid',
name: 'acknowledge',
label: 'Acknowledge',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=markPaid`,
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=acknowledge`,
visible: (objectData) => {
return (
objectData?.state?.type == 'sent' ||
objectData?.state?.type == 'partiallyPaid'
)
return objectData?.state?.type == 'sent'
}
},
{
@ -105,29 +116,28 @@ export const Invoice = {
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=cancel`,
disabled: (objectData) => {
return (
objectData?.state?.type == 'cancelled' ||
objectData?.state?.type == 'paid'
)
const allowedStates = [
'acknowledged',
'partiallyPaid',
'overdue',
'sent'
]
return !allowedStates.includes(objectData?.state?.type)
},
visible: (objectData) => {
return (
objectData?.state?.type == 'draft' ||
objectData?.state?.type == 'sent'
)
return objectData?.state?.type != 'draft'
}
}
],
group: ['vendor', 'customer', 'invoiceType'],
filters: ['vendor', 'customer', 'invoiceType'],
group: ['orderType'],
filters: ['to', 'from', 'orderType'],
sorters: ['createdAt', 'state', 'updatedAt', 'invoiceDate', 'dueDate'],
columns: [
'_id',
'_reference',
'state',
'invoiceType',
'vendor',
'customer',
'orderType',
'to',
'from',
'invoiceDate',
'dueDate',
'totalAmount',
@ -159,6 +169,7 @@ export const Invoice = {
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
required: true,
objectType: 'invoice',
showCopy: true,
@ -172,27 +183,11 @@ export const Invoice = {
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'invoiceDate',
label: 'Invoice Date',
type: 'date',
readOnly: false
},
{
name: 'dueDate',
label: 'Due Date',
type: 'date',
readOnly: false
},
{
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor',
showHyperlink: true,
visible: (objectData) => {
return objectData?.invoiceType === 'purchase' || objectData?.vendor
}
name: 'issuedAt',
label: 'Issued At',
type: 'dateTime',
readOnly: false,
required: true
},
{
name: 'orderType',
@ -201,6 +196,12 @@ export const Invoice = {
masterFilter: ['purchaseOrder', 'salesOrder'],
required: true
},
{
name: 'dueAt',
label: 'Due At',
type: 'dateTime',
required: true
},
{
name: 'order',
label: 'Order',
@ -208,38 +209,62 @@ export const Invoice = {
objectType: (objectData) => {
return objectData?.orderType
},
masterFilter: (objectData) => {
return {
vendor: objectData?.vendor?._id
}
},
required: true,
showHyperlink: true
},
{
name: 'sentAt',
label: 'Sent At',
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true
},
{
name: 'paidAt',
label: 'Paid At',
name: 'from',
label: 'From',
required: true,
type: 'object',
objectType: 'vendor',
showHyperlink: true,
readOnly: true,
value: (objectData) => {
if (objectData?.orderType == 'purchaseOrder') {
return objectData?.order?.vendor
} else {
return null
}
}
},
{
name: 'acknowledgedAt',
label: 'Acknowledged At',
type: 'dateTime',
readOnly: true
},
{
name: 'to',
label: 'To',
required: true,
type: 'object',
objectType: 'client',
showHyperlink: true,
readOnly: true,
value: (objectData) => {
if (objectData?.orderType == 'salesOrder') {
return objectData?.order?.client
} else {
return null
}
}
},
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true
},
{
name: 'overdueAt',
label: 'Overdue At',
type: 'dateTime',
readOnly: true
},
{
name: 'totalTaxAmount',
label: 'Total Tax Amount',
@ -249,6 +274,12 @@ export const Invoice = {
readOnly: true,
columnWidth: 175
},
{
name: 'paidAt',
label: 'Paid At',
type: 'dateTime',
readOnly: true
},
{
name: 'totalAmountWithTax',
label: 'Total Amount w/ Tax',
@ -293,44 +324,294 @@ export const Invoice = {
roundNumber: 2,
columnWidth: 175,
readOnly: true
},
{
name: 'invoiceOrderItems',
label: 'Invoice Order Items',
type: 'objectChildren',
objectType: 'orderItem',
properties: [
{
name: 'orderItem',
label: 'Order Item',
type: 'object',
objectType: 'orderItem',
required: true,
columnWidth: 300,
showHyperlink: true
},
{
name: 'invoiceQuantity',
label: 'Quantity',
type: 'number',
required: true,
columnWidth: 175
},
{
name: 'invoiceAmount',
label: 'Invoice Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
required: true,
columnWidth: 150
},
{
name: 'taxRate',
label: 'Tax Rate',
type: 'object',
objectType: 'taxRate',
required: false,
showHyperlink: true,
columnWidth: 200
},
{
name: 'invoiceAmountWithTax',
label: 'Invoice Amount w/ Tax',
type: 'number',
prefix: '£',
roundNumber: 2,
required: true,
readOnly: true,
columnWidth: 200,
value: (objectData) => {
const invoiceAmount = objectData?.invoiceAmount || 0
if (objectData?.taxRate?.rateType == 'percentage') {
return (
(invoiceAmount * (1 + objectData?.taxRate?.rate / 100)).toFixed(
2
) || undefined
)
} else if (objectData?.taxRate?.rateType == 'amount') {
return (
(invoiceAmount + objectData?.taxRate?.rate).toFixed(2) ||
undefined
)
} else {
return invoiceAmount || 0
}
}
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total Quantity',
type: 'number',
property: 'invoiceQuantity',
value: (objectData) => {
return objectData?.invoiceOrderItems?.reduce(
(acc, item) => acc + (item.invoiceQuantity || 0),
0
)
}
},
{
name: 'totalAmount',
label: 'Total Amount',
type: 'number',
property: 'invoiceAmount',
prefix: '£',
fixedNumber: 2,
value: (objectData) => {
return objectData?.invoiceOrderItems
?.reduce(
(acc, item) =>
acc + (Number.parseFloat(item.invoiceAmount) || 0),
0
)
.toFixed(2)
}
},
{
name: 'totalAmountWithTax',
label: 'Total Amount w/ Tax',
type: 'number',
property: 'invoiceAmountWithTax',
prefix: '£',
fixedNumber: 2,
value: (objectData) => {
return objectData?.invoiceOrderItems
?.reduce(
(acc, item) =>
acc + (Number.parseFloat(item.invoiceAmountWithTax) || 0),
0
)
.toFixed(2)
}
}
]
},
{
name: 'invoiceShipments',
label: 'Invoice Shipments',
type: 'objectChildren',
objectType: 'shipment',
properties: [
{
name: 'shipment',
label: 'Shipment',
type: 'object',
objectType: 'shipment',
required: true,
columnWidth: 300,
showHyperlink: true
},
{
name: 'invoiceAmount',
label: 'Invoice Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
columnWidth: 175,
required: true
},
{
name: 'taxRate',
label: 'Tax Rate',
type: 'object',
objectType: 'taxRate',
required: false,
showHyperlink: true,
columnWidth: 200
},
{
name: 'invoiceAmountWithTax',
label: 'Invoice Amount w/ Tax',
type: 'number',
prefix: '£',
roundNumber: 2,
required: true,
readOnly: true,
columnWidth: 200,
value: (objectData) => {
const invoiceAmount = objectData?.invoiceAmount || 0
if (objectData?.taxRate?.rateType == 'percentage') {
return (
(invoiceAmount * (1 + objectData?.taxRate?.rate / 100)).toFixed(
2
) || undefined
)
} else if (objectData?.taxRate?.rateType == 'amount') {
return (
(invoiceAmount + objectData?.taxRate?.rate).toFixed(2) ||
undefined
)
} else {
return invoiceAmount || 0
}
}
}
],
rollups: [
{
name: 'totalAmount',
label: 'Total Amount',
type: 'number',
property: 'invoiceAmount',
prefix: '£',
roundNumber: 2,
value: (objectData) => {
return objectData?.invoiceShipments
?.reduce(
(acc, shipment) => acc + (shipment.invoiceAmount || 0),
0
)
.toFixed(2)
}
},
{
name: 'totalAmountWithTax',
label: 'Total Amount w/ Tax',
type: 'number',
property: 'invoiceAmountWithTax',
prefix: '£',
roundNumber: 2,
value: (objectData) => {
return objectData?.invoiceShipments
?.reduce(
(acc, shipment) =>
acc + (Number.parseFloat(shipment.invoiceAmountWithTax) || 0),
0
)
.toFixed(2)
}
}
]
}
],
stats: [
{
name: 'draft.count',
name: 'draft.draftCount.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'sent.count',
name: 'draft.draftGrandTotalAmount.sum',
label: 'Draft Grand Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'default'
},
{
name: 'sent.sentCount.count',
label: 'Sent',
type: 'number',
color: 'cyan'
},
{
name: 'partiallyPaid.count',
name: 'acknowledged.acknowledgedCount.count',
label: 'Acknowledged',
type: 'number',
color: 'purple'
},
{
name: 'due.dueCount.count',
label: 'Due',
type: 'number',
color: 'warning',
sum: [
'sent.sentCount.count',
'partiallyPaid.partiallyPaidCount.count',
'overdue.overdueCount.count',
'acknowledged.acknowledgedCount.count'
]
},
{
name: 'due.dueGrandTotalAmount.sum',
label: 'Due Grand Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'warning',
sum: [
'sent.sentGrandTotalAmount.sum',
'partiallyPaid.partiallyPaidGrandTotalAmount.sum',
'overdue.overdueGrandTotalAmount.sum',
'acknowledged.acknowledgedGrandTotalAmount.sum'
]
},
{
name: 'partiallyPaid.partiallyPaidCount.count',
label: 'Partially Paid',
type: 'number',
color: 'processing'
},
{
name: 'paid.count',
label: 'Paid',
type: 'number',
color: 'success'
},
{
name: 'overdue.count',
name: 'overdue.overdueCount.count',
label: 'Overdue',
type: 'number',
color: 'error'
},
{
name: 'cancelled.count',
label: 'Cancelled',
name: 'overdue.overdueGrandTotalAmount.sum',
label: 'Overdue Grand Total Amount',
type: 'number',
color: 'default'
prefix: '£',
roundNumber: 2,
color: 'error'
}
]
}

View File

@ -37,7 +37,7 @@ export const Job = {
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload`
}
],
columns: ['_id', 'quantity', 'state', 'gcodeFile', 'createdAt'],
columns: ['_reference', 'quantity', 'state', 'gcodeFile', 'createdAt'],
filters: ['state', '_id', 'gcodeFile', 'quantity'],
sorters: ['createdAt', 'state', 'quantity', 'gcodeFile'],
properties: [

View File

@ -34,6 +34,7 @@ export const Note = {
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'note',
showCopy: true,
readOnly: true

View File

@ -58,7 +58,7 @@ export const NoteType = {
}
}
],
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
columns: ['name', '_reference', 'color', 'active', 'createdAt', 'updatedAt'],
filters: ['name', '_id', 'color', 'active'],
sorters: ['name', 'color', 'active', 'createdAt', 'updatedAt'],
properties: [

View File

@ -81,8 +81,8 @@ export const OrderItem = {
filters: ['itemType', 'item', 'order'],
sorters: ['createdAt', 'updatedAt', 'itemAmount', 'quantity'],
columns: [
'_id',
'_reference',
'name',
'state',
'itemType',
'item',
@ -93,6 +93,14 @@ export const OrderItem = {
'totalAmountWithTax',
'order',
'shipment',
'invoicedAmount',
'invoicedAmountWithTax',
'invoicedQuantity',
'invoicedAmountRemaining',
'invoicedAmountWithTaxRemaining',
'invoicedQuantityRemaining',
'orderedAt',
'receivedAt',
'createdAt',
'updatedAt'
],
@ -116,6 +124,7 @@ export const OrderItem = {
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'orderItem',
showCopy: true,
readOnly: true
@ -126,7 +135,30 @@ export const OrderItem = {
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
type: 'text',
readOnly: true,
value: (objectData) => {
return objectData?.item?.name
}
},
{
name: 'orderedAt',
label: 'Ordered At',
type: 'dateTime',
required: false,
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'receivedAt',
label: 'Received At',
type: 'dateTime',
required: false,
readOnly: true
},
{
name: 'orderType',
label: 'Order Type',
@ -166,7 +198,7 @@ export const OrderItem = {
name: 'itemType',
label: 'Item Type',
type: 'objectType',
masterFilter: ['part', 'packaging', 'filament'],
masterFilter: ['part', 'packaging', 'filament', 'product'],
required: true,
columnWidth: 175
},
@ -201,6 +233,7 @@ export const OrderItem = {
prefix: '£',
min: 0,
step: 0.01,
fixedNumber: 2,
readOnly: (objectData) => {
return objectData?.syncAmount != null
},
@ -231,6 +264,7 @@ export const OrderItem = {
min: 0,
step: 0.01,
columnWidth: 150,
fixedNumber: 2,
readOnly: true,
value: (objectData) => {
if (objectData?.itemAmount && objectData?.quantity) {
@ -272,6 +306,7 @@ export const OrderItem = {
label: 'Total Amount w/ Tax',
type: 'number',
required: true,
fixedNumber: 2,
readOnly: true,
prefix: '£',
min: 0,
@ -298,6 +333,70 @@ export const OrderItem = {
return totalAmount || 0
}
}
},
{
name: 'invoicedAmount',
label: 'Invoiced Amount',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
fixedNumber: 2,
min: 0,
step: 0.01,
columnWidth: 150
},
{
name: 'invoicedAmountWithTax',
label: 'Invoiced Amount w/ Tax',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
fixedNumber: 2,
min: 0,
step: 0.01,
columnWidth: 200
},
{
name: 'invoicedQuantity',
label: 'Invoiced Quantity',
type: 'number',
required: false,
readOnly: true,
columnWidth: 150
},
{
name: 'invoicedAmountRemaining',
label: 'Remaining Invoiced Amount',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 225,
fixedNumber: 2
},
{
name: 'invoicedAmountWithTaxRemaining',
label: 'Remaining Invoiced Amount w/ Tax',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
min: 0,
fixedNumber: 2,
step: 0.01,
columnWidth: 275
},
{
name: 'invoicedQuantityRemaining',
label: 'Remaining Invoiced Quantity',
type: 'number',
required: false,
readOnly: true,
columnWidth: 225
}
]
}

View File

@ -57,7 +57,7 @@ export const Part = {
}
}
],
columns: ['name', '_id', 'product', 'globalPricing', 'createdAt'],
columns: ['name', '_reference', 'product', 'globalPricing', 'createdAt'],
filters: ['name', '_id', 'product', 'globalPricing'],
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
properties: [

View File

@ -20,7 +20,7 @@ export const PartStock = {
filters: ['_id', 'part', 'startingQuantity', 'currentQuantity'],
sorters: ['part', 'startingQuantity', 'currentQuantity'],
columns: [
'_id',
'_reference',
'state',
'startingQuantity',
'currentQuantity',

View File

@ -0,0 +1,252 @@
import PaymentIcon from '../../components/Icons/PaymentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import EditIcon from '../../components/Icons/EditIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const Payment = {
name: 'payment',
label: 'Payment',
prefix: 'PAY',
icon: PaymentIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/finance/payments/info?paymentId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'cancelEdit',
label: 'Cancel Edit',
type: 'button',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{ type: 'divider' },
{
name: 'post',
label: 'Post',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
{
name: 'cancel',
label: 'Cancel',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=cancel`,
disabled: (objectData) => {
return objectData?.state?.type == 'cancelled'
},
visible: (objectData) => {
return (
objectData?.state?.type == 'draft' ||
objectData?.state?.type == 'posted'
)
}
}
],
group: ['vendor', 'client', 'invoice'],
filters: ['vendor', 'client', 'invoice'],
sorters: ['createdAt', 'state', 'updatedAt', 'paymentDate'],
columns: [
'_reference',
'state',
'invoice',
'vendor',
'client',
'paymentDate',
'amount',
'paymentMethod',
'createdAt',
'updatedAt'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
columnFixed: 'left',
objectType: 'payment',
columnWidth: 140,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
required: true,
objectType: 'payment',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'invoice',
label: 'Invoice',
type: 'object',
objectType: 'invoice',
required: true,
showHyperlink: true
},
{
name: 'useRemainingAmount',
label: 'Use Remaining Amount',
type: 'boolean',
required: true
},
{
name: 'vendor',
label: 'Vendor',
type: 'object',
objectType: 'vendor',
showHyperlink: true,
readOnly: true
},
{
name: 'client',
label: 'Client',
type: 'object',
objectType: 'client',
showHyperlink: true,
readOnly: true
},
{
name: 'paymentDate',
label: 'Payment Date',
type: 'dateTime',
required: true
},
{
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true
},
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true
},
{
name: 'amount',
label: 'Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
required: true,
columnWidth: 150
},
{
name: 'paymentMethod',
label: 'Payment Method',
type: 'string',
required: false
},
{
name: 'notes',
label: 'Notes',
type: 'text',
required: false
}
],
stats: [
{
name: 'draft.draftCount.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'draft.draftAmount.sum',
label: 'Draft Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'default'
},
{
name: 'posted.postedCount.count',
label: 'Posted',
type: 'number',
color: 'cyan'
},
{
name: 'posted.postedAmount.sum',
label: 'Posted Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'cyan'
}
]
}

View File

@ -212,7 +212,15 @@ export const Printer = {
]
}
],
columns: ['name', '_id', 'state', 'host', 'tags', 'connectedAt', 'updatedAt'],
columns: [
'name',
'_reference',
'state',
'host',
'tags',
'connectedAt',
'updatedAt'
],
filters: ['name', '_id', 'state', 'tags'],
sorters: ['name', 'state', 'connectedAt'],
group: ['tags'],

View File

@ -58,7 +58,7 @@ export const Product = {
}
}
],
columns: ['_id', '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: [

View File

@ -103,7 +103,13 @@ export const PurchaseOrder = {
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newInvoice`,
disabled: (objectData) => {
return objectData?.state?.type != 'received'
const allowedStates = [
'received',
'shipped',
'partiallyReceived',
'partiallyShipped'
]
return !allowedStates.includes(objectData?.state?.type)
}
},
{
@ -169,7 +175,6 @@ export const PurchaseOrder = {
filters: ['vendor'],
sorters: ['createdAt', 'state', 'updatedAt'],
columns: [
'_id',
'_reference',
'state',
'vendor',
@ -203,6 +208,7 @@ export const PurchaseOrder = {
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
required: true,
objectType: 'purchaseOrder',
showCopy: true,
@ -240,7 +246,7 @@ export const PurchaseOrder = {
columnWidth: 175
},
{
name: 'CompletedAt',
name: 'completedAt',
label: 'Completed At',
type: 'dateTime',
readOnly: true
@ -308,7 +314,7 @@ export const PurchaseOrder = {
name: 'acknowledged.count',
label: 'Acknowledged',
type: 'number',
color: 'processing'
color: 'purple'
},
{
name: 'partiallyShipped.count',

View File

@ -0,0 +1,343 @@
import SalesOrderIcon from '../../components/Icons/SalesOrderIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import EditIcon from '../../components/Icons/EditIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const SalesOrder = {
name: 'salesOrder',
label: 'Sales Order',
prefix: 'SOR',
icon: SalesOrderIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/sales/salesorders/info?salesOrderId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'cancelEdit',
label: 'Cancel Edit',
type: 'button',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{ type: 'divider' },
{
name: 'New Order Item',
label: 'New Order Item',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=newOrderItem`,
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'New Shipment',
label: 'New Shipment',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=newShipment`,
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'New Invoice',
label: 'New Invoice',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=newInvoice`,
disabled: (objectData) => {
const allowedStates = [
'delivered',
'sent',
'confirmed',
'shipped',
'partiallyDelivered',
'partiallyShipped'
]
return !allowedStates.includes(objectData?.state?.type)
}
},
{ type: 'divider' },
{
name: 'post',
label: 'Post',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
{
name: 'confirm',
label: 'Confirm',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=confirm`,
visible: (objectData) => {
return objectData?.state?.type == 'sent'
}
},
{
name: 'complete',
label: 'Complete',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=complete`,
disabled: (objectData) => {
return objectData?.state?.type != 'delivered'
},
visible: (objectData) => {
return objectData?.state?.type == 'delivered'
}
},
{
name: 'cancel',
label: 'Cancel',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=cancel`,
disabled: (objectData) => {
return objectData?.state?.type == 'cancelled'
},
visible: (objectData) => {
return (
objectData?.state?.type != 'draft' &&
objectData?.state?.type != 'completed' &&
objectData?.state?.type != 'delivered'
)
}
}
],
group: ['client'],
filters: ['client'],
sorters: ['createdAt', 'state', 'updatedAt'],
columns: [
'_reference',
'state',
'client',
'totalAmount',
'totalAmountWithTax',
'totalTaxAmount',
'shippingAmount',
'shippingAmountWithTax',
'grandTotalAmount',
'createdAt',
'updatedAt',
'client'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
columnFixed: 'left',
objectType: 'salesOrder',
columnWidth: 140,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
required: true,
objectType: 'salesOrder',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{ name: 'postedAt', label: 'Posted At', type: 'dateTime', readOnly: true },
{
name: 'client',
label: 'Client',
required: true,
type: 'object',
objectType: 'client',
showHyperlink: true
},
{
name: 'confirmedAt',
label: 'Confirmed At',
type: 'dateTime',
readOnly: true
},
{
name: 'totalTaxAmount',
label: 'Total Tax Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 175
},
{
name: 'completedAt',
label: 'Completed At',
type: 'dateTime',
readOnly: true
},
{
name: 'totalAmountWithTax',
label: 'Total Amount w/ Tax',
type: 'number',
prefix: '£',
readOnly: true,
columnWidth: 175,
roundNumber: 2
},
{
name: 'shippingAmount',
label: 'Shipping Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 150
},
{
name: 'shippingAmountWithTax',
label: 'Shipping Amount w/ Tax',
type: 'number',
prefix: '£',
readOnly: true,
roundNumber: 2,
columnWidth: 200
},
{
name: 'totalAmount',
label: 'Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 150
},
{
name: 'grandTotalAmount',
label: 'Grand Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
columnWidth: 175,
readOnly: true
}
],
stats: [
{
name: 'draft.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'sent.count',
label: 'Sent',
type: 'number',
color: 'cyan'
},
{
name: 'confirmed.count',
label: 'Confirmed',
type: 'number',
color: 'purple'
},
{
name: 'partiallyShipped.count',
label: 'Partially Shipped',
type: 'number',
color: 'processing'
},
{
name: 'shipped.count',
label: 'Shipped',
type: 'number',
color: 'processing'
},
{
name: 'partiallyDelivered.count',
label: 'Partially Delivered',
type: 'number',
color: 'success'
},
{
name: 'delivered.count',
label: 'Delivered',
type: 'number',
color: 'success'
}
]
}

View File

@ -123,7 +123,6 @@ export const Shipment = {
'deliveredAt'
],
columns: [
'_id',
'_reference',
'state',
'orderType',
@ -132,6 +131,10 @@ export const Shipment = {
'amountWithTax',
'taxRate',
'taxAmount',
'invoicedAmount',
'invoicedAmountWithTax',
'invoicedAmountRemaining',
'invoicedAmountWithTaxRemaining',
'trackingNumber',
'shippedAt',
'expectedAt',
@ -159,6 +162,7 @@ export const Shipment = {
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'shipment',
showCopy: true,
readOnly: true
@ -288,6 +292,54 @@ export const Shipment = {
}
return 0
}
},
{
name: 'invoicedAmount',
label: 'Invoiced Amount',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
fixedNumber: 2,
min: 0,
step: 0.01,
columnWidth: 150
},
{
name: 'invoicedAmountWithTax',
label: 'Invoiced Amount w/ Tax',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
fixedNumber: 2,
min: 0,
step: 0.01,
columnWidth: 200
},
{
name: 'invoicedAmountRemaining',
label: 'Remaining Invoiced Amount',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 225,
fixedNumber: 2
},
{
name: 'invoicedAmountWithTaxRemaining',
label: 'Remaining Invoiced Amount w/ Tax',
type: 'number',
required: false,
readOnly: true,
prefix: '£',
min: 0,
fixedNumber: 2,
step: 0.01,
columnWidth: 275
}
]
}

View File

@ -17,7 +17,7 @@ export const StockAudit = {
}
],
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`,
columns: ['_id', 'state', 'createdAt', 'updatedAt'],
columns: ['_reference', 'state', 'createdAt', 'updatedAt'],
filters: ['_id'],
sorters: ['createdAt', 'updatedAt'],
group: ['state'],

View File

@ -6,7 +6,7 @@ export const StockEvent = {
prefix: 'SEV',
icon: StockEventIcon,
actions: [],
columns: ['_id', 'owner', 'parent', 'value', 'createdAt'],
columns: ['_reference', 'owner', 'parent', 'value', 'createdAt'],
filters: ['_id', 'owner', 'parent'],
sorters: ['createdAt'],
properties: [
@ -26,6 +26,16 @@ export const StockEvent = {
value: null,
readOnly: true
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'stockEvent',
value: null,
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
@ -41,6 +51,7 @@ export const StockEvent = {
return objectData.ownerType
},
columnFixed: 'left',
columnWidth: 200,
value: null,
showCopy: true,
showHyperlink: true
@ -53,6 +64,7 @@ export const StockEvent = {
return objectData?.parentType
},
value: null,
columnWidth: 200,
showCopy: true,
showHyperlink: true
},

View File

@ -29,7 +29,7 @@ export const SubJob = {
}
}
],
columns: ['_id', 'printer', 'job', 'state', 'createdAt'],
columns: ['_reference', 'printer', 'job', 'state', 'createdAt'],
filters: ['state', '_id', 'job', 'printer'],
sorters: ['createdAt', 'state'],
group: ['job'],

View File

@ -70,7 +70,7 @@ export const TaxRate = {
],
columns: [
'name',
'_id',
'_reference',
'rate',
'rateType',
'active',

View File

@ -69,7 +69,7 @@ export const TaxRecord = {
}
],
columns: [
'_id',
'_reference',
'taxRate',
'transactionType',
'transaction',

View File

@ -24,7 +24,7 @@ export const User = {
`/dashboard/management/users/info?userId=${_id}&action=reload`
}
],
columns: ['name', '_id', 'username', 'email', 'role', 'createdAt'],
columns: ['name', '_reference', 'username', 'email', 'role', 'createdAt'],
filters: ['name', '_id', 'email', 'role'],
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
properties: [

View File

@ -70,7 +70,7 @@ export const Vendor = {
],
columns: [
'name',
'_id',
'_reference',
'country',
'email',
'website',

View File

@ -7,6 +7,12 @@ const Invoices = lazy(
const InvoiceInfo = lazy(
() => import('../components/Dashboard/Finance/Invoices/InvoiceInfo.jsx')
)
const Payments = lazy(
() => import('../components/Dashboard/Finance/Payments.jsx')
)
const PaymentInfo = lazy(
() => import('../components/Dashboard/Finance/Payments/PaymentInfo.jsx')
)
const FinanceOverview = lazy(
() => import('../components/Dashboard/Finance/FinanceOverview.jsx')
)
@ -22,6 +28,12 @@ const FinanceRoutes = [
key='invoices-info'
path='finance/invoices/info'
element={<InvoiceInfo />}
/>,
<Route key='payments' path='finance/payments' element={<Payments />} />,
<Route
key='payments-info'
path='finance/payments/info'
element={<PaymentInfo />}
/>
]

View File

@ -0,0 +1,41 @@
import { lazy } from 'react'
import { Route } from 'react-router-dom'
const Clients = lazy(
() => import('../components/Dashboard/Sales/Clients.jsx')
)
const ClientInfo = lazy(
() => import('../components/Dashboard/Sales/Clients/ClientInfo.jsx')
)
const SalesOrders = lazy(
() => import('../components/Dashboard/Sales/SalesOrders.jsx')
)
const SalesOrderInfo = lazy(
() => import('../components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx')
)
const SalesOverview = lazy(
() => import('../components/Dashboard/Sales/SalesOverview.jsx')
)
const SalesRoutes = [
<Route
key='overview'
path='sales/overview'
element={<SalesOverview />}
/>,
<Route key='clients' path='sales/clients' element={<Clients />} />,
<Route
key='clients-info'
path='sales/clients/info'
element={<ClientInfo />}
/>,
<Route key='salesorders' path='sales/salesorders' element={<SalesOrders />} />,
<Route
key='salesorders-info'
path='sales/salesorders/info'
element={<SalesOrderInfo />}
/>
]
export default SalesRoutes

View File

@ -1,5 +1,6 @@
export { default as ProductionRoutes } from './ProductionRoutes'
export { default as InventoryRoutes } from './InventoryRoutes'
export { default as FinanceRoutes } from './FinanceRoutes'
export { default as SalesRoutes } from './SalesRoutes'
export { default as ManagementRoutes } from './ManagementRoutes'
export { default as DeveloperRoutes } from './DeveloperRoutes'