web2019/static/js/shop/Shop.jsx

616 lines
22 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 {copy, itemsUnfoldedList, nbrOccupiedSlotsInCrate, remove, reorder} from "./utils";
import {Layout} from "./Layout.jsx";
import {Backlog} from "./Backlog.jsx";
import {OrderPanel} from "./OrderPanel.jsx";
import {CrateMode} from "./CrateMode.jsx";
import {Crate} from "./Crate.jsx";
import {Cart} from "./Cart.jsx";
import {OrderSummary} from "./OrderSummary.jsx";
import {OrderForm} from "./OrderForm.jsx";
import {TriggerWarnings} from "./warnings";
import {FillResources} from "./count_resources";
/**
* Component that render the entire shop
*/
export class Shop extends 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.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.handleCardsUpdated = this.handleCardsUpdated.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: 'cart',
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.
*/
if (
(prevState.columns.cart.items !== this.state.columns.cart.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);
}
handleCardsUpdated() {
this.checkAlerts(this.state.columns.cart.items);
}
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, newAdded) {
const {
source,
destination,
} = result;
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]);
}
if (!destination) {
if (source.droppableId === 'cart') {
this.setState({
...this.state,
newCardJustAdded: false,
columns: {
...this.state.columns,
[source.droppableId]: {
...this.state.columns[source.droppableId],
items: remove(
this.state.columns[source.droppableId].items,
source.index,
),
itemsData: remove(
this.state.columns[source.droppableId].itemsData,
source.index,
)
},
},
});
}
return;
}
switch(source.droppableId) {
case 'backlog':
if (source.droppableId !== destination.droppableId) {
this.setState({
...this.state,
newCardJustAdded: newAdded ? true : false,
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],
dragged_items,
destination,
),
},
},
});
}
break;
case destination.droppableId:
this.setState({
...this.state,
newCardJustAdded: false,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: reorder(
this.state.columns[destination.droppableId].items,
source.index,
destination.index,
),
itemsData: reorder(
this.state.columns[destination.droppableId].itemsData,
source.index,
destination.index,
),
},
},
});
break;
default:
break;
}
}
handleClickToggleMobileSideMenu() {
this.setState({
...this.state,
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
});
}
handleClickCloseRFQFeedback() {
this.setState({
shouldShowRFQFeedback: false,
});
}
checkAlerts(newItems) {
console.log('--- START CHECKING CRATE WARNING ---');
const {
currentMode,
crateModeSlots,
crateRules,
} = this.state;
let itemsCloned = Array.from(newItems);
const rules = {};
itemsCloned.forEach((elem, idx) => {
if (!(idx in itemsCloned)) itemsCloned[idx] = elem;
if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) {
itemsCloned[idx].options_data = this.state.columns.cart.itemsData[idx].options_data;
}
});
itemsCloned = FillResources(itemsCloned);
itemsCloned = TriggerWarnings(itemsCloned);
// check number of slot in crate
const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
if (nbrOccupied > crateModeSlots[currentMode]) {
rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
} else if (crateModeSlots[currentMode] === 21 && nbrOccupied <= 10) {
rules[crateRules.compactSlot.type] = {...crateRules.compactSlot};
}
// update state with rules
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
itemsData: itemsCloned,
}
},
rules: {
...rules,
},
});
}
render() {
const {
currency,
currentItemHovered,
currentMode,
crateModeSlots,
crateModeItems,
items,
columns,
rules,
mobileSideMenuShouldOpen,
newCardJustAdded,
isProcessing,
shouldShowRFQFeedback,
RFQBodyType,
RFQBodyOrder,
isProcessingComplete,
} = this.state;
const isMobile = window.deviceIsMobile();
const isTouch = window.isTouchEnabled();
return (
<DragDropContext onDragEnd={this.handleOnDragEnd}>
<Layout
showRFQFeedback={shouldShowRFQFeedback}
RFQBodyType={RFQBodyType}
RFQBodyOrder={RFQBodyOrder}
className="shop"
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
isMobile={isMobile}
newCardJustAdded={newCardJustAdded}
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
onClickLoadCustomConf={this.handleLoadCustomConf}
items={items}
aside={
<Backlog
currency={currency}
items={items}
data={columns['backlog']}
onClickAddItem={this.handleClickAddItem}
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
isMobile={isMobile}>
</Backlog>
}
main={(
<OrderPanel
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
onClickOpenImport={this.handleClickOpenImport}
isMobile={isMobile}
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>)}
crateMode={
<CrateMode
items={crateModeItems}
mode={currentMode}
onClickMode={this.handleCrateModeChange}>
</CrateMode>}
crate={
<Crate
cart={
<Cart
nbrSlots={crateModeSlots[currentMode]}
data={columns['cart']}
isMobile={isMobile}
isTouch={isTouch}
itemHovered={currentItemHovered}
onToggleOverlayRemove={this.handleToggleOverlayRemove}
onClickRemoveItem={this.handleDeleteItem}
onClickItem={this.handleShowOverlayRemove}
onCardUpdate={this.handleCardsUpdated}>
</Cart>
}
rules={Object.values(rules).filter(rule => rule)}>
</Crate>
}
summaryPrice={
<OrderSummary
currency={currency}
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}>
</OrderSummary>
}
form={
<OrderForm
isProcessingComplete={isProcessingComplete}
processingComplete={this.handleProcessingComplete}
isProcessing={isProcessing}
onClickSubmit={this.handleClickSubmit}
onClickShow={this.handleClickShowOrder}>
</OrderForm>
}>
</OrderPanel>
)}>
</Layout>
</DragDropContext>
);
}
}