web2019/static/js/shop/Shop.jsx

589 lines
19 KiB
JavaScript

import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import {FilterOptions} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {DragDropContext} from "@hello-pangea/dnd";
import {copyFromBacklog, itemsUnfoldedList, nbrOccupiedSlotsInCrate, remove, reorder} from "./utils";
import {Layout} from "./Layout.jsx";
import {Backlog} from "./Backlog.jsx";
import {OrderPanel} from "./OrderPanel.jsx";
import {OrderSummary} from "./OrderSummary.jsx";
import {OrderForm} from "./OrderForm.jsx";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {FillResources} from "./count_resources";
import {CrateList} from "./CrateList.jsx";
/**
* Component that render the entire shop
*/
export function Shop() {
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.checkAlertsInNewState = this.checkAlertsInNewState.bind(this);
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
this.handleClickSubmit = this.handleClickSubmit.bind(this);
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
this.handleShowOverlayRemove = this.handleShowOverlayRemove.bind(this);
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
this.handleClickShowOrder = this.handleClickShowOrder.bind(this);
this.handleClickOpenImport = this.handleClickOpenImport.bind(this);
this.handleLoadCustomConf = this.handleLoadCustomConf.bind(this);
this.onAddCrate = this.onAddCrate.bind(this);
this.onDeleteCrate = this.onDeleteCrate.bind(this);
this.onCrateModeChange = this.onCrateModeChange.bind(this);
this.onCrateSelected = this.onCrateSelected.bind(this);
this.onCrateChanged = this.onCrateChanged.bind(this);
this.timer = null;
this.timer_remove = null;
}
componentDidMount() {
const source = {
droppableId: 'backlog',
indexes: [
itemsUnfoldedList.findIndex(element => element === "eem_pwr_mod"),
itemsUnfoldedList.findIndex(element => element === "kasli")
],
};
const destination = {
droppableId: 'crate0',
index: 0,
};
this.handleOnDragEnd({
source,
destination
});
}
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.
*/
console.log("componentDidUpdate")
return;
if (
(prevState.columns.crates !== this.state.columns.crates.items) ||
(prevState.currentMode !== this.state.currentMode)
) {
this.checkAlerts(this.state.columns.cart.items);
}
if (this.state.newCardJustAdded) {
this.timer = setTimeout(() => {
this.setState({
newCardJustAdded: false,
});
}, 2000);
}
}
componentWillUnmount() {
clearTimeout(this.timer);
}
onCrateChanged(crate_id) {
// kinda silly that hover over cards triggers checkAlerts
this.checkAlertsInNewState(crate_id, this.state);
}
handleCrateModeChange(mode) {
this.setState({
currentMode: mode,
});
}
handleDeleteItem(index) {
let cloned = Array.from(this.state.columns.cart.items);
let cloned_data = Array.from(this.state.columns.cart.itemsData);
cloned.splice(index, 1);
cloned_data.splice(index, 1);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: cloned,
itemsData: cloned_data,
},
},
});
}
handleDeleteAllItems() {
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: [],
itemsData: []
},
},
});
}
handleMouseEnterItem(id) {
this.setState({
...this.state,
currentItemHovered: id,
});
}
handleMouseLeaveItem() {
this.setState({
...this.state,
currentItemHovered: null,
});
}
handleClickAddItem(index, tap) {
const source = {
droppableId: 'backlog',
index: index,
};
const destination = {
droppableId: 'cart',
index: this.state.columns.cart.items.length,
};
this.handleOnDragEnd({
source,
destination
}, tap);
}
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
};
}),
}
},
});
}
handleShowOverlayRemove(index) {
if (this.timer_remove)
clearTimeout(this.timer_remove);
this.handleToggleOverlayRemove(index, true);
this.timer_remove = setTimeout(() => {
this.handleToggleOverlayRemove(index, false);
}, 2000);
}
handleClickShowOrder() {
const crate = {
items: [],
type: this.state.currentMode,
};
const clonedCart = Array.from(this.state.columns.cart.items);
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
for (const i in clonedCart) {
const item = clonedCart[i];
const item_data = clonedCartData[i];
crate.items.push({
'pn': item.name_number,
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
});
}
this.setState({
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'show',
RFQBodyOrder: JSON.stringify(crate, null, 2),
});
}
handleClickOpenImport() {
this.setState({
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'import',
});
}
handleLoadCustomConf(customconf) {
if (!customconf) {return; }
const items = this.props.data.items;
let new_items = [];
let new_items_data = [];
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(),
options_data: item.options ? item.options : null,
}));
new_items_data.push({options_data: item.options ? item.options : null});
}
});
return item;
});
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: new_items,
itemsData: new_items_data,
},
},
currentMode: customconf.type,
});
});
}
handleClickSubmit(note, email) {
const crate = {
items: [],
type: this.state.currentMode,
};
const clonedCart = Array.from(this.state.columns.cart.items);
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
for (const i in clonedCart) {
const item = clonedCart[i];
const item_data = clonedCartData[i];
crate.items.push({
'pn': item.name_number,
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
});
}
const {data} = this.props;
this.setState({isProcessing: true});
fetch(data.API_RFQ, {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email,
note,
configuration: JSON.stringify(crate)
})
}).then(response => {
if (response.status !== 200) {
throw Error("Response status is not OK: " + response.status + ".\n" + response);
}
this.setState({
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'email',
isProcessingComplete: true,
});
}).catch(err => {
console.error("Request failed, reason:", 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@m-labs.hk");
});
})
}
handleOnDragEnd(result) {
/**
* 4 cases:
* - from backlog to one of the crate - add to the correct crate in correct order
* - from one crate to another - delete from one crate and put to another in correct order
* - within one crate - reorder
* - from crate to backlog - delete
* */
const {
source,
destination,
} = result;
/* better move to another function
let dragged_items = [];
if (source.indexes) {
source.indexes.forEach((card_index, _) => {
dragged_items.push(itemsUnfoldedList[card_index]);
})
} else if (source.index >= 0) {
dragged_items.push(itemsUnfoldedList[source.index]);
}*/
console.log('==> result', result);
// dropped outside the list
if (!destination) {
return;
}
switch (source.droppableId) {
// TODO add delete functionality
case destination.droppableId:
/*this.setState({
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[destination.droppableId]: {
...this.state.columns.crates[destination.droppableId],
items: reorder(
this.state.columns.crates[source.droppableId].items,
source.index,
destination.index
)}}
}
});*/
this.checkAlertsInNewState(destination.droppableId, {
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[destination.droppableId]: {
...this.state.columns.crates[destination.droppableId],
items: reorder(
this.state.columns.crates[source.droppableId].items,
source.index,
destination.index
)}}
}
});
break;
case 'backlog':
/*this.setState({
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[destination.droppableId]: {
...this.state.columns.crates[destination.droppableId],
items: copyFromBacklog(
this.state.items,
this.state.columns.crates[destination.droppableId],
source,
destination
)}}
}
});*/
this.checkAlertsInNewState(destination.droppableId, {
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[destination.droppableId]: {
...this.state.columns.crates[destination.droppableId],
items: copyFromBacklog(
this.state.items,
this.state.columns.crates[destination.droppableId],
source,
destination
)}}
}
});
break;
default:
this.setState({
columns: move(
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
source,
destination
)
});
break;
}
}
handleClickToggleMobileSideMenu() {
this.setState({
...this.state,
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
});
}
handleClickCloseRFQFeedback() {
this.setState({
shouldShowRFQFeedback: false,
});
}
checkAlertsInNewState(crate_id, new_state) {
console.log('--- START CHECKING CRATE WARNING ---');
let itemsCloned = Array.from(new_state.columns.crates[crate_id].items);
const crate_warnings = TriggerCrateWarnings(new_state.columns.crates[crate_id]);
itemsCloned.forEach((elem, _idx) => {
if (!elem.options_data && !!elem.options) {
elem.options_data = {}
}
});
itemsCloned = FillResources(itemsCloned);
itemsCloned = TriggerWarnings(itemsCloned);
// update state with rules
this.setState({
...this.state,
columns: {
...new_state.columns,
crates: {
...new_state.columns.crates,
[crate_id]: {
...new_state.columns.crates[crate_id],
items: itemsCloned,
warnings: crate_warnings
},
}
}
});
}
onAddCrate(id) {
this.setState({
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[id]: {
crate_type: "rack",
items: []
}}
}
});
}
onDeleteCrate(id) {
let new_state = {
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[id]: undefined
}
}};
delete new_state.columns.crates[id];
this.setState(new_state);
}
onCrateSelected(id) {
console.log(id)
this.setState({
active_crate: id
})
}
onCrateModeChange(crate_id, new_mode) {
let new_state = {
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[crate_id]: {
...this.state.columns.crates[crate_id],
crate_type: new_mode
}
}
}};
this.setState(new_state);
}
return (
<DragDropContext onDragEnd={this.handleOnDragEnd}>
<Layout
aside={
<Backlog/>
}
main={(
<OrderPanel
title="Order hardware"
description={(<p className="description">Drag and drop the cards you want into the crate below to see how the combination would look like. Setup card's configuration by tapping at the top of the card, most of the options can be modified after shipment. If you have any issues with this ordering system, or if you need other configurations, email us directly anytime at <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. The price is estimated and must be confirmed by a quote.</p>)}
/>
)}>
</Layout>
</DragDropContext>
);
}