Compare commits
6 Commits
8e393e229f
...
c57446836e
| Author | SHA1 | Date | |
|---|---|---|---|
| c57446836e | |||
| f289bbb6b2 | |||
| 6725b1c399 | |||
| b4512a1948 | |||
| 4f0fe89398 | |||
| 17da8a4407 |
@ -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 |
19
assets/icons/listingvarienticon.svg
Normal file
19
assets/icons/listingvarienticon.svg
Normal 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 |
8
assets/icons/stocklocationicon.svg
Normal file
8
assets/icons/stocklocationicon.svg
Normal 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 |
9
assets/icons/stocktransfericon.svg
Normal file
9
assets/icons/stocktransfericon.svg
Normal 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 |
11
src/App.jsx
11
src/App.jsx
@ -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 />}
|
||||
|
||||
@ -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',
|
||||
|
||||
111
src/components/Dashboard/Inventory/StockLocations.jsx
Normal file
111
src/components/Dashboard/Inventory/StockLocations.jsx
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
111
src/components/Dashboard/Inventory/StockTransfers.jsx
Normal file
111
src/components/Dashboard/Inventory/StockTransfers.jsx
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
107
src/components/Dashboard/Sales/Listings.jsx
Normal file
107
src/components/Dashboard/Sales/Listings.jsx
Normal 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
|
||||
298
src/components/Dashboard/Sales/Listings/ListingInfo.jsx
Normal file
298
src/components/Dashboard/Sales/Listings/ListingInfo.jsx
Normal 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
|
||||
87
src/components/Dashboard/Sales/Listings/NewListing.jsx
Normal file
87
src/components/Dashboard/Sales/Listings/NewListing.jsx
Normal 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
|
||||
50
src/components/Dashboard/Sales/Listings/PublishListing.jsx
Normal file
50
src/components/Dashboard/Sales/Listings/PublishListing.jsx
Normal 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
|
||||
50
src/components/Dashboard/Sales/Listings/UnpublishListing.jsx
Normal file
50
src/components/Dashboard/Sales/Listings/UnpublishListing.jsx
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
46
src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx
Normal file
46
src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx
Normal 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
|
||||
46
src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx
Normal file
46
src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx
Normal 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
|
||||
68
src/components/Dashboard/Sales/Marketplaces/authUtils.js
Normal file
68
src/components/Dashboard/Sales/Marketplaces/authUtils.js
Normal 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}`)
|
||||
}
|
||||
@ -44,4 +44,3 @@ CancelSalesOrder.propTypes = {
|
||||
}
|
||||
|
||||
export default CancelSalesOrder
|
||||
|
||||
|
||||
@ -44,4 +44,3 @@ ConfirmSalesOrder.propTypes = {
|
||||
}
|
||||
|
||||
export default ConfirmSalesOrder
|
||||
|
||||
|
||||
@ -44,4 +44,3 @@ PostSalesOrder.propTypes = {
|
||||
}
|
||||
|
||||
export default PostSalesOrder
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
111
src/components/Dashboard/common/ObjectCard.jsx
Normal file
111
src/components/Dashboard/common/ObjectCard.jsx
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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}
|
||||
|
||||
6
src/components/Icons/ListingIcon.jsx
Normal file
6
src/components/Icons/ListingIcon.jsx
Normal 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
|
||||
8
src/components/Icons/ListingVarientIcon.jsx
Normal file
8
src/components/Icons/ListingVarientIcon.jsx
Normal 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
|
||||
8
src/components/Icons/StockLocationIcon.jsx
Normal file
8
src/components/Icons/StockLocationIcon.jsx
Normal 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
|
||||
8
src/components/Icons/StockTransferIcon.jsx
Normal file
8
src/components/Icons/StockTransferIcon.jsx
Normal 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
|
||||
@ -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) {
|
||||
|
||||
@ -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',
|
||||
|
||||
266
src/database/models/Listing.js
Normal file
266
src/database/models/Listing.js
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
243
src/database/models/ListingVarient.js
Normal file
243
src/database/models/ListingVarient.js
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
81
src/database/models/StockLocation.js
Normal file
81
src/database/models/StockLocation.js
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
224
src/database/models/StockTransfer.js
Normal file
224
src/database/models/StockTransfer.js
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user