2019-10-02 12:16:07 +08:00
|
|
|
'use strict';
|
|
|
|
|
2023-06-28 11:41:06 +08:00
|
|
|
import React from "react";
|
|
|
|
import axios from "axios";
|
|
|
|
import ReactDOM from "react-dom";
|
|
|
|
import PropTypes from "prop-types";
|
|
|
|
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
|
2023-06-28 11:41:06 +08:00
|
|
|
const data = window.shop_data;
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
|
|
|
|
const custom = {
|
|
|
|
opacity: snapshot.isDragging ? .7 : 1,
|
|
|
|
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!snapshot.isDropAnimating) {
|
|
|
|
return { ...style, ...custom};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (removeAnim) {
|
|
|
|
// cannot be 0, but make it super tiny
|
|
|
|
custom.transitionDuration = '0.001s';
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...style,
|
|
|
|
...custom,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const cartStyle = (style, snapshot) => {
|
|
|
|
const isDraggingOver = snapshot.isDraggingOver;
|
|
|
|
return {
|
|
|
|
...style,
|
|
|
|
...{
|
|
|
|
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
|
|
|
|
border: isDraggingOver ? '1px dashed #ccc' : '0',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const nbrConnectorsStyle = (data) => {
|
|
|
|
if (!data || !data.nbrCurrentSlot) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
let p = data.nbrCurrentSlot * 100 / data.nbrSlotMax;
|
|
|
|
if (p > 100) {
|
|
|
|
p = 100;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
width: `${p}%`,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const nbrClocksStyle = (data) => {
|
|
|
|
if (!data || !data.nbrCurrentClock) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
let p = data.nbrCurrentClock * 100 / data.nbrClockMax;
|
|
|
|
if (p > 100) {
|
|
|
|
p = 100;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
width: `${p}%`,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const copy = (
|
|
|
|
model,
|
|
|
|
source,
|
|
|
|
destination,
|
2023-06-30 10:10:15 +08:00
|
|
|
draggableSource,
|
2019-10-02 12:16:07 +08:00
|
|
|
droppableDestination
|
|
|
|
) => {
|
|
|
|
const destClone = Array.from(destination.items);
|
|
|
|
|
|
|
|
destClone.splice(droppableDestination.index, 0, {
|
2023-06-30 10:10:15 +08:00
|
|
|
...model[draggableSource],
|
2019-10-02 12:16:07 +08:00
|
|
|
id: uuidv4(),
|
|
|
|
});
|
|
|
|
|
|
|
|
return destClone;
|
|
|
|
};
|
|
|
|
|
|
|
|
const reorder = (list, startIndex, endIndex) => {
|
|
|
|
const result = Array.from(list);
|
|
|
|
const [removed] = result.splice(startIndex, 1);
|
|
|
|
result.splice(endIndex, 0, removed);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
const remove = (list, startIndex) => {
|
|
|
|
const result = Array.from(list);
|
|
|
|
result.splice(startIndex, 1);
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
const nbrOccupiedSlotsInCrate = (items) => {
|
|
|
|
return items.reduce((prev, next) => {
|
|
|
|
return prev + (next.hp === 8 ? 2 : 1);
|
|
|
|
}, 0);
|
|
|
|
};
|
|
|
|
|
2019-11-09 18:41:09 +08:00
|
|
|
function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
|
|
|
|
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
|
|
|
|
// changes: return amount if error in order to avoid empty value
|
|
|
|
try {
|
|
|
|
decimalCount = Math.abs(decimalCount);
|
|
|
|
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
|
|
|
|
|
|
|
|
const negativeSign = amount < 0 ? "-" : "";
|
|
|
|
|
|
|
|
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
|
|
|
|
let j = (i.length > 3) ? i.length % 3 : 0;
|
|
|
|
|
|
|
|
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
|
|
|
|
} catch (e) {
|
|
|
|
return amount;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that provides a base layout (aside/main) for the page.
|
|
|
|
*/
|
|
|
|
class Layout extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
aside: PropTypes.any,
|
|
|
|
main: PropTypes.any,
|
2019-12-26 21:31:37 +08:00
|
|
|
mobileSideMenuShouldOpen: PropTypes.bool,
|
2020-01-22 22:47:10 +08:00
|
|
|
isMobile: PropTypes.bool,
|
|
|
|
newCardJustAdded: PropTypes.bool,
|
2019-12-26 21:31:37 +08:00
|
|
|
onClickToggleMobileSideMenu: PropTypes.func,
|
2020-04-14 13:46:51 +08:00
|
|
|
onClickCloseRFQFeedback: PropTypes.func,
|
2020-04-14 14:56:17 +08:00
|
|
|
RFQBodyType: PropTypes.string,
|
2020-04-14 15:08:28 +08:00
|
|
|
RFQBodyOrder: PropTypes.string,
|
2020-04-14 16:51:33 +08:00
|
|
|
onClickLoadCustomConf: PropTypes.func,
|
2020-04-20 14:51:35 +08:00
|
|
|
items: PropTypes.object,
|
2019-12-26 21:31:37 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static get defaultProps() {
|
|
|
|
return {
|
|
|
|
mobileSideMenuShouldOpen: false,
|
2019-10-02 12:16:07 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
customconf: '',
|
|
|
|
error: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.handleCustomConfig = this.handleCustomConfig.bind(this);
|
|
|
|
this.handleClickLoad = this.handleClickLoad.bind(this);
|
|
|
|
this.checkValidation = this.checkValidation.bind(this);
|
2020-04-20 14:51:35 +08:00
|
|
|
|
|
|
|
// retrieve list of available pn
|
|
|
|
const items_keys = Object.keys(props.items);
|
|
|
|
this.list_pn = items_keys.map(function (key) {
|
|
|
|
return props.items[key].name_number;
|
|
|
|
});
|
2020-04-14 16:51:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
handleCustomConfig(e) {
|
|
|
|
const value = e.target.value;
|
|
|
|
|
|
|
|
this.checkValidation(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
checkValidation(conf) {
|
|
|
|
let conf_obj;
|
|
|
|
|
|
|
|
try {
|
|
|
|
conf_obj = JSON.parse(conf);
|
|
|
|
} catch (e) {
|
|
|
|
return this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
customconf_ready: null,
|
|
|
|
error: 'invalid format',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!conf_obj) {
|
|
|
|
return this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
customconf_ready: null,
|
|
|
|
error: 'invalid format',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((!conf_obj.items || !conf_obj.type) &&
|
|
|
|
(Object.prototype.toString.call(conf_obj.items) !== '[object Array]' ||
|
|
|
|
Object.prototype.toString.call(conf_obj.type) !== '[object String]')) {
|
|
|
|
return this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
customconf_ready: null,
|
|
|
|
error: 'invalid format',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (conf_obj.type !== "desktop" && conf_obj.type !== "rack") {
|
|
|
|
return this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
customconf_ready: null,
|
|
|
|
error: 'invalid format',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
conf_obj.items.map(function (item) {
|
|
|
|
try {
|
|
|
|
return JSON.parse(item);
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
conf_obj.items = conf_obj.items.filter(function (item) {
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (conf_obj.items.filter(function (item) {
|
|
|
|
return Object.prototype.toString.call(item) !== '[object Object]' || !item.pn || Object.prototype.toString.call(item.pn) !== '[object String]';
|
|
|
|
}).length > 0) {
|
|
|
|
return this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
customconf_ready: null,
|
|
|
|
error: 'invalid format',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
conf_obj.items = conf_obj.items.map(function (item) {
|
|
|
|
return {
|
|
|
|
pn: item.pn,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2020-04-20 14:51:35 +08:00
|
|
|
const self = this;
|
|
|
|
const unknow_pn = conf_obj.items.filter(function (item_pn) {
|
|
|
|
return self.list_pn.includes(item_pn.pn) === false;
|
|
|
|
}).map(function (item_pn) {
|
|
|
|
return item_pn.pn;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (unknow_pn.length > 0) {
|
|
|
|
return this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
customconf_ready: null,
|
|
|
|
error: `${unknow_pn.join(', ')} unknown${unknow_pn.length > 1 ? 's':''} pn number`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
customconf: conf,
|
|
|
|
error: null,
|
|
|
|
customconf_ready: conf_obj,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleClickLoad() {
|
2021-06-01 11:42:46 +08:00
|
|
|
this.checkValidation(this.state.customconf);
|
2020-04-14 16:51:33 +08:00
|
|
|
|
|
|
|
if (this.props.onClickLoadCustomConf) {
|
|
|
|
this.props.onClickLoadCustomConf(this.state.customconf_ready);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
aside,
|
|
|
|
main,
|
2019-12-26 21:31:37 +08:00
|
|
|
mobileSideMenuShouldOpen,
|
2020-01-22 22:47:10 +08:00
|
|
|
isMobile,
|
|
|
|
newCardJustAdded,
|
2020-04-14 13:46:51 +08:00
|
|
|
onClickToggleMobileSideMenu,
|
|
|
|
onClickCloseRFQFeedback,
|
|
|
|
showRFQFeedback,
|
2020-04-14 14:56:17 +08:00
|
|
|
RFQBodyType,
|
2020-04-14 15:08:28 +08:00
|
|
|
RFQBodyOrder,
|
2019-10-02 12:16:07 +08:00
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="layout">
|
|
|
|
|
2019-12-26 21:31:37 +08:00
|
|
|
<aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
|
2019-10-02 12:16:07 +08:00
|
|
|
|
2019-12-26 21:31:37 +08:00
|
|
|
{mobileSideMenuShouldOpen ? (
|
|
|
|
<section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
|
|
|
|
) : (
|
|
|
|
<section className="main">{main}</section>
|
|
|
|
)}
|
2019-10-02 12:16:07 +08:00
|
|
|
|
2020-01-22 22:47:10 +08:00
|
|
|
{isMobile && newCardJustAdded ? (
|
|
|
|
<div className="feedback-add-success">
|
|
|
|
✓ added
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
|
2020-04-14 14:56:17 +08:00
|
|
|
<div class={`modal fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'block':'none'}} id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
|
|
|
<div class="modal-dialog" role="document">
|
|
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-body rfqFeedback">
|
2020-04-14 13:46:51 +08:00
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
<div className="d-flex w-100">
|
2020-04-14 13:46:51 +08:00
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
{RFQBodyType === 'email' ? (
|
|
|
|
<div className="d-flex">
|
2020-04-14 14:56:17 +08:00
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
<div>
|
|
|
|
<img width="30px" src="/images/shop/icon-done.svg" alt="close" />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div style={{'padding': '0 .5em'}}>
|
|
|
|
We've received your request and will be in contact soon.
|
|
|
|
</div>
|
2020-04-14 14:56:17 +08:00
|
|
|
|
|
|
|
</div>
|
2020-04-14 15:08:28 +08:00
|
|
|
) : null }
|
2020-04-14 14:56:17 +08:00
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
{RFQBodyType === 'show' ? (
|
|
|
|
<p>
|
|
|
|
{RFQBodyOrder}
|
|
|
|
</p>
|
|
|
|
) : null}
|
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
{RFQBodyType === 'import' ? (
|
|
|
|
<div className="w-100">
|
|
|
|
|
|
|
|
<form className="form w-100">
|
2020-04-14 17:54:31 +08:00
|
|
|
<div className="form-group">
|
|
|
|
<p className="small">
|
2020-04-16 10:33:28 +08:00
|
|
|
Input the JSON description below. Should be something like:
|
2020-04-14 17:54:31 +08:00
|
|
|
<br />
|
2021-03-22 18:30:56 +08:00
|
|
|
{JSON.stringify({"items":[{"pn":"1124"},{"pn":"2118"},{"pn":"2118"},{"pn":"2128"}],"type":"desktop"})}
|
2020-04-14 17:54:31 +08:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
<div className="form-group w-100">
|
|
|
|
<textarea
|
|
|
|
onChange={this.handleCustomConfig}
|
|
|
|
value={this.state.customconf}
|
|
|
|
className="form-control w-100"
|
|
|
|
rows="5"
|
2020-04-16 10:33:28 +08:00
|
|
|
placeholder="Input JSON description here." />
|
2020-04-14 16:51:33 +08:00
|
|
|
</div>
|
|
|
|
{this.state.error ? (
|
|
|
|
<div className="form-group">
|
|
|
|
<p className="text-danger">{this.state.error}</p>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</form>
|
|
|
|
|
2020-04-14 17:54:31 +08:00
|
|
|
<div className="d-flex flex-column flex-sm-row justify-content-end">
|
2023-07-11 13:50:32 +08:00
|
|
|
<a type="button" onClick={onClickCloseRFQFeedback} class="btn btn-sm btn-outline-primary m-0 mb-2 mb-sm-0 me-sm-2">Close</a>
|
|
|
|
<a type="button" onClick={this.handleClickLoad} class={`btn btn-sm btn-primary m-0 ms-sm-2 ${this.state.error ? 'disabled':''}`}>Load configuration</a>
|
2020-04-14 17:54:31 +08:00
|
|
|
</div>
|
2020-04-14 16:51:33 +08:00
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
<div>
|
|
|
|
<button onClick={onClickCloseRFQFeedback}>
|
|
|
|
<img src="/images/shop/icon-close.svg" alt="close" />
|
|
|
|
</button>
|
2020-04-14 14:56:17 +08:00
|
|
|
</div>
|
2020-04-14 15:08:28 +08:00
|
|
|
|
|
|
|
</div>
|
2020-04-14 14:56:17 +08:00
|
|
|
|
|
|
|
</div>
|
2020-04-14 16:51:33 +08:00
|
|
|
|
2020-04-14 14:56:17 +08:00
|
|
|
</div>
|
2020-04-14 13:46:51 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
<div onClick={onClickCloseRFQFeedback} className={`modal-backdrop fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'initial':'none'}}></div>
|
2020-04-14 14:56:17 +08:00
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that renders a product.
|
|
|
|
* Used in the aside (e.g backlog of product)
|
|
|
|
*/
|
|
|
|
class ProductItem extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
id: PropTypes.string.isRequired,
|
|
|
|
name: PropTypes.string.isRequired,
|
2020-03-02 20:57:38 +08:00
|
|
|
name_codename: PropTypes.string,
|
2019-10-02 12:16:07 +08:00
|
|
|
price: PropTypes.number.isRequired,
|
|
|
|
currency: PropTypes.string.isRequired,
|
|
|
|
image: PropTypes.string.isRequired,
|
|
|
|
specs: PropTypes.array,
|
2022-01-19 14:27:09 +08:00
|
|
|
datasheet_file: PropTypes.string,
|
|
|
|
datasheet_name: PropTypes.string,
|
2019-10-02 12:16:07 +08:00
|
|
|
onClickAddItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
|
|
|
|
}
|
|
|
|
|
2023-06-30 10:10:15 +08:00
|
|
|
handleOnClickAddItem(id, tap, e) {
|
2019-10-02 12:16:07 +08:00
|
|
|
if (this.props.onClickAddItem) {
|
2023-06-30 10:10:15 +08:00
|
|
|
this.props.onClickAddItem(id, tap);
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
id,
|
|
|
|
name,
|
2020-03-02 20:57:38 +08:00
|
|
|
name_codename,
|
2019-10-02 12:16:07 +08:00
|
|
|
price,
|
|
|
|
currency,
|
|
|
|
image,
|
|
|
|
specs,
|
2022-01-19 14:27:09 +08:00
|
|
|
datasheet_file,
|
|
|
|
datasheet_name,
|
2019-10-02 12:16:07 +08:00
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const render_specs = (specs && specs.length > 0 && (
|
|
|
|
<ul>
|
|
|
|
{specs.map((spec, index) =>
|
|
|
|
<li key={index}>{spec}</li>
|
|
|
|
)}
|
|
|
|
</ul>
|
|
|
|
));
|
|
|
|
|
2022-01-19 14:27:09 +08:00
|
|
|
const render_datasheet_link = (datasheet_file && datasheet_name && (
|
|
|
|
<div className="ds">
|
|
|
|
<span class='doc-icon'></span>
|
|
|
|
<a href={datasheet_file} target="_blank" rel="noopener noreferrer">
|
|
|
|
{datasheet_name}
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
));
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
return (
|
|
|
|
<section className="productItem">
|
|
|
|
|
|
|
|
<div className="content">
|
2020-03-02 20:57:38 +08:00
|
|
|
<h3 style={{ 'margin-bottom': name_codename ? '5px' : '20px'}}>{name}</h3>
|
|
|
|
{name_codename ? (
|
|
|
|
<p>{name_codename}</p>
|
|
|
|
) : null }
|
2019-10-02 12:16:07 +08:00
|
|
|
|
2019-11-09 18:41:09 +08:00
|
|
|
<div className="price">{`${currency} ${formatMoney(price)}`}</div>
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
{render_specs}
|
2022-01-19 14:27:09 +08:00
|
|
|
|
|
|
|
{render_datasheet_link}
|
2019-10-02 12:16:07 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="content">
|
|
|
|
|
2023-06-30 10:10:15 +08:00
|
|
|
<button onClick={this.handleOnClickAddItem.bind(this, id, true)}>
|
2019-10-02 12:16:07 +08:00
|
|
|
<img src="/images/shop/icon-add.svg" alt="add" />
|
|
|
|
</button>
|
|
|
|
|
2023-06-30 10:10:15 +08:00
|
|
|
<Draggable draggableId={id}>
|
2019-10-02 12:16:07 +08:00
|
|
|
{(provided, snapshot) => (
|
|
|
|
<React.Fragment>
|
|
|
|
<img
|
|
|
|
ref={provided.innerRef}
|
|
|
|
{...provided.draggableProps}
|
|
|
|
{...provided.dragHandleProps}
|
|
|
|
style={productStyle(
|
|
|
|
provided.draggableProps.style,
|
|
|
|
snapshot,
|
|
|
|
true, // hack: remove weird animation after a drop
|
|
|
|
)}
|
|
|
|
src={image} />
|
|
|
|
|
|
|
|
{/* Allows to simulate a clone */}
|
|
|
|
{snapshot.isDragging && (
|
2020-01-19 22:15:51 +08:00
|
|
|
<img className="simclone" src={image} />
|
2019-10-02 12:16:07 +08:00
|
|
|
)}
|
|
|
|
</React.Fragment>
|
|
|
|
)}
|
|
|
|
</Draggable>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that renders a product.
|
|
|
|
* Used in the crate
|
|
|
|
*/
|
|
|
|
class ProductCartItem extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile: PropTypes.bool,
|
2019-10-02 12:16:07 +08:00
|
|
|
hovered: PropTypes.bool,
|
|
|
|
index: PropTypes.number.isRequired,
|
|
|
|
model: PropTypes.object.isRequired,
|
|
|
|
data: PropTypes.object,
|
|
|
|
onToggleProgress: PropTypes.func,
|
|
|
|
onToggleWarning: PropTypes.func,
|
|
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
|
|
onClickRemoveItem: PropTypes.func,
|
2020-04-20 17:22:38 +08:00
|
|
|
shouldTooltipWarningClassInverted: PropTypes.bool,
|
2019-10-02 12:16:07 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static get defaultProps() {
|
|
|
|
return {
|
|
|
|
hovered: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
|
|
|
|
this.handleOnMouseEnterWarningItem = this.handleOnMouseEnterWarningItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveWarningItem = this.handleOnMouseLeaveWarningItem.bind(this);
|
|
|
|
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
|
|
|
|
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterItem(index, e) {
|
|
|
|
if (this.props.onToggleProgress) {
|
|
|
|
this.props.onToggleProgress(index, true);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveItem(index, e) {
|
|
|
|
if (this.props.onToggleProgress) {
|
|
|
|
this.props.onToggleProgress(index, false);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterWarningItem(index, isWarning, e) {
|
|
|
|
if (!isWarning) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.props.onToggleWarning) {
|
|
|
|
this.props.onToggleWarning(index, true);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveWarningItem(index, isWarning, e) {
|
|
|
|
if (!isWarning) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.props.onToggleWarning) {
|
|
|
|
this.props.onToggleWarning(index, false);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterRemoveItem(index, e) {
|
2020-01-21 00:43:33 +08:00
|
|
|
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
|
2019-10-02 12:16:07 +08:00
|
|
|
this.props.onToggleOverlayRemove(index, true);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveRemoveItem(index, e) {
|
2020-01-21 00:43:33 +08:00
|
|
|
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
|
2019-10-02 12:16:07 +08:00
|
|
|
this.props.onToggleOverlayRemove(index, false);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnClickRemoveItem(index, e) {
|
|
|
|
if (this.props.onClickRemoveItem) {
|
|
|
|
this.props.onClickRemoveItem(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
hovered,
|
|
|
|
model,
|
|
|
|
data,
|
|
|
|
index,
|
2020-04-20 17:22:38 +08:00
|
|
|
shouldTooltipWarningClassInverted,
|
2019-10-02 12:16:07 +08:00
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
let warning;
|
|
|
|
if (data && data.warnings) {
|
|
|
|
const warningsKeys = Object.keys(data.warnings);
|
|
|
|
if (warningsKeys && warningsKeys.length > 0) {
|
|
|
|
// we display only the first warning
|
|
|
|
warning = data.warnings[warningsKeys[0]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let render_progress;
|
|
|
|
if (model.showProgress && data) {
|
|
|
|
switch(model.type) {
|
|
|
|
case 'kasli':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
2019-12-13 09:33:00 +08:00
|
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>
|
|
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>
|
2019-10-02 12:16:07 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
2021-03-23 00:40:59 +08:00
|
|
|
case 'vhdcicarrier':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
|
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
case 'zotino':
|
|
|
|
case 'hd68':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
2019-12-13 09:33:00 +08:00
|
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors used`}</p>
|
2019-10-02 12:16:07 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'clocker':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
2019-12-13 09:33:00 +08:00
|
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>
|
2019-10-02 12:16:07 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Draggable draggableId={model.id} index={index}>
|
|
|
|
|
|
|
|
{(provided, snapshot) => (
|
|
|
|
<div
|
|
|
|
ref={provided.innerRef}
|
|
|
|
{...provided.draggableProps}
|
|
|
|
{...provided.dragHandleProps}
|
|
|
|
style={{ ...productStyle(
|
|
|
|
provided.draggableProps.style,
|
|
|
|
snapshot,
|
|
|
|
true,
|
|
|
|
hovered ? true : false,
|
|
|
|
model.selected ? true : false,
|
|
|
|
)}}
|
|
|
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
|
|
|
|
>
|
|
|
|
|
|
|
|
{/* warning container */}
|
|
|
|
<div
|
|
|
|
className="progress-container warning"
|
|
|
|
onMouseEnter={this.handleOnMouseEnterWarningItem.bind(this, index, warning)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveWarningItem.bind(this, index, warning)}>
|
|
|
|
|
|
|
|
{warning && (
|
|
|
|
<img className="alert-warning" src={warning ? `/images${warning.icon}` : null} />
|
|
|
|
)}
|
|
|
|
|
2020-04-21 10:18:46 +08:00
|
|
|
{warning && model.showWarning && (
|
2020-04-20 17:22:38 +08:00
|
|
|
<div className={`k-popup-warning ${shouldTooltipWarningClassInverted ? 'inverted': ''}`}>
|
2019-10-02 12:16:07 +08:00
|
|
|
<p className="rule warning">
|
|
|
|
<i>{warning.message}</i>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
2020-03-02 20:57:38 +08:00
|
|
|
<h6>{model.name_number}</h6>
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
<div
|
|
|
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
|
|
|
>
|
|
|
|
|
|
|
|
<img
|
|
|
|
className='item-cart'
|
|
|
|
src={`/images${model.image}`} />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* remove container */}
|
|
|
|
<div
|
|
|
|
style={{'display': model.showOverlayRemove ? 'flex':'none'}}
|
|
|
|
className="overlayRemove"
|
|
|
|
onClick={this.handleOnClickRemoveItem.bind(this, index)}>
|
|
|
|
|
|
|
|
<img src="/images/shop/icon-remove.svg" alt="rm"/>
|
|
|
|
|
2019-11-11 13:39:17 +08:00
|
|
|
<p>Remove</p>
|
2019-10-02 12:16:07 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* progression container */}
|
|
|
|
<div
|
|
|
|
className="progress-container"
|
|
|
|
onMouseEnter={this.handleOnMouseEnterItem.bind(this, index)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveItem.bind(this, index)}>
|
|
|
|
|
|
|
|
{model.nbrSlotMax > 0 && (
|
|
|
|
<div className="nbr-connectors">
|
|
|
|
<div style={{ ...nbrConnectorsStyle(data)}}></div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{model.nbrClockMax > 0 && (
|
|
|
|
<div className="nbr-clocks">
|
|
|
|
<div style={{ ...nbrClocksStyle(data)}}></div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* progress info when mouse over */}
|
|
|
|
{render_progress}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
</Draggable>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that displays a placeholder inside crate.
|
|
|
|
* Allows to display how it remains space for the current crate.
|
|
|
|
*/
|
|
|
|
class FakePlaceholder extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
isDraggingOver: PropTypes.bool,
|
|
|
|
nbrSlots: PropTypes.number.isRequired,
|
|
|
|
items: PropTypes.array.isRequired,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
isDraggingOver,
|
|
|
|
nbrSlots,
|
|
|
|
items,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const fakePlaceholder = [];
|
|
|
|
const nbrOccupied = nbrOccupiedSlotsInCrate(items);
|
|
|
|
|
|
|
|
for (var i = (nbrSlots - nbrOccupied); i > 0; i--) {
|
|
|
|
fakePlaceholder.push(
|
|
|
|
<div key={i} style={{
|
|
|
|
display: isDraggingOver ? 'none' : 'block',
|
|
|
|
border: '1px dashed #ccc',
|
|
|
|
width: '45px',
|
|
|
|
marginBottom: '5px',
|
|
|
|
}}></div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<React.Fragment>
|
|
|
|
{fakePlaceholder}
|
|
|
|
</React.Fragment>
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that displays a list of <ProductCartItem>
|
|
|
|
*/
|
|
|
|
class Cart extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile: PropTypes.bool,
|
2019-10-02 12:16:07 +08:00
|
|
|
nbrSlots: PropTypes.number,
|
|
|
|
itemHovered: PropTypes.string,
|
|
|
|
data: PropTypes.object.isRequired,
|
|
|
|
onToggleProgress: PropTypes.func,
|
|
|
|
onToggleWarning: PropTypes.func,
|
|
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
|
|
onClickRemoveItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile,
|
2019-10-02 12:16:07 +08:00
|
|
|
nbrSlots,
|
|
|
|
itemHovered,
|
|
|
|
data,
|
|
|
|
onToggleProgress,
|
|
|
|
onToggleWarning,
|
|
|
|
onToggleOverlayRemove,
|
|
|
|
onClickRemoveItem,
|
|
|
|
} = this.props;
|
|
|
|
|
2020-04-20 17:22:38 +08:00
|
|
|
const nbrOccupied = nbrOccupiedSlotsInCrate(data.items);
|
|
|
|
|
|
|
|
const shouldTooltipWarningClassInverted = nbrSlots - nbrOccupied < 5;
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
const products = data.items.map((item, index) => {
|
|
|
|
let itemData;
|
|
|
|
if (data.itemsData && index in data.itemsData) {
|
|
|
|
itemData = data.itemsData[index];
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<ProductCartItem
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile={isMobile}
|
2019-10-02 12:16:07 +08:00
|
|
|
hovered={item.id === itemHovered}
|
|
|
|
key={item.id}
|
|
|
|
index={index}
|
|
|
|
data={itemData}
|
2020-04-20 17:22:38 +08:00
|
|
|
shouldTooltipWarningClassInverted={shouldTooltipWarningClassInverted && index > 10}
|
2019-10-02 12:16:07 +08:00
|
|
|
onToggleProgress={onToggleProgress}
|
|
|
|
onToggleWarning={onToggleWarning}
|
|
|
|
onToggleOverlayRemove={onToggleOverlayRemove}
|
|
|
|
onClickRemoveItem={onClickRemoveItem}
|
|
|
|
model={item}>
|
|
|
|
</ProductCartItem>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Droppable droppableId={data.id} direction="horizontal">
|
|
|
|
|
|
|
|
{(provided, snapshot) => (
|
|
|
|
<div
|
|
|
|
ref={provided.innerRef}
|
|
|
|
{...provided.droppableProps}
|
|
|
|
style={cartStyle(
|
|
|
|
provided.droppableProps.style,
|
|
|
|
snapshot,
|
|
|
|
)}
|
|
|
|
className="items-cart-list">
|
|
|
|
|
|
|
|
{products}
|
|
|
|
|
|
|
|
{provided.placeholder && (
|
|
|
|
<div style={{ display: 'none' }}>
|
|
|
|
{provided.placeholder}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<FakePlaceholder
|
|
|
|
nbrSlots={nbrSlots}
|
|
|
|
items={data.items}
|
|
|
|
isDraggingOver={snapshot.isDraggingOver} />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
</Droppable>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that displays crate modes
|
|
|
|
*/
|
|
|
|
class CrateMode extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
items: PropTypes.array.isRequired,
|
|
|
|
mode: PropTypes.string.isRequired,
|
|
|
|
onClickMode: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.handleOnClickMode = this.handleOnClickMode.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnClickMode(mode, e) {
|
|
|
|
if (this.props.onClickMode) {
|
|
|
|
this.props.onClickMode(mode);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
mode,
|
|
|
|
items,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="crate-mode">
|
|
|
|
{items.map(item => (
|
|
|
|
<a
|
|
|
|
key={item.id}
|
|
|
|
className={mode == item.id ? 'active' : ''}
|
|
|
|
onClick={this.handleOnClickMode.bind(this, item.id)}
|
|
|
|
href="#"
|
|
|
|
role="button">{item.name}</a>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that displays the main crate with reminder rules.
|
|
|
|
* It includes <Cart> and rules
|
|
|
|
*/
|
|
|
|
class Crate extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
rules: PropTypes.array,
|
|
|
|
cart: PropTypes.element,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
rules,
|
|
|
|
cart,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="crate">
|
|
|
|
|
|
|
|
<div className="crate-products">
|
|
|
|
|
|
|
|
{cart}
|
|
|
|
|
|
|
|
{rules && rules.length > 0 && (
|
|
|
|
<div className="crate-info">
|
|
|
|
{rules.map((rule, index) => (
|
2019-11-06 10:13:14 +08:00
|
|
|
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
|
2019-10-02 12:16:07 +08:00
|
|
|
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
|
|
|
|
</p>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that renders all things for order.
|
|
|
|
* It acts like-a layout, this component do nothing more.
|
|
|
|
*/
|
|
|
|
class OrderPanel extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
title: PropTypes.string,
|
|
|
|
description: PropTypes.string,
|
|
|
|
crateMode: PropTypes.element,
|
|
|
|
crate: PropTypes.element,
|
|
|
|
summaryPrice: PropTypes.element,
|
|
|
|
form: PropTypes.element,
|
2019-12-26 21:31:37 +08:00
|
|
|
isMobile: PropTypes.bool,
|
|
|
|
onClickToggleMobileSideMenu: PropTypes.func,
|
2020-04-14 16:51:33 +08:00
|
|
|
onClickOpenImport: PropTypes.func,
|
2019-10-02 12:16:07 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
title,
|
|
|
|
description,
|
|
|
|
crateMode,
|
|
|
|
crate,
|
|
|
|
summaryPrice,
|
|
|
|
form,
|
2019-12-26 21:31:37 +08:00
|
|
|
isMobile,
|
2020-04-14 16:51:33 +08:00
|
|
|
onClickToggleMobileSideMenu,
|
|
|
|
onClickOpenImport,
|
2019-10-02 12:16:07 +08:00
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<section className="panel">
|
|
|
|
|
|
|
|
<h2>{title}</h2>
|
|
|
|
|
|
|
|
<div className="control">
|
|
|
|
<p className="description">{description}</p>
|
|
|
|
|
|
|
|
{crateMode}
|
|
|
|
</div>
|
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
className="btn btn-sm btn-outline-primary m-0 mb-2"
|
|
|
|
style={{'cursor': 'pointer'}}
|
2020-04-16 10:33:28 +08:00
|
|
|
value="Import JSON"
|
2020-04-14 16:51:33 +08:00
|
|
|
onClick={onClickOpenImport} />
|
|
|
|
</div>
|
|
|
|
|
2020-01-15 19:06:18 +08:00
|
|
|
{isMobile ? (
|
|
|
|
<div className="mobileBtnDisplaySideMenu">
|
|
|
|
<button onClick={onClickToggleMobileSideMenu}>
|
|
|
|
<img src="/images/shop/icon-add.svg" alt="add" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
{crate}
|
|
|
|
|
|
|
|
<section className="summary">
|
|
|
|
{summaryPrice}
|
|
|
|
|
|
|
|
{form}
|
|
|
|
</section>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Components that renders the form to request quote.
|
|
|
|
*/
|
|
|
|
class OrderForm extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
2020-04-14 13:46:51 +08:00
|
|
|
isProcessing: PropTypes.bool,
|
|
|
|
isProcessingComplete: PropTypes.bool,
|
2019-10-02 12:16:07 +08:00
|
|
|
onClickSubmit: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2020-04-14 13:46:51 +08:00
|
|
|
this.state = {
|
|
|
|
note: '',
|
|
|
|
email: '',
|
|
|
|
error: {
|
|
|
|
note: null,
|
|
|
|
email: null,
|
|
|
|
},
|
|
|
|
empty: {
|
|
|
|
note: null,
|
|
|
|
email: null,
|
|
|
|
},
|
|
|
|
};
|
2019-10-02 12:16:07 +08:00
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
this.handleEmail = this.handleEmail.bind(this);
|
|
|
|
this.handleNote = this.handleNote.bind(this);
|
2019-10-02 12:16:07 +08:00
|
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
2020-04-14 13:46:51 +08:00
|
|
|
this.resetEmptyError = this.resetEmptyError.bind(this);
|
|
|
|
this.checkValidation = this.checkValidation.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
checkValidation() {
|
|
|
|
let isValid = true;
|
|
|
|
let validationFields = {...this.state};
|
|
|
|
|
|
|
|
const {
|
|
|
|
isEmpty: isEmailEmpty,
|
|
|
|
isError: isEmailError
|
|
|
|
} = this.validateEmail(this.state.email);
|
|
|
|
|
|
|
|
validationFields = {
|
|
|
|
...validationFields,
|
|
|
|
error: {
|
|
|
|
...this.state.error,
|
|
|
|
email: isEmailError,
|
|
|
|
},
|
|
|
|
empty: {
|
|
|
|
...this.state.empty,
|
|
|
|
email: isEmailEmpty,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState(validationFields);
|
|
|
|
|
|
|
|
isValid =
|
|
|
|
!isEmailEmpty &&
|
|
|
|
!isEmailError
|
|
|
|
|
|
|
|
return isValid;
|
|
|
|
}
|
|
|
|
|
|
|
|
validateEmail(value) {
|
|
|
|
let isEmpty = null;
|
|
|
|
let isError = null;
|
|
|
|
|
|
|
|
const { t } = this.props;
|
|
|
|
|
|
|
|
if (!value || value.trim() === '') {
|
|
|
|
isEmpty = true;
|
|
|
|
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
|
|
|
|
isError = {
|
|
|
|
message: 'Your email is incomplete',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return { isEmpty, isError };
|
|
|
|
}
|
|
|
|
|
|
|
|
validateNote(value) {
|
|
|
|
let isEmpty = null;
|
|
|
|
|
|
|
|
if (!value || value.trim() === '') {
|
|
|
|
isEmpty = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { isEmpty };
|
|
|
|
}
|
|
|
|
|
|
|
|
resetEmptyError(key) {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
error: {
|
|
|
|
...this.state.error,
|
|
|
|
[key]: null,
|
|
|
|
},
|
|
|
|
empty: {
|
|
|
|
...this.state.empty,
|
|
|
|
[key]: null,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleEmail(e) {
|
|
|
|
const value = e.target.value;
|
|
|
|
const { isEmpty, isError } = this.validateEmail(value);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
email: value,
|
|
|
|
error: {
|
|
|
|
...this.state.error,
|
|
|
|
email: isError,
|
|
|
|
},
|
|
|
|
empty: {
|
|
|
|
...this.state.empty,
|
|
|
|
email: isEmpty,
|
|
|
|
}
|
|
|
|
});
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
handleNote(e) {
|
|
|
|
const value = e.target.value;
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
this.setState({
|
2020-04-14 13:46:51 +08:00
|
|
|
...this.state,
|
|
|
|
note: value,
|
2019-10-02 12:16:07 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleSubmit(event) {
|
2020-04-14 13:46:51 +08:00
|
|
|
event.preventDefault();
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
if (this.props.onClickSubmit) {
|
2020-04-14 13:46:51 +08:00
|
|
|
// check validation input fields
|
|
|
|
const isValidated = this.checkValidation();
|
|
|
|
if (!isValidated) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.props.onClickSubmit(this.state.note, this.state.email);
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2020-04-14 13:46:51 +08:00
|
|
|
const {
|
|
|
|
handleEmail,
|
|
|
|
handleNote,
|
|
|
|
resetEmptyError,
|
|
|
|
handleSubmit,
|
|
|
|
} = this;
|
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
const {
|
|
|
|
onClickShow,
|
|
|
|
} = this.props;
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
const {
|
|
|
|
email,
|
|
|
|
note,
|
|
|
|
error,
|
|
|
|
empty
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
const { isProcessing, isProcessingComplete } = this.props;
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
return (
|
|
|
|
<div className="summary-form">
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
<form onSubmit={handleSubmit} noValidate>
|
|
|
|
|
|
|
|
<input
|
|
|
|
className={`${error && error.email ? 'errorField':''}`}
|
|
|
|
type="email"
|
|
|
|
placeholder="Email"
|
|
|
|
onFocus={() => resetEmptyError('email')}
|
|
|
|
onChange={handleEmail}
|
|
|
|
onBlur={handleEmail}
|
|
|
|
value={email} />
|
|
|
|
|
|
|
|
{ empty && empty.email ? (
|
|
|
|
<div className="error">
|
|
|
|
<small>Required</small>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
{ error && error.email ? (
|
|
|
|
<div className="error">
|
|
|
|
<small>Your email is incomplete</small>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
<textarea
|
2020-04-14 13:46:51 +08:00
|
|
|
onChange={handleNote}
|
|
|
|
value={note}
|
2019-10-02 12:16:07 +08:00
|
|
|
rows="5"
|
|
|
|
placeholder="Additional notes" />
|
2020-04-14 13:46:51 +08:00
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
<div className="d-flex flex-column flex-sm-row justify-content-between">
|
|
|
|
<input
|
2023-07-11 13:50:32 +08:00
|
|
|
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
|
2020-04-14 15:08:28 +08:00
|
|
|
style={{'cursor': 'pointer', 'fontWeight': '700'}}
|
2020-04-16 10:33:28 +08:00
|
|
|
value="Show JSON"
|
2020-04-14 15:08:28 +08:00
|
|
|
onClick={onClickShow} />
|
|
|
|
|
2023-07-11 13:50:32 +08:00
|
|
|
<input className="btn btn-primary w-100 m-0 ms-sm-2" type="submit" value={`${isProcessing ? 'Processing ...' : 'Request quote'}`} />
|
2020-04-14 15:08:28 +08:00
|
|
|
</div>
|
2020-04-14 13:46:51 +08:00
|
|
|
{/*This will open an email window. Send the email to make your request.*/}
|
2019-10-02 12:16:07 +08:00
|
|
|
</form>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Components that displays the list of card that are used in the crate.
|
|
|
|
* It is a summary of purchase
|
|
|
|
*/
|
|
|
|
class OrderSumary extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
2019-11-06 10:20:06 +08:00
|
|
|
currency: PropTypes.string,
|
2019-10-02 12:16:07 +08:00
|
|
|
modes: PropTypes.array,
|
|
|
|
currentMode: PropTypes.string,
|
|
|
|
summary: PropTypes.array,
|
|
|
|
itemsData: PropTypes.array,
|
|
|
|
onDeleteItem: PropTypes.func,
|
|
|
|
onDeleteAllItems: PropTypes.func,
|
|
|
|
onMouseEnterItem: PropTypes.func,
|
|
|
|
onMouseLeaveItem: PropTypes.func,
|
|
|
|
onClickSelectItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.handleOnDeleteItem = this.handleOnDeleteItem.bind(this);
|
|
|
|
this.handleOnDeleteAllItems = this.handleOnDeleteAllItems.bind(this);
|
|
|
|
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
|
|
|
|
this.handleOnClickSelectItem = this.handleOnClickSelectItem.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnDeleteItem(index, e) {
|
|
|
|
if (this.props.onDeleteItem) {
|
|
|
|
this.props.onDeleteItem(index);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnDeleteAllItems(e) {
|
|
|
|
if (this.props.onDeleteAllItems) {
|
|
|
|
this.props.onDeleteAllItems();
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterItem(id, e) {
|
|
|
|
if (this.props.onMouseEnterItem) {
|
|
|
|
this.props.onMouseEnterItem(id);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveItem(e) {
|
|
|
|
if (this.props.onMouseLeaveItem) {
|
|
|
|
this.props.onMouseLeaveItem();
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnClickSelectItem(index, e) {
|
|
|
|
if (e.target.tagName !== 'IMG') {
|
|
|
|
if (this.props.onClickSelectItem) {
|
|
|
|
this.props.onClickSelectItem(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
2019-11-06 10:20:06 +08:00
|
|
|
currency,
|
2019-10-02 12:16:07 +08:00
|
|
|
modes,
|
|
|
|
currentMode,
|
|
|
|
summary,
|
|
|
|
itemsData,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const mode = modes.find(elem => elem.id === currentMode);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="summary-price">
|
|
|
|
|
|
|
|
<table>
|
|
|
|
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<td colSpan="2" className="summary-remove-all">
|
|
|
|
<span className="item-card-name">Remove all cards</span>
|
|
|
|
|
|
|
|
<button onClick={this.handleOnDeleteAllItems}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
|
|
</button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
{mode && (
|
|
|
|
<tr>
|
|
|
|
<td className="item-card-name">{mode.name}</td>
|
|
|
|
<td className="price">
|
2019-11-09 18:05:15 +08:00
|
|
|
<div>
|
2019-11-09 18:41:09 +08:00
|
|
|
{`${currency} ${formatMoney(mode.price)}`}
|
2019-11-09 18:05:15 +08:00
|
|
|
|
|
|
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<span style={{
|
|
|
|
'display': 'inline-block',
|
|
|
|
'width': '30px',
|
|
|
|
}}> </span>
|
2019-10-02 12:16:07 +08:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
)}
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
{summary.map((item, index) => {
|
|
|
|
let alert;
|
|
|
|
let warning;
|
|
|
|
|
|
|
|
if (itemsData[index]) {
|
|
|
|
alert = itemsData[index];
|
|
|
|
const warningsKeys = Object.keys(alert.warnings);
|
|
|
|
if (warningsKeys && warningsKeys.length > 0) {
|
|
|
|
warning = alert.warnings[warningsKeys[0]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<tr key={item.id}
|
|
|
|
className={`hoverable ${item.selected ? 'selected' : ''}`}
|
|
|
|
onClick={this.handleOnClickSelectItem.bind(this, index)}
|
|
|
|
onMouseEnter={this.handleOnMouseEnterItem.bind(this, item.id)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveItem}>
|
|
|
|
|
|
|
|
<td className="item-card-name">
|
2020-03-02 21:27:31 +08:00
|
|
|
<div>{`${item.name_number} ${item.name} ${item.name_codename}`}</div>
|
2019-10-02 12:16:07 +08:00
|
|
|
</td>
|
|
|
|
|
|
|
|
<td className="price">
|
|
|
|
<div>
|
2019-11-09 18:41:09 +08:00
|
|
|
{`${currency} ${formatMoney(item.price)}`}
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
<button onClick={this.handleOnDeleteItem.bind(this, index)}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{warning && (
|
|
|
|
<img
|
|
|
|
style={{'marginLeft': '10px'}}
|
|
|
|
className="alert-warning"
|
|
|
|
src={`/images/${warning.icon}`}
|
|
|
|
/>
|
|
|
|
)}
|
2019-11-09 18:05:15 +08:00
|
|
|
|
|
|
|
{!warning && (
|
|
|
|
<span style={{
|
|
|
|
'display': 'inline-block',
|
|
|
|
'width': '30px',
|
|
|
|
}}> </span>
|
|
|
|
)}
|
2019-10-02 12:16:07 +08:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tbody>
|
|
|
|
|
|
|
|
<tfoot>
|
|
|
|
<tr>
|
|
|
|
<td className="item-card-name">Price estimate</td>
|
|
|
|
<td className="price">
|
2019-11-09 18:41:09 +08:00
|
|
|
<div>
|
|
|
|
{summary.length ? (
|
|
|
|
`${currency} ${formatMoney(summary.reduce(
|
|
|
|
(prev, next) => {
|
|
|
|
return prev + next.price;
|
|
|
|
}, 0
|
|
|
|
) + mode.price)}`
|
|
|
|
) : (
|
|
|
|
`${currency} ${formatMoney(mode.price)}`
|
|
|
|
)}
|
|
|
|
|
|
|
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
|
|
|
|
</button>
|
|
|
|
</div>
|
2019-10-02 12:16:07 +08:00
|
|
|
|
2019-11-09 18:41:09 +08:00
|
|
|
<span style={{
|
|
|
|
'display': 'inline-block',
|
|
|
|
'width': '30px',
|
|
|
|
}}> </span>
|
2019-10-02 12:16:07 +08:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</tfoot>
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that renders the backlog in the aside
|
|
|
|
*/
|
|
|
|
class Backlog extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
2019-11-06 10:20:06 +08:00
|
|
|
currency: PropTypes.string,
|
2019-10-02 12:16:07 +08:00
|
|
|
data: PropTypes.object.isRequired,
|
|
|
|
items: PropTypes.object,
|
2019-12-26 21:31:37 +08:00
|
|
|
isMobile: PropTypes.bool,
|
2019-10-02 12:16:07 +08:00
|
|
|
onClickAddItem: PropTypes.func,
|
2019-12-26 21:31:37 +08:00
|
|
|
onClickToggleMobileSideMenu: PropTypes.func,
|
2019-10-02 12:16:07 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static get defaultProps() {
|
|
|
|
return {
|
|
|
|
items: {},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
2019-11-06 10:20:06 +08:00
|
|
|
currency,
|
2019-10-02 12:16:07 +08:00
|
|
|
data,
|
|
|
|
items,
|
|
|
|
onClickAddItem,
|
2019-12-26 21:31:37 +08:00
|
|
|
onClickToggleMobileSideMenu,
|
|
|
|
isMobile,
|
2019-10-02 12:16:07 +08:00
|
|
|
} = this.props;
|
|
|
|
|
2023-06-30 10:10:15 +08:00
|
|
|
|
|
|
|
const ordered_groups = data.categories.map(groupItem => ({ name: groupItem.name,
|
|
|
|
items: groupItem.itemIds.map(itemId => items[itemId])
|
|
|
|
}));
|
|
|
|
const groups = ordered_groups.map((group, g_index) => {
|
|
|
|
return (
|
2023-07-10 17:46:45 +08:00
|
|
|
<div className="accordion-item">
|
|
|
|
<h2 className="accordion-header">
|
|
|
|
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
|
|
|
data-bs-target={`#collapse${g_index}`} aria-expanded="true" aria-controls={`collapse${g_index}`}>
|
2023-06-30 10:10:15 +08:00
|
|
|
{group.name}
|
|
|
|
</button>
|
|
|
|
</h2>
|
2023-07-10 17:46:45 +08:00
|
|
|
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
|
|
|
|
data-bs-parent="#accordion_categories">
|
|
|
|
<div className="accordion-body">
|
2023-06-30 10:10:15 +08:00
|
|
|
{group.items.map(item => (
|
|
|
|
<ProductItem
|
|
|
|
key={item.id}
|
|
|
|
id={item.id}
|
|
|
|
name={`${item.name_number} ${item.name}`}
|
|
|
|
name_codename={item.name_codename}
|
|
|
|
price={item.price}
|
|
|
|
currency={currency}
|
|
|
|
image={`/images/${item.image}`}
|
|
|
|
specs={item.specs}
|
|
|
|
datasheet_file={item.datasheet_file}
|
|
|
|
datasheet_name={item.datasheet_name}
|
|
|
|
onClickAddItem={onClickAddItem}
|
|
|
|
></ProductItem>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Droppable
|
|
|
|
droppableId={data.id}
|
|
|
|
isDropDisabled={true}>
|
|
|
|
|
|
|
|
{(provided) => (
|
|
|
|
<div
|
|
|
|
className="backlog-container"
|
|
|
|
ref={provided.innerRef}
|
|
|
|
{...provided.droppableProps}>
|
|
|
|
|
2019-12-26 21:31:37 +08:00
|
|
|
{isMobile ? (
|
|
|
|
<div className="mobileCloseMenu">
|
|
|
|
<button onClick={onClickToggleMobileSideMenu}>
|
|
|
|
<img src="/images/shop/icon-close-white.svg" alt="add" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
|
2023-07-10 17:46:45 +08:00
|
|
|
<div className="accordion accordion-flush" id="accordion_categories">
|
2023-06-30 10:10:15 +08:00
|
|
|
{groups}
|
|
|
|
</div>
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
{provided.placeholder && (
|
|
|
|
<div style={{ display: 'none' }}>
|
|
|
|
{provided.placeholder}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
</Droppable>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that render the entire shop
|
|
|
|
*/
|
|
|
|
class Shop extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
data: PropTypes.object.isRequired,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.state = this.props.data;
|
|
|
|
this.handleCrateModeChange = this.handleCrateModeChange.bind(this);
|
|
|
|
this.handleOnDragEnd = this.handleOnDragEnd.bind(this);
|
|
|
|
this.handleDeleteItem = this.handleDeleteItem.bind(this);
|
|
|
|
this.handleDeleteAllItems = this.handleDeleteAllItems.bind(this);
|
|
|
|
this.handleMouseEnterItem = this.handleMouseEnterItem.bind(this);
|
|
|
|
this.handleMouseLeaveItem = this.handleMouseLeaveItem.bind(this);
|
|
|
|
this.handleClickAddItem = this.handleClickAddItem.bind(this);
|
|
|
|
this.checkAlerts = this.checkAlerts.bind(this);
|
|
|
|
this.handleToggleItemProgress = this.handleToggleItemProgress.bind(this);
|
|
|
|
this.handleToggleItemWarning = this.handleToggleItemWarning.bind(this);
|
|
|
|
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
|
|
|
|
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
|
|
|
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
2019-12-26 21:31:37 +08:00
|
|
|
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
2020-04-14 13:46:51 +08:00
|
|
|
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
|
2020-04-14 15:08:28 +08:00
|
|
|
this.handleClickShowOrder = this.handleClickShowOrder.bind(this);
|
2020-04-14 16:51:33 +08:00
|
|
|
this.handleClickOpenImport = this.handleClickOpenImport.bind(this);
|
|
|
|
this.handleLoadCustomConf = this.handleLoadCustomConf.bind(this);
|
2020-01-22 22:47:10 +08:00
|
|
|
|
|
|
|
this.timer = null;
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
// index 0 is a Kasli, we place it as a default conf on the crate.
|
2023-06-30 10:10:15 +08:00
|
|
|
const sourceIds = Array.from(this.state.columns.backlog.categories.map(groupId => groupId.itemIds).flat())
|
2019-10-02 12:16:07 +08:00
|
|
|
const source = {
|
|
|
|
droppableId: 'backlog',
|
2023-06-30 10:10:15 +08:00
|
|
|
index: null,
|
2019-10-02 12:16:07 +08:00
|
|
|
};
|
|
|
|
const destination = {
|
|
|
|
droppableId: 'cart',
|
|
|
|
index: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.handleOnDragEnd({
|
|
|
|
source,
|
|
|
|
destination,
|
2023-06-30 10:10:15 +08:00
|
|
|
draggableId: sourceIds[0],
|
2019-10-02 12:16:07 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
|
|
/**
|
|
|
|
* We check alerts (reminder + warning) only when items inside crate or
|
|
|
|
* crate mode change.
|
|
|
|
*
|
|
|
|
* In the function checkAlerts, we DO NOT want to change items as we will
|
|
|
|
* trigger again this function (componentDidUpdate) and thus,
|
|
|
|
* making an infinite loop.
|
|
|
|
*/
|
|
|
|
if (
|
|
|
|
(prevState.columns.cart.items !== this.state.columns.cart.items) ||
|
|
|
|
(prevState.currentMode !== this.state.currentMode)
|
|
|
|
) {
|
|
|
|
this.checkAlerts(
|
|
|
|
prevState.columns.cart.items,
|
|
|
|
this.state.columns.cart.items);
|
|
|
|
}
|
2020-01-22 22:47:10 +08:00
|
|
|
|
|
|
|
if (this.state.newCardJustAdded) {
|
|
|
|
this.timer = setTimeout(() => {
|
|
|
|
this.setState({
|
|
|
|
newCardJustAdded: false,
|
|
|
|
});
|
|
|
|
}, 2000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
clearTimeout(this.timer);
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
handleCrateModeChange(mode) {
|
|
|
|
this.setState({
|
|
|
|
currentMode: mode,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleDeleteItem(index) {
|
|
|
|
const cloned = Array.from(this.state.columns.cart.items);
|
|
|
|
cloned.splice(index, 1);
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: cloned,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleDeleteAllItems() {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: [],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleMouseEnterItem(id) {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
currentItemHovered: id,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleMouseLeaveItem() {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
currentItemHovered: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-06-30 10:10:15 +08:00
|
|
|
handleClickAddItem(id, tap) {
|
2019-10-02 12:16:07 +08:00
|
|
|
const source = {
|
|
|
|
droppableId: 'backlog',
|
2023-06-30 10:10:15 +08:00
|
|
|
index: null,
|
2019-10-02 12:16:07 +08:00
|
|
|
};
|
|
|
|
const destination = {
|
|
|
|
droppableId: 'cart',
|
|
|
|
index: this.state.columns.cart.items.length,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.handleOnDragEnd({
|
|
|
|
source,
|
|
|
|
destination,
|
2023-06-30 10:10:15 +08:00
|
|
|
draggableId: id
|
2020-01-22 22:47:10 +08:00
|
|
|
}, tap);
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
handleToggleItemProgress(index, show) {
|
|
|
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: itemsCloned.map((item, i) => {
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
showProgress: i === index ? show : false,
|
|
|
|
showOverlayRemove: false,
|
|
|
|
showWarning: false,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleToggleItemWarning(index, show) {
|
|
|
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: itemsCloned.map((item, i) => {
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
showWarning: i === index ? show : false,
|
|
|
|
showProgress: false,
|
|
|
|
showOverlayRemove: false,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleClickSelectItem(index) {
|
|
|
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: itemsCloned.map((item, id) => {
|
|
|
|
return {...item, selected: id === index ? true : false};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleToggleOverlayRemove(index, show) {
|
|
|
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: itemsCloned.map((item, id) => {
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
showOverlayRemove: id === index ? show : false,
|
|
|
|
showProgress: false,
|
|
|
|
showWarning: false,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-14 15:08:28 +08:00
|
|
|
handleClickShowOrder() {
|
|
|
|
const crate = {
|
|
|
|
items: [],
|
|
|
|
type: this.state.currentMode,
|
|
|
|
};
|
|
|
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
|
|
|
for (const i in clonedCart) {
|
|
|
|
const item = clonedCart[i];
|
|
|
|
crate.items.push({
|
|
|
|
'pn': item.name_number,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
isProcessing: false,
|
|
|
|
shouldShowRFQFeedback: true,
|
|
|
|
RFQBodyType: 'show',
|
|
|
|
RFQBodyOrder: JSON.stringify(crate),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-14 16:51:33 +08:00
|
|
|
handleClickOpenImport() {
|
|
|
|
this.setState({
|
|
|
|
isProcessing: false,
|
|
|
|
shouldShowRFQFeedback: true,
|
|
|
|
RFQBodyType: 'import',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleLoadCustomConf(customconf) {
|
2020-04-14 17:54:31 +08:00
|
|
|
if (!customconf) {return; }
|
|
|
|
|
|
|
|
const items = this.props.data.items;
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
let new_items = [];
|
|
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: [],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}, function () {
|
|
|
|
|
|
|
|
customconf.items.map(function (item) {
|
|
|
|
Object.keys(items).map(key => {
|
|
|
|
if (item.pn && item.pn === items[key].name_number) {
|
|
|
|
new_items.push(Object.assign({
|
|
|
|
...items[key],
|
|
|
|
}, {
|
|
|
|
id: uuidv4(),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
items: new_items,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
currentMode: customconf.type,
|
|
|
|
});
|
|
|
|
});
|
2020-04-14 16:51:33 +08:00
|
|
|
}
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
handleClickSubmit(note, email) {
|
2019-10-02 12:16:07 +08:00
|
|
|
const crate = {
|
|
|
|
items: [],
|
|
|
|
type: this.state.currentMode,
|
|
|
|
};
|
|
|
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
|
|
|
for (const i in clonedCart) {
|
|
|
|
const item = clonedCart[i];
|
|
|
|
crate.items.push({
|
2020-03-23 12:35:31 +08:00
|
|
|
'pn': item.name_number,
|
2019-10-02 12:16:07 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
const {data} = this.props;
|
|
|
|
|
|
|
|
this.setState({isProcessing: true});
|
|
|
|
|
|
|
|
axios.post(data.API_RFQ, {
|
|
|
|
email,
|
2020-04-15 21:17:48 +08:00
|
|
|
note,
|
2020-04-16 10:31:31 +08:00
|
|
|
configuration: JSON.stringify(crate)
|
2020-04-14 13:46:51 +08:00
|
|
|
}).then(response => {
|
2020-04-14 14:56:17 +08:00
|
|
|
this.setState({
|
|
|
|
isProcessing: false,
|
|
|
|
shouldShowRFQFeedback: true,
|
|
|
|
RFQBodyType: 'email',
|
|
|
|
isProcessingComplete: true,
|
|
|
|
});
|
2020-04-14 13:46:51 +08:00
|
|
|
}).catch(err => {
|
|
|
|
this.setState({isProcessing: false}, () => {
|
|
|
|
alert("We cannot receive your request. Try using the export by coping the configuration and send it to us at sales[at]m-labs.hk");
|
|
|
|
});
|
|
|
|
})
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
|
2020-01-22 22:47:10 +08:00
|
|
|
handleOnDragEnd(result, newAdded) {
|
2019-10-02 12:16:07 +08:00
|
|
|
const {
|
|
|
|
source,
|
|
|
|
destination,
|
|
|
|
draggableId,
|
|
|
|
} = result;
|
|
|
|
|
|
|
|
if (!destination) {
|
|
|
|
if (source.droppableId === 'cart') {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
2020-01-22 22:47:10 +08:00
|
|
|
newCardJustAdded: false,
|
2019-10-02 12:16:07 +08:00
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
[source.droppableId]: {
|
|
|
|
...this.state.columns[source.droppableId],
|
|
|
|
items: remove(
|
|
|
|
this.state.columns[source.droppableId].items,
|
|
|
|
source.index,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(source.droppableId) {
|
|
|
|
|
|
|
|
case 'backlog':
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
2020-01-22 22:47:10 +08:00
|
|
|
newCardJustAdded: newAdded ? true : false,
|
2019-10-02 12:16:07 +08:00
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
[destination.droppableId]: {
|
|
|
|
...this.state.columns[destination.droppableId],
|
|
|
|
items: copy(
|
|
|
|
this.state.items,
|
|
|
|
this.state.columns[source.droppableId],
|
|
|
|
this.state.columns[destination.droppableId],
|
2023-06-30 10:10:15 +08:00
|
|
|
draggableId,
|
2019-10-02 12:16:07 +08:00
|
|
|
destination,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
|
|
|
|
case destination.droppableId:
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
2020-01-22 22:47:10 +08:00
|
|
|
newCardJustAdded: false,
|
2019-10-02 12:16:07 +08:00
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
[destination.droppableId]: {
|
|
|
|
...this.state.columns[destination.droppableId],
|
|
|
|
items: reorder(
|
|
|
|
this.state.columns[destination.droppableId].items,
|
|
|
|
source.index,
|
|
|
|
destination.index,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-26 21:31:37 +08:00
|
|
|
handleClickToggleMobileSideMenu() {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-14 13:46:51 +08:00
|
|
|
handleClickCloseRFQFeedback() {
|
|
|
|
this.setState({
|
|
|
|
shouldShowRFQFeedback: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
checkAlerts(prevItems, newItems) {
|
|
|
|
console.log('--- START CHECKING CRATE WARNING ---');
|
|
|
|
|
|
|
|
const {
|
|
|
|
currentMode,
|
|
|
|
crateModeSlots,
|
|
|
|
crateRules,
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
const itemsCloned = Array.from(newItems);
|
|
|
|
const itemsData = {};
|
|
|
|
const rules = {};
|
|
|
|
|
|
|
|
|
|
|
|
// check number of slot in crate
|
|
|
|
const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
|
|
|
|
if (nbrOccupied > crateModeSlots[currentMode]) {
|
|
|
|
rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
|
2020-01-27 23:09:29 +08:00
|
|
|
} else if (crateModeSlots[currentMode] === 21 && nbrOccupied <= 10) {
|
|
|
|
rules[crateRules.compactSlot.type] = {...crateRules.compactSlot};
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// check the number of EEM connectors available for all Kasli
|
|
|
|
const idxK = itemsCloned.reduce((prev, next, i) => {
|
2021-03-23 00:40:59 +08:00
|
|
|
if (next.type === 'kasli' || next.type === 'vhdcicarrier') {
|
2019-10-02 12:16:07 +08:00
|
|
|
prev.push(i);
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, []);
|
|
|
|
for (let i = 0; i <= idxK.length - 1; i++) {
|
|
|
|
let slots;
|
|
|
|
let nbUsedSlot = 0;
|
|
|
|
let nbrCurrentClock = 0;
|
|
|
|
let idx = idxK[i];
|
|
|
|
|
|
|
|
if (i !== idxK.length - 1) {
|
|
|
|
slots = itemsCloned.slice(idx + 1, idxK[i + 1]);
|
|
|
|
} else {
|
|
|
|
slots = itemsCloned.slice(idx + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i == 0) {
|
|
|
|
const slots_need_resource = itemsCloned.slice(0, idx);
|
|
|
|
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
|
|
|
|
|
|
|
|
if (idx_need != -1) {
|
|
|
|
if (idx_need in itemsData) {
|
|
|
|
if ('warnings' in itemsData[idx_need]) {
|
|
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
|
|
} else {
|
|
|
|
itemsData[idx_need].warnings = {};
|
|
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
itemsData[idx_need] = {...itemsCloned[idx_need]};
|
|
|
|
itemsData[idx_need].warnings = {};
|
|
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nbUsedSlot = slots
|
|
|
|
.filter(item => item.type !== 'idc-bnc')
|
|
|
|
.reduce((prev, next) => {
|
|
|
|
return prev + next.slotOccupied;
|
|
|
|
}, 0);
|
|
|
|
nbrCurrentClock = slots
|
|
|
|
.reduce((prev, next) => {
|
|
|
|
return next.type === 'clocker' ? prev + next.clockOccupied : prev;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
if (idx in itemsData) {
|
|
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
|
|
if (!('warnings' in itemsData[idx])) {
|
|
|
|
itemsData[idx].warnings = {};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
|
|
itemsData[idx].warnings = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
|
2020-01-23 02:06:56 +08:00
|
|
|
if (itemsCloned[idx].rules.maxSlot.message) {
|
|
|
|
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
|
|
|
|
}
|
2019-10-02 12:16:07 +08:00
|
|
|
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
|
|
|
|
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
|
|
|
|
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (itemsCloned.length > (idx + 1)) {
|
|
|
|
const ddkali = itemsCloned[idx + 1];
|
2021-03-23 00:40:59 +08:00
|
|
|
if (ddkali.type === 'kasli' || ddkali.type === 'vhdcicarrier') {
|
2019-10-02 12:16:07 +08:00
|
|
|
rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-27 23:38:44 +08:00
|
|
|
if (idxK.length === 0) {
|
|
|
|
const slots_need_resource = itemsCloned.slice(0);
|
|
|
|
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
|
|
|
|
|
|
|
|
if (idx_need != -1) {
|
|
|
|
if (idx_need in itemsData) {
|
|
|
|
if ('warnings' in itemsData[idx_need]) {
|
|
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
|
|
} else {
|
|
|
|
itemsData[idx_need].warnings = {};
|
|
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
itemsData[idx_need] = {...itemsCloned[idx_need]};
|
|
|
|
itemsData[idx_need].warnings = {};
|
|
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
|
|
|
|
// check number of clock connector available
|
|
|
|
const idxC = itemsCloned.reduce((prev, next, i) => {
|
2021-03-22 18:30:56 +08:00
|
|
|
if (next.type === 'kasli' || next.type === 'clocker') {
|
2019-10-02 12:16:07 +08:00
|
|
|
prev.push(i);
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, []);
|
|
|
|
for (let i = 0; i <= idxC.length - 1; i++) {
|
|
|
|
let slots;
|
|
|
|
let nbrCurrentClock = 0;
|
|
|
|
let idx = idxC[i];
|
|
|
|
|
|
|
|
if (i !== idxC.length - 1) {
|
|
|
|
slots = itemsCloned.slice(idx + 1, idxC[i + 1]);
|
|
|
|
} else {
|
|
|
|
slots = itemsCloned.slice(idx + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
nbrCurrentClock = slots.reduce((prev, next) => {
|
|
|
|
return prev + next.clockOccupied;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
if (idx in itemsData) {
|
|
|
|
if (itemsData[idx].nbrCurrentClock) {
|
|
|
|
itemsData[idx].nbrCurrentClock += nbrCurrentClock;
|
|
|
|
} else {
|
|
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
|
|
itemsData[idx].warnings = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
|
|
|
|
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
|
|
|
|
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-10 12:27:06 +08:00
|
|
|
// check for number of recommended EEM connectors
|
2019-10-02 12:16:07 +08:00
|
|
|
['novo', 'urukul', 'koster'].map(_type => {
|
|
|
|
if (itemsCloned.find(elem => elem.type === _type)) {
|
|
|
|
rules[this.state.items[_type].rules.connectors.type] = {...this.state.items[_type].rules.connectors};
|
|
|
|
}
|
|
|
|
return _type;
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2020-01-31 20:51:25 +08:00
|
|
|
if (itemsCloned.find(elem => elem.type === 'urukul')) {
|
|
|
|
if (this.state.items['urukul'].rules.info) {
|
|
|
|
rules[this.state.items['urukul'].rules.info.type] = {...this.state.items['urukul'].rules.info};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
// check if IDC-BNC is correctly positionned (after Zotino or HD68)
|
|
|
|
const idxIDCBNC = itemsCloned.reduce((prev, next, i) => {
|
|
|
|
if (next.type === 'idc-bnc') {
|
|
|
|
prev.push(i);
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, []);
|
|
|
|
for (var i = idxIDCBNC.length - 1; i >= 0; i--) {
|
|
|
|
const ce = idxIDCBNC[i];
|
|
|
|
let shouldWarning = false;
|
|
|
|
|
|
|
|
if (ce == 0) {
|
|
|
|
shouldWarning = true;
|
|
|
|
} else if (ce >= 1) {
|
|
|
|
const pe = idxIDCBNC[i] - 1;
|
|
|
|
if (itemsCloned[pe].type !== 'zotino' &&
|
|
|
|
itemsCloned[pe].type !== 'hd68' &&
|
|
|
|
itemsCloned[pe].type !== 'idc-bnc') {
|
|
|
|
shouldWarning = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shouldWarning) {
|
|
|
|
itemsData[ce] = {...itemsCloned[ce]};
|
|
|
|
itemsData[ce].warnings = {};
|
|
|
|
itemsData[ce].warnings.wrong = {...itemsCloned[ce].rules.wrong};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// check number of IDC-BNC adapters for a Zotino and HD68-IDC
|
|
|
|
const idxZH = itemsCloned.reduce((prev, next, i) => {
|
|
|
|
if (next.type === 'zotino' || next.type === 'hd68') {
|
|
|
|
prev.push(i);
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, []);
|
|
|
|
for (let i = 0; i <= idxZH.length - 1; i++) {
|
|
|
|
let slots;
|
|
|
|
let nbUsedSlot = 0;
|
|
|
|
let idx = idxZH[i];
|
|
|
|
|
|
|
|
if (i !== idxZH.length - 1) {
|
|
|
|
slots = itemsCloned.slice(idx + 1, idxZH[i + 1]);
|
|
|
|
} else {
|
|
|
|
slots = itemsCloned.slice(idx + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
let stopCount = false;
|
|
|
|
nbUsedSlot = slots.reduce((prev, next, ci, ca) => {
|
|
|
|
if (ci === 0 && next.type === 'idc-bnc') {
|
|
|
|
return prev + 1;
|
|
|
|
} else if (ca[0].type === 'idc-bnc' && ci > 0 && ca[ci - 1].type === 'idc-bnc') {
|
|
|
|
if (next.type !== 'idc-bnc') { stopCount = true; }
|
|
|
|
return prev + (next.type === 'idc-bnc' && !stopCount ? 1 : 0);
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
if (idx in itemsData) {
|
|
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
|
|
if (!('warnings' in itemsData[idx])) {
|
|
|
|
itemsData[idx].warnings = {};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
|
|
itemsData[idx].warnings = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nbUsedSlot > 0) {
|
2020-01-23 02:06:56 +08:00
|
|
|
if (itemsCloned[idx].rules.maxSlot.message) {
|
|
|
|
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
|
|
|
|
}
|
2019-10-02 12:16:07 +08:00
|
|
|
}
|
|
|
|
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
|
|
|
|
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if HD68-IDC has at least 1 IDC-BNC adapter
|
|
|
|
if (itemsCloned[idx].type === 'hd68') {
|
|
|
|
let shouldWarning = false;
|
|
|
|
|
|
|
|
if (idx < itemsCloned.length - 1) {
|
|
|
|
if (itemsCloned[idx + 1].type !== 'idc-bnc') {
|
|
|
|
shouldWarning = true;
|
|
|
|
}
|
|
|
|
} else if (idx === itemsCloned.length - 1) {
|
|
|
|
shouldWarning = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shouldWarning) {
|
|
|
|
if (idx in itemsData) {
|
|
|
|
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
|
|
|
|
} else {
|
|
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
|
|
itemsData[idx].warnings = {};
|
|
|
|
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update state with rules
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
itemsData: {
|
|
|
|
...itemsData,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
rules: {
|
|
|
|
...rules,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
|
|
|
const {
|
2019-11-06 10:20:06 +08:00
|
|
|
currency,
|
2019-10-02 12:16:07 +08:00
|
|
|
currentItemHovered,
|
|
|
|
currentMode,
|
|
|
|
crateModeSlots,
|
|
|
|
crateModeItems,
|
|
|
|
items,
|
|
|
|
columns,
|
|
|
|
rules,
|
2019-12-26 21:31:37 +08:00
|
|
|
mobileSideMenuShouldOpen,
|
2020-01-22 22:47:10 +08:00
|
|
|
newCardJustAdded,
|
2020-04-14 13:46:51 +08:00
|
|
|
isProcessing,
|
|
|
|
shouldShowRFQFeedback,
|
2020-04-14 14:56:17 +08:00
|
|
|
RFQBodyType,
|
2020-04-14 15:08:28 +08:00
|
|
|
RFQBodyOrder,
|
2020-04-14 13:46:51 +08:00
|
|
|
isProcessingComplete,
|
2019-10-02 12:16:07 +08:00
|
|
|
} = this.state;
|
|
|
|
|
2020-01-21 00:43:33 +08:00
|
|
|
const isMobile = window.deviceIsMobile();
|
|
|
|
|
2019-10-02 12:16:07 +08:00
|
|
|
return (
|
|
|
|
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
|
|
|
|
|
|
|
<Layout
|
2020-04-14 13:46:51 +08:00
|
|
|
showRFQFeedback={shouldShowRFQFeedback}
|
2020-04-14 14:56:17 +08:00
|
|
|
RFQBodyType={RFQBodyType}
|
2020-04-14 15:08:28 +08:00
|
|
|
RFQBodyOrder={RFQBodyOrder}
|
2019-10-02 12:16:07 +08:00
|
|
|
className="shop"
|
2019-12-26 21:31:37 +08:00
|
|
|
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
2020-01-22 22:47:10 +08:00
|
|
|
isMobile={isMobile}
|
|
|
|
newCardJustAdded={newCardJustAdded}
|
2019-12-26 21:31:37 +08:00
|
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
2020-04-14 13:46:51 +08:00
|
|
|
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
|
2020-04-14 16:51:33 +08:00
|
|
|
onClickLoadCustomConf={this.handleLoadCustomConf}
|
2020-04-20 14:51:35 +08:00
|
|
|
items={items}
|
2019-10-02 12:16:07 +08:00
|
|
|
aside={
|
|
|
|
<Backlog
|
2019-11-06 10:20:06 +08:00
|
|
|
currency={currency}
|
2019-10-02 12:16:07 +08:00
|
|
|
items={items}
|
|
|
|
data={columns['backlog']}
|
2019-12-26 21:31:37 +08:00
|
|
|
onClickAddItem={this.handleClickAddItem}
|
|
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile={isMobile}>
|
2019-10-02 12:16:07 +08:00
|
|
|
</Backlog>
|
|
|
|
}
|
|
|
|
main={(
|
|
|
|
<OrderPanel
|
2019-12-26 21:31:37 +08:00
|
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
2020-04-14 16:51:33 +08:00
|
|
|
onClickOpenImport={this.handleClickOpenImport}
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile={isMobile}
|
2019-10-02 12:16:07 +08:00
|
|
|
title="Order hardware"
|
|
|
|
description="
|
2019-11-06 14:48:26 +08:00
|
|
|
Drag and drop the cards you want into the crate below to see how the combination would look like. If you have any issues with this ordering system, or if you need other configurations, email us directly anytime at sales@m-****.hk. The price is estimated and must be confirmed by a quote."
|
2019-10-02 12:16:07 +08:00
|
|
|
crateMode={
|
|
|
|
<CrateMode
|
|
|
|
items={crateModeItems}
|
|
|
|
mode={currentMode}
|
|
|
|
onClickMode={this.handleCrateModeChange}>
|
|
|
|
</CrateMode>}
|
|
|
|
crate={
|
|
|
|
<Crate
|
|
|
|
cart={
|
|
|
|
<Cart
|
|
|
|
nbrSlots={crateModeSlots[currentMode]}
|
|
|
|
data={columns['cart']}
|
2020-01-21 00:43:33 +08:00
|
|
|
isMobile={isMobile}
|
2019-10-02 12:16:07 +08:00
|
|
|
itemHovered={currentItemHovered}
|
|
|
|
onToggleProgress={this.handleToggleItemProgress}
|
|
|
|
onToggleWarning={this.handleToggleItemWarning}
|
|
|
|
onToggleOverlayRemove={this.handleToggleOverlayRemove}
|
|
|
|
onClickRemoveItem={this.handleDeleteItem}>
|
|
|
|
</Cart>
|
|
|
|
}
|
|
|
|
rules={Object.values(rules).filter(rule => rule)}>
|
|
|
|
</Crate>
|
|
|
|
}
|
|
|
|
summaryPrice={
|
|
|
|
<OrderSumary
|
2019-11-06 10:20:06 +08:00
|
|
|
currency={currency}
|
2019-10-02 12:16:07 +08:00
|
|
|
currentMode={currentMode}
|
|
|
|
modes={crateModeItems}
|
|
|
|
summary={columns['cart'].items}
|
|
|
|
itemsData={columns.cart.itemsData}
|
|
|
|
onMouseEnterItem={this.handleMouseEnterItem}
|
|
|
|
onMouseLeaveItem={this.handleMouseLeaveItem}
|
|
|
|
onDeleteItem={this.handleDeleteItem}
|
|
|
|
onDeleteAllItems={this.handleDeleteAllItems}
|
|
|
|
onClickSelectItem={this.handleClickSelectItem}>
|
|
|
|
</OrderSumary>
|
|
|
|
}
|
|
|
|
form={
|
|
|
|
<OrderForm
|
2020-04-14 13:46:51 +08:00
|
|
|
isProcessingComplete={isProcessingComplete}
|
|
|
|
processingComplete={this.handleProcessingComplete}
|
|
|
|
isProcessing={isProcessing}
|
2020-04-14 15:08:28 +08:00
|
|
|
onClickSubmit={this.handleClickSubmit}
|
|
|
|
onClickShow={this.handleClickShowOrder}>
|
2019-10-02 12:16:07 +08:00
|
|
|
</OrderForm>
|
|
|
|
}>
|
|
|
|
</OrderPanel>
|
|
|
|
)}>
|
|
|
|
</Layout>
|
|
|
|
|
|
|
|
</DragDropContext>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ReactDOM.render(
|
|
|
|
<Shop data={data} />,
|
|
|
|
document.querySelector('#root-shop'),
|
|
|
|
);
|