code from amapei

This commit is contained in:
cagette@ct8
2020-09-26 18:25:18 +00:00
parent 42fe367911
commit 72b4699871
1966 changed files with 220437 additions and 2 deletions
Executable
+262
View File
@@ -0,0 +1,262 @@
//React lib
import react.ReactMacro.jsx;
import react.ReactDOM;
import react.*;
import react.router.*;
//custom components
import react.order.*;
import react.product.*;
import react.store.*;
import react.map.*;
import react.user.*;
//require bootstrap JS since it's bundled with browserify
//@:jsRequire('bootstrap') extern class Bootstrap{}
//@:jsRequire('jquery') extern class JQ extends js.jquery.JQuery{}
class App {
public static var instance : App;
public var LANG : String;
public var currency : String; //currency symbol like € or $
public var t : sugoi.i18n.GetText;//gettext translator
//i dont want to use redux now... saved state from react.OrderBox
public static var SAVED_ORDER_STATE : Dynamic;
function new(?lang="fr",?currency="€") {
//singleton
instance = this;
if(lang!=null) this.LANG = lang;
this.currency = currency;
}
/**
* Returns a jquery object like $() in javascript
* @deprecated
*/
public static inline function j(r:Dynamic):js.JQuery {
return new js.JQuery(r);
}
public static inline function jq(r:Dynamic):js.jquery.JQuery{
return new js.jquery.JQuery(r);
}
/**
* The JS App will be available as "_" in the document.
*/
public static function main() {
//untyped js.Browser.window.$ = js.Lib.require("jQuery");
untyped js.Browser.window._ = new App();
}
public function getCart() {
return new ShopCart();
}
public function getTagger(cid:Int ) {
return new Tagger(cid);
}
public function getTuto(name:String, step:Int) {
new Tuto(name,step);
}
/**
* remove method for IE compat
*/
public function remove(el:js.html.Element){
if (el == null) return;
el.parentElement.removeChild(el);
}
public function getVATBox(ttcprice:Float,currency:String,rates:String,vat:Float,formName:String){
var input = js.Browser.document.querySelector('form input[name="${formName}_price"]');
remove( js.Browser.document.querySelector('form input[name="${formName}_vat"]').parentElement.parentElement );
ReactDOM.render(jsx('<$VATBox ttc="$ttcprice" currency="$currency" vatRates="$rates" vat="$vat" formName="$formName"/>'), input.parentElement);
//remove(input);
}
/*public function getProductComposer(){
//js.Browser.document.addEventListener("DOMContentLoaded", function(event) {
//ReactDOM.render(jsx('<$ComposerApp/>'), js.Browser.document.getElementById("app"));
//});
}*/
/**
* Removes the form element and replace it by a react js component
* @param divId
* @param productName
* @param txpProductId
* @param formName
*/
public function getProductInput(divId:String, productName:String, txpProductId:String, formName:String ){
js.Browser.document.addEventListener("DOMContentLoaded", function(event) {
//dirty stuff to remove "real" input, and replace it by the react one
App.j("form input[name='"+formName+"_name']").parent().parent().remove();
App.j("form select[name='" + formName+"_txpProductId']").parent().parent().remove();
if (txpProductId == null) txpProductId = "";
ReactDOM.render(jsx('<$ProductInput productName="$productName" txpProductId="$txpProductId" formName="$formName"/>'), js.Browser.document.getElementById(divId));
});
}
public function initReportHeader(){
ReactDOM.render(jsx('<$ReportHeader />'), js.Browser.document.querySelector('div.reportHeaderContainer'));
}
public function initOrderBox(userId:Int, distributionId:Int, contractId:Int, contractType:Int, date:String, place:String, userName:String, currency:String, hasPayments:Bool,callbackUrl:String){
untyped App.j("#myModal").modal();
var onValidate = function() js.Browser.location.href = callbackUrl;
var node = js.Browser.document.querySelector('#myModal .modal-body');
ReactDOM.unmountComponentAtNode(node); //the previous modal DOM element is still there, so we need to destroy it
ReactDOM.render(jsx('<$OrderBox userId="$userId" distributionId="$distributionId"
contractId="$contractId" contractType="$contractType" date="$date" place="$place" userName="$userName"
onValidate=$onValidate currency=$currency hasPayments=$hasPayments />'),node,postReact);
}
function postReact(){
trace("post react");
haxe.Timer.delay(function(){
untyped jq('[data-toggle="tooltip"]').tooltip();
untyped jq('[data-toggle="popover"]').popover();
},500);
}
public static function roundTo(n:Float, r:Int):Float {
return Math.round(n * Math.pow(10,r)) / Math.pow(10,r) ;
}
/**
* Ajax loads a page and display it in a modal window
* @param url
* @param title
*/
public function overlay(url:String,?title,?large=true) {
if (title != null) title = StringTools.urlDecode(title);
var r = new haxe.Http(url);
r.onData = function(data) {
//setup body and title
var m = App.j("#myModal");
m.find(".modal-body").html(data);
if (title != null) m.find(".modal-title").html(title);
if (!large) m.find(".modal-dialog").removeClass("modal-lg");
untyped App.j('#myModal').modal(); //bootstrap 3 modal window
}
r.request();
}
/**
* Displays a login box
*/
public function loginBox(redirectUrl:String,?message:String,?phoneRequired=false) {
var m = App.j("#myModal");
m.find(".modal-title").html("S'identifier");
m.find(".modal-dialog").removeClass("modal-lg");
untyped m.modal();
ReactDOM.render(jsx('<$LoginBox redirectUrl="$redirectUrl" message=$message phoneRequired="$phoneRequired"/>'), js.Browser.document.querySelector('#myModal .modal-body'));
return false;
}
/**
* Displays a sign up box
*/
public function registerBox(redirectUrl:String,?message:String,?phoneRequired=false) {
var m = App.j("#myModal");
m.find(".modal-title").html("S'inscrire");
m.find(".modal-dialog").removeClass("modal-lg");
untyped m.modal();
ReactDOM.render(jsx('<$RegisterBox redirectUrl="$redirectUrl" message=$message phoneRequired="$phoneRequired"/>'), js.Browser.document.querySelector('#myModal .modal-body'));
return false;
}
public function shop(place:Int, date:String) {
ReactDOM.render(jsx('<$Store date=$date place=$place/>'), js.Browser.document.querySelector('#shop'));
}
public function groupMap(lat:String,lng:String,address:String) {
ReactDOM.render(jsx('<$GroupMapRoot lat="$lat" lng="$lng" address="$address"/>'), js.Browser.document.querySelector('#map'));
}
/**
* Helper to get values of a bunch of checked checkboxes
* @param formSelector
*/
public function getCheckboxesId(formSelector:String):Array<String>{
var out = [];
var checkboxes = js.Browser.document.querySelectorAll(formSelector + " input[type=checkbox]");
for ( input in checkboxes ){
var input : js.html.InputElement = cast input;
if ( input.checked ) out.push(input.value);
}
return out;
}
#if plugins
public function getHostedPlugin(){
return new hosted.js.App();
}
#end
/**
* set up a warning message when leaving the page
*/
public function setWarningOnUnload(active:Bool, ?msg:String){
if (active){
js.Browser.window.addEventListener("beforeunload", warn);
}else{
js.Browser.window.removeEventListener("beforeunload", warn);
}
}
function warn(e:js.html.Event) {
var msg = "Voulez vous vraiment quitter cette page ?";
//js.Browser.window.confirm(msg);
untyped e.returnValue = msg; //Gecko + IE
e.preventDefault();
return msg; //Gecko + Webkit, Safari, Chrome etc.
}
/**
* Anti Doubleclick with btn elements.
* Can be bypassed by adding a .btn-noAntiDoubleClick class
*/
public function antiDoubleClick(){
for( n in js.Browser.document.querySelectorAll(".btn:not(.btn-noAntiDoubleClick)") ){
n.addEventListener("click",function(e:js.html.MouseEvent){
var x = untyped e.target;
x.classList.add("disabled");
haxe.Timer.delay(function(){
x.classList.remove("disabled");
},1000);
});
}
}
}
+445
View File
@@ -0,0 +1,445 @@
import Common;
import js.JQuery;
/**
* JS Shopping Cart
*
* @author fbarbut<francois.barbut@gmail.com>
*/
class ShopCart
{
public var products : Map<Int,ProductInfo>; //product db
public var productsArray : Array<ProductInfo>; //to keep order of products
public var categories : Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>; //categ db
public var pinnedCategories : Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>; //categ db
public var order : OrderInSession;
var loader : JQuery; //ajax loader gif
//for scroll mgmt
var cartTop : Int;
var cartLeft : Int;
var cartWidth : Int;
var jWindow : JQuery;
var cartContainer : JQuery;
var date : String;
var place : Int;
public function new()
{
products = new Map();
productsArray = [];
order = cast { products:[] };
categories = [];
pinnedCategories = [];
}
public function add(pid:Int) {
loader.show();
var q = App.j('#productQt' + pid).val();
var qt = 0.0;
var p = this.products.get(pid);
if (p.hasFloatQt) {
q = StringTools.replace(q, ",", ".");
qt = Std.parseFloat(q);
}else {
qt = Std.parseInt(q);
}
if (qt == null) {
qt = 1;
}
//trace("qté : "+qt);
//add server side
var r = new haxe.Http('/shop/add/$pid/$qt');
r.onData = function(data:String) {
loader.hide();
var d = haxe.Json.parse(data);
if (!d.success) js.Browser.alert("Erreur : "+d);
//add locally
subAdd(pid, qt);
render();
}
r.request();
}
function subAdd(pid, qt:Float ) {
for ( p in order.products) {
if (p.productId == pid) {
p.quantity += qt;
render();
return;
}
}
order.products.push( { productId:pid, quantity:qt } );
}
/**
* Render the shopping cart and total
*/
function render() {
var c = App.j("#cart");
c.empty();
//render items in shopping cart
c.append( Lambda.map(order.products, function( x ) {
var p = this.products.get(x.productId);
if (p == null) {
//the product may have been disabled by an admin
return "";
}
var btn = "<a onClick='cart.remove(" + p.id + ")' class='btn btn-default btn-xs' data-toggle='tooltip' data-placement='top' title='Retirer de la commande'><span class='glyphicon glyphicon-remove'></span></a>&nbsp;";
return "<div class='row'>
<div class = 'order col-md-9' > <b> " + x.quantity + " </b> x " + p.name+" </div>
<div class = 'col-md-3'> "+btn+"</div>
</div>";
}).join("\n") );
//compute total price
var total = 0.0;
for (p in order.products) {
var pinfo = products.get(p.productId);
if (pinfo == null) continue;
total += p.quantity * pinfo.price;
}
var ffilter = new sugoi.form.filters.FloatFilter();
var total = ffilter.filterString(Std.string(App.roundTo(total,2)));
c.append("<div class='total'>TOTAL : " + total + "</div>");
if (order.products.length > 0){
App.instance.setWarningOnUnload(true,"Vous avez une commande en cours. Si vous quittez cette page sans confirmer, votre commande sera perdue.");
}else{
App.instance.setWarningOnUnload(false);
}
}
function findCategoryName(cid:Int):String{
for ( cg in this.categories ){
for (c in cg.categs){
if (cid == c.id) {
return c.name;
}
}
}
for ( cg in this.pinnedCategories ){
for (c in cg.categs){
if (cid == c.id) {
return c.name;
}
}
}
return null;
}
/**
* Dynamically sort products by categories
*/
public function sortProductsBy(){
//store products by groups
var groups = new Map<Int,{name:String,products:Array<ProductInfo>}>();
var pinned = new Map<Int,{name:String,products:Array<ProductInfo>}>();
var firstCategGroup = this.categories[0].categs;
//trace(firstCategGroup);
//trace(pinnedCategories);
var pList = this.productsArray.copy();
//for ( p in pList) trace(p.name+" : " + p.categories);
//trace("----------------");
//sort by categs
for ( p in pList.copy() ){
//trace(p.name+" : " + p.categories);
untyped p.element.remove();
for ( categ in p.categories){
if (Lambda.find(firstCategGroup, function(c) return c.id == categ) != null){
//is in this category group
var g = groups.get(categ);
if ( g == null){
var name = findCategoryName(categ);
g = {name:name,products:[]};
}
g.products.push(p);
//trace("remove " + p.name);
pList.remove(p);
groups.set(categ, g);
}
else{
// is in pinned group ?
var isInPinnedCateg = false;
for ( cg in pinnedCategories){
if (Lambda.find(cg.categs, function(c) return c.id == categ) != null){
isInPinnedCateg = true;
break;
}
}
if (isInPinnedCateg){
var c = pinned.get(categ);
if ( c == null){
var name = findCategoryName(categ);
c = {name:name,products:[]};
}
c.products.push(p);
//trace( "add " + p.name+" in PINNED");
pList.remove(p);
pinned.set(categ, c);
}else{
//not in the selected categ nor in pinned groups
continue;
}
}
}
}
//if some untagged products remain
if (pList.length > 0){
groups.set(0,{name:"Autres",products:pList});
}
//trace("----------------");
//render
var container = App.j(".shop .body");
//render firts "pinned" groups , then "groups"
for ( source in [pinned, groups]){
for (o in source){
if (o.products.length == 0) continue;
container.append("<div class='col-md-12 col-xs-12 col-sm-12 col-lg-12'><div class='catHeader'>" + o.name + "</div></div>");
for ( p in o.products){
//trace("GROUP "+o.name+" : "+p.name);
//if the element has already been inserted, we need to clone it
if (untyped p.element.parent().length == 0){
container.append( untyped p.element );
}else{
var clone = untyped p.element.clone();
container.append( clone );
}
}
}
}
App.j(".product").show();
}
/**
* is shopping cart empty ?
*/
public function isEmpty(){
return order.products.length == 0;
}
/**
* submit cart
*/
public function submit() {
var req = new haxe.Http("/shop/submit");
req.onData = function(d) {
App.instance.setWarningOnUnload(false);
js.Browser.location.href = "/shop/validate/"+place+"/"+date;
}
req.addParameter("data", haxe.Json.stringify(order));
req.request(true);
}
/**
* filter products by category
*/
public function filter(cat:Int) {
//icone sur bouton
App.j(".tag").removeClass("active").children().remove("span");//clean
var bt = App.j("#tag" + cat);
bt.addClass("active").prepend("<span class ='glyphicon glyphicon-ok'></span> ");
//affiche/masque produits
for (p in products) {
if (cat==0 || Lambda.has(p.categories, cat)) {
App.j(".shop .product" + p.id).fadeIn(300);
}else {
App.j(".shop .product" + p.id).fadeOut(300);
}
}
}
/**
* remove a product from cart
* @param pid
*/
public function remove(pid:Int ) {
loader.show();
//add server side
var r = new haxe.Http('/shop/remove/$pid');
r.onData = function(data:String) {
loader.hide();
var d = haxe.Json.parse(data);
if (!d.success) js.Browser.alert("Erreur : "+d);
//remove locally
for ( p in order.products.copy()) {
if (p.productId == pid) {
order.products.remove(p);
render();
return;
}
}
render();
}
r.request();
}
/**
* loads products DB and existing cart in ajax
*/
public function init(place:Int,date:String) {
this.place = place;
this.date = date;
loader = App.j("#cartContainer #loader");
var req = new haxe.Http("/shop/init/"+place+"/"+date);
req.onData = function(data) {
loader.hide();
var data : {
products:Array<ProductInfo>,
categories:Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>,
order:OrderInSession } = haxe.Unserializer.run(data);
//populate local categories lists
for ( cg in data.categories){
if (cg.pinned){
pinnedCategories.push(cg);
}else{
categories.push(cg);
}
}
//product DB
for (p in data.products) {
//catch dom element for further usage
untyped p.element = App.j(".product"+p.id);
var id : Int = p.id;
//var id : Int = p.id;
//id = id + 1;
this.products.set(id, p);
this.productsArray.push(p);
//trace(p.name+" : " + p.categories);
}
//existing order
for ( p in data.order.products) {
subAdd(p.productId,p.quantity );
}
render();
sortProductsBy();
}
req.request();
//DISABLED : pb quand le panier est plus haut que l'ecran
//scroll mgmt, only for large screens. Otherwise let the cart on page bottom.
/*if (js.Browser.window.matchMedia("(min-width: 1024px)").matches) {
jWindow = App.j(js.Browser.window);
cartContainer = App.j("#cartContainer");
cartTop = cartContainer.position().top;
cartLeft = cartContainer.position().left;
cartWidth = cartContainer.width();
jWindow.scroll(onScroll);
}*/
}
/**
* keep the cart on top when scrolling
* @param e
*/
public function onScroll(e:Dynamic) {
//cart container top position
if (jWindow.scrollTop() > cartTop) {
//trace("absolute !");
cartContainer.addClass("scrolled");
cartContainer.css('left', Std.string(cartLeft) + "px");
cartContainer.css('top', Std.string(/*cartTop*/10) + "px");
cartContainer.css('width', Std.string(cartWidth) + "px");
}else {
cartContainer.removeClass("scrolled");
cartContainer.css('left',"");
cartContainer.css('top', "");
cartContainer.css('width', "");
}
}
}
Executable
+151
View File
@@ -0,0 +1,151 @@
package ;
import Common;
import js.JQuery;
/**
*
* Tag products with categories
*
* @author fbarbut<francois.barbut@gmail.com>
*/
@:keep
class Tagger
{
var contractId : Int;
var data:TaggerInfos;
var pids : Array<Int>; //selected product Ids
public function new(cid:Int)
{
contractId = cid;
pids = [];
}
public function init() {
var req = new haxe.Http("/product/categorizeInit/"+contractId);
req.onData = function(_data) {
data = haxe.Json.parse(_data);
render();
}
req.request();
}
function render() {
var html = new StringBuf();
html.add("<table class='table'>");
for (p in data.products) {
html.add("<tr class='p"+p.product.id+"'>");
var checked = Lambda.has(pids,p.product.id) ? "checked" : "";
html.add('<td><input type="checkbox" name="p${p.product.id}" $checked/></td>');
html.add("<td>" + p.product.name+"</td>");
var tags = [];
//trace('product tags ${p.categories} from tags ${data.categories}');
for (c in p.categories) {
//trouve le nom du tag
var name = "";
var color = "";
for (gc in data.categories) {
for ( t in gc.tags) {
if (c == t.id) {
name = t.name;
color = gc.color;
break;
}
}
}
//var bt = App.jq("<a>[X]</a>")
tags.push("<span class='tag t"+c+"' style='background-color:"+color+";cursor:pointer;'>"+name+"</span>");
}
html.add("<td class='tags'>"+ tags.join(" ") +"</td>");
html.add("</tr>");
}
html.add("</table>");
App.jq("#tagger").html(html.toString());
App.jq("#tagger .tag").click(function(e) {
var el : js.html.Element = cast e.currentTarget;
//find tag Id
var tid = Std.parseInt(el.getAttribute('class').split(" ")[1].substr(1));
//find product Id
var pid = Std.parseInt(el.parentElement.parentElement.getAttribute('class').substr(1));
//remove element
el.remove();
//datas
remove(tid,pid);
});
}
public function add() {
var tagId = Std.parseInt(App.jq("#tag").val());
if (tagId == 0) js.Browser.alert("Impossible de trouver la catégorie selectionnée");
pids = [];
for ( e in App.jq("#tagger input:checked").elements() ) {
pids.push(Std.parseInt(e.attr("name").substr(1)));
}
if (pids.length == 0) js.Browser.alert("Sélectionnez un produit afin de pouvoir lui attribuer une catégorie");
for (p in pids) {
addTag(tagId, p);
}
render();
}
public function remove(tagId:Int,productId:Int) {
//data
for ( p in data.products) {
if (p.product.id == productId) {
for ( t in p.categories) {
if (t == tagId) p.categories.remove(t);
}
}
}
}
function addTag(tagId:Int, productId:Int) {
//check for doubles
for ( p in data.products) {
if (p.product.id == productId) {
for (t in p.categories) {
if (t == tagId) return;
}
}
}
//data
for ( p in data.products) {
if (p.product.id == productId) {
p.categories.push(tagId);
break;
}
}
}
public function submit() {
var req = new haxe.Http("/product/categorizeSubmit/" + contractId);
req.addParameter("data", haxe.Json.stringify(data));
req.onData = function(_data) {
js.Browser.alert(_data);
}
req.request(true);
}
}
+131
View File
@@ -0,0 +1,131 @@
package;
import Common;
/**
* Tutorial javascript widget
* @author fbarbut<francois.barbut@gmail.com>
*/
class Tuto
{
var name:String;
var step:Int;
static var LAST_ELEMENT :String = null; //last hightlit element
public function new(name:String, step:Int){
this.name = name;
this.step = step;
TutoDatas.get(name, init);
}
/**
* asyn init
* @param tuto
*/
function init(tuto)
{
var s = tuto.steps[step];
//close previous popovers
var p = App.jq(".popover");
untyped p.popover('hide');
var t = App.instance.t;
if (t == null) trace("Gettext translator is null");
if (s == null) {
/**
* tutorial is finished : display a modal window
*/
var m = App.jq('#myModal');
untyped m.modal('show');
m.addClass("help");
m.find(".modal-header").html("<span class='glyphicon glyphicon-hand-right'></span> "+tuto.name);
m.find(".modal-body").html("<span class='glyphicon glyphicon-ok'></span> "+t._("This tutorial is over."));
var bt = App.jq("<a class='btn btn-default'><span class='glyphicon glyphicon-chevron-right'></span> "+t._("Come back to tutorials page")+"</a>");
bt.click(function(?_) {
untyped m.modal('hide');
js.Browser.location.href = "/contract?stopTuto=1";
});
m.find(".modal-footer").append(bt);
m.find(".modal-dialog").removeClass("modal-lg"); //small window pls
}else if (s.element == null) {
/**
* no element, make a modal window (usually its the first step of the tutorial)
*/
var m = App.jq('#myModal');
untyped m.modal('show');
m.addClass("help");
m.find(".modal-body").html(s.text);
m.find(".modal-header").html("<span class='glyphicon glyphicon-hand-right'></span> "+tuto.name);
var bt = App.jq("<a class='btn btn-default'><span class='glyphicon glyphicon-chevron-right'></span> "+t._("OK")+"</a>");
bt.click(function(?_) {
untyped m.modal('hide');
new Tuto(name, step + 1);
});
m.find(".modal-footer").append(bt);
m.find(".modal-dialog").removeClass("modal-lg"); //small window pls
}else {
//prepare Bootstrap "popover"
var x = App.jq(s.element).first().attr("title", tuto.name+" <div class='pull-right'>"+(step+1)+"/"+tuto.steps.length+"</div>");
var text = "<p>" + s.text + "</p>";
var bt = null;
switch(s.action) {
case TANext :
bt = App.jq("<p><a class='btn btn-default btn-sm'><span class='glyphicon glyphicon-chevron-right'></span> "+t._("Next")+"</a></p>");
bt.click(function(?_) {
//untyped m.modal('hide');
new Tuto(name, step + 1);
if(LAST_ELEMENT!=null) App.jq(s.element).removeClass("highlight");
});
default:
}
//configure and open popover
var p = switch(s.placement) {
case TPTop: "top";
case TPBottom : "bottom";
case TPLeft : "left";
case TPRight : "right";
default : null;
}
var options = { container:"body", content:text, html:true , placement:p};
untyped x.popover(options).popover('show');
//add a footer
var footer = App.jq("<div class='footer'><div class='pull-left'></div><div class='pull-right'></div></div>");
if (bt != null) footer.find(".pull-right").append(bt);
footer.find(".pull-left").append(makeCloseButton(t._('Stop')));
App.jq(".popover .popover-content").append(footer);
//highlight
App.jq(s.element).first().addClass("highlight");
LAST_ELEMENT = s.element;
}
}
/**
* Make a "close" bt
*/
function makeCloseButton(?text) {
var bt = App.jq("<a class='btn btn-default btn-sm'><span class='glyphicon glyphicon-remove'></span> "+(text==null?"":text)+"</a>");
bt.click(function(?_) {
js.Browser.location.href = "/contract?stopTuto=1";
});
return bt;
}
}
+28
View File
@@ -0,0 +1,28 @@
//
// npm dependencies library
//
(function(scope) {
'use-strict';
scope.__registry__ = Object.assign({}, scope.__registry__, {
//
// list npm modules required in Haxe
//
'bootstrap': require('bootstrap'),
'react': require('react'),
'react-dom': require('react-dom'),
'react-bootstrap-typeahead': require('react-bootstrap-typeahead'),
'react-datetime': require('react-datetime'),
'leaflet': require('leaflet'),
'react-leaflet': require('react-leaflet'),
'react-places-autocomplete': require('react-places-autocomplete'),
'geolib': require('geolib'),
'react-router':require('react-router'),
'react-router-dom':require('react-router-dom'),
});
if (process.env.NODE_ENV !== 'production') {
// enable React hot-reload
require('haxe-modular');
}
})(typeof $hx_scope != "undefined" ? $hx_scope : $hx_scope = {});
+7
View File
@@ -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 {}
+29
View File
@@ -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>
');
}
}
+26
View File
@@ -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>
');
}
}
+94
View File
@@ -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);
}
}
+17
View File
@@ -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{}
+125
View File
@@ -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;
}
}
+252
View File
@@ -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="&amp;copy <a href=&quot;http://osm.org/copyright&quot;>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;
}
}
+312
View File
@@ -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>&nbsp;
<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">&nbsp;</div>
<div className="col-md-9">
<div className="form-group-container">
Trouvez un groupe Cagette près de chez vous &nbsp;
<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>');
}
}
+112
View File
@@ -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});
});
*/
}
}
+179
View File
@@ -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)}&nbsp;${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 !');
}*/
}
}
+224
View File
@@ -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>
&nbsp;
<$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();
}
}
+104
View File
@@ -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();
}
}*/
}
+39
View File
@@ -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} &euro;
</div>
</div>');
}
}
+168
View File
@@ -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;
}
}
+41
View File
@@ -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);
}
}
}
+103
View File
@@ -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>
');
}
}
+45
View File
@@ -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>
');
});
}
}
+65
View File
@@ -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>
');
}
}
+73
View File
@@ -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}/>
');
});
}
}
+229
View File
@@ -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>
');
}
}
+139
View File
@@ -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>&nbsp;&nbsp;
<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();
}
}
+165
View File
@@ -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});
}
}
+20
View File
@@ -0,0 +1,20 @@
package test;
import js.Browser;
import utest.Runner;
import utest.ui.Report;
import test.utils.TestCartUtils;
class TestAll
{
public static function main()
{
var runner = new Runner();
// Utils
runner.addCase(new TestCartUtils());
Report.create(runner);
runner.run();
}
}
+44
View File
@@ -0,0 +1,44 @@
[
{
"contractTaxName": null,
"desc": "Des grattons fait avec du cochon gascon, élevé en plein air et selon les préceptes de l'agriculture biologique. Meilleur que des rillettes!",
"vatValue": 0.234597156398104,
"price": 4.5,
"name": "Gratton de porc noir",
"type": 0,
"contractTax": null,
"contractId": 4672,
"orderable": true,
"hasFloatQt": false,
"id": 56610,
"qt": 190,
"unitType": 2,
"ref": "GRA-1",
"vat": 5.5,
"categories": null,
"stock": null,
"organic": false,
"image": "/img/taxo/cat10.png"
},
{
"contractTaxName": null,
"desc": "Un pâté savoureux qui se mange sans modération! Date de péremption : 1 Avril 2019 Lot : L16AV01P Poids : 200gr Ingrédients : Viande de porc issue de lagriculture biologique, Sel, Poivre",
"vatValue": 0.297156398104265,
"price": 5.7,
"name": "Pâté de porc noir",
"type": 0,
"contractTax": null,
"contractId": 4672,
"orderable": true,
"hasFloatQt": false,
"id": 56611,
"qt": 1,
"unitType": 0,
"ref": "PAPN-1",
"vat": 5.5,
"categories": null,
"stock": null,
"organic": false,
"image": "/file/2521_6a77c954191e5a2d8cd5a5d8cade4f6b.PNG"
}
]
+58
View File
@@ -0,0 +1,58 @@
package test.utils;
import sys.io.File.getContent;
import haxe.Json.parse;
import utest.Assert;
import utils.CartUtils;
import Common;
class TestCartUtils
{
static var products:Array<ProductInfo>;
public function new() {
var http = new haxe.Http("localhost/js/test/mocks.json?format=json");
products = parse(getContent("js/test/mocks.json"));
}
public function testAddToCart()
{
var order = {
products: [{
product: products[0],
quantity: 1
}],
total: products[0].price
};
var newProduct = products[1];
order = CartUtils.addToCart(order, newProduct, 1);
Assert.equals(order.products.length, 2);
Assert.equals(order.total, products[0].price + products[1].price);
order = CartUtils.addToCart(order, newProduct, 3);
Assert.equals(order.products[1].quantity, 4);
Assert.equals(order.total, products[0].price + 4 * products[1].price);
}
public function testRemoveFromCart()
{
var order = {
products: [{
product: products[0],
quantity: 5
}, {
product: products[1],
quantity: 3
}],
total: 5 * products[0].price + 3 * products[1].price
};
order = CartUtils.removeFromCart(order, products[0], 2);
Assert.equals(order.products[0].quantity, 3);
Assert.equals(order.total, 3 * products[0].price + 3 * products[1].price);
order = CartUtils.removeFromCart(order, products[1]);
Assert.equals(order.products.length, 1);
Assert.equals(order.total, 3 * products[0].price);
}
}
+64
View File
@@ -0,0 +1,64 @@
package utils;
import Math;
using Lambda;
import Common;
class CartUtils {
public static function addToCart(order:OrderSimple, productToAdd:ProductInfo, quantity:Int):OrderSimple {
var products = order.products.copy();
var total = order.total;
var existingProduct = products.find(function(p) {
return p.product.id == productToAdd.id;
});
if (existingProduct == null)
products.push({
product: productToAdd,
quantity: quantity
});
else
existingProduct.quantity += quantity;
total += quantity * productToAdd.price;
total = Math.round(total * 100) / 100; // to avoid calculation errors
return {
products: products,
total: total
};
}
public static function removeFromCart(order:OrderSimple, productToRemove:ProductInfo, ?quantity:Int):OrderSimple {
var products = order.products.copy();
var total = order.total;
var existingProduct = products.find(function(p) {
return p.product.id == productToRemove.id;
});
if (quantity == null)
quantity = existingProduct.quantity;
if (existingProduct == null)
throw "Can't remove a non existing product";
else if (quantity >= existingProduct.quantity)
products.remove(existingProduct)
else
existingProduct.quantity -= quantity;
if (products.length == 0)
total = 0;
else {
total -= Math.min(quantity, existingProduct.quantity) * productToRemove.price;
total = Math.round(total * 100) / 100; // to avoid calculation errors
}
return {
products: products,
total: total
};
}
}
+115
View File
@@ -0,0 +1,115 @@
package utils;
import haxe.Json;
import js.Promise;
import js.html.XMLHttpRequest;
@:enum abstract HttpMethod(String) to String {
var POST = 'POST';
var GET = 'GET';
var HEAD = 'HEAD';
var PUT = 'PUT';
var DELETE = 'DELETE';
var TRACE = 'TRACE';
var OPTIONS = 'OPTIONS';
var CONNECT = 'CONNECT';
var PATCH = 'GET';
}
@:enum abstract FetchFormat(String) from String to String {
var PLAIN_TEXT = "text/plain";
var JSON = "application/json";
}
//json version of a tink.core.Error
typedef ErrorInfos = {error:{code:Int,message:String,stack:String}}
/**
* Manage HTTP request to a REST API.
*
* POST requests can only have a single JSON object (payload)
*/
class HttpUtil
{
static public function fetch(
url: String,
?method: HttpMethod = GET,
?params: Dynamic = null,
?accept: FetchFormat = PLAIN_TEXT,
?contentType: String = JSON
): Promise<Dynamic> {
return new Promise(function(resolve: Dynamic->Void, reject) {
var data: String = null;
if (params != null)
{
if (params.body != null){
data = Json.stringify(params.body);
} else if(method==POST) {
data = Json.stringify(params);
} else {
url += (url.indexOf('?') > -1) ? '&' : '?';
url += objToString(params);
}
}
var http = new XMLHttpRequest();
http.open(method, url, true);
if (contentType != null && contentType.length > 0)
http.setRequestHeader("Content-type", contentType);
if (accept != null)
http.setRequestHeader("Accept", accept);
http.onreadystatechange = function() {
//trace("readystate",http.readyState, http.status);
if (http.readyState == 4){
switch (http.status){
case 200:
switch (accept){
case JSON:
try {
var json = Json.parse(http.responseText);
resolve(json);
} catch (err: Dynamic){
reject(err);
}
default:
resolve(http.responseText);
}
case 204:
resolve(true);
default:
reject(http.responseText);
}
}
};
http.send(data);
});
}
static public function objToString(obj: Dynamic): String
{
var str = "";
var cpt = 0;
for (key in Reflect.fields(obj))
{
var value: Dynamic = Reflect.field(obj, key);
if (value == null) continue;
if (cpt++ > 0)
str += "&";
if (Std.is(value, Array) && value.length > 0)
str += '$key=${value.join(";")}';
else if (Std.string(value) != "") // String / Int / Float
str += '$key=${StringTools.trim(Std.string(value))}';
}
return str;
}
}