code from amapei
This commit is contained in:
@@ -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
@@ -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> ";
|
||||
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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = {});
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 l’agriculture 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"
|
||||
}
|
||||
]
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user