Compare commits

...

6 Commits

56 changed files with 4044 additions and 182 deletions

View File

@ -1,16 +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 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>
<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 transform="matrix(0.630094,0,0,0.630094,12.050043,9.166267)">
<path d="M14.688,30.601L14.688,17.094C14.688,15.264 15.627,13.647 17.191,12.746L29.089,5.879C30.674,4.944 32.523,4.944 34.111,5.878L46.001,12.745C47.566,13.647 48.504,15.264 48.504,17.094L48.504,30.521L60.25,37.305C61.815,38.206 62.754,39.824 62.754,41.653L62.754,55.39C62.754,57.22 61.813,58.84 60.251,59.729L48.363,66.604C46.769,67.533 44.919,67.532 43.339,66.605L31.662,59.857L19.995,66.604C18.401,67.533 16.552,67.532 14.971,66.605L3.077,59.732C1.511,58.84 0.57,57.22 0.57,55.39L0.57,41.653C0.57,39.824 1.509,38.206 3.073,37.306L14.688,30.601ZM22.461,16.011L31.554,21.217L40.687,15.981L32,10.966L31.986,10.958C31.74,10.809 31.453,10.809 31.207,10.958L31.193,10.966L22.461,16.011ZM28.869,35.565L28.869,25.991L20.135,20.989L20.135,30.103C20.135,30.312 20.215,30.501 20.356,30.648L28.869,35.565ZM43.118,60.173L43.118,50.551L34.386,45.55L34.386,54.723C34.406,54.993 34.565,55.222 34.804,55.364L43.118,60.173ZM20.205,60.177L28.527,55.364C28.782,55.213 28.931,54.957 28.931,54.662L28.931,45.503L20.205,50.498L20.205,60.177ZM14.751,60.174L14.751,50.551L6.017,45.549L6.017,54.662C6.017,54.958 6.179,55.212 6.437,55.364L14.751,60.174ZM34.324,35.617L42.645,30.805C42.901,30.653 43.05,30.398 43.05,30.103L43.05,20.943L34.324,25.938L34.324,35.617ZM36.709,40.57L45.803,45.777L54.936,40.54L46.249,35.526L46.234,35.517C45.989,35.368 45.702,35.368 45.457,35.517L45.442,35.526L36.709,40.57ZM48.573,60.177L56.895,55.364C57.15,55.213 57.299,54.957 57.299,54.662L57.299,45.503L48.573,50.498L48.573,60.177ZM8.341,40.57L17.436,45.777L26.526,40.564L17.621,35.418L17.618,35.417C17.44,35.381 17.256,35.417 17.09,35.517L17.075,35.526L8.341,40.57Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,19 @@
<?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="M34.148,61L12.892,61C6.308,61 3,57.755 3,51.265L3,12.766C3,6.276 6.308,3 12.892,3L51.107,3C57.723,3 60.999,6.276 60.999,12.766L60.999,34.148C59.513,32.753 57.802,31.593 55.927,30.73L55.927,13.05C55.927,9.773 54.163,8.072 51.044,8.072L12.955,8.072C9.805,8.072 8.072,9.774 8.072,13.05L8.072,50.982C8.072,54.258 9.805,55.928 12.955,55.928L30.73,55.928C31.593,57.803 32.752,59.514 34.148,61ZM29.009,48.611L24.649,51.133C23.644,51.718 22.48,51.718 21.483,51.134L13.989,46.803C13.002,46.241 12.409,45.22 12.409,44.067L12.409,35.412C12.409,34.259 13.001,33.24 13.987,32.672L21.305,28.448L21.305,19.937C21.305,18.784 21.897,17.765 22.882,17.197L30.379,12.87C31.377,12.281 32.542,12.281 33.543,12.87L41.035,17.197C42.021,17.765 42.612,18.784 42.612,19.937L42.612,28.397L44.285,29.364C37.862,30.639 32.6,35.156 30.28,41.135L30.28,37.837L24.781,40.985L24.781,47.084L29.331,44.452C29.113,45.602 28.999,46.788 28.999,48C28.999,48.205 29.003,48.408 29.009,48.611ZM33.677,31.608L38.92,28.576C39.082,28.481 39.176,28.32 39.176,28.134L39.176,22.362L33.677,25.51L33.677,31.608ZM26.202,19.254L31.932,22.535L37.687,19.236L32.213,16.076L32.204,16.071C32.049,15.977 31.868,15.977 31.713,16.071L31.705,16.076L26.202,19.254ZM17.306,34.729L23.036,38.01L28.764,34.726L23.153,31.483L23.151,31.483C23.039,31.46 22.923,31.482 22.818,31.546L22.809,31.551L17.306,34.729ZM30.241,31.576L30.241,25.543L24.737,22.391L24.737,28.134C24.737,28.266 24.788,28.384 24.876,28.477L30.241,31.576ZM21.345,47.081L21.345,41.018L15.841,37.866L15.841,43.608C15.841,43.795 15.944,43.955 16.106,44.051L21.345,47.081Z"/>
<g transform="matrix(1.077085,0,0,1.077085,-2.466684,-4.933344)">
<path d="M46.854,34.29C55.053,34.29 61.709,40.946 61.709,49.145C61.709,57.344 55.053,64 46.854,64C38.656,64 32,57.344 32,49.145C32,40.946 38.656,34.29 46.854,34.29ZM46.854,38.468C40.962,38.468 36.177,43.252 36.177,49.145C36.177,55.038 40.962,59.822 46.854,59.822C52.747,59.822 57.531,55.038 57.531,49.145C57.531,43.252 52.747,38.468 46.854,38.468Z"/>
</g>
<g transform="matrix(1,0,0,1,-1.649781,-7.506927)">
<circle cx="49.627" cy="50.064" r="3.017"/>
</g>
<g transform="matrix(1,0,0,1,-6.45365,0.751087)">
<circle cx="49.627" cy="50.064" r="3.017"/>
</g>
<g transform="matrix(1,0,0,1,3.071774,0.733775)">
<circle cx="49.627" cy="50.064" r="3.017"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,8 @@
<?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;">
<path d="M43.002,56.36L34.763,61.13C32.69,62.329 30.313,62.329 28.262,61.13L7.686,49.23C5.644,48.067 4.441,45.98 4.441,43.613L4.441,19.857C4.441,17.49 5.644,15.403 7.686,14.218L28.262,2.357C30.313,1.136 32.69,1.136 34.763,2.357L55.317,14.218C57.359,15.403 58.563,17.49 58.563,19.857L58.563,29.046C58.328,29.131 58.089,29.228 57.849,29.338L53.423,31.365L53.423,23.065L34.072,34.087L34.072,40.228L32.231,41.071L32.221,41.076C30.543,41.84 29.474,43.061 28.932,44.416L28.932,34.121L9.576,23.093L9.576,42.668C9.576,43.743 10.14,44.685 11.082,45.245L28.484,55.304C28.7,55.436 28.732,55.463 28.932,55.556L28.932,48.876L29.226,49.54L29.779,50.37L30.487,51.108C31.389,51.893 32.586,52.451 34.072,52.537L34.072,55.556C34.36,55.41 34.43,55.359 34.74,55.186L39.287,52.558L42.996,52.566L43.002,56.36ZM19.147,13.561L11.812,17.8C11.583,17.927 11.53,17.997 11.392,18.125L31.484,29.577L38.568,25.534C34.008,22.886 22.086,15.408 19.147,13.561ZM43.622,22.649L51.582,18.106C51.464,17.992 51.421,17.927 51.213,17.8L32.992,7.283C32.05,6.719 30.953,6.719 30.011,7.283L24.048,10.729L43.622,22.649Z"/>
<g transform="matrix(0.512064,0,0,0.512064,31.502,31.239544)">
<path d="M3.845,24.535C-2.218,27.285 -0.765,35.739 5.782,35.754L28.016,35.801C28.235,35.801 28.298,35.879 28.298,36.098L28.329,58.239C28.345,64.864 36.86,66.16 39.673,59.973L62.563,10.082C65.594,3.411 60.548,-1.433 53.891,1.614L3.845,24.535ZM15.626,28.02C15.454,28.02 15.407,27.895 15.61,27.801L53.079,10.754C53.313,10.661 53.469,10.707 53.329,11.02L36.219,48.457C36.157,48.614 36.032,48.567 36.032,48.41L36.141,31.973C36.157,29.067 35.001,27.895 32.079,27.91L15.626,28.02Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

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;">
<path d="M37.609,59.482L34.763,61.13C32.69,62.329 30.313,62.329 28.262,61.13L7.686,49.23C5.644,48.067 4.441,45.98 4.441,43.613L4.441,19.857C4.441,17.49 5.644,15.403 7.686,14.218L28.262,2.357C30.313,1.136 32.69,1.136 34.763,2.357L55.317,14.218C57.359,15.403 58.563,17.49 58.563,19.857L58.563,29.069L53.423,29.069L53.423,23.065L48.562,25.834L48.238,24.714L47.694,23.67L46.948,22.757L46.033,22.016L45.35,21.663L51.582,18.106C51.464,17.992 51.421,17.927 51.213,17.8L32.992,7.283C32.05,6.719 30.953,6.719 30.011,7.283L24.048,10.729L41.312,21.242L40.408,21.525L39.397,22.064L38.468,22.819L36.769,24.469C31.434,21.272 21.754,15.2 19.147,13.561L11.812,17.8C11.583,17.927 11.53,17.997 11.392,18.125L31.484,29.577L31.548,29.541L30.36,30.694C30.348,30.712 29.57,31.624 29.57,31.624L28.978,32.699L28.621,33.855L28.613,33.939L9.576,23.093L9.576,42.668C9.576,43.743 10.14,44.685 11.082,45.245L28.484,55.304C28.672,55.419 28.72,55.454 28.861,55.522L28.943,55.802L29.486,56.835L30.221,57.739L31.121,58.481L32.155,59.031L33.284,59.369L34.463,59.482L37.609,59.482ZM34.072,42.976L34.072,47.577L33.291,47.651L32.166,47.984L31.133,48.529L30.231,49.266L29.492,50.169L28.946,51.202L28.932,51.249L28.932,37.203L28.987,37.379L29.571,38.438L30.361,39.372L34.072,42.976Z"/>
<g transform="matrix(0.504578,0,0,0.504578,32,24.523529)">
<path d="M53.106,52.746L42.032,52.551L4.882,52.551C2.095,52.551 0,54.639 0,57.433C0,60.236 2.095,62.347 4.882,62.347L42.032,62.347L53.106,62.113C56.566,62.084 58.318,59.77 58.318,57.426C58.318,55.106 56.566,52.769 53.106,52.746ZM38.717,69.984C37.78,70.874 37.339,72.072 37.339,73.386C37.339,76.204 39.34,78.237 42.19,78.237C43.434,78.237 44.82,77.654 45.709,76.765L61.858,61.062C63.936,59.039 63.944,55.842 61.858,53.836L45.709,38.133C44.82,37.244 43.434,36.661 42.19,36.661C39.34,36.661 37.339,38.694 37.339,41.512C37.339,42.826 37.78,44 38.717,44.906L47.179,53.013L52.155,57.433L47.036,62.028L38.717,69.984Z" style="fill-rule:nonzero;"/>
<path d="M10.303,16.133C6.843,16.155 5.114,18.475 5.114,20.82C5.114,23.14 6.843,25.477 10.303,25.507L21.408,25.717L58.527,25.717C61.321,25.717 63.409,23.629 63.409,20.827C63.409,18.032 61.321,15.945 58.527,15.945L21.408,15.945L10.303,16.133ZM24.691,33.354L16.373,25.422L11.261,20.827L16.23,16.383L24.691,8.3C25.652,7.387 26.101,6.22 26.101,4.906C26.101,2.063 24.069,0.055 21.242,0.055C19.982,0.055 18.62,0.63 17.724,1.527L1.582,17.205C-0.535,19.236 -0.496,22.433 1.582,24.449L17.724,40.127C18.62,41.024 19.982,41.606 21.242,41.606C24.069,41.606 26.101,39.591 26.101,36.748C26.101,35.442 25.652,34.244 24.691,33.354Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -30,6 +30,7 @@ import { ElectronProvider } from './components/Dashboard/context/ElectronContext
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import {
ProductionRoutes,
@ -59,7 +60,11 @@ const AppContent = () => {
theme={themeConfig}
renderEmpty={() => (
<div style={{ margin: '32px' }}>
<MissingPlaceholder message='No data.' hasBackground={false} />
<MissingPlaceholder
message='No data.'
hasBackground={false}
hasBorder={false}
/>
</div>
)}
>
@ -101,6 +106,10 @@ const AppContent = () => {
path='/auth/callback'
element={<AuthCallback />}
/>
<Route
path='/auth/marketplace/callback'
element={<MarketplaceAuthCallback />}
/>
<Route
path='/email/notification'
element={<EmailNotificationTemplate />}

View File

@ -9,6 +9,8 @@ import PurchaseOrderIcon from '../../Icons/PurchaseOrderIcon'
import ShipmentIcon from '../../Icons/ShipmentIcon'
import OrderItemIcon from '../../Icons/OrderItemIcon'
import InventoryIcon from '../../Icons/InventoryIcon'
import StockLocationIcon from '../../Icons/StockLocationIcon'
import StockTransferIcon from '../../Icons/StockTransferIcon'
const items = [
{
@ -57,6 +59,12 @@ const items = [
path: '/dashboard/inventory/shipments'
},
{ type: 'divider' },
{
key: 'stocklocations',
label: 'Stock Locations',
icon: <StockLocationIcon />,
path: '/dashboard/inventory/stocklocations'
},
{
key: 'stockevents',
label: 'Stock Events',
@ -68,6 +76,12 @@ const items = [
label: 'Stock Audits',
icon: <StockAuditIcon />,
path: '/dashboard/inventory/stockaudits'
},
{
key: 'stocktransfers',
label: 'Stock Transfers',
icon: <StockTransferIcon />,
path: '/dashboard/inventory/stocktransfers'
}
]
@ -76,6 +90,8 @@ const routeKeyMap = {
'/dashboard/inventory/filamentstocks': 'filamentstocks',
'/dashboard/inventory/partstocks': 'partstocks',
'/dashboard/inventory/productstocks': 'productstocks',
'/dashboard/inventory/stocklocations': 'stocklocations',
'/dashboard/inventory/stocktransfers': 'stocktransfers',
'/dashboard/inventory/stockevents': 'stockevents',
'/dashboard/inventory/stockaudits': 'stockaudits',
'/dashboard/inventory/purchaseorders': 'purchaseorders',

View File

@ -0,0 +1,111 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewStockLocation from './StockLocations/NewStockLocation'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const StockLocations = () => {
const tableRef = useRef()
const [newOpen, setNewOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('stockLocations')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockLocation')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('StockLocations')
const actionItems = {
items: [
{
label: 'New Stock Location',
key: 'new',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'new') {
setNewOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='stockLocation'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='stockLocation' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='stockLocation'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
/>
</Flex>
<Modal
open={newOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={640}
onCancel={() => {
setNewOpen(false)
}}
destroyOnHidden={true}
>
<NewStockLocation
onOk={() => {
setNewOpen(false)
tableRef.current?.reload()
}}
reset={newOpen}
/>
</Modal>
</>
)
}
export default StockLocations

View File

@ -0,0 +1,68 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewStockLocation = ({ onOk, reset }) => {
return (
<NewObjectForm type={'stockLocation'} reset={reset} defaultValues={{}}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Details',
key: 'details',
content: (
<ObjectInfo
type='stockLocation'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='stockLocation'
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 Stock Location'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewStockLocation.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewStockLocation

View File

@ -0,0 +1,212 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.jsx'
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 UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('StockLocationInfo')
log.setLevel(config.logLevel)
const StockLocationInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const stockLocationId = new URLSearchParams(location.search).get(
'stockLocationId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'StockLocationInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
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
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='stockLocation'
id={stockLocationId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Stock Location' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='stockLocation'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='stockLocation'
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='Stock Location'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={stockLocationId}
type='stockLocation'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='stockLocation'
objectData={objectData}
labelWidth='175px'
/>
)
}}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={stockLocationId} type='stockLocation' />
</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': stockLocationId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default StockLocationInfo

View File

@ -0,0 +1,111 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewStockTransfer from './StockTransfers/NewStockTransfer'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const StockTransfers = () => {
const tableRef = useRef()
const [newOpen, setNewOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('stockTransfers')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockTransfer')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('StockTransfers')
const actionItems = {
items: [
{
label: 'New Stock Transfer',
key: 'new',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'new') {
setNewOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='stockTransfer'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='stockTransfer' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='stockTransfer'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
/>
</Flex>
<Modal
open={newOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={960}
onCancel={() => {
setNewOpen(false)
}}
destroyOnHidden={true}
>
<NewStockTransfer
onOk={() => {
setNewOpen(false)
tableRef.current?.reload()
}}
reset={newOpen}
/>
</Modal>
</>
)
}
export default StockTransfers

View File

@ -0,0 +1,61 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewStockTransfer = ({ onOk, reset }) => {
return (
<NewObjectForm
type={'stockTransfer'}
reset={reset}
defaultValues={{ state: { type: 'draft' }, lines: [] }}
>
{({ handleSubmit, submitLoading, objectData }) => {
const steps = [
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='stockTransfer'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
lines: false,
_reference: false,
postedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={true}
title='New Stock Transfer'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewStockTransfer.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewStockTransfer

View File

@ -0,0 +1,46 @@
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 PostStockTransfer = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'StockTransfer',
'post'
)
if (result) {
message.success('Stock transfer posted')
onOk(result)
}
} catch (error) {
console.error('Error posting stock transfer:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Post this stock transfer?'}
description={`Receiving will move stock to the target locations, create destination stock rows, record stock events owned by this transfer, and fill in the "to" stock on each line.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostStockTransfer.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostStockTransfer

View File

@ -0,0 +1,269 @@
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 useCollapseState from '../../hooks/useCollapseState.jsx'
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 { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js'
import PostStockTransfer from './PostStockTransfer.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import StockTransferIcon from '../../../Icons/StockTransferIcon.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 UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const StockTransferInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const stockTransferId = new URLSearchParams(location.search).get(
'stockTransferId'
)
const [postOpen, setPostOpen] = useState(false)
const [collapseState, updateCollapseState] = useCollapseState(
'StockTransferInfo',
{
info: true,
lines: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
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
},
post: () => {
setPostOpen(true)
return true
}
}
const editDisabled =
getModelByName('stockTransfer')
?.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='stockTransfer'
id={stockTransferId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Transfer' },
{ key: 'lines', label: 'Lines' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='stockTransfer'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='stockTransfer'
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={stockTransferId}
type='stockTransfer'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Stock transfer'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='stockTransfer'
objectData={objectData}
labelWidth='175px'
visibleProperties={{ lines: false }}
/>
</InfoCollapse>
<InfoCollapse
title='Lines'
icon={<StockTransferIcon />}
active={collapseState.lines}
onToggle={(expanded) =>
updateCollapseState('lines', expanded)
}
collapseKey='lines'
>
<ObjectProperty
{...getModelProperty('stockTransfer', 'lines')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={stockTransferId} type='stockTransfer' />
</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': stockTransferId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={postOpen}
onCancel={() => {
setPostOpen(false)
}}
width={520}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostStockTransfer
onOk={() => {
setPostOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default StockTransferInfo

View File

@ -0,0 +1,253 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Modal } 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 UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import PublishListingVarient from './PublishListingVarient.jsx'
import UnpublishListingVarient from './UnpublishListingVarient.jsx'
import { Card } from 'antd'
const log = loglevel.getLogger('ListingVarientInfo')
log.setLevel(config.logLevel)
const ListingVarientInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const listingVarientId = new URLSearchParams(location.search).get(
'listingVarientId'
)
const [publishOpen, setPublishOpen] = useState(false)
const [unpublishOpen, setUnpublishOpen] = useState(false)
const [collapseState, updateCollapseState] = useCollapseState(
'ListingVarientInfo',
{
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
},
publish: () => {
setPublishOpen(true)
return true
},
unpublish: () => {
setUnpublishOpen(true)
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='listingVarient'
id={listingVarientId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Listing Varient Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='listingVarient'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='listingVarient'
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}
>
<ObjectForm
id={listingVarientId}
type='listingVarient'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Listing Varient Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='listingVarient'
objectData={objectData}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={listingVarientId} type='listingVarient' />
</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': listingVarientId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={publishOpen}
onCancel={() => setPublishOpen(false)}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PublishListingVarient
objectData={objectFormState.objectData}
onOk={() => {
setPublishOpen(false)
actions.reload()
}}
/>
</Modal>
<Modal
open={unpublishOpen}
onCancel={() => setUnpublishOpen(false)}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<UnpublishListingVarient
objectData={objectFormState.objectData}
onOk={() => {
setUnpublishOpen(false)
actions.reload()
}}
/>
</Modal>
</>
)
}
export default ListingVarientInfo

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 NewListingVarient = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'listingVarient'}
defaultValues={{ status: 'draft', ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='listingVarient'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='listingVarient'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='listingVarient'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
lastSyncedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Listing Varient'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewListingVarient.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewListingVarient

View File

@ -0,0 +1,54 @@
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 PublishListingVarient = ({ onOk, objectData }) => {
const [loading, setLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePublish = async () => {
setLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'ListingVarient',
'publish'
)
if (result) {
message.success('Published successfully')
onOk(result)
}
} catch (error) {
console.error('Error publishing listing variant:', error)
} finally {
setLoading(false)
}
}
const ref =
objectData?.title ||
objectData?._reference ||
objectData?.name ||
objectData?._id
return (
<MessageDialogView
title='Publish this listing variant on the marketplace?'
description={`Publishes this SKU on the connected marketplace so the item goes live when a draft listing exists (eBay).${
ref ? ` (variant: ${ref})` : ''
}`}
onOk={handlePublish}
okText='Publish'
okLoading={loading}
/>
)
}
PublishListingVarient.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PublishListingVarient

View File

@ -0,0 +1,54 @@
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 UnpublishListingVarient = ({ onOk, objectData }) => {
const [loading, setLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleUnpublish = async () => {
setLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'ListingVarient',
'unpublish'
)
if (result) {
message.success('Unpublished successfully')
onOk(result)
}
} catch (error) {
console.error('Error unpublishing listing variant:', error)
} finally {
setLoading(false)
}
}
const ref =
objectData?.title ||
objectData?._reference ||
objectData?.name ||
objectData?._id
return (
<MessageDialogView
title="Withdraw this variant from the marketplace?"
description={`Ends the live marketplace listing for this SKU where applicable (eBay).${
ref ? ` (variant: ${ref})` : ''
}`}
onOk={handleUnpublish}
okText='Unpublish'
okLoading={loading}
/>
)
}
UnpublishListingVarient.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default UnpublishListingVarient

View File

@ -0,0 +1,107 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewListing from './Listings/NewListing'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const Listings = () => {
const [newListingOpen, setNewListingOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('listings')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('listings')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('Listings')
const actionItems = {
items: [
{
label: 'New Listing',
key: 'newListing',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newListing') {
setNewListingOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='listing'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='listing' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='listing'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
/>
</Flex>
<Modal
open={newListingOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewListingOpen(false)
}}
destroyOnHidden={true}
>
<NewListing
onOk={() => {
setNewListingOpen(false)
tableRef.current?.reload()
}}
reset={newListingOpen}
/>
</Modal>
</>
)
}
export default Listings

View File

@ -0,0 +1,298 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Modal } 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 ListingVarientIcon from '../../../Icons/ListingVarientIcon.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 UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import NewListingVarient from '../ListingVarients/NewListingVarient.jsx'
import PublishListing from './PublishListing.jsx'
import UnpublishListing from './UnpublishListing.jsx'
import { Card } from 'antd'
const log = loglevel.getLogger('ListingInfo')
log.setLevel(config.logLevel)
const ListingInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const listingVarientsTableRef = useRef(null)
const actionHandlerRef = useRef(null)
const listingId = new URLSearchParams(location.search).get('listingId')
const [newListingVarientOpen, setNewListingVarientOpen] = useState(false)
const [publishListingOpen, setPublishListingOpen] = useState(false)
const [unpublishListingOpen, setUnpublishListingOpen] = useState(false)
const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', {
info: true,
listingVarients: 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
},
newListingVarient: () => {
setNewListingVarientOpen(true)
return true
},
publish: () => {
setPublishListingOpen(true)
return true
},
unpublish: () => {
setUnpublishListingOpen(true)
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='listing'
id={listingId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Listing Information' },
{ key: 'listingVarients', label: 'Listing Varients' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='listing'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='listing'
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}
>
<ObjectForm
id={listingId}
type='listing'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Listing Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='listing'
objectData={objectData}
/>
</InfoCollapse>
<InfoCollapse
title='Listing Varients'
icon={<ListingVarientIcon />}
active={collapseState.listingVarients}
onToggle={(expanded) =>
updateCollapseState('listingVarients', expanded)
}
collapseKey='listingVarients'
>
<ObjectTable
type='listingVarient'
masterFilter={{
'listing._id': listingId
}}
visibleColumns={{ listing: false }}
ref={listingVarientsTableRef}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={listingId} type='listing' />
</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': listingId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={newListingVarientOpen}
onCancel={() => {
setNewListingVarientOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewListingVarient
onOk={() => {
setNewListingVarientOpen(false)
listingVarientsTableRef.current?.reload()
}}
reset={newListingVarientOpen}
defaultValues={{
listing: { _id: listingId }
}}
/>
</Modal>
<Modal
open={publishListingOpen}
onCancel={() => setPublishListingOpen(false)}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PublishListing
objectData={objectFormState.objectData}
onOk={() => {
setPublishListingOpen(false)
actions.reload()
listingVarientsTableRef.current?.reload?.()
}}
/>
</Modal>
<Modal
open={unpublishListingOpen}
onCancel={() => setUnpublishListingOpen(false)}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<UnpublishListing
objectData={objectFormState.objectData}
onOk={() => {
setUnpublishListingOpen(false)
actions.reload()
listingVarientsTableRef.current?.reload?.()
}}
/>
</Modal>
</>
)
}
export default ListingInfo

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 NewListing = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'listing'}
defaultValues={{ status: 'draft', ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='listing'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='listing'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='listing'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
lastSyncedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Listing'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewListing.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewListing

View File

@ -0,0 +1,50 @@
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 PublishListing = ({ onOk, objectData }) => {
const [loading, setLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePublish = async () => {
setLoading(true)
try {
const result = await sendObjectFunction(objectData._id, 'Listing', 'publish')
if (result) {
message.success('Published successfully')
onOk(result)
}
} catch (error) {
console.error('Error publishing listing:', error)
} finally {
setLoading(false)
}
}
const ref =
objectData?.title ||
objectData?._reference ||
objectData?.name ||
objectData?._id
return (
<MessageDialogView
title='Publish this listing on the marketplace?'
description={`Each variant with a SKU will be published on the connected marketplace. Variants that are already active are skipped.${
ref ? ` (listing: ${ref})` : ''
}`}
onOk={handlePublish}
okText='Publish'
okLoading={loading}
/>
)
}
PublishListing.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PublishListing

View File

@ -0,0 +1,50 @@
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 UnpublishListing = ({ onOk, objectData }) => {
const [loading, setLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleUnpublish = async () => {
setLoading(true)
try {
const result = await sendObjectFunction(objectData._id, 'Listing', 'unpublish')
if (result) {
message.success('Unpublished successfully')
onOk(result)
}
} catch (error) {
console.error('Error unpublishing listing:', error)
} finally {
setLoading(false)
}
}
const ref =
objectData?.title ||
objectData?._reference ||
objectData?.name ||
objectData?._id
return (
<MessageDialogView
title='Unpublish all variants for this listing?'
description={`Ends live marketplace listings for every active variant. On eBay the item is withdrawn and can be published again later.${
ref ? ` (listing: ${ref})` : ''
}`}
onOk={handleUnpublish}
okText='Unpublish'
okLoading={loading}
/>
)
}
UnpublishListing.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default UnpublishListing

View File

@ -0,0 +1,68 @@
import PropTypes from 'prop-types'
import { Result, Typography, Flex, Button } from 'antd'
import CopyButton from '../../common/CopyButton'
import MarketplaceIcon from '../../../Icons/MarketplaceIcon'
import { getMarketplaceCallbackUrl } from './authUtils'
const { Text } = Typography
const ConfigureMarketplace = ({
onConnect,
isConnected = false,
loading = false,
disabled = false
}) => {
const callbackUrl = getMarketplaceCallbackUrl()
return (
<Flex vertical align='center'>
<Result
title='Marketplace Connection'
subTitle={
<Text>
Use this callback URL for TikTok Shop redirects and as the eBay
RuName accept URL target. Click Connect to authorize your
marketplace account.
</Text>
}
icon={<MarketplaceIcon />}
>
<Flex
vertical
gap='middle'
align='center'
style={{ minWidth: '395px' }}
>
<Flex justify='center'>
<Flex gap='small' align='center' justify='center'>
<CopyButton size='default' text={callbackUrl} />
<Text code style={{ fontSize: '14px', wordBreak: 'break-all' }}>
{callbackUrl}
</Text>
</Flex>
</Flex>
<Flex justify='center'>
<Button
type='primary'
onClick={onConnect}
loading={loading}
disabled={disabled}
key='connect'
>
{isConnected ? 'Reconnect' : 'Connect'}
</Button>
</Flex>
</Flex>
</Result>
</Flex>
)
}
ConfigureMarketplace.propTypes = {
onConnect: PropTypes.func,
isConnected: PropTypes.bool,
loading: PropTypes.bool,
disabled: PropTypes.bool
}
export default ConfigureMarketplace

View File

@ -0,0 +1,186 @@
import { useContext, useEffect, useMemo, useState } from 'react'
import { Alert, Button, Card, Flex, message } from 'antd'
import CheckIcon from '../../../Icons/CheckIcon'
import { useLocation, useNavigate } from 'react-router-dom'
import { LoadingOutlined } from '@ant-design/icons'
import { ApiServerContext } from '../../context/ApiServerContext'
import {
clearMarketplaceAuthState,
parseMarketplaceAuthState,
readMarketplaceAuthState
} from './authUtils'
import { AuthContext } from '../../context/AuthContext'
import AuthParticles from '../../../App/AppParticles'
import FarmControlLogo from '../../../Logos/FarmControlLogo'
import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon'
import ArrowLeftIcon from '../../../Icons/ArrowLeftIcon'
const MarketplaceAuthCallback = () => {
const location = useLocation()
const navigate = useNavigate()
const { token } = useContext(AuthContext)
const { sendObjectFunction, connected } = useContext(ApiServerContext)
const [status, setStatus] = useState('loading')
const [redirectLoading, setRedirectLoading] = useState(false)
const [resultMessage, setResultMessage] = useState('')
const params = useMemo(
() => new URLSearchParams(location.search),
[location.search]
)
const rawState = params.get('state') || ''
const parsedState = parseMarketplaceAuthState(rawState)
const storedState = readMarketplaceAuthState(rawState)
const marketplaceId =
params.get('marketplaceId') ||
parsedState?.marketplaceId ||
storedState?.marketplaceId ||
''
const returnTo =
parsedState?.returnTo ||
storedState?.returnTo ||
(marketplaceId
? `/dashboard/sales/marketplaces/info?marketplaceId=${marketplaceId}`
: '/dashboard/sales/marketplaces')
useEffect(() => {
if (!connected || token == null || !token) {
return
}
const error = params.get('error') || params.get('error_description')
const code = params.get('code') || params.get('auth_code')
if (error) {
setStatus('error')
setResultMessage(error)
if (rawState) {
clearMarketplaceAuthState(rawState)
}
return
}
if (!marketplaceId) {
setStatus('error')
setResultMessage('Could not determine which marketplace to connect.')
return
}
if (!code) {
setStatus('error')
setResultMessage('Authorization code was not returned by the provider.')
return
}
let cancelled = false
const exchangeCode = async () => {
try {
const result = await sendObjectFunction(
marketplaceId,
'Marketplace',
'auth/exchange',
{
code,
state: rawState
}
)
if (!result?.success) {
throw new Error(
result?.error || 'Failed to complete marketplace authorization.'
)
}
if (cancelled) {
return
}
setStatus('success')
setResultMessage('Marketplace authorization completed successfully.')
message.success('Marketplace connected')
if (rawState) {
clearMarketplaceAuthState(rawState)
}
} catch (error) {
if (cancelled) {
return
}
console.error('Marketplace auth callback failed:', error)
setStatus('error')
setResultMessage(
error?.response?.data?.error ||
error?.message ||
'Failed to complete marketplace authorization.'
)
}
}
exchangeCode()
return () => {
cancelled = true
}
}, [marketplaceId, params, rawState, sendObjectFunction, token, connected])
return (
<>
<AuthParticles />
<Flex
align='center'
justify='center'
vertical
style={{ height: '100vh' }}
gap='large'
>
<Card style={{ borderRadius: 20 }}>
<Flex vertical align='center'>
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
</Flex>
</Card>
{status != 'error' && status != 'success' && (
<Alert
message='Completing provider authorization...'
icon={<LoadingOutlined />}
type='info'
showIcon
/>
)}
{status === 'success' && (
<Alert
message={resultMessage}
icon={<CheckIcon />}
type='success'
showIcon
/>
)}
{status === 'error' && (
<Alert
message={resultMessage}
icon={<ExclamationOctagonIcon />}
type='error'
showIcon
/>
)}
<Flex gap='middle'>
<Button
icon={<ArrowLeftIcon />}
onClick={() => {
setRedirectLoading(true)
navigate(returnTo)
}}
loading={redirectLoading}
disabled={status === 'loading' || redirectLoading}
size='large'
></Button>
</Flex>
</Flex>
</>
)
}
export default MarketplaceAuthCallback

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react'
import { useContext, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex } from 'antd'
import { Space, Flex, Modal, message } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config'
import useCollapseState from '../../hooks/useCollapseState'
@ -22,6 +22,15 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import { Card } from 'antd'
import SyncListings from './SyncListings.jsx'
import SyncOrders from './SyncOrders.jsx'
import ConfigureMarketplace from './ConfigureMarketplace.jsx'
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import { ElectronContext } from '../../context/ElectronContext.jsx'
import {
buildMarketplaceAuthState,
storeMarketplaceAuthState
} from './authUtils.js'
const log = loglevel.getLogger('MarketplaceInfo')
log.setLevel(config.logLevel)
@ -30,12 +39,20 @@ const MarketplaceInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const marketplaceId = new URLSearchParams(location.search).get('marketplaceId')
const [collapseState, updateCollapseState] = useCollapseState('MarketplaceInfo', {
info: true,
notes: true,
auditLogs: true
})
const { getMarketplaceAuthUrl, refreshMarketplaceAuth } =
useContext(ApiServerContext)
const { openExternalUrl } = useContext(ElectronContext)
const marketplaceId = new URLSearchParams(location.search).get(
'marketplaceId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'MarketplaceInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
@ -44,6 +61,55 @@ const MarketplaceInfo = () => {
loading: false,
objectData: {}
})
const [syncListingsOpen, setSyncListingsOpen] = useState(false)
const [syncOrdersOpen, setSyncOrdersOpen] = useState(false)
const [configureModalOpen, setConfigureModalOpen] = useState(false)
const startAuthorization = async () => {
const objectData = objectFormState.objectData
if (!objectData?._id) return
if (objectFormState.isEditing) {
message.warning('Save marketplace changes before starting authorization')
return
}
const returnTo = `/dashboard/sales/marketplaces/info?marketplaceId=${objectData._id}`
const state = buildMarketplaceAuthState({
marketplaceId: objectData._id,
returnTo
})
storeMarketplaceAuthState(state, {
marketplaceId: objectData._id,
returnTo
})
const result = await getMarketplaceAuthUrl(objectData._id, state)
if (!result?.url) {
message.error('Authorization URL was not returned')
return
}
const openedExternally = openExternalUrl(result.url)
if (!openedExternally) {
window.location.assign(result.url)
}
}
const handleRefreshToken = async () => {
const objectData = objectFormState.objectData
if (!objectData?._id) return
const result = await refreshMarketplaceAuth(objectData._id)
if (!result?.success) {
message.error(result?.error || 'Token refresh failed')
return
}
message.success('Marketplace token refreshed')
objectFormRef?.current?.handleFetchObject?.()
}
const actions = {
reload: () => {
@ -65,6 +131,30 @@ const MarketplaceInfo = () => {
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
},
syncListings: () => {
setSyncListingsOpen(true)
return true
},
syncOrders: () => {
setSyncOrdersOpen(true)
return true
},
configure: () => {
setConfigureModalOpen(true)
return true
},
connect: () => {
startAuthorization()
return true
},
reconnect: () => {
startAuthorization()
return true
},
refreshToken: () => {
handleRefreshToken()
return true
}
}
@ -154,6 +244,7 @@ const MarketplaceInfo = () => {
loading={loading}
isEditing={isEditing}
type='marketplace'
labelWidth={215}
objectData={objectData}
/>
)}
@ -193,6 +284,62 @@ const MarketplaceInfo = () => {
</Flex>
</ScrollBox>
</Flex>
<Modal
open={syncListingsOpen}
onCancel={() => setSyncListingsOpen(false)}
width={500}
footer={null}
destroyOnClose
centered
>
<SyncListings
onOk={() => {
setSyncListingsOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={syncOrdersOpen}
onCancel={() => setSyncOrdersOpen(false)}
width={500}
footer={null}
destroyOnClose
centered
>
<SyncOrders
onOk={() => {
setSyncOrdersOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={configureModalOpen}
onCancel={() => setConfigureModalOpen(false)}
width={650}
footer={null}
destroyOnClose
centered
>
<ConfigureMarketplace
onConnect={() => {
startAuthorization()
setConfigureModalOpen(false)
}}
isConnected={
!!(
objectFormState.objectData?.config?.refreshToken ||
objectFormState.objectData?.config?.accessToken ||
objectFormState.objectData?.config?.shopCipher
)
}
loading={objectFormState.loading}
disabled={objectFormState.isEditing}
/>
</Modal>
</>
)
}

View File

@ -2,12 +2,21 @@ import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
import { getMarketplaceCallbackUrl } from './authUtils'
const NewMarketplace = ({ onOk, defaultValues }) => {
const callbackUrl = getMarketplaceCallbackUrl()
return (
<NewObjectForm
type={'marketplace'}
defaultValues={{ active: true, ...defaultValues }}
defaultValues={{
active: true,
config: {
redirectUri: callbackUrl
},
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
@ -60,14 +69,23 @@ const NewMarketplace = ({ onOk, defaultValues }) => {
_id: false,
createdAt: false,
updatedAt: false,
'config.appId': false,
'config.certId': false,
'config.devId': false,
'config.userToken': false,
authConnected: false,
'config.clientId': false,
'config.clientSecret': false,
'config.ruName': false,
'config.marketplaceId': false,
'config.sandbox': false,
'config.verificationToken': false,
'config.redirectUri': false,
'config.accessToken': false,
'config.refreshToken': false,
'config.refreshTokenExpiresAt': false,
'config.accessTokenExpiresAt': false,
'config.lastTokenRefreshAt': false,
connectedAt: false,
'config.appKey': false,
'config.appSecret': false
'config.appSecret': false,
'config.shopCipher': false
}}
isEditing={false}
objectData={objectData}

View File

@ -0,0 +1,46 @@
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 SyncListings = ({ onOk, objectData }) => {
const [syncLoading, setSyncLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleSync = async () => {
setSyncLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Marketplace',
'sync/items'
)
if (result) {
message.success('Listings synced successfully')
onOk(result)
}
} catch (error) {
console.error('Error syncing listings:', error)
} finally {
setSyncLoading(false)
}
}
return (
<MessageDialogView
title='Sync listings from marketplace?'
description={`This will sync product listings from ${objectData?.name || objectData?._reference || objectData?._id} to your local database.`}
onOk={handleSync}
okText='Sync'
okLoading={syncLoading}
/>
)
}
SyncListings.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default SyncListings

View File

@ -0,0 +1,46 @@
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 SyncOrders = ({ onOk, objectData }) => {
const [syncLoading, setSyncLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleSync = async () => {
setSyncLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Marketplace',
'sync/orders'
)
if (result) {
message.success('Orders synced successfully')
onOk(result)
}
} catch (error) {
console.error('Error syncing orders:', error)
} finally {
setSyncLoading(false)
}
}
return (
<MessageDialogView
title='Sync orders from marketplace?'
description={`This will sync orders from ${objectData?.name || objectData?._reference || objectData?._id} to your local database.`}
onOk={handleSync}
okText='Sync'
okLoading={syncLoading}
/>
)
}
SyncOrders.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default SyncOrders

View File

@ -0,0 +1,68 @@
export const MARKETPLACE_AUTH_STATE_KEY = 'marketplace-auth-state'
export function buildMarketplaceAuthState({ marketplaceId, returnTo } = {}) {
return btoa(
JSON.stringify({
marketplaceId,
returnTo
})
)
}
export function parseMarketplaceAuthState(rawState) {
if (!rawState) {
return null
}
try {
return JSON.parse(atob(rawState))
} catch {
return null
}
}
export function getMarketplaceCallbackUrl() {
if (typeof window === 'undefined') {
return ''
}
return `${window.location.origin}/auth/marketplace/callback`
}
export function storeMarketplaceAuthState(rawState, data) {
if (typeof window === 'undefined' || !rawState) {
return
}
window.sessionStorage.setItem(
`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`,
JSON.stringify(data)
)
}
export function readMarketplaceAuthState(rawState) {
if (typeof window === 'undefined' || !rawState) {
return null
}
const stored = window.sessionStorage.getItem(
`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`
)
if (!stored) {
return null
}
try {
return JSON.parse(stored)
} catch {
return null
}
}
export function clearMarketplaceAuthState(rawState) {
if (typeof window === 'undefined' || !rawState) {
return
}
window.sessionStorage.removeItem(`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`)
}

View File

@ -44,4 +44,3 @@ CancelSalesOrder.propTypes = {
}
export default CancelSalesOrder

View File

@ -44,4 +44,3 @@ ConfirmSalesOrder.propTypes = {
}
export default ConfirmSalesOrder

View File

@ -44,4 +44,3 @@ PostSalesOrder.propTypes = {
}
export default PostSalesOrder

View File

@ -4,6 +4,7 @@ import ClientIcon from '../../Icons/ClientIcon'
import SalesIcon from '../../Icons/SalesIcon'
import SalesOrderIcon from '../../Icons/SalesOrderIcon'
import MarketplaceIcon from '../../Icons/MarketplaceIcon'
import ListingIcon from '../../Icons/ListingIcon'
const items = [
{
@ -30,6 +31,12 @@ const items = [
label: 'Marketplaces',
icon: <MarketplaceIcon />,
path: '/dashboard/sales/marketplaces'
},
{
key: 'listings',
label: 'Listings',
icon: <ListingIcon />,
path: '/dashboard/sales/listings'
}
]
@ -37,7 +44,9 @@ const routeKeyMap = {
'/dashboard/sales/overview': 'overview',
'/dashboard/sales/clients': 'clients',
'/dashboard/sales/salesorders': 'salesorders',
'/dashboard/sales/marketplaces': 'marketplaces'
'/dashboard/sales/marketplaces': 'marketplaces',
'/dashboard/sales/listings': 'listings',
'/dashboard/sales/listingvarients': 'listings'
}
const SalesSidebar = (props) => {

View File

@ -11,7 +11,7 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
return (
<>
<Flex>
<Flex style={{ minWidth: 0 }}>
{showLink ? (
<Link href={`mailto:${email}`} style={{ marginRight: 8 }}>
{email}

View File

@ -4,13 +4,22 @@ import PropTypes from 'prop-types'
const { Text } = Typography
const MissingPlaceholder = ({ message, hasBackground = true }) => {
const MissingPlaceholder = ({
message,
hasBackground = true,
hasBorder = true
}) => {
return (
<Card
size='small'
style={{
background: hasBackground == false ? 'transparent' : undefined,
border: hasBackground == false ? '1px solid rgb(0 0 0 / 7%)' : undefined
border:
hasBorder == false
? 'none'
: hasBackground == false
? '1px solid rgb(0 0 0 / 7%)'
: undefined
}}
>
<Flex
@ -32,7 +41,8 @@ const MissingPlaceholder = ({ message, hasBackground = true }) => {
MissingPlaceholder.propTypes = {
message: PropTypes.string.isRequired,
hasBackground: PropTypes.bool
hasBackground: PropTypes.bool,
hasBorder: PropTypes.bool
}
export default MissingPlaceholder

View File

@ -0,0 +1,111 @@
import { Descriptions, Card, Flex, Divider } from 'antd'
import PropTypes from 'prop-types'
import ObjectProperty from './ObjectProperty'
import { createElement } from 'react'
const ObjectCard = ({
model,
modelProperties,
visibleColumns = {},
record,
isEditing = false,
rowActions = [],
renderActions,
cardStyle = 'borderless'
}) => {
const descriptionItems = []
const modelIcon = createElement(model.icon, { style: { fontSize: 24 } })
model.columns.forEach((colName) => {
const prop = modelProperties.find((p) => p.name === colName)
if (prop) {
if (
(Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false) ||
prop.name == 'name' ||
(prop.name == 'state' && visibleColumns?.name == true)
) {
return
}
descriptionItems.push(
<Descriptions.Item label={prop.label} key={prop.name} colspan={2}>
<ObjectProperty
{...prop}
longId={false}
objectData={record}
isEditing={isEditing}
name={prop.name}
/>
</Descriptions.Item>
)
}
})
var actions = undefined
if (rowActions.length > 0) {
actions = renderActions(record)
}
return (
<Card
styles={{ body: { padding: 18 } }}
style={{ width: '100%' }}
variant={cardStyle}
>
<Flex vertical gap={8}>
{visibleColumns?.name == true && (
<Flex align='center' gap={12}>
{modelIcon}
<ObjectProperty
{...model.properties.find((p) => p.name === 'name')}
objectData={record}
isEditing={isEditing}
style={{
fontSize: 20,
fontWeight: '600',
lineHeight: 1.2
}}
/>
{visibleColumns?.state == true && (
<ObjectProperty
{...model.properties.find((p) => p.name === 'state')}
objectData={record}
/>
)}
</Flex>
)}
<Descriptions
column={1}
size='small'
style={{ width: '100%', tableLayout: 'fixed' }}
className='objectTableDescritions'
>
{descriptionItems}
</Descriptions>
{actions && (
<>
<Divider style={{ margin: '2px 0 0 0' }} />
<Flex align='flex-end' gap={10}>
{actions}
</Flex>
</>
)}
</Flex>
</Card>
)
}
ObjectCard.propTypes = {
model: PropTypes.object.isRequired,
modelProperties: PropTypes.array.isRequired,
visibleColumns: PropTypes.object,
record: PropTypes.object.isRequired,
isEditing: PropTypes.bool,
rowActions: PropTypes.array,
renderActions: PropTypes.func.isRequired,
cardStyle: PropTypes.string
}
export default ObjectCard

View File

@ -92,6 +92,7 @@ const ObjectProperty = ({
loading = false,
rollups = [],
showCard = true,
style = {},
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
@ -143,6 +144,10 @@ const ObjectProperty = ({
if (name?.includes('.')) {
formItemName = name ? name.split('.') : undefined
}
// For state type with options (editable), form field is state.type
if (type === 'state' && options?.length && !readOnly) {
formItemName = ['state', 'type']
}
var textParams = { style: { whiteSpace: 'nowrap', minWidth: '0' } }
@ -306,7 +311,7 @@ const ObjectProperty = ({
case 'text':
if (value != null && value != '') {
return (
<Text ellipsis>
<Text ellipsis style={style}>
{prefix}
{value}
{suffix}
@ -662,6 +667,18 @@ const ObjectProperty = ({
{...inputProps}
/>
)
case 'state':
if (options?.length && !readOnly) {
return (
<CustomSelect
placeholder={'Select a ' + label.toLowerCase() + '...'}
options={Array.isArray(options) ? options : []}
{...inputProps}
{...(useFormItem ? {} : { value: value?.type })}
/>
)
}
return null
case 'priceMode':
return (
<Select
@ -872,7 +889,8 @@ ObjectProperty.propTypes = {
loading: PropTypes.bool,
rollups: PropTypes.arrayOf(PropTypes.object),
canAddRemove: PropTypes.bool,
showCard: PropTypes.bool
showCard: PropTypes.bool,
style: PropTypes.object
}
export default ObjectProperty

View File

@ -11,10 +11,8 @@ import {
import {
Table,
Skeleton,
Card,
Row,
Col,
Descriptions,
Flex,
Spin,
Button,
@ -35,6 +33,7 @@ import {
getModelByName
} from '../../../database/ObjectModels'
import ObjectProperty from './ObjectProperty'
import ObjectCard from './ObjectCard'
import FilterSidebar from './FilterSidebar'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
@ -128,7 +127,7 @@ const ObjectTable = forwardRef(
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
}
if (cards) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 280px)'
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
}
if (isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 244px)'
@ -857,78 +856,25 @@ const ObjectTable = forwardRef(
>
{tableData.map((record) => (
<Col xs={24} sm={12} md={12} lg={8} xl={6} xxl={6} key={record._id}>
<Card
style={{ width: '100%', overflow: 'hidden' }}
styles={{ body: { padding: 0 } }}
loading={record.isSkeleton}
variant={'borderless'}
>
<div style={{ width: '100%', overflow: 'hidden' }}>
<RowForm
record={record}
isEditing={isEditing}
onRegister={registerForm}
>
<Flex align={'center'} vertical gap={'middle'}>
<Descriptions
column={1}
size='small'
bordered={true}
style={{ width: '100%', tableLayout: 'fixed' }}
className='objectTableDescritions'
>
{(() => {
const descriptionItems = []
// Add columns in the order specified by model.columns (same logic as table)
model.columns.forEach((colName) => {
const prop = modelProperties.find(
(p) => p.name === colName
)
if (prop) {
// Check if column should be visible based on visibleColumns prop
if (
Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false
) {
return // Skip this column if it's not visible
}
descriptionItems.push(
<Descriptions.Item
label={prop.label}
key={prop.name}
colspan={2}
>
<ObjectProperty
{...prop}
longId={false}
objectData={record}
isEditing={isEditing}
name={prop.name}
/>
</Descriptions.Item>
)
}
})
// Add actions if they exist (same as table)
if (rowActions.length > 0) {
descriptionItems.push(
<Descriptions.Item
label={'Actions'}
key={'actions'}
>
{renderActions(record)}
</Descriptions.Item>
)
}
return descriptionItems
})()}
</Descriptions>
<ObjectCard
model={model}
modelProperties={modelProperties}
visibleColumns={visibleColumns}
record={record}
isEditing={isEditing}
rowActions={rowActions}
renderActions={renderActions}
/>
</Flex>
</RowForm>
</Card>
</div>
</Col>
))}
</Row>
@ -955,28 +901,29 @@ const ObjectTable = forwardRef(
const tableContent = (
<Flex gap={'middle'} vertical style={{ flex: 1, minWidth: 0 }}>
<Table
ref={tableRef}
dataSource={tableData}
columns={columnsWithSkeleton}
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
components={components}
onRow={onRow}
/>
{cards ? (
<Spin indicator={<LoadingOutlined />} spinning={loading}>
{renderCards()}
</Spin>
) : null}
) : (
<Table
ref={tableRef}
dataSource={tableData}
columns={columnsWithSkeleton}
className='dashboard-table'
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
components={components}
onRow={onRow}
/>
)}
</Flex>
)

View File

@ -80,6 +80,30 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'default'
text = 'Draft'
break
case 'active':
status = 'success'
text = 'Active'
break
case 'inactive':
status = 'default'
text = 'Inactive'
break
case 'deleted':
status = 'error'
text = 'Deleted'
break
case 'suspended':
status = 'warning'
text = 'Suspended'
break
case 'syncing':
status = 'processing'
text = 'Syncing'
break
case 'disconnected':
status = 'default'
text = 'Disconnected'
break
case 'failed':
status = 'error'
text = 'Failed'

View File

@ -30,10 +30,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
</Link>
) : (
<>
<Text
style={{ marginRight: 8, minWidth: 0, flex: 1, width: 0 }}
ellipsis
>
<Text style={{ marginRight: 8, minWidth: 0 }} ellipsis>
{url}
</Text>
<Tooltip title='Open URL' arrow={false}>

View File

@ -939,6 +939,37 @@ const ApiServerProvider = ({ children }) => {
}
}
const getObjectFunction = async (id, type, functionName, params = {}) => {
const url = `${config.backendUrl}/${type.toLowerCase()}s/${id}/${functionName}`
logger.debug(`Fetching object function ${functionName} for ${id} at ${url}`)
try {
const response = await axios.get(url, {
params,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
getObjectFunction(id, type, functionName, params)
})
return {}
}
}
const getMarketplaceAuthUrl = async (marketplaceId, state) => {
return getObjectFunction(marketplaceId, 'Marketplace', 'auth/url', {
state
})
}
const refreshMarketplaceAuth = async (marketplaceId) => {
return sendObjectFunction(marketplaceId, 'Marketplace', 'auth/refresh')
}
// Export list data to CSV and download
const exportToCsv = async (objectType) => {
try {
@ -1551,6 +1582,7 @@ const ApiServerProvider = ({ children }) => {
updateObject,
updateMultipleObjects,
createObject,
getObjectFunction,
sendObjectFunction,
deleteObject,
subscribeToObjectUpdates,
@ -1590,7 +1622,9 @@ const ApiServerProvider = ({ children }) => {
deleteNotificationApi,
deleteAllNotificationsApi,
registerNotificationListener,
unregisterNotificationListener
unregisterNotificationListener,
getMarketplaceAuthUrl,
refreshMarketplaceAuth
}}
>
{contextHolder}

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,8 @@ import { StockEvent } from './models/StockEvent'
import { StockAudit } from './models/StockAudit'
import { PartStock } from './models/PartStock'
import { ProductStock } from './models/ProductStock'
import { StockLocation } from './models/StockLocation'
import { StockTransfer } from './models/StockTransfer'
import { PurchaseOrder } from './models/PurchaseOrder'
import { OrderItem } from './models/OrderItem'
import { Shipment } from './models/Shipment'
@ -40,6 +42,8 @@ import { Payment } from './models/Payment.js'
import { Client } from './models/Client.js'
import { SalesOrder } from './models/SalesOrder.js'
import { Marketplace } from './models/Marketplace.js'
import { Listing } from './models/Listing.js'
import { ListingVarient } from './models/ListingVarient.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
@ -66,6 +70,8 @@ export const objectModels = [
StockAudit,
PartStock,
ProductStock,
StockLocation,
StockTransfer,
PurchaseOrder,
OrderItem,
Shipment,
@ -84,7 +90,9 @@ export const objectModels = [
Payment,
Client,
SalesOrder,
Marketplace
Marketplace,
Listing,
ListingVarient
]
// Re-export individual models for direct access
@ -112,6 +120,8 @@ export {
StockAudit,
PartStock,
ProductStock,
StockLocation,
StockTransfer,
PurchaseOrder,
OrderItem,
Shipment,
@ -130,7 +140,9 @@ export {
Payment,
Client,
SalesOrder,
Marketplace
Marketplace,
Listing,
ListingVarient
}
export function getModelByName(name, ignoreCase = false) {

View File

@ -23,6 +23,7 @@ export const FilamentStock = {
'currentWeight',
'startingWeight',
'filamentSku',
'stockLocation',
'createdAt',
'updatedAt'
],
@ -81,6 +82,15 @@ export const FilamentStock = {
showHyperlink: true,
columnWidth: 200
},
{
name: 'stockLocation',
label: 'Stock location',
type: 'object',
objectType: 'stockLocation',
required: false,
showHyperlink: true,
columnWidth: 200
},
{
name: 'currentWeight',
label: 'Current Weight',

View File

@ -0,0 +1,266 @@
import ListingIcon from '../../components/Icons/ListingIcon'
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'
import PlusIcon from '../../components/Icons/PlusIcon'
export const Listing = {
name: 'listing',
label: 'Listing',
prefix: 'LST',
icon: ListingIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/sales/listings/info?listingId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'newListingVarient',
label: 'New Listing Varient',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=newListingVarient`
},
{ type: 'divider' },
{
name: 'publish',
label: 'Publish',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=publish`,
visible: (objectData) =>
objectData?.state?.type === 'draft' ||
objectData?.state?.type === 'inactive',
disabled: (objectData) => objectData?.state?.type === 'syncing'
},
{
name: 'unpublish',
label: 'Unpublish',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=unpublish`,
visible: (objectData) => objectData?.state?.type === 'active',
disabled: (objectData) => objectData?.state?.type === 'syncing'
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/listings/info?listingId=${_id}&action=delete`
}
],
columns: [
'_reference',
'title',
'product',
'vendor',
'stockLocation',
'marketplace',
'state',
'price',
'currency',
'lastSyncedAt',
'createdAt',
'updatedAt'
],
filters: [
'title',
'_id',
'product',
'vendor',
'stockLocation',
'marketplace',
'state',
'createdAt',
'updatedAt'
],
sorters: [
'title',
'vendor',
'state',
'price',
'lastSyncedAt',
'createdAt',
'updatedAt',
'_id'
],
group: ['marketplace', 'product', 'vendor', 'stockLocation'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'listing',
showCopy: true,
columnWidth: 140
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'listing',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'title',
label: 'Title',
columnFixed: 'left',
type: 'text',
readOnly: false,
required: false,
columnWidth: 250
},
{
name: 'lastSyncedAt',
label: 'Last Synced',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'product',
label: 'Product',
type: 'object',
objectType: 'product',
showHyperlink: true,
readOnly: false,
required: false,
columnWidth: 200
},
{
name: 'vendor',
label: 'Vendor',
type: 'object',
objectType: 'vendor',
showHyperlink: true,
readOnly: false,
required: true,
columnWidth: 200
},
{
name: 'stockLocation',
label: 'Stock Location',
type: 'object',
objectType: 'stockLocation',
showHyperlink: true,
readOnly: false,
required: true,
columnWidth: 200
},
{
name: 'marketplace',
label: 'Marketplace',
type: 'object',
objectType: 'marketplace',
showHyperlink: true,
readOnly: false,
required: true,
columnWidth: 200
},
{
name: 'state',
label: 'State',
type: 'state',
objectType: 'listing',
readOnly: true,
columnWidth: 130
},
{
name: 'price',
label: 'Price',
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
readOnly: true,
required: false,
columnWidth: 120
},
{
name: 'currency',
label: 'Currency',
type: 'text',
readOnly: true,
required: false,
columnWidth: 100
},
{
name: 'url',
label: 'URL',
type: 'url',
readOnly: true,
required: false,
columnWidth: 250
}
]
}

View File

@ -0,0 +1,243 @@
import ListingVarientIcon from '../../components/Icons/ListingVarientIcon'
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 ListingVarient = {
name: 'listingVarient',
label: 'Listing Varient',
prefix: 'LVR',
icon: ListingVarientIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/sales/listingvarients/info?listingVarientId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'publish',
label: 'Publish',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=publish`,
visible: (objectData) =>
objectData?.state?.type === 'draft' ||
objectData?.state?.type === 'inactive',
disabled: (objectData) => objectData?.state?.type === 'syncing'
},
{
name: 'unpublish',
label: 'Unpublish',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=unpublish`,
visible: (objectData) => objectData?.state?.type === 'active',
disabled: (objectData) => objectData?.state?.type === 'syncing'
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=delete`
}
],
columns: [
'_reference',
'listing',
'product',
'productSku',
'state',
'price',
'currency',
'lastSyncedAt',
'createdAt',
'updatedAt'
],
filters: [
'_id',
'listing',
'product',
'productSku',
'state',
'createdAt',
'updatedAt'
],
sorters: [
'state',
'price',
'lastSyncedAt',
'createdAt',
'updatedAt',
'_id'
],
group: ['listing', 'state'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'listingVarient',
showCopy: true,
columnWidth: 140
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'listingVarient',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'listing',
label: 'Listing',
type: 'object',
objectType: 'listing',
showHyperlink: true,
readOnly: false,
required: true,
columnWidth: 200
},
{
name: 'product',
label: 'Product',
type: 'object',
objectType: 'product',
showHyperlink: true,
readOnly: false,
required: false,
columnWidth: 200
},
{
name: 'productSku',
label: 'Product SKU',
type: 'object',
objectType: 'productSku',
showHyperlink: true,
readOnly: false,
required: false,
columnWidth: 200
},
{
name: 'state',
label: 'State',
type: 'state',
objectType: 'listingVarient',
readOnly: true,
columnWidth: 130
},
{
name: 'price',
label: 'Price',
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
readOnly: false,
required: false,
columnWidth: 120
},
{
name: 'currency',
label: 'Currency',
type: 'text',
readOnly: false,
required: false,
columnWidth: 100
},
{
name: 'priceTaxRate',
label: 'Tax Rate',
type: 'object',
objectType: 'taxRate',
showHyperlink: true,
required: false,
columnWidth: 150
},
{
name: 'priceWithTax',
label: 'Price w/ Tax',
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
readOnly: true,
required: false,
columnWidth: 130
},
{
name: 'lastSyncedAt',
label: 'Last Synced',
type: 'dateTime',
readOnly: true,
columnWidth: 175
}
]
}

View File

@ -4,6 +4,7 @@ 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 LinkIcon from '../../components/Icons/LinkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const Marketplace = {
@ -59,6 +60,67 @@ export const Marketplace = {
}
},
{ type: 'divider' },
{
name: 'syncListings',
label: 'Sync Listings',
type: 'button',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=syncListings`,
disabled: (objectData) => {
return objectData?.state?.type != 'ready'
}
},
{
name: 'syncOrders',
label: 'Sync Orders',
type: 'button',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=syncOrders`,
disabled: (objectData) => {
return objectData?.state?.type != 'ready'
}
},
{ type: 'divider' },
{
name: 'connect',
label: 'Connect',
type: 'button',
icon: LinkIcon,
url: (_id) =>
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=connect`,
disabled: (objectData) => {
return (
objectData?.state?.type == 'ready' ||
objectData?.state?.type == 'connecting' ||
objectData?.state?.type == 'syncing'
)
}
},
{
name: 'reconnect',
label: 'Reconnect',
type: 'button',
icon: LinkIcon,
url: (_id) =>
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=reconnect`,
disabled: (objectData) => {
return objectData?.state?.type != 'ready'
}
},
{
name: 'refreshToken',
label: 'Refresh Token',
type: 'button',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=refreshToken`,
disabled: (objectData) => {
return objectData?.state?.type != 'ready'
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
@ -72,12 +134,34 @@ export const Marketplace = {
'_reference',
'name',
'provider',
'authConnected',
'state',
'connected',
'config.accessTokenExpiresAt',
'active',
'createdAt',
'updatedAt'
],
filters: ['name', '_id', 'provider', 'active', 'createdAt', 'updatedAt'],
sorters: ['name', 'provider', 'active', 'createdAt', 'updatedAt', '_id'],
filters: [
'name',
'_id',
'provider',
'active',
'connected',
'state',
'createdAt',
'updatedAt'
],
sorters: [
'name',
'provider',
'active',
'connected',
'state',
'createdAt',
'updatedAt',
'_id'
],
group: ['provider'],
properties: [
{
@ -121,12 +205,12 @@ export const Marketplace = {
columnWidth: 200
},
{
name: 'active',
label: 'Active',
type: 'bool',
readOnly: false,
required: true,
columnWidth: 125
name: 'connectedAt',
label: 'Connected At',
type: 'dateTime',
readOnly: true,
showSince: true,
columnWidth: 175
},
{
name: 'provider',
@ -140,10 +224,67 @@ export const Marketplace = {
],
columnWidth: 150
},
{
name: 'config.accessTokenExpiresAt',
label: 'Access Token Expires At',
type: 'dateTime',
readOnly: true,
columnWidth: 215
},
{
name: 'config.appId',
label: 'App ID',
name: 'state',
label: 'State',
type: 'state',
objectType: 'marketplace',
readOnly: true,
columnWidth: 130
},
{
name: 'active',
label: 'Active',
type: 'bool',
readOnly: false,
required: true,
columnWidth: 125
},
{
name: 'connected',
label: 'Connected',
type: 'bool',
readOnly: true,
columnWidth: 125
},
{
name: 'config.refreshTokenExpiresAt',
label: 'Refresh Token Expires At',
type: 'dateTime',
readOnly: true,
showSince: true,
columnWidth: 200
},
{
name: 'config.clientId',
label: 'Client ID',
type: 'secret',
readOnly: false,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.lastTokenRefreshAt',
label: 'Last Token Refresh At',
type: 'dateTime',
readOnly: true,
showSince: true,
columnWidth: 200
},
{
name: 'config.clientSecret',
label: 'Client Secret',
type: 'secret',
readOnly: false,
required: false,
@ -151,49 +292,67 @@ export const Marketplace = {
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.certId',
label: 'Cert ID',
type: 'secret',
readOnly: false,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.devId',
label: 'Dev ID',
type: 'secret',
readOnly: false,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.userToken',
label: 'User Token',
type: 'secret',
readOnly: false,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.siteId',
label: 'Site ID',
name: 'config.ruName',
label: 'RuName',
type: 'text',
readOnly: false,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.marketplaceId',
label: 'Marketplace ID',
type: 'text',
readOnly: false,
required: false,
columnWidth: 160,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.sandbox',
label: 'Sandbox',
type: 'bool',
readOnly: false,
required: false,
columnWidth: 120,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.accessToken',
label: 'Access Token',
name: 'config.verificationToken',
label: 'Verification Token',
type: 'secret',
readOnly: false,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'etsy'
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.refreshToken',
label: 'Refresh Token',
type: 'secret',
readOnly: true,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.accessToken',
label: 'Access Token',
type: 'secret',
readOnly: true,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'ebay'
},
{
name: 'config.redirectUri',
label: 'Redirect URI',
type: 'url',
readOnly: false,
required: false,
columnWidth: 280,
visible: (objectData) => objectData?.provider === 'tiktokShop'
},
{
name: 'config.refreshToken',
@ -202,6 +361,12 @@ export const Marketplace = {
readOnly: false,
required: false,
columnWidth: 200,
value: (objectData) => {
if (objectData?.config?.refreshToken) {
return '••••••••••••••••••••••••••••••••••••'
}
return undefined
},
visible: (objectData) => objectData?.provider === 'etsy'
},
{
@ -213,6 +378,22 @@ export const Marketplace = {
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'etsy'
},
{
name: 'config.accessToken',
label: 'Access Token',
type: 'secret',
readOnly: (objectData) => objectData?.provider === 'tiktokShop',
required: false,
columnWidth: 200,
value: (objectData) => {
if (objectData?.config?.accessToken) {
return '••••••••••••••••••••••••••••••••••••'
}
return undefined
},
visible: (objectData) =>
objectData?.provider === 'etsy' || objectData?.provider === 'tiktokShop'
},
{
name: 'config.appKey',
label: 'App Key',
@ -235,10 +416,45 @@ export const Marketplace = {
name: 'config.shopCipher',
label: 'Shop Cipher',
type: 'text',
readOnly: false,
readOnly: true,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'tiktokShop'
},
{
name: 'config.shopName',
label: 'Shop Name',
type: 'text',
readOnly: true,
required: false,
columnWidth: 200,
visible: (objectData) => objectData?.provider === 'tiktokShop'
},
{
name: 'config.shopRegion',
label: 'Shop Region',
type: 'text',
readOnly: true,
required: false,
columnWidth: 140,
visible: (objectData) => objectData?.provider === 'tiktokShop'
},
{
name: 'config.callbackUrl',
label: 'Callback URL',
type: 'url',
readOnly: true,
required: false,
columnWidth: 280,
visible: (objectData) =>
objectData?.provider === 'ebay' ||
objectData?.provider === 'tiktokShop',
value: () => {
if (typeof window === 'undefined') {
return undefined
}
return `${window.location.origin}/auth/marketplace/callback`
}
}
]
}

View File

@ -25,6 +25,7 @@ export const PartStock = {
'startingQuantity',
'currentQuantity',
'partSku',
'stockLocation',
'createdAt',
'updatedAt'
],
@ -75,7 +76,7 @@ export const PartStock = {
readOnly: false,
columnWidth: 200,
required: true,
masterFilter: ['subJob']
masterFilter: ['subJob', 'stockTransfer']
},
{
name: 'partSku',
@ -86,6 +87,15 @@ export const PartStock = {
showHyperlink: true,
columnWidth: 200
},
{
name: 'stockLocation',
label: 'Stock location',
type: 'object',
objectType: 'stockLocation',
required: false,
showHyperlink: true,
columnWidth: 200
},
{
name: 'source',

View File

@ -92,6 +92,7 @@ export const ProductStock = {
'state',
'currentQuantity',
'productSku',
'stockLocation',
'createdAt',
'updatedAt'
],
@ -151,6 +152,18 @@ export const ProductStock = {
showHyperlink: true,
columnWidth: 200
},
{
name: 'stockLocation',
label: 'Stock location',
type: 'object',
objectType: 'stockLocation',
required: false,
showHyperlink: true,
columnWidth: 200,
readOnly: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'currentQuantity',
label: 'Current Quantity',

View File

@ -0,0 +1,81 @@
import StockLocationIcon from '../../components/Icons/StockLocationIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const StockLocation = {
name: 'stockLocation',
label: 'Stock Location',
prefix: 'SLN',
icon: StockLocationIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/inventory/stocklocations/info?stockLocationId=${_id}`
}
],
url: (id) => `/dashboard/inventory/stocklocations/info?stockLocationId=${id}`,
filters: ['_id', 'name'],
sorters: ['name', 'createdAt'],
columns: ['_reference', 'name', 'address', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'stockLocation',
showCopy: true,
readOnly: true,
columnWidth: 140
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'stockLocation',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'name',
label: 'Name',
type: 'text',
required: true,
columnWidth: 220
},
{
name: 'address',
label: 'Address',
type: 'address',
readOnly: false,
required: true,
columnWidth: 250
}
],
stats: [
{
name: 'total.count',
label: 'Locations',
type: 'number',
color: 'default'
}
]
}

View File

@ -0,0 +1,224 @@
import StockTransferIcon from '../../components/Icons/StockTransferIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const StockTransfer = {
name: 'stockTransfer',
label: 'Stock Transfer',
prefix: 'STT',
icon: StockTransferIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/inventory/stocktransfers/info?stockTransferId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/inventory/stocktransfers/info?stockTransferId=${_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/inventory/stocktransfers/info?stockTransferId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/stocktransfers/info?stockTransferId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/inventory/stocktransfers/info?stockTransferId=${_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/inventory/stocktransfers/info?stockTransferId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
}
],
url: (id) => `/dashboard/inventory/stocktransfers/info?stockTransferId=${id}`,
filters: ['_id', 'state'],
sorters: ['createdAt', 'postedAt'],
columns: ['_reference', 'state', 'postedAt', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'stockTransfer',
showCopy: true,
readOnly: true,
columnWidth: 140
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
objectType: 'stockTransfer',
showCopy: true,
readOnly: true
},
{
name: 'state',
label: 'State',
type: 'state',
readOnly: true,
columnWidth: 120
},
{
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'lines',
label: 'Lines',
type: 'objectChildren',
required: false,
canAddRemove: true,
columns: [
'fromStockType',
'fromStock',
'quantity',
'toStockLocation',
'toStockType',
'toStock'
],
properties: [
{
name: 'fromStockType',
label: 'From type',
type: 'objectType',
required: true,
columnWidth: 180,
masterFilter: ['filamentStock', 'partStock', 'productStock']
},
{
name: 'fromStock',
label: 'From stock',
type: 'object',
objectType: (row) => row?.fromStockType,
required: true,
showHyperlink: true,
columnWidth: 230
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true,
min: 0,
columnWidth: 140,
suffix: (row) =>
row?.fromStockType === 'filamentStock' ? 'g net' : null
},
{
name: 'toStockLocation',
label: 'To location',
type: 'object',
objectType: 'stockLocation',
required: true,
showHyperlink: true,
columnWidth: 230
},
{
name: 'toStockType',
label: 'To type',
type: 'objectType',
readOnly: true,
columnWidth: 180,
visible: (row) => Boolean(row?.toStockType)
},
{
name: 'toStock',
label: 'To stock',
type: 'object',
objectType: (row) => row?.toStockType,
readOnly: true,
showHyperlink: true,
columnWidth: 230,
visible: (row) => Boolean(row?.toStock)
}
]
}
],
stats: [
{
name: 'draft.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'posted.count',
label: 'Posted',
type: 'number',
color: 'success'
}
]
}

View File

@ -55,6 +55,22 @@ const ShipmentInfo = lazy(
const InventoryOverview = lazy(
() => import('../components/Dashboard/Inventory/InventoryOverview.jsx')
)
const StockLocations = lazy(
() => import('../components/Dashboard/Inventory/StockLocations.jsx')
)
const StockLocationInfo = lazy(
() =>
import('../components/Dashboard/Inventory/StockLocations/StockLocationInfo.jsx')
)
const StockTransfers = lazy(
() => import('../components/Dashboard/Inventory/StockTransfers.jsx')
)
const StockTransferInfo = lazy(
() =>
import(
'../components/Dashboard/Inventory/StockTransfers/StockTransferInfo.jsx'
)
)
const InventoryRoutes = [
<Route
@ -92,6 +108,26 @@ const InventoryRoutes = [
path='inventory/productstocks/info'
element={<ProductStockInfo />}
/>,
<Route
key='stocklocations'
path='inventory/stocklocations'
element={<StockLocations />}
/>,
<Route
key='stocklocations-info'
path='inventory/stocklocations/info'
element={<StockLocationInfo />}
/>,
<Route
key='stocktransfers'
path='inventory/stocktransfers'
element={<StockTransfers />}
/>,
<Route
key='stocktransfers-info'
path='inventory/stocktransfers/info'
element={<StockTransferInfo />}
/>,
<Route
key='stockevents'
path='inventory/stockevents'

View File

@ -1,9 +1,7 @@
import { lazy } from 'react'
import { Route } from 'react-router-dom'
const Clients = lazy(
() => import('../components/Dashboard/Sales/Clients.jsx')
)
const Clients = lazy(() => import('../components/Dashboard/Sales/Clients.jsx'))
const ClientInfo = lazy(
() => import('../components/Dashboard/Sales/Clients/ClientInfo.jsx')
)
@ -22,32 +20,56 @@ const Marketplaces = lazy(
const MarketplaceInfo = lazy(
() => import('../components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx')
)
const Listings = lazy(
() => import('../components/Dashboard/Sales/Listings.jsx')
)
const ListingInfo = lazy(
() => import('../components/Dashboard/Sales/Listings/ListingInfo.jsx')
)
const ListingVarientInfo = lazy(
() =>
import('../components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx')
)
const SalesRoutes = [
<Route
key='overview'
path='sales/overview'
element={<SalesOverview />}
/>,
<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'
path='sales/salesorders'
element={<SalesOrders />}
/>,
<Route
key='salesorders-info'
path='sales/salesorders/info'
element={<SalesOrderInfo />}
/>,
<Route key='marketplaces' path='sales/marketplaces' element={<Marketplaces />} />,
<Route
key='marketplaces'
path='sales/marketplaces'
element={<Marketplaces />}
/>,
<Route
key='marketplaces-info'
path='sales/marketplaces/info'
element={<MarketplaceInfo />}
/>,
<Route key='listings' path='sales/listings' element={<Listings />} />,
<Route
key='listings-info'
path='sales/listings/info'
element={<ListingInfo />}
/>,
<Route
key='listingvarients-info'
path='sales/listingvarients/info'
element={<ListingVarientInfo />}
/>
]
export default SalesRoutes