code from amapei
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package react;
|
||||
|
||||
/**
|
||||
* @doc https://www.npmjs.com/package/react-bootstrap-datetimepicker
|
||||
*/
|
||||
@:jsRequire('react-bootstrap-datetimepicker')
|
||||
extern class DateTimeField extends react.ReactComponent {}
|
||||
@@ -0,0 +1,29 @@
|
||||
package react;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
/**
|
||||
* A Error Div
|
||||
*/
|
||||
class Error extends react.ReactComponentOfProps<{error:String}>
|
||||
{
|
||||
|
||||
public function new(props:Dynamic)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
|
||||
override public function render(){
|
||||
|
||||
if (props.error == null) return null;
|
||||
|
||||
return jsx('<div className="alert alert-danger">
|
||||
<span className="glyphicon glyphicon-exclamation-sign"></span> ${props.error}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package react;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
/**
|
||||
* A message Div
|
||||
*/
|
||||
class Message extends react.ReactComponentOfProps<{message:String}>
|
||||
{
|
||||
|
||||
public function new(props:Dynamic)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
|
||||
if (props.message == null) return null;
|
||||
|
||||
return jsx('<div className="alert alert-warning">
|
||||
<span className="glyphicon glyphicon glyphicon-info-sign"></span> ${props.message}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package react;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
//datepicker broken if called like this //import react.DateTimeField.*;
|
||||
//@:jsRequire('react-bootstrap-datetimepicker')
|
||||
//extern class DateTimeField extends react.ReactComponent {}
|
||||
|
||||
|
||||
/**
|
||||
* @doc https://github.com/YouCanBookMe/react-datetime
|
||||
*/
|
||||
@:jsRequire('react-datetime')
|
||||
extern class DateTime extends react.ReactComponent {}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @author fbarbut
|
||||
*/
|
||||
class ReportHeader extends react.ReactComponentOfState<OrdersReportOptions>
|
||||
{
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
state = {startDate:null, endDate:null, groupBy:null, contracts:[]};
|
||||
|
||||
//load fr locale of moment.js
|
||||
var moment = js.Lib.require('moment');
|
||||
js.Lib.require('moment/locale/fr');
|
||||
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
|
||||
return jsx('<div className="reportHeader">
|
||||
<div className="col-md-3">
|
||||
<div className="input-group">
|
||||
<span className="input-group-addon">
|
||||
<span className="glyphicon glyphicon-calendar"></span>
|
||||
</span>
|
||||
<DateTime name="startDate_PROUT" onChange={onDateChange} locale="fr" dateFormat="LLLL" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="col-md-3">
|
||||
<DateTime name="endDate" onChange={onDateChange} inputFormat="YYYY-MM-DD HH:mm:ss" />
|
||||
</div>
|
||||
|
||||
<div className="col-md-3">
|
||||
<select className="form-control" onChange={onGroupByChange}>
|
||||
<option value="ByMember">Par adhérent</option>
|
||||
<option value="ByProduct">Par Produit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<a className="btn btn-primary">Afficher</a>
|
||||
</div>
|
||||
</div>');
|
||||
|
||||
}
|
||||
|
||||
function onDateChange(e:js.html.Event){
|
||||
trace("onDateChange");
|
||||
//var name :String = untyped e.target.name;
|
||||
//var value :String = untyped e.target.value;
|
||||
//trace('$name $value');
|
||||
trace(e);
|
||||
//e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* @doc https://facebook.github.io/react/docs/forms.html
|
||||
*/
|
||||
function onGroupByChange(e:js.html.Event){
|
||||
e.preventDefault();
|
||||
trace("onGRoupByChange");
|
||||
var name :String = untyped e.target.name;
|
||||
var value :String = untyped e.target.value;
|
||||
if (value == "ByMember"){
|
||||
state.groupBy = ByMember;
|
||||
}else{
|
||||
state.groupBy = ByProduct;
|
||||
}
|
||||
trace(state);
|
||||
setState(state);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package react;
|
||||
|
||||
/**
|
||||
*
|
||||
* Use the 1.4.x version. The 2.x version is still buggy
|
||||
*
|
||||
* @doc https://www.npmjs.com/package/react-bootstrap-typeahead
|
||||
*/
|
||||
@:jsRequire('react-bootstrap-typeahead', 'Typeahead')
|
||||
extern class Typeahead extends react.ReactComponent{}
|
||||
|
||||
/**
|
||||
* @doc https://github.com/ericgio/react-bootstrap-typeahead/blob/803f61c1c8d0c943106233ed3c9306acc19b5b2b/docs/API.md#asynctypeahead
|
||||
* Async component is needed when options and searches are managed asynchronously
|
||||
*/
|
||||
@:jsRequire('react-bootstrap-typeahead', 'AsyncTypeahead')
|
||||
extern class AsyncTypeahead extends react.ReactComponent{}
|
||||
@@ -0,0 +1,125 @@
|
||||
package react;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
// I need to store also the "input" because of https://stackoverflow.com/questions/29140354/how-to-handle-decimal-values-in-reacts-onchange-event-for-input
|
||||
typedef VATBoxState = {ht:Float, ttc:Float, vat:Float, htInput:String, ttcInput:String,lastEdited:String};
|
||||
|
||||
|
||||
/**
|
||||
* A box to manage prices with and without VAT
|
||||
* @author fbarbut
|
||||
*/
|
||||
class VATBox extends react.ReactComponentOfPropsAndState<{ttc:Float,currency:String,vatRates:String,vat:Float,formName:String},VATBoxState>
|
||||
{
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
//trace(props);
|
||||
|
||||
this.state = {
|
||||
ht : round(props.ttc/(1+props.vat/100)),
|
||||
htInput : Std.string(round(props.ttc/(1+props.vat/100))),
|
||||
ttc : round(props.ttc),
|
||||
ttcInput : Std.string(round(props.ttc)),
|
||||
vat:props.vat,
|
||||
lastEdited:null
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
|
||||
var rates :Array<Float>= props.vatRates.split("|").map(Std.parseFloat);
|
||||
|
||||
var options = [for (r in rates) jsx('<option key="$r" value="$r">$r %</option>') ];
|
||||
var priceInputName = props.formName+"_price";
|
||||
var vatInputName = props.formName+"_vat";
|
||||
|
||||
return jsx('<div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-4 text-center"> Hors taxe </div>
|
||||
<div className="col-md-4 text-center"> Taux de TVA </div>
|
||||
<div className="col-md-4 text-center"> TTC </div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
<div className="input-group">
|
||||
<input type="text" name="htInput" value="${state.htInput}" className="form-control" onChange={onChange}/>
|
||||
<div className="input-group-addon">${props.currency}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<select name="vat" className="form-control" onChange={onChange} defaultValue=${state.vat}>
|
||||
${options}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<div className="input-group">
|
||||
<input type="text" name="ttcInput" value="${state.ttcInput}" className="form-control" onChange={onChange}/>
|
||||
<div className="input-group-addon">${props.currency}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="$priceInputName" value="${state.ttc}" />
|
||||
<input type="hidden" name="$vatInputName" value="${state.vat}" />
|
||||
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recompute prices
|
||||
*/
|
||||
function onChange(e:js.html.Event){
|
||||
|
||||
e.preventDefault();
|
||||
var name :String = untyped e.target.name;
|
||||
var input : String = Std.string(untyped e.target.value);
|
||||
if (input == null || input == "") input = "0";
|
||||
input = StringTools.replace(input, ",", ".");
|
||||
var value : Float = Std.parseFloat(input);
|
||||
if (value == null) value = 0;
|
||||
|
||||
var rate = 1 + (state.vat / 100);
|
||||
//trace('name:$name - raw:' + untyped e.target.value+' - input:$input - value:$value ');
|
||||
|
||||
switch(name){
|
||||
case "htInput":
|
||||
|
||||
this.setState(cast {ht:value , htInput:input , ttc: round(value * rate), ttcInput:round(value * rate) , lastEdited:"htInput"});
|
||||
|
||||
case "ttcInput":
|
||||
this.setState(cast {ht: round(value / rate), htInput : round(value/rate), ttcInput:input , ttc:value , lastEdited:"ttcInput"});
|
||||
|
||||
case "vat":
|
||||
rate = 1 + (value / 100);
|
||||
if (state.lastEdited == "htInput"){
|
||||
//compute ttc from ht
|
||||
this.setState(cast { vat:value, ht:state.ht, htInput:state.ht, ttc:round(state.ht * rate) , ttcInput:round(state.ht * rate)} );
|
||||
}else{
|
||||
//compute ht from ttc
|
||||
this.setState(cast { vat:value, ht: round( state.ttc/rate ), htInput: round( state.ttc/rate ), ttc:state.ttc , ttcInput:state.ttc} );
|
||||
}
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
inline function round(f:Float):Float{
|
||||
return Math.round(f * 100) / 100;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package react.map;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import leaflet.L;
|
||||
|
||||
using Lambda;
|
||||
|
||||
|
||||
/**
|
||||
* Externs for react-leaflet
|
||||
* @doc https://react-leaflet.js.org/docs/en/intro.html
|
||||
*/
|
||||
@:jsRequire('react-leaflet', 'Map')
|
||||
extern class LeafMap extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'TileLayer')
|
||||
extern class TileLayer extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'Marker')
|
||||
extern class Marker extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'CircleMarker')
|
||||
extern class CircleMarker extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'Popup')
|
||||
extern class Popup extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'FeatureGroup')
|
||||
extern class FeatureGroup extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'LayerGroup')
|
||||
extern class LayerGroup extends ReactComponent {}
|
||||
|
||||
|
||||
/*
|
||||
extern class L2 {
|
||||
static function icon(a:Dynamic):Dynamic;
|
||||
static function latLng(lat:Float, lng:Float):Dynamic;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* GroupItem
|
||||
* @author rcrestey
|
||||
*/
|
||||
typedef GroupMapProps = {
|
||||
var addressCoord:Dynamic;
|
||||
var groups:Array<GroupOnMap>;
|
||||
var fetchGroupsInsideBox:Box->Void;
|
||||
var groupFocusedId:Int;
|
||||
};
|
||||
|
||||
typedef GroupMapState = {
|
||||
var isFitting:Bool;
|
||||
var focusedMarker:Dynamic;
|
||||
};
|
||||
|
||||
typedef Box = {
|
||||
var minLat:Float;
|
||||
var maxLat:Float;
|
||||
var minLng:Float;
|
||||
var maxLng:Float;
|
||||
};
|
||||
|
||||
class GroupMap extends ReactComponentOfPropsAndState<GroupMapProps, GroupMapState> {
|
||||
static inline var DEFAULT_LAT = 46.52863469527167; // center of France
|
||||
static inline var DEFAULT_LNG = 2.43896484375; // center of France
|
||||
static inline var INIT_ZOOM = 6;
|
||||
static inline var DEFAULT_ZOOM = 13;
|
||||
|
||||
var map:Dynamic;
|
||||
var featureGroup:Dynamic;
|
||||
var markerMap = new Map<Int,Dynamic>();
|
||||
|
||||
var groupIcon = L.icon({
|
||||
iconUrl: '/img/marker.svg',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -30],
|
||||
className: 'icon'
|
||||
});
|
||||
|
||||
var homeIcon = L.icon({
|
||||
iconUrl: '/img/home.svg',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20],
|
||||
popupAnchor: [0, -30],
|
||||
className: 'icon'
|
||||
});
|
||||
|
||||
function new() {
|
||||
super();
|
||||
state = {
|
||||
isFitting: false,
|
||||
focusedMarker: null
|
||||
};
|
||||
}
|
||||
|
||||
function getMap(element:Dynamic):Void {
|
||||
map = element.leafletElement;
|
||||
}
|
||||
|
||||
function getFeatureGroup(element:Dynamic):Void {
|
||||
featureGroup = element.leafletElement;
|
||||
setState({
|
||||
isFitting: true
|
||||
}, fitBounds);
|
||||
}
|
||||
|
||||
function getMarker(element:Dynamic, id:Int):Void {
|
||||
if (element != null)
|
||||
markerMap.set(id, element.leafletElement);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call API to get groups in the current bounding box
|
||||
*/
|
||||
function getGroups() {
|
||||
var bounds = map.getBounds();
|
||||
var southWest = bounds.getSouthWest();
|
||||
var northEast = bounds.getNorthEast();
|
||||
|
||||
props.fetchGroupsInsideBox({
|
||||
minLat: southWest.lat,
|
||||
maxLat: northEast.lat,
|
||||
minLng: southWest.lng,
|
||||
maxLng: northEast.lng
|
||||
});
|
||||
}
|
||||
|
||||
function fitBounds() {
|
||||
map.fitBounds(featureGroup.getBounds(), {
|
||||
padding: [30, 30]
|
||||
});
|
||||
}
|
||||
|
||||
function handleMoveEnd() {
|
||||
if (
|
||||
props.addressCoord != null &&
|
||||
!Lambda.empty(props.groups) &&
|
||||
map.distance(map.getCenter(), props.addressCoord) == 0
|
||||
)
|
||||
setState({
|
||||
isFitting: true
|
||||
}, fitBounds);
|
||||
else if (state.isFitting)
|
||||
setState({
|
||||
isFitting: false
|
||||
});
|
||||
else
|
||||
getGroups();
|
||||
}
|
||||
|
||||
override public function componentDidMount() {
|
||||
if (props.addressCoord == null)
|
||||
getGroups();
|
||||
}
|
||||
|
||||
override public function shouldComponentUpdate(nextProps:GroupMapProps, nextState:GroupMapState) {
|
||||
if (nextState.focusedMarker != state.focusedMarker)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
override public function componentDidUpdate(prevProps:GroupMapProps, prevState:GroupMapState) {
|
||||
if (props.groupFocusedId != null) {
|
||||
if (
|
||||
prevProps.groupFocusedId != props.groupFocusedId
|
||||
|| state.focusedMarker == null
|
||||
) {
|
||||
if (state.focusedMarker != null)
|
||||
state.focusedMarker.closePopup();
|
||||
|
||||
var focusedMarker = markerMap.get(props.groupFocusedId);
|
||||
focusedMarker.openPopup();
|
||||
|
||||
setState({
|
||||
focusedMarker: focusedMarker
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (prevProps.groupFocusedId != null && state.focusedMarker != null) {
|
||||
state.focusedMarker.closePopup();
|
||||
|
||||
setState({
|
||||
focusedMarker: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override public function render() {
|
||||
var center = props.addressCoord == null
|
||||
? L.latLng(DEFAULT_LAT, DEFAULT_LNG)
|
||||
: props.addressCoord;
|
||||
|
||||
var zoom = props.addressCoord == null
|
||||
? INIT_ZOOM
|
||||
: DEFAULT_ZOOM;
|
||||
|
||||
return jsx('
|
||||
<LeafMap
|
||||
center=${center}
|
||||
zoom=${zoom}
|
||||
ref=${getMap}
|
||||
onMoveEnd=${handleMoveEnd}
|
||||
>
|
||||
<TileLayer
|
||||
attribution="&copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
|
||||
url="https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiYnViYXIiLCJhIjoiY2loM2lubmZpMDBwcGtxbHlwdmw0bXRkbCJ9.rfgXPakoGnXZ3wIGA3-1kQ"
|
||||
id="bubar.cih3inmqd00tjuxm7oc2532l0"
|
||||
/>
|
||||
<FeatureGroup ref=${getFeatureGroup}>
|
||||
${renderGroupMarkers()}
|
||||
${renderHomeMarker()}
|
||||
</FeatureGroup>
|
||||
</LeafMap>
|
||||
');
|
||||
}
|
||||
|
||||
function renderGroupMarkers() {
|
||||
var markers = props.groups.map(function(group) {
|
||||
var coord = [group.place.latitude, group.place.longitude];
|
||||
|
||||
function markerGetter(e:Dynamic) {
|
||||
getMarker(e, group.place.id);
|
||||
}
|
||||
|
||||
var image = group.image==null ? null : jsx('<img className="groupImage img-responsive" src=${group.image}/>');
|
||||
|
||||
return jsx('
|
||||
<Marker
|
||||
position=${coord}
|
||||
ref=${markerGetter}
|
||||
key=${group.place.id}
|
||||
icon=${groupIcon}
|
||||
>
|
||||
<Popup className="popup">
|
||||
<div>
|
||||
<a href=${"/group/"+group.id} target="_blank">
|
||||
$image
|
||||
<div className="groupName">${group.name}</div>
|
||||
</a>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
');
|
||||
});
|
||||
|
||||
return jsx('<div>${markers}</div>');
|
||||
}
|
||||
|
||||
function renderHomeMarker() {
|
||||
if (props.addressCoord != null)
|
||||
return jsx('<Marker position=${props.addressCoord} icon=${homeIcon} />');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package react.map;
|
||||
import js.Promise;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import utils.HttpUtil;
|
||||
import leaflet.L;
|
||||
import Common;
|
||||
using Lambda;
|
||||
|
||||
@:jsRequire('react-places-autocomplete', 'default')
|
||||
extern class Autocomplete extends ReactComponent {}
|
||||
|
||||
@:jsRequire('react-places-autocomplete')
|
||||
extern class GeoUtil {
|
||||
static function geocodeByAddress(address:Dynamic):Promise<Dynamic>;
|
||||
}
|
||||
|
||||
@:jsRequire('geolib')
|
||||
extern class Geolib {
|
||||
static function getDistance(start:Dynamic, end:Dynamic):Float;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups Map
|
||||
* @author rcrestey
|
||||
*/
|
||||
|
||||
typedef GroupMapRootState = {
|
||||
var point:Dynamic;
|
||||
var address:String;
|
||||
var groups:Array<GroupOnMap>;
|
||||
var groupFocusedId:Int;
|
||||
var isInit:Bool;
|
||||
};
|
||||
|
||||
typedef GroupMapRootProps = {
|
||||
var lat:Float;
|
||||
var lng:Float;
|
||||
var address:String;
|
||||
};
|
||||
|
||||
class GroupMapRoot extends ReactComponentOfState<GroupMapRootState>{
|
||||
|
||||
static inline var GROUP_MAP_URL = '/api/group/map';
|
||||
|
||||
var distanceMap = new Map<Int,Dynamic>();
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
state = {
|
||||
point: L.latLng(props.lat, props.lng),
|
||||
address: props.address,
|
||||
groups: [],
|
||||
groupFocusedId: null,
|
||||
isInit: false
|
||||
};
|
||||
}
|
||||
|
||||
function onChange(address) {
|
||||
setState({
|
||||
address: address
|
||||
});
|
||||
}
|
||||
|
||||
function openPopup(group:Dynamic) {
|
||||
setState({
|
||||
groupFocusedId: group.place.id
|
||||
});
|
||||
}
|
||||
|
||||
function closePopup() {
|
||||
setState({
|
||||
groupFocusedId: null
|
||||
});
|
||||
}
|
||||
|
||||
function geocodeByAddress(address:String):Promise<Dynamic> {
|
||||
return GeoUtil.geocodeByAddress(address)
|
||||
.then(function(results) {
|
||||
var lat = results[0].geometry.location.lat();
|
||||
var lng = results[0].geometry.location.lng();
|
||||
|
||||
return {lat: lat, lng: lng};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call API to find groups at $lat and $lng
|
||||
* @param lat -
|
||||
* @param lng -
|
||||
*/
|
||||
function fetchGroups(lat:Float, lng:Float) {
|
||||
HttpUtil.fetch(GROUP_MAP_URL, GET, {lat: lat, lng: lng}, JSON)
|
||||
.then(function(results) {
|
||||
setState({
|
||||
point: L.latLng(lat, lng),
|
||||
groups: results.groups,
|
||||
isInit: true
|
||||
}, fillDistanceMap);
|
||||
})
|
||||
.catchError(function(error) {
|
||||
trace('Error', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call API to look for groups in the defined bounding box
|
||||
*/
|
||||
var wait : Bool;
|
||||
function fetchGroupsInsideBox(newBox) {
|
||||
|
||||
switch(wait){
|
||||
case null,false : wait = true;
|
||||
case true : trace("stop"); return;
|
||||
}
|
||||
|
||||
HttpUtil.fetch(GROUP_MAP_URL, GET, newBox, JSON)
|
||||
.then(function(results) {
|
||||
wait = false;
|
||||
setState({
|
||||
groups: results.groups
|
||||
}, fillDistanceMap);
|
||||
});
|
||||
/*.catchError(function(error) {
|
||||
trace('Error', error + " stack:"+haxe.CallStack.toString(haxe.CallStack.exceptionStack())) ;
|
||||
wait = false;
|
||||
});*/
|
||||
}
|
||||
|
||||
function getGroupDistance(group:GroupOnMap):Float {
|
||||
if (state.point == null)
|
||||
return null;
|
||||
|
||||
var start = {
|
||||
latitude: state.point.lat,
|
||||
longitude: state.point.lng
|
||||
};
|
||||
var end = {
|
||||
latitude: group.place.latitude,
|
||||
longitude: group.place.longitude
|
||||
};
|
||||
|
||||
return Geolib.getDistance(start, end);
|
||||
}
|
||||
|
||||
function fillDistanceMap() {
|
||||
for (group in state.groups) {
|
||||
distanceMap.set(group.place.id, getGroupDistance(group));
|
||||
}
|
||||
|
||||
orderGroupsByDistance(state.groups);
|
||||
|
||||
setState({
|
||||
groups: state.groups
|
||||
});
|
||||
}
|
||||
|
||||
function orderGroupsByDistance(groups:Array<GroupOnMap>) {
|
||||
groups.sort(function(a, b) {
|
||||
return distanceMap.get(a.place.id) - distanceMap.get(b.place.id);
|
||||
});
|
||||
}
|
||||
|
||||
function convertDistance(distance:Int):String { // to test
|
||||
if (distance > 10000)
|
||||
return Math.floor(distance / 1000) + ' km';
|
||||
if (distance > 1000)
|
||||
return Math.floor(distance / 100) / 10 + ' km';
|
||||
return distance + ' m';
|
||||
}
|
||||
|
||||
function handleSelect(address:String) {
|
||||
setState({
|
||||
address: address
|
||||
});
|
||||
|
||||
geocodeByAddress(address)
|
||||
.then(function(coord) {
|
||||
fetchGroups(coord.lat, coord.lng);
|
||||
})
|
||||
.catchError(function(error) {
|
||||
trace('Error', error);
|
||||
});
|
||||
}
|
||||
|
||||
override public function componentDidMount() {
|
||||
if (state.point != null)
|
||||
fetchGroups(state.point.lat, state.point.lng);
|
||||
else if (state.address != '')
|
||||
handleSelect(state.address);
|
||||
}
|
||||
|
||||
function renderSuggestion(obj:Dynamic) {
|
||||
return jsx('
|
||||
<div className="autocomplete-item">
|
||||
<i className="fa fa-map-marker autocomplete-icon" />
|
||||
<strong>${obj.formattedSuggestion.mainText}</strong>
|
||||
<small className="text-muted">${obj.formattedSuggestion.secondaryText}</small>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
override public function render() {
|
||||
var inputProps = {
|
||||
value: state.address,
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
var cssClasses = {
|
||||
root: 'form-group',
|
||||
input: 'autocomplete-input',
|
||||
autocompleteContainer: 'autocomplete-results',
|
||||
};
|
||||
|
||||
return jsx('
|
||||
<div className="group-map">
|
||||
<div className="row">
|
||||
<div id="logo" className="col-md-3"> </div>
|
||||
<div className="col-md-9">
|
||||
<div className="form-group-container">
|
||||
Trouvez un groupe Cagette près de chez vous
|
||||
<Autocomplete
|
||||
inputProps=${inputProps}
|
||||
onSelect=${handleSelect}
|
||||
classNames=${cssClasses}
|
||||
renderSuggestion=${renderSuggestion}
|
||||
placeHolder="Saisissez votre adresse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-3" id="groupsContainer">${renderGroupList()}</div>
|
||||
<div className="col-md-9" id="mapContainer">${renderGroupMap()}</div>
|
||||
</div>
|
||||
</div>');
|
||||
}
|
||||
|
||||
function renderGroupMap() {
|
||||
if (!state.isInit)
|
||||
return null;
|
||||
|
||||
return jsx('
|
||||
<GroupMap
|
||||
addressCoord=${state.point}
|
||||
groups=${state.groups}
|
||||
fetchGroupsInsideBox=${fetchGroupsInsideBox}
|
||||
groupFocusedId=${state.groupFocusedId}
|
||||
/>
|
||||
');
|
||||
}
|
||||
|
||||
function renderGroupList() {
|
||||
var groups = state.groups.map(function(group) {
|
||||
return renderGroup(group);
|
||||
});
|
||||
|
||||
return jsx('
|
||||
<div className="groups">
|
||||
${groups}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a group in the left list
|
||||
*/
|
||||
function renderGroup(group:GroupOnMap) {
|
||||
var address = [
|
||||
group.place.address1,
|
||||
group.place.address2,
|
||||
[group.place.zipCode, group.place.city].join(" "),
|
||||
];
|
||||
|
||||
var addressBlock = Lambda.array(address.mapi(function(index, element) {
|
||||
if (element != null){
|
||||
return jsx('<div key=${index}>$element</div>');
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
var distance = null;
|
||||
if (distanceMap.get(group.place.id) != null)
|
||||
distance = jsx('<div className="distance">${convertDistance(distanceMap.get(group.place.id))}</div>');
|
||||
|
||||
var classNames = ['clickable groupBlock'];
|
||||
if (group.place.id == state.groupFocusedId)
|
||||
classNames.push('focused');
|
||||
|
||||
var img = if(group.image==null) {
|
||||
null;
|
||||
}else{
|
||||
jsx('<img src="${group.image}" className="img-responsive" />');
|
||||
}
|
||||
|
||||
return jsx('<a target="_blank"
|
||||
onMouseEnter=${function() { openPopup(group); }}
|
||||
onMouseLeave=${closePopup}
|
||||
className=${classNames.join(' ')}
|
||||
key=${group.place.id}
|
||||
href=${"/group/"+group.id}
|
||||
>
|
||||
$img
|
||||
<h4>${group.name}</h4>
|
||||
<div className="address">${addressBlock}</div>
|
||||
${distance}
|
||||
</a>');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package react.order;
|
||||
import react.ReactDOM;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import utils.HttpUtil;
|
||||
import react.product.ProductSelect;
|
||||
import react.router.Redirect;
|
||||
import react.router.Link;
|
||||
|
||||
|
||||
/**
|
||||
* A box to add an order to a member
|
||||
* @author fbarbut
|
||||
*/
|
||||
class InsertOrder extends react.ReactComponentOfPropsAndState<{contractId:Int,userId:Int,distributionId:Int,onInsert:UserOrder->Void},{products:Array<ProductInfo>,error:String,selected:Int}>
|
||||
{
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
state = {products:[],error:null,selected:null};
|
||||
}
|
||||
|
||||
override function componentDidMount()
|
||||
{
|
||||
//load product list
|
||||
HttpUtil.fetch("/api/product/get/", GET, {contractId:props.contractId},PLAIN_TEXT)
|
||||
.then(function(data:String) {
|
||||
|
||||
/*var data : {products:Array<ProductInfo>} = haxe.Json.parse(data);
|
||||
for( p in data.products) {
|
||||
p.unitType = Type.createEnumIndex(UnitType,untyped p.unitType);
|
||||
}*/
|
||||
|
||||
var data : {products:Array<ProductInfo>} = tink.Json.parse(data);
|
||||
setState({products:data.products, error:null,selected:null});
|
||||
|
||||
}).catchError(function(data) {
|
||||
var data = Std.string(data);
|
||||
trace("Error",data);
|
||||
if(data.substr(0,1)=="{"){
|
||||
//json error from server
|
||||
var data : ErrorInfos = haxe.Json.parse(data);
|
||||
setState(cast {error:data.error.message} );
|
||||
}else{
|
||||
//js error
|
||||
setState(cast {error:data} );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
//redirect to orderBox if a product is selected
|
||||
var redirect = if(state.selected!=null) jsx('<$Redirect to="/" />') else null;
|
||||
|
||||
return jsx('
|
||||
<div>
|
||||
$redirect
|
||||
<h3>Choisissez le produit à ajouter</h3>
|
||||
<$Link className="btn btn-default" to="/"><span className="glyphicon glyphicon-chevron-left"></span> Retour</$Link>
|
||||
<$Error error="${state.error}" />
|
||||
<hr />
|
||||
<$ProductSelect onSelect=$onSelectProduct products=${state.products} />
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function onSelectProduct(p:ProductInfo){
|
||||
var uo : UserOrder = cast {
|
||||
id:null,
|
||||
product:p,
|
||||
quantity:1,
|
||||
productId:p.id,
|
||||
productPrice:p.price,
|
||||
paid:false,
|
||||
invert:false,
|
||||
user2:null
|
||||
};
|
||||
props.onInsert(uo);
|
||||
setState(cast {selected:p.id});
|
||||
|
||||
//do not insert order now, just warn the OrderBox
|
||||
/*
|
||||
//insert order
|
||||
var data = [{id:null,productId:p.id,qt:1,paid:false,invert:false,user2:null} ];
|
||||
var req = {
|
||||
orders:haxe.Json.stringify(data),
|
||||
distributionId : props.distributionId,
|
||||
contractId : props.contractId
|
||||
};
|
||||
var r = HttpUtil.fetch("/api/order/update/"+props.userId, POST, req, JSON);
|
||||
r.then(function(d:Dynamic) {
|
||||
|
||||
if (Reflect.hasField(d, "error")) {
|
||||
setState(cast {error:d.error.message});
|
||||
}else{
|
||||
//WOOT
|
||||
//trace("OK");
|
||||
//go to OrderBox with a redirect
|
||||
setState(cast {selected:p.id});
|
||||
}
|
||||
}).catchError(function(d) {
|
||||
trace("PROMISE ERROR", d);
|
||||
setState(cast {error:d.error.message});
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package react.order;
|
||||
import react.ReactDOM;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import react.product.Product;
|
||||
|
||||
/**
|
||||
* A User order
|
||||
* @author fbarbut
|
||||
*/
|
||||
class Order extends react.ReactComponentOfPropsAndState<{order:UserOrder,onUpdate:UserOrder->Void,parentBox:react.order.OrderBox},{order:UserOrder,inputValue:String}>
|
||||
{
|
||||
var hasPayments :Bool;
|
||||
var currency : String;
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
state = {order:props.order,inputValue:null};
|
||||
hasPayments = props.parentBox.props.hasPayments;
|
||||
currency = props.parentBox.props.currency;
|
||||
|
||||
if (state.order.productUnit == null) state.order.productUnit = Piece;
|
||||
if (state.order.productQt == null) state.order.productQt = 1;
|
||||
|
||||
state.inputValue = if ( isSmartQtInput(state.order) ){
|
||||
Std.string(round(state.order.quantity * state.order.productQt));
|
||||
}else{
|
||||
Std.string(state.order.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
var o = state.order;
|
||||
/*var unit = if (o.productHasFloatQt || o.productHasVariablePrice){
|
||||
jsx('<div className="col-md-1">${Formatting.unit(o.productUnit)}</div>');
|
||||
}else{
|
||||
jsx('<div className="col-md-1"></div>');
|
||||
}*/
|
||||
|
||||
/*
|
||||
//use smart qt only if hasFloatQt
|
||||
var productName = if (o.productHasFloatQt || o.productHasVariablePrice){
|
||||
jsx('<div className="col-md-3">${o.productName}</div>');
|
||||
}else{
|
||||
jsx('<div className="col-md-3">${o.productName} ${o.productQt} ${Formatting.unit(o.productUnit)}</div>');
|
||||
}
|
||||
*/
|
||||
/*var productName = if (o.productHasFloatQt || o.productHasVariablePrice){
|
||||
jsx('<div className="col-md-3">${o.productName}</div>');*/
|
||||
|
||||
var input = if (isSmartQtInput(o)){
|
||||
jsx('<div className="input-group">
|
||||
<input type="text" className="form-control input-sm text-right" value="${state.inputValue}" onChange=${onChange} onKeyPress=${onKeyPress}/>
|
||||
<div className="input-group-addon">${Formatting.unit(o.productUnit)}</div>
|
||||
</div>');
|
||||
}else{
|
||||
jsx('<div className="input-group">
|
||||
<input type="text" className="form-control input-sm text-right" value="${state.inputValue}" onChange=${onChange} onKeyPress=${onKeyPress}/>
|
||||
</div>');
|
||||
}
|
||||
|
||||
var alternated = if(props.parentBox.props.contractType==0 && props.parentBox.state.users!=null){
|
||||
//constant orders
|
||||
var options = props.parentBox.state.users.map(function(x) return jsx('<option key=${x.id} value=${x.id}>${x.name}</option>') );
|
||||
|
||||
var checkbox = if(o.invertSharedOrder){
|
||||
jsx('<input data-toggle="tooltip" title="Inverser l\'alternance" checked="checked" type="checkbox" value="1" onChange=$onChangeInvert />');
|
||||
}else{
|
||||
jsx('<input data-toggle="tooltip" title="Inverser l\'alternance" type="checkbox" value="1" onChange=$onChangeInvert />');
|
||||
}
|
||||
|
||||
jsx('<div>
|
||||
<select className="form-control input-sm" style=${{width:"150px",display:"inline-block"}} onChange=${onChangeUser2} value=${o.userId2}>
|
||||
<option value="0">-</option>
|
||||
$options
|
||||
</select>
|
||||
$checkbox
|
||||
</div>');
|
||||
}else{
|
||||
null;
|
||||
}
|
||||
|
||||
return jsx('<div className="productOrder row">
|
||||
<div className="col-md-4">
|
||||
<$Product productInfo=${o.product} />
|
||||
</div>
|
||||
|
||||
<div className="col-md-1 ref">
|
||||
${o.productRef}
|
||||
</div>
|
||||
|
||||
<div className="col-md-1">
|
||||
${round(o.quantity * o.productPrice)} ${currency}
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
$input
|
||||
${makeInfos()}
|
||||
</div>
|
||||
|
||||
${paidInput()}
|
||||
|
||||
<div className="col-md-3">$alternated</div>
|
||||
|
||||
</div>');
|
||||
}
|
||||
|
||||
function round(f){
|
||||
return Formatting.formatNum(f);
|
||||
}
|
||||
|
||||
function paidInput(){
|
||||
if(hasPayments) return null;
|
||||
if(state.order.paid){
|
||||
return jsx('<div className="col-md-1"><input type="checkbox" name="paid" value="1" checked="checked" onChange=${onChangePaid} /></div>');
|
||||
}else{
|
||||
return jsx('<div className="col-md-1"><input type="checkbox" name="paid" value="1" onChange=${onChangePaid} /></div>');
|
||||
}
|
||||
}
|
||||
|
||||
function makeInfos(){
|
||||
var o = state.order;
|
||||
return if ( isSmartQtInput(o) ){
|
||||
jsx('<div className="infos">
|
||||
<b> ${round(o.quantity)} </b> x <b>${o.productQt} ${Formatting.unit(o.productUnit)}</b > ${o.productName}
|
||||
</div>');
|
||||
}else{
|
||||
null;
|
||||
}
|
||||
}
|
||||
|
||||
function isSmartQtInput(o:UserOrder):Bool{
|
||||
return o.product.hasFloatQt || o.product.variablePrice || o.product.wholesale;
|
||||
}
|
||||
|
||||
function onChange(e:js.html.Event){
|
||||
e.preventDefault();
|
||||
var value :String = untyped (e.target.value == "") ? "0" : e.target.value;
|
||||
state.inputValue = value;
|
||||
var v = Formatting.parseFloat(value);
|
||||
var o = state.order;
|
||||
if ( isSmartQtInput(o) ){
|
||||
//the value is a smart qt, so we need re-compute the quantity
|
||||
o.quantity = v / o.productQt;
|
||||
}else{
|
||||
o.quantity = v;
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
if (props.onUpdate != null) props.onUpdate(state.order);
|
||||
}
|
||||
|
||||
function onChangePaid(e:js.html.Event){
|
||||
state.order.paid = untyped e.target.checked;
|
||||
this.setState(state);
|
||||
if (props.onUpdate != null) props.onUpdate(state.order);
|
||||
}
|
||||
|
||||
function onChangeInvert(e:js.html.Event){
|
||||
state.order.invertSharedOrder = untyped e.target.checked;
|
||||
this.setState(state);
|
||||
if (props.onUpdate != null) props.onUpdate(state.order);
|
||||
}
|
||||
|
||||
function onChangeUser2(e:js.html.Event){
|
||||
var v = Std.parseInt(untyped e.target.value);
|
||||
state.order.userId2 = v==0 ? null : v;
|
||||
this.setState(state);
|
||||
if (props.onUpdate != null) props.onUpdate(state.order);
|
||||
}
|
||||
|
||||
function onKeyPress(event:js.html.KeyboardEvent){
|
||||
/*if(event.key == 'Enter'){
|
||||
trace('enter !');
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package react.order;
|
||||
import react.ReactDOM;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import utils.HttpUtil;
|
||||
import react.router.HashRouter;
|
||||
import react.router.Route;
|
||||
import react.router.Switch;
|
||||
import react.router.Link;
|
||||
|
||||
typedef OrderBoxState = {
|
||||
orders:Array<UserOrder>,
|
||||
error:String,
|
||||
users:Null<Array<UserInfo>>,
|
||||
};
|
||||
typedef OrderBoxProps = {
|
||||
userId:Int,
|
||||
distributionId:Int,
|
||||
contractId:Int,
|
||||
contractType:Int,
|
||||
date:String,
|
||||
place:String,
|
||||
userName:String,
|
||||
onValidate:Void->Void,
|
||||
currency:String,
|
||||
hasPayments:Bool
|
||||
};
|
||||
|
||||
/**
|
||||
* A box to edit/add orders of a member
|
||||
* @author fbarbut
|
||||
*/
|
||||
class OrderBox extends react.ReactComponentOfPropsAndState<OrderBoxProps,OrderBoxState>
|
||||
{
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
state = { orders : [], error : null, users:null };
|
||||
}
|
||||
|
||||
override function componentDidMount()
|
||||
{
|
||||
|
||||
//request api avec user + distrib
|
||||
HttpUtil.fetch("/api/order/get/"+props.userId, GET, {distributionId:props.distributionId,contractId:props.contractId}, PLAIN_TEXT)
|
||||
.then(function(data:String) {
|
||||
|
||||
var data : {orders:Array<UserOrder>} = tink.Json.parse(data);
|
||||
/*for( o in orders){
|
||||
//convert ints to enums, enums have been lost in json serialization
|
||||
o.productUnit = Type.createEnumIndex(Unit, cast o.productUnit );
|
||||
}*/
|
||||
setState({orders:data.orders, error:null});
|
||||
|
||||
if(props.contractType==0) loadUsers();
|
||||
|
||||
}).catchError(function(data) {
|
||||
var data = Std.string(data);
|
||||
trace("Error",data);
|
||||
if(data.substr(0,1)=="{"){
|
||||
//json error from server
|
||||
var data : ErrorInfos = haxe.Json.parse(data);
|
||||
setState( cast {error:data.error.message} );
|
||||
}else{
|
||||
//js error
|
||||
setState( cast {error:data} );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* load user list when contract is constant orders
|
||||
*/
|
||||
function loadUsers(){
|
||||
HttpUtil.fetch("/api/user/getFromGroup/", GET, {}, PLAIN_TEXT)
|
||||
.then(function(data:String) {
|
||||
|
||||
var data : {users:Array<UserInfo>} = tink.Json.parse(data);
|
||||
setState({users:data.users, error:null});
|
||||
|
||||
}).catchError(function(data) {
|
||||
|
||||
var data = Std.string(data);
|
||||
if(data.substr(0,1)=="{"){
|
||||
//json error from server
|
||||
var data : ErrorInfos = haxe.Json.parse(data);
|
||||
setState( cast {error:data.error.message} );
|
||||
}else{
|
||||
//js error
|
||||
setState( cast {error:data} );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
|
||||
//edit orders
|
||||
|
||||
|
||||
var renderOrders = this.state.orders.map(function(o){
|
||||
var k :String = if(o.id!=null) {
|
||||
Std.string(o.id);
|
||||
} else {
|
||||
o.productId+"-"+Std.random(99999);
|
||||
};
|
||||
return jsx('<$Order key="$k" order="$o" onUpdate=$onUpdate parentBox=${this} />') ;
|
||||
} );
|
||||
|
||||
|
||||
var delivery = if(props.date==null){
|
||||
null;
|
||||
}else{
|
||||
jsx('<p>Pour la livraison du <b>${props.date}</b> à <b>${props.place}</b></p>');
|
||||
}
|
||||
|
||||
var renderOrderBox = function() return jsx('
|
||||
<div onKeyPress=${onKeyPress}>
|
||||
<h3>Commandes de ${props.userName}</h3>
|
||||
$delivery
|
||||
<$Error error="${state.error}" />
|
||||
<hr/>
|
||||
<div className="row tableHeader">
|
||||
<div className="col-md-4">Produit</div>
|
||||
<div className="col-md-1">Ref.</div>
|
||||
<div className="col-md-1">Prix</div>
|
||||
<div className="col-md-2">Qté</div>
|
||||
<div className="col-md-1">Payé</div>
|
||||
<div className="col-md-3">Alterné avec</div>
|
||||
</div>
|
||||
${renderOrders}
|
||||
<div>
|
||||
<a onClick=${onClick} className="btn btn-primary">
|
||||
<span className="glyphicon glyphicon-chevron-right"></span> Valider
|
||||
</a>
|
||||
|
||||
<$Link className="btn btn-default" to="/insert"><span className="glyphicon glyphicon-plus-sign"></span> Ajouter un produit</$Link>
|
||||
</div>
|
||||
</div>
|
||||
');
|
||||
|
||||
|
||||
var onProductSelected = function(uo:UserOrder){
|
||||
|
||||
var existingOrder = Lambda.find(state.orders,function(x) return x.productId==uo.productId );
|
||||
if(existingOrder!=null){
|
||||
existingOrder.quantity += uo.quantity;
|
||||
this.setState(this.state);
|
||||
}else{
|
||||
this.state.orders.push(uo);
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
//insert product box
|
||||
var renderInsertBox = function(){
|
||||
return jsx('<$InsertOrder contractId="${props.contractId}" userId="${props.userId}" distributionId="${props.distributionId}" onInsert=$onProductSelected/>');
|
||||
}
|
||||
|
||||
return jsx('<$HashRouter>
|
||||
<$Switch>
|
||||
<$Route path="/" exact=$true render=$renderOrderBox />
|
||||
<$Route path="/insert" exact=$true render=$renderInsertBox />
|
||||
</$Switch>
|
||||
</$HashRouter>');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* called when an order is updated
|
||||
*/
|
||||
function onUpdate(data:UserOrder){
|
||||
/*trace("ON UPDATE : " + data);
|
||||
for ( o in state.orders){
|
||||
if (o.id == data.id) {
|
||||
o.quantity = data.quantity;
|
||||
o.paid = data.paid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(this.state);*/
|
||||
}
|
||||
|
||||
/**
|
||||
* submit updated orders to the API
|
||||
*/
|
||||
function onClick(?_){
|
||||
|
||||
var data = new Array<{id:Int,productId:Int,qt:Float,paid:Bool,invertSharedOrder:Bool,userId2:Int}>();
|
||||
for ( o in state.orders) data.push({id:o.id, productId : o.productId, qt: o.quantity, paid : o.paid, invertSharedOrder:o.invertSharedOrder, userId2:o.userId2});
|
||||
|
||||
var req = { orders:data };
|
||||
|
||||
var p = HttpUtil.fetch("/api/order/update/"+props.userId+"?distributionId="+props.distributionId+"&contractId="+props.contractId, POST, req, JSON);
|
||||
p.then(function(data:Dynamic) {
|
||||
|
||||
//WOOT
|
||||
if (props.onValidate != null) props.onValidate();
|
||||
|
||||
}).catchError(function(data) {
|
||||
var data = Std.string(data);
|
||||
trace("Error",data);
|
||||
if(data.substr(0,1)=="{"){
|
||||
//json error from server
|
||||
var data : ErrorInfos = haxe.Json.parse(data);
|
||||
setState( cast {error:data.error.message} );
|
||||
}else{
|
||||
//js error
|
||||
setState( cast {error:data} );
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function onKeyPress(e:js.html.KeyboardEvent){
|
||||
if(e.key=="Enter") onClick();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package react;
|
||||
|
||||
import api.react.ReactMacro.jsx;
|
||||
import js.html.InputElement;
|
||||
import Common;
|
||||
|
||||
typedef ComposerAppState = {
|
||||
products:Array<{id:Int,name:String,qt:Float,unit:UnitType}>
|
||||
}
|
||||
|
||||
typedef ComposerAppRefs = {
|
||||
pi:ProductInput,
|
||||
productContainer:js.html.DivElement,
|
||||
qt:InputElement,
|
||||
unit:js.html.SelectElement,
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite product composer
|
||||
*
|
||||
*/
|
||||
class ComposerApp extends ReactComponentOfStateAndRefs<ComposerAppState, ComposerAppRefs>
|
||||
{
|
||||
/*var products:Array<{id:Int,name:String,?qt:Float,?unit:UnitType}>;
|
||||
|
||||
|
||||
public function new(props:Dynamic)
|
||||
{
|
||||
|
||||
super(props);
|
||||
products = [{id:1,name:"pipo"},{id:2,name:"Loclac"}];
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
return jsx('
|
||||
<div className="ComposerApp" style={{margin:"10px"}} >
|
||||
|
||||
<div className="form-inline">
|
||||
|
||||
<ProductInput ref="pi"/>
|
||||
|
||||
<input ref="qt" onChange="$onChange" className="form-control" type="text" name="qt" placeholder="Quantité" />
|
||||
|
||||
<select ref="unit" className="form-control" name="unit">
|
||||
${getUnits()}
|
||||
</select>
|
||||
|
||||
<a className="btn btn-primary" onClick=$addItem>
|
||||
<span className="glyphicon glyphicon-plus"></span> Ajouter
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="container" ref="productContainer">
|
||||
${createChildren()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
');
|
||||
|
||||
}
|
||||
|
||||
function onChange(){
|
||||
|
||||
}
|
||||
|
||||
function getUnits(){
|
||||
var out = [];
|
||||
for ( c in Unit.createAll()){
|
||||
|
||||
out.push(jsx( '<option value="{c.getIndex()}"> {Std.string(c)} </option>'));
|
||||
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function createChildren()
|
||||
{
|
||||
|
||||
|
||||
return [for (p in products) jsx('<ProductComp key={p.id} name={p.name} qt={p.qt} unit={p.unit}/>')];
|
||||
}
|
||||
|
||||
function addItem(){
|
||||
var text :String = refs.pi.refs.input.value;
|
||||
if (text.length > 0)
|
||||
{
|
||||
trace("add " + text);
|
||||
trace("qt " + this.refs.qt.value);
|
||||
trace("unit " + this.refs.unit.selectedIndex);
|
||||
|
||||
var qt = Std.parseFloat(this.refs.qt.value);
|
||||
var unit = UnitType.createByIndex(this.refs.unit.selectedIndex);
|
||||
var id = Std.random(999);
|
||||
products.push( {id:id, name:text,qt:qt,unit:unit});
|
||||
|
||||
setState({products:[{id:id,name:text, qt:qt, unit:unit}]});
|
||||
//this.forceUpdate();
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package react.product;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
/**
|
||||
* A Product
|
||||
* @author fbarbut
|
||||
*/
|
||||
class Product extends react.ReactComponentOfProps<{productInfo:ProductInfo}>
|
||||
{
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
var p :ProductInfo = props.productInfo;
|
||||
|
||||
//convert int to enum
|
||||
//p.unitType = Type.createEnumIndex(Common.Unit,cast p.unit);
|
||||
|
||||
//var unit = ;
|
||||
var imgStyle = {width:'64px',height:'64px','backgroundImage':'url("${p.image}")'};
|
||||
var divStyle = p.active ? {} : {opacity: 0.4};
|
||||
|
||||
return jsx('<div className="product row" style=$divStyle>
|
||||
<div className="col-md-4">
|
||||
<div src="${p.image}" className="productImg" style=$imgStyle/>
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<strong>${p.name}</strong> ${p.qt} ${Formatting.unit(p.unitType)}<br/>
|
||||
${p.price} €
|
||||
</div>
|
||||
</div>');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package react.product;
|
||||
import react.ReactDOM;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import react.Typeahead;
|
||||
|
||||
typedef ProductInputProps = {
|
||||
formName:String,
|
||||
txpProductId:Int,
|
||||
productName:String,
|
||||
}
|
||||
typedef ProductInputState = {
|
||||
txpProductId:Int,
|
||||
productName:String,
|
||||
categoryId:Int,
|
||||
breadcrumb:String,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Product Text Input with autocompletion
|
||||
*
|
||||
* @author fbarbut
|
||||
*/
|
||||
class ProductInput extends react.ReactComponentOfPropsAndState<ProductInputProps,ProductInputState>
|
||||
{
|
||||
|
||||
public static var DICO : TxpDictionnary = null;
|
||||
var options : Array<{id:Int,label:String}>;
|
||||
|
||||
public function new(props:ProductInputProps)
|
||||
{
|
||||
super(props);
|
||||
options = [];
|
||||
this.state = {
|
||||
txpProductId : props.txpProductId,
|
||||
productName : props.productName,
|
||||
categoryId : 0,
|
||||
breadcrumb : ""
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
var inputName :String = props.formName+"_name";
|
||||
var txpProductInputName :String = props.formName+"_txpProductId";
|
||||
|
||||
return jsx('
|
||||
<div className="row">
|
||||
|
||||
<div className="col-md-8">
|
||||
<AsyncTypeahead
|
||||
placeholder="Saisissez un nom de produit"
|
||||
options=$options
|
||||
onSearch=$onSearch
|
||||
minLength={3}
|
||||
style={{width:"350px"}}
|
||||
onChange=$onChange
|
||||
onInputChange=$onInputChange
|
||||
selected={["${state.productName}"]}
|
||||
isLoading=$true
|
||||
/>
|
||||
<div className = "txpProduct" > ${state.breadcrumb}</div>
|
||||
|
||||
<input className="txpProduct" type="hidden" name="$txpProductInputName" value="${state.txpProductId}" />
|
||||
<input className="txpProduct" type="hidden" name="$inputName" value="${state.productName}" />
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<img ref="image" className="img-thumbnail" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when typing is stopped
|
||||
* @param o
|
||||
*/
|
||||
function onSearch(o){
|
||||
//trace("on search : "+o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each time a single letter change in the input
|
||||
* @param input
|
||||
*/
|
||||
function onInputChange(input:String){
|
||||
trace('on input change $input');
|
||||
this.setState({productName:input});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is selected in suggestions
|
||||
*/
|
||||
function onChange(selection:Array<{label:String,id:Int}>){
|
||||
|
||||
if (selection == null || selection.length == 0) return;
|
||||
|
||||
trace("on change "+selection[0]);
|
||||
|
||||
var product = Lambda.find(DICO.products, function(x) return x.id == selection[0].id);
|
||||
setTaxo(product);
|
||||
this.setState({productName:selection[0].label});
|
||||
}
|
||||
|
||||
/**
|
||||
* init typeahead auto-completion features when component is mounted
|
||||
*/
|
||||
override function componentDidMount(){
|
||||
|
||||
//get dictionnary
|
||||
if (DICO == null){
|
||||
|
||||
var r = new haxe.Http("/product/getTaxo");
|
||||
r.onData = function(data){
|
||||
//load dico
|
||||
DICO = haxe.Unserializer.run(data);
|
||||
|
||||
for ( p in DICO.products){
|
||||
options.push({label:p.name,id:p.id});
|
||||
}
|
||||
|
||||
//default values of input
|
||||
if (props.txpProductId != null){
|
||||
var txp = Lambda.find(DICO.products, function(x) return x.id == props.txpProductId);
|
||||
setTaxo(txp);
|
||||
}
|
||||
};
|
||||
r.request();
|
||||
}
|
||||
}
|
||||
|
||||
function setTaxo(txp:{id:Int, name:String, category:Int, subCategory:Int}){
|
||||
|
||||
if (txp == null) return;
|
||||
|
||||
//trace(txp);
|
||||
|
||||
this.setState({
|
||||
categoryId:txp.category,
|
||||
txpProductId:txp.id,
|
||||
breadcrumb:getBreadcrumb(txp)/*,
|
||||
productName:product.name //do not override product name ! */
|
||||
});
|
||||
|
||||
this.refs.image.src="/img/taxo/cat"+txp.category+".png";
|
||||
}
|
||||
|
||||
/**
|
||||
* generate string like "fruits & vegetables / vegetables / carrots"
|
||||
* @param name
|
||||
*/
|
||||
function getBreadcrumb(product){
|
||||
//cat
|
||||
var str = DICO.categories.get(product.category).name;
|
||||
if (product.subCategory != null){
|
||||
str += " / " + DICO.subCategories.get(product.subCategory).name;
|
||||
}
|
||||
str += " / " + product.name;
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package react.product;
|
||||
import react.ReactDOM;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import utils.HttpUtil;
|
||||
|
||||
/**
|
||||
* A Product selector
|
||||
* @author fbarbut
|
||||
*/
|
||||
class ProductSelect extends react.ReactComponentOfPropsAndState<{onSelect:ProductInfo->Void,products:Array<ProductInfo>},{selected:Int}>
|
||||
{
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
state = { selected : null };
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
|
||||
var products = props.products.map(function(info){
|
||||
//var selector = info.id==state.selected ? jsx(''):jsx('<div className="clickable"><$Product productInfo=$info /></div>');
|
||||
return jsx('<div key=${info.id} className="col-md-6" onClick=${onClick.bind(info.id)}>
|
||||
<div className="clickable"><$Product productInfo=$info /></div>
|
||||
</div>');
|
||||
});
|
||||
|
||||
return jsx('<div className="productSelect">${products}</div>');
|
||||
}
|
||||
|
||||
function onClick(i:Int){
|
||||
this.setState(cast {selected:i});
|
||||
if(props.onSelect!=null){
|
||||
var p = Lambda.find(props.products,function(x) return x.id==i);
|
||||
props.onSelect(p);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package react.store;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
typedef CartProps = {
|
||||
var order:OrderSimple;
|
||||
var addToCart:ProductInfo -> Int -> Void;
|
||||
var removeFromCart:ProductInfo -> ?Int -> Void;
|
||||
var submitOrder:OrderSimple -> Void;
|
||||
};
|
||||
|
||||
class Cart extends react.ReactComponentOfProps<CartProps>
|
||||
{
|
||||
|
||||
function addToCart(product:ProductInfo, quantity:Int):Void {
|
||||
props.addToCart(product, quantity);
|
||||
}
|
||||
|
||||
function removeFromCart(product:ProductInfo, quantity:Int):Void {
|
||||
props.removeFromCart(product, quantity);
|
||||
}
|
||||
|
||||
function removeAllFromCart(product:ProductInfo):Void {
|
||||
props.removeFromCart(product);
|
||||
}
|
||||
|
||||
function submitOrder():Void {
|
||||
props.submitOrder(props.order);
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
return jsx('
|
||||
<div className="cart">
|
||||
<h3>Ma Commande</h3>
|
||||
${renderProducts()}
|
||||
${renderFooter()}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderProducts() {
|
||||
var productsToOrder = props.order.products.map(function(product:ProductWithQuantity) {
|
||||
var quantity = product.quantity;
|
||||
var product = product.product;
|
||||
|
||||
return jsx('
|
||||
<div className="product-to-order" key=${product.name}>
|
||||
<div>${product.name}</div>
|
||||
<div>$quantity</div>
|
||||
<div className="cart-action-buttons">
|
||||
<div onClick=${function(){
|
||||
this.addToCart(product, 1);
|
||||
}}>
|
||||
+
|
||||
</div>
|
||||
<div onClick=${function(){
|
||||
this.removeFromCart(product, 1);
|
||||
}}>
|
||||
-
|
||||
</div>
|
||||
<div onClick=${function(){
|
||||
this.removeAllFromCart(product);
|
||||
}}>
|
||||
x
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
');
|
||||
});
|
||||
|
||||
return jsx('
|
||||
<div className="products-to-order">
|
||||
${productsToOrder}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderFooter() {
|
||||
var buttonClasses = ["order-button"];
|
||||
var submit = submitOrder;
|
||||
|
||||
if (props.order.products.length == 0) {
|
||||
buttonClasses.push("order-button--disabled");
|
||||
submit = null;
|
||||
}
|
||||
|
||||
return jsx('
|
||||
<div className="cart-footer">
|
||||
<div className="total">
|
||||
Total
|
||||
<div>${props.order.total} €</div>
|
||||
</div>
|
||||
<div className=${buttonClasses.join(" ")} onClick=$submit>Commander</div>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package react.store;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
using Lambda;
|
||||
|
||||
typedef FiltersProps = {
|
||||
var categories:Array<CategoryInfo>;
|
||||
var filters:Array<String>;
|
||||
var toggleFilter:String -> Void;
|
||||
};
|
||||
|
||||
class Filters extends react.ReactComponentOfProps<FiltersProps>
|
||||
{
|
||||
override public function render(){
|
||||
return jsx('
|
||||
<div className="filters">
|
||||
<h3>Filtres</h3>
|
||||
${renderFilters()}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderFilters() {
|
||||
return props.categories.map(function(category) {
|
||||
var classNames = ["filter"];
|
||||
if (props.filters.has(category.name))
|
||||
classNames.push("active");
|
||||
|
||||
return jsx('
|
||||
<div
|
||||
className=${classNames.join(" ")}
|
||||
key=${category.id}
|
||||
onClick=${function(){
|
||||
props.toggleFilter(category.name);
|
||||
}}
|
||||
>
|
||||
${category.name}
|
||||
</div>
|
||||
');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package react.store;
|
||||
|
||||
import js.Browser.window;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
typedef ProductProps = {
|
||||
var product:ProductInfo;
|
||||
var addToCart:ProductInfo -> Int -> Void;
|
||||
};
|
||||
|
||||
typedef ProductState = {
|
||||
var quantity:Int;
|
||||
};
|
||||
|
||||
class Product extends react.ReactComponentOfPropsAndState<ProductProps, ProductState>
|
||||
{
|
||||
static inline var OVERLAY_URL = '/shop/productInfo';
|
||||
static inline var IMAGE_WIDTH = 120;
|
||||
static inline var IMAGE_HEIGHT = 120;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
state = {
|
||||
quantity: 1
|
||||
};
|
||||
}
|
||||
|
||||
function openOverlay() {
|
||||
untyped window._.overlay('$OVERLAY_URL/${props.product.id}', props.product.name);
|
||||
}
|
||||
|
||||
function updateQuantity(event:Dynamic) {
|
||||
var quantity = Std.parseInt(event.target.value);
|
||||
|
||||
if (Std.is(quantity, Int) && quantity > 0)
|
||||
setState({
|
||||
quantity: Std.int(quantity)
|
||||
});
|
||||
}
|
||||
|
||||
function addToCart() {
|
||||
props.addToCart(props.product, state.quantity);
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
var product = props.product;
|
||||
|
||||
return jsx('
|
||||
<div className="product">
|
||||
<img src=${product.image} width=${IMAGE_WIDTH+'px'} height=${IMAGE_HEIGHT+'px'} alt={product.name} />
|
||||
<div className="body">
|
||||
<a onClick=$openOverlay>
|
||||
${product.name}
|
||||
</a>
|
||||
<div>${product.price} €</div>
|
||||
<input type="number" value=${state.quantity} onChange=$updateQuantity />
|
||||
<div className="button" onClick=$addToCart>Ajouter</div>
|
||||
</div>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package react.store;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
using Lambda;
|
||||
|
||||
typedef ProductListProps = {
|
||||
var categories:Array<CategoryInfo>;
|
||||
var productsBySubcategoryIdMap:Map<Int, Array<ProductInfo>>;
|
||||
var filters:Array<String>;
|
||||
var addToCart:ProductInfo -> Int -> Void;
|
||||
};
|
||||
|
||||
class ProductList extends react.ReactComponentOfProps<ProductListProps>
|
||||
{
|
||||
override public function render(){
|
||||
return jsx('
|
||||
<div className="categories">
|
||||
${renderCategories()}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderCategories() {
|
||||
return props.categories.map(function(category) {
|
||||
if (!props.filters.has(category.name))
|
||||
return null;
|
||||
|
||||
return jsx('
|
||||
<div className="category" key=${category.name}>
|
||||
<h2>${category.name}</h2>
|
||||
<div className="subCategories">
|
||||
${renderSubCategories(category)}
|
||||
</div>
|
||||
</div>
|
||||
');
|
||||
});
|
||||
}
|
||||
|
||||
function renderSubCategories(category) {
|
||||
var subCategories = category.subcategories.map(function(category) {
|
||||
if (!props.productsBySubcategoryIdMap.exists(category.id))
|
||||
return jsx('<div key=${category.name}>Loading...</div>');
|
||||
|
||||
var products = props.productsBySubcategoryIdMap.get(category.id);
|
||||
|
||||
return jsx('
|
||||
<div className="sub-category" key=${category.name}>
|
||||
<h3>${category.name}</h3>
|
||||
<div className="products">
|
||||
${renderProducts(products)}
|
||||
</div>
|
||||
</div>
|
||||
');
|
||||
});
|
||||
|
||||
return jsx('
|
||||
<div className="sub-categories">
|
||||
$subCategories
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderProducts(products) {
|
||||
return products.map(function(product) {
|
||||
return jsx('
|
||||
<Product product=${product} key=${product.id} addToCart=${props.addToCart}/>
|
||||
');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package react.store;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import haxe.Json;
|
||||
|
||||
using Lambda;
|
||||
|
||||
import Common;
|
||||
import utils.CartUtils;
|
||||
import utils.HttpUtil;
|
||||
|
||||
typedef StoreProps = {
|
||||
var place:Int;
|
||||
var date:String;
|
||||
};
|
||||
|
||||
typedef StoreState = {
|
||||
var place:PlaceInfos;
|
||||
var orderByEndDates:Array<OrderByEndDate>;
|
||||
var categories:Array<CategoryInfo>;
|
||||
var productsBySubcategoryIdMap:Map<Int, Array<ProductInfo>>;
|
||||
var order:OrderSimple;
|
||||
var filters:Array<String>;
|
||||
};
|
||||
|
||||
class Store extends react.ReactComponentOfPropsAndState<StoreProps, StoreState>
|
||||
{
|
||||
static inline var CATEGORY_URL = '/api/shop/categories';
|
||||
static inline var PRODUCT_URL = '/api/shop/products';
|
||||
static inline var INIT_URL = '/api/shop/init';
|
||||
static inline var VIEW_URL = '/place/view';
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
state = {
|
||||
place: null,
|
||||
orderByEndDates: [],
|
||||
categories: [],
|
||||
filters: [],
|
||||
productsBySubcategoryIdMap: new Map(),
|
||||
order: {
|
||||
products: [],
|
||||
total: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
override function componentDidMount() {
|
||||
var categoriesRequest = HttpUtil.fetch(CATEGORY_URL, GET, {date: props.date, place: props.place}, JSON);
|
||||
var initRequest = HttpUtil.fetch(INIT_URL, GET, {date: props.date, place: props.place}, JSON);
|
||||
|
||||
initRequest.then(function(infos:Dynamic) {
|
||||
setState({
|
||||
place: infos.place,
|
||||
orderByEndDates: infos.orderEndDates
|
||||
});
|
||||
})
|
||||
.catchError(function(error) {
|
||||
trace("ERROR", error);
|
||||
});
|
||||
|
||||
categoriesRequest.then(function(categories:Dynamic) {
|
||||
var categories:Array<CategoryInfo> = categories.categories;
|
||||
var subCategories = [];
|
||||
|
||||
for (category in categories) {
|
||||
subCategories = subCategories.concat(category.subcategories);
|
||||
}
|
||||
|
||||
setState({
|
||||
categories: categories,
|
||||
filters: categories.map(function(category) {
|
||||
return category.name;
|
||||
})
|
||||
});
|
||||
|
||||
subCategories.map(function(category:CategoryInfo) {
|
||||
return HttpUtil.fetch(PRODUCT_URL, GET, {date: props.date, place: props.place, subcategory: category.id}, JSON)
|
||||
.then(function(result) {
|
||||
var productsBySubcategoryIdMapCopy = [
|
||||
for (key in state.productsBySubcategoryIdMap.keys())
|
||||
key => state.productsBySubcategoryIdMap.get(key)
|
||||
];
|
||||
productsBySubcategoryIdMapCopy.set(category.id, result.products);
|
||||
|
||||
setState({
|
||||
productsBySubcategoryIdMap: productsBySubcategoryIdMapCopy
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catchError(function(error) {
|
||||
trace("ERROR", error);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleFilter(category:String) {
|
||||
var filters = state.filters.copy();
|
||||
|
||||
if (state.filters.find(function(categoryInFilter) {
|
||||
return category == categoryInFilter;
|
||||
}) != null)
|
||||
filters.remove(category);
|
||||
else
|
||||
filters.push(category);
|
||||
|
||||
if (filters.length == 0)
|
||||
filters = state.categories.map(function(category) {
|
||||
return category.name;
|
||||
});
|
||||
|
||||
setState({
|
||||
filters: filters
|
||||
});
|
||||
}
|
||||
|
||||
function addToCart(productToAdd:ProductInfo, quantity:Int):Void {
|
||||
setState({
|
||||
order: CartUtils.addToCart(state.order, productToAdd, quantity)
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromCart(productToRemove:ProductInfo, ?quantity:Int):Void {
|
||||
setState({
|
||||
order: CartUtils.removeFromCart(state.order, productToRemove, quantity)
|
||||
});
|
||||
}
|
||||
|
||||
function submitOrder(order:OrderSimple) {
|
||||
var orderInSession = {
|
||||
total: order.total,
|
||||
products: order.products.map(function(p:ProductWithQuantity){
|
||||
return {
|
||||
productId: p.product.id,
|
||||
quantity: p.quantity
|
||||
};
|
||||
})
|
||||
}
|
||||
trace('Order', orderInSession);
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
return jsx('
|
||||
<div className="shop">
|
||||
${renderHeader()}
|
||||
<ProductList
|
||||
categories=${state.categories}
|
||||
productsBySubcategoryIdMap=${state.productsBySubcategoryIdMap}
|
||||
filters=${state.filters}
|
||||
addToCart=$addToCart
|
||||
/>
|
||||
<Filters
|
||||
categories=${state.categories}
|
||||
filters=${state.filters}
|
||||
toggleFilter=$toggleFilter
|
||||
/>
|
||||
<Cart
|
||||
order=${state.order}
|
||||
addToCart=$addToCart
|
||||
removeFromCart=$removeFromCart
|
||||
submitOrder=$submitOrder
|
||||
/>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (state.orderByEndDates == null || state.orderByEndDates.length == 0)
|
||||
return null;
|
||||
|
||||
var endDates;
|
||||
|
||||
if (state.orderByEndDates.length == 1) {
|
||||
var orderEndDate = state.orderByEndDates[0].date;
|
||||
endDates = [jsx('<div key=$orderEndDate>La commande fermera le $orderEndDate</div>')];
|
||||
}
|
||||
else {
|
||||
endDates = state.orderByEndDates.map(function(order) {
|
||||
if (order.contracts.length == 1)
|
||||
return jsx('
|
||||
<div key=${order.date}>
|
||||
La commande ${order.contracts[0]} fermera le: ${order.date}
|
||||
</div>
|
||||
');
|
||||
|
||||
return jsx('
|
||||
<div key=${order.date}>
|
||||
Les autres commandes fermeront: ${order.date}
|
||||
</div>
|
||||
');
|
||||
});
|
||||
}
|
||||
|
||||
var viewUrl = '$VIEW_URL/${props.place}';
|
||||
var addressBlock = Lambda.array([
|
||||
state.place.address1,
|
||||
state.place.address2,
|
||||
[state.place.zipCode, state.place.city].join(" "),
|
||||
].mapi(function(index, element) {
|
||||
if (element == null)
|
||||
return null;
|
||||
return jsx('<div className="address" key=$index>$element</div>');
|
||||
}));
|
||||
|
||||
return jsx('
|
||||
<div className="shop-header">
|
||||
<div>
|
||||
<div className="shop-distribution">
|
||||
Distribution le ${props.date}
|
||||
</div>
|
||||
|
||||
<div className="shop-order-ends">
|
||||
$endDates
|
||||
</div>
|
||||
</div>
|
||||
<div className="shop-place">
|
||||
<span className="info">
|
||||
<span className="glyphicon glyphicon-map-marker"></span>
|
||||
<a href=$viewUrl>${state.place.name}</a>
|
||||
</span>
|
||||
<div>
|
||||
$addressBlock
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package react.user;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
|
||||
typedef LoginBoxProps = {
|
||||
redirectUrl:String,
|
||||
message:String,
|
||||
?phoneRequired:Bool
|
||||
}
|
||||
|
||||
typedef LoginBoxState = {
|
||||
email:String,
|
||||
password:String,
|
||||
error:String
|
||||
}
|
||||
|
||||
/**
|
||||
* Login Box
|
||||
* @author fbarbut
|
||||
*/
|
||||
class LoginBox extends react.ReactComponentOfPropsAndState<LoginBoxProps,LoginBoxState>
|
||||
{
|
||||
|
||||
public function new(props:LoginBoxProps)
|
||||
{
|
||||
if (props.redirectUrl == null) props.redirectUrl = "/";
|
||||
if (props.message == "") props.message = null;
|
||||
super(props);
|
||||
this.state = {email:"", password:"", error:null};
|
||||
}
|
||||
|
||||
function setError(err:String){
|
||||
this.setState(cast {error:err});
|
||||
}
|
||||
|
||||
override public function render(){
|
||||
|
||||
return jsx('<div onKeyPress=$onKeyPress>
|
||||
<$Error error="${state.error}" />
|
||||
<$Message message="${props.message}" />
|
||||
<form action="" method="post" className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<label htmlFor="email" className="col-sm-4 control-label">Email : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="email" className="form-control" type="text" name="email" value="${state.email}" required="1" onChange={onChange}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password" className="col-sm-4 control-label">Mot de passe : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="password" type="password" name="password" value="${state.password}" className="form-control" required="1" onChange={onChange}/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center">
|
||||
<a onClick={submit} className="btn btn-primary btn-lg" ><span className="glyphicon glyphicon-user"></span> S\'identifier</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="/user/forgottenPassword">Mot de passe oublié ?</a>
|
||||
</p>
|
||||
</form>
|
||||
<!--
|
||||
<hr/>
|
||||
<p className="text-center">
|
||||
<b>C\'est votre première visite sur Cagette.net ?</b>
|
||||
<a onClick={registerBox} className="btn btn-default"><span className="glyphicon glyphicon-chevron-right"></span> S\'inscrire</a>
|
||||
</p>
|
||||
-->
|
||||
</div>');
|
||||
}
|
||||
|
||||
/**
|
||||
* @doc https://facebook.github.io/react/docs/forms.html
|
||||
*/
|
||||
function onChange(e:js.html.Event){
|
||||
e.preventDefault();
|
||||
|
||||
var name :String = untyped e.target.name;
|
||||
var value :String = untyped /*(e.target.value == "") ? null :*/ e.target.value;
|
||||
Reflect.setField(state, name, value);
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* displays a registerBox
|
||||
*/
|
||||
public function registerBox(){
|
||||
|
||||
var body = js.Browser.document.querySelector('#myModal .modal-body');
|
||||
ReactDOM.unmountComponentAtNode( body );
|
||||
|
||||
js.Browser.document.querySelector("#myModal .modal-title").innerHTML = "Inscription";
|
||||
ReactDOM.render(jsx('<$RegisterBox redirectUrl="${props.redirectUrl}" phoneRequired="${props.phoneRequired}"/>'), body );
|
||||
}
|
||||
|
||||
public function submit(?e:js.html.Event){
|
||||
|
||||
if (state.email == ""){
|
||||
setError("Veuillez saisir votre email");
|
||||
return;
|
||||
}
|
||||
if (state.password == ""){
|
||||
setError("Veuillez saisir votre mot de passe");
|
||||
return;
|
||||
}
|
||||
|
||||
//lock button
|
||||
var el: js.html.Element = null;
|
||||
if(e!=null){
|
||||
el = cast e.target;
|
||||
el.classList.add("disabled");
|
||||
}
|
||||
|
||||
|
||||
var req = new haxe.Http("/api/user/login");
|
||||
req.addParameter("email", state.email);
|
||||
req.addParameter("password", state.password);
|
||||
req.addParameter("redirecturl", props.redirectUrl);
|
||||
|
||||
req.onData = req.onError = function(d){
|
||||
|
||||
var d = req.responseData;
|
||||
|
||||
if(e!=null) el.classList.remove("disabled");
|
||||
|
||||
var d = haxe.Json.parse(d);
|
||||
if (Reflect.hasField(d, "error")) setError(d.error.message);
|
||||
if (Reflect.hasField(d, "success")) js.Browser.window.location.href = props.redirectUrl;
|
||||
}
|
||||
req.request(true);
|
||||
}
|
||||
|
||||
function onKeyPress(e:js.html.KeyboardEvent){
|
||||
if(e.key=="Enter") submit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package react.user;
|
||||
import react.ReactDOM;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
|
||||
typedef RegisterBoxState = {firstName:String, lastName:String, email:String, password:String, error:String, phone:String};
|
||||
typedef RegisterBoxProps = {redirectUrl:String,message:String,phoneRequired:Bool};
|
||||
|
||||
|
||||
/**
|
||||
* Registration box ( sign up )
|
||||
* @author fbarbut
|
||||
*/
|
||||
class RegisterBox extends react.ReactComponentOfPropsAndState<RegisterBoxProps,RegisterBoxState>
|
||||
{
|
||||
|
||||
|
||||
public function new(props:RegisterBoxProps)
|
||||
{
|
||||
if (props.redirectUrl == null) props.redirectUrl = "/";
|
||||
super(props);
|
||||
this.state = {firstName:"",lastName:"",email:"",password:"",error:null,phone:""};
|
||||
}
|
||||
|
||||
|
||||
override public function render(){
|
||||
|
||||
//tips for conditionnal rendering : https://github.com/massiveinteractive/haxe-react#gotchas
|
||||
var phone = null;
|
||||
if (props.phoneRequired){
|
||||
phone = jsx('<div className="form-group">
|
||||
<label htmlFor="phone" className="col-sm-4 control-label">Téléphone : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="phone" type="text" className="form-control" name="phone" value="${state.phone}" onChange={onChange} />
|
||||
</div>
|
||||
</div>');
|
||||
}
|
||||
|
||||
return jsx('
|
||||
<div>
|
||||
<$Error error="${state.error}" />
|
||||
<$Message message="${props.message}" />
|
||||
<form action="" method="post" className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<label htmlFor="firstName" className="col-sm-4 control-label">Prénom : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="firstName" type="text" name="firstName" value="${state.firstName}" className="form-control" onChange={onChange}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="lastName" className="col-sm-4 control-label">Nom : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="lastName" type="text" name="lastName" value="${state.lastName}" className="form-control" onChange={onChange}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email" className="col-sm-4 control-label">Email : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="email" type="text" className="form-control" name="email" value="${state.email}" onChange={onChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${phone}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password" className="col-sm-4 control-label">Mot de passe : </label>
|
||||
<div className="col-sm-8">
|
||||
<input id="password" type="password" name="password" value="${state.password}" className="form-control" onChange={onChange}/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center">
|
||||
<a onClick={submit} className="btn btn-primary btn-lg" ><span className="glyphicon glyphicon-chevron-right"></span> Inscription</a>
|
||||
</p>
|
||||
</form>
|
||||
<hr/>
|
||||
<p className="text-center">
|
||||
<b>Déjà inscrit ? </b>
|
||||
<a onClick={loginBox} className="btn btn-default"><span className="glyphicon glyphicon-user"></span> Connectez-vous ici</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @doc https://facebook.github.io/react/docs/forms.html
|
||||
*/
|
||||
function onChange(e:js.html.Event){
|
||||
|
||||
e.preventDefault();
|
||||
var name :String = untyped e.target.name;
|
||||
var value :String = untyped e.target.value;
|
||||
//trace('onChange : $name = $value');
|
||||
Reflect.setField(state, name, value);
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* displays a login box
|
||||
*/
|
||||
public function loginBox(){
|
||||
|
||||
var body = js.Browser.document.querySelector('#myModal .modal-body');
|
||||
ReactDOM.unmountComponentAtNode( body );
|
||||
|
||||
js.Browser.document.querySelector("#myModal .modal-title").innerHTML = "Connexion";
|
||||
ReactDOM.render(jsx('<$LoginBox redirectUrl="${props.redirectUrl}" />'), body );
|
||||
}
|
||||
|
||||
|
||||
public function submit(e:js.html.Event ){
|
||||
|
||||
if (state.email == ""){
|
||||
setError("Veuillez saisir votre email");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.password == ""){
|
||||
setError("Veuillez saisir un mot de passe");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.firstName == ""){
|
||||
setError("Veuillez saisir votre prénom");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.lastName == ""){
|
||||
setError("Veuillez saisir votre nom de famille");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.phone == "" && props.phoneRequired){
|
||||
setError("Veuillez saisir votre numéro de téléphone");
|
||||
return;
|
||||
}
|
||||
|
||||
//lock button
|
||||
var el : js.html.Element = cast e.target;
|
||||
el.classList.add("disabled");
|
||||
|
||||
var req = new haxe.Http("/api/user/register");
|
||||
req.addParameter("firstName", state.firstName);
|
||||
req.addParameter("lastName", state.lastName);
|
||||
req.addParameter("email", state.email);
|
||||
req.addParameter("password", state.password);
|
||||
req.addParameter("redirecturl", props.redirectUrl);
|
||||
if(props.phoneRequired) req.addParameter("phone", state.phone);
|
||||
|
||||
req.onData = req.onError = function(d){
|
||||
var d = req.responseData;
|
||||
el.classList.remove("disabled");
|
||||
var d = haxe.Json.parse(d);
|
||||
if (Reflect.hasField(d, "error")) setError(d.error.message);
|
||||
if (Reflect.hasField(d, "success")) js.Browser.window.location.href = props.redirectUrl;
|
||||
}
|
||||
|
||||
req.request(true);
|
||||
}
|
||||
|
||||
function setError(err:String){
|
||||
this.setState(cast {error:err});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user