code from amapei

This commit is contained in:
cagette@ct8
2020-09-26 18:25:18 +00:00
parent 42fe367911
commit 72b4699871
1966 changed files with 220437 additions and 2 deletions
Executable
+37
View File
@@ -0,0 +1,37 @@
*.n
*.php
config.xml
www/plugin/*
Makefile
tmp/*
*node_modules*
*.map
.haxelib
html
.temp
*.tar
lang/master/tpl/plugin/*
lang/fr/tpl/*
lang/en/tpl/*
lang/de/tpl/*
lang/es/tpl/*
lang/fr/tmp/*.n
lang/en/tmp/*.n
lang/de/tmp/*.n
lang/es/tmp/*.n
www/lang/*.mo
www/file/*
www/js/app.js
www/js/libs.js
www/css/.sass-cache
www/css/*.css
Executable
+340
View File
@@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+93
View File
@@ -0,0 +1,93 @@
ROPTS=-zav --no-p --chmod=u=rw,g=rw,o=r --delete --exclude=www/.htaccess --exclude=.svn --exclude=.git --exclude=*.mtt --exclude=tpl/css --exclude=www/file --exclude=*node_modules* --exclude=*.php --exclude=tests.n
LANG=fr
PLUGINS?=0
ENV?="dev"
#To compile project with plugins, add PLUGINS=1 or type `export PLUGINS=1`
install:
#copy config file from template
cp config.xml.dist config.xml
@if [ $(ENV) = "dev" ]; then \
make install_dev; \
fi
#setup dev environnement from source
install_dev:
#set config file to debug=1
@sed -e 's?debug=.*?debug="1"?g' --in-place config.xml
#install haxe dependencies in .haxelib
haxelib newrepo
haxelib -always install cagette.hxml
haxelib -always install cagetteJs.hxml
#template tools
haxelib run templo
sudo mv temploc2 /usr/bin
#extra plugins
@if [ $(PLUGINS) = 1 ]; then \
haxelib git cagette-pro git@bitbucket.org:bubar/cagette-pro.git; \
haxelib git cagette-hosted git@bitbucket.org:bubar/cagette-hosted.git; \
haxelib git cagette-wholesale-order git@github.com:bablukid/cagette-wholesale-order.git; \
fi
#install npm dependencies
npm install
npm run libs:dev
npm run build:js
#compile
@make css
@make compile
@make frontend
@echo "Well, it looks like everything is fine : librairies are installed, backend and frontend has been compiled !"
#compile backend to Neko
compile:
@if [ $(PLUGINS) = 1 ]; then \
echo "compile Cagette.net with plugins"; \
haxe cagetteAllPlugins.hxml; \
else \
echo "compile Cagette.net core"; \
haxe cagette.hxml; \
fi
#compile SASS files to CSS
css:
npm run build:sass
#compile frontend to JS
frontend:
@if [ $(PLUGINS) = 1 ]; then \
haxe cagetteAllPluginsJs.hxml; \
else \
haxe cagetteJs.hxml; \
fi
#update POT file from source
i18n:
haxe potGeneration.hxml
#compile templates in each language, required for production env.
templates:
haxe cagette.hxml -D i18n_generation
@make LANG=fr ctemplates
@make LANG=en ctemplates
@make LANG=de ctemplates
ctemplates:
(cd lang/$(LANG)/tpl; temploc2 -macros macros.mtt -output ../tmp/ *.mtt */*.mtt */*/*.mtt */*/*/*.mtt */*/*/*/*.mtt)
#Compile and run unit tests
tests:
@if [ $(PLUGINS) = 1 ]; then \
haxe testsPlugins.hxml; \
else \
haxe tests.hxml; \
fi
@docker-compose run --workdir="/var/www/www" neko neko tests.n mysql://root:root@mysql/tests
cp_plugin:
cp -R .haxelib/cagette-hosted/git/tpl/* lang/master/tpl/plugin/hosted/
cp -R .haxelib/cagette-pro/git/tpl/* lang/master/tpl/plugin/pro/
cp -R .haxelib/cagette-wholesale-order/git/tpl/* lang/master/tpl/plugin/who/
Regular → Executable
+17 -2
View File
@@ -1,2 +1,17 @@
CAGETTE from Artcode # Cagette.net
==================== Le logiciel libre des circuits courts.
[![Build Status](https://travis-ci.org/bablukid/cagette.svg?branch=master)](https://travis-ci.org/bablukid/cagette)
http://www.cagette.net
Documentation : http://www.cagette.net/wiki
English :
Cagette.net is an open source web app to help people build a better and sustainable food system.
Some people call it a "foodhub" : A mix between a groupware and a marketplace, helping consumers to order food from local farmers and producers.
## Build & install
Read the Developer page on the wiki to know more about this : http://wiki.cagette.net/dev:index
+11
View File
@@ -0,0 +1,11 @@
<VirtualHost *:80>
ServerName cagette
DirectoryIndex index.n
DocumentRoot %TRAVIS_BUILD_DIR%/www
<Directory %TRAVIS_BUILD_DIR%/>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
+22
View File
@@ -0,0 +1,22 @@
-lib dbadmin
-lib templo
-lib weblog
-lib sugoi
-lib hxevents
-lib cagette-hosted
-lib hscript
-lib ufront-mail
-cp common
-cp src
-php www/
-D spod_macro
-main App
-D debugip=localhost:18080
-D weblogid=cagette
--next
-dce no
-cp common
-cp js
-lib sugoi
-main App
-js www/js/app.js
+18
View File
@@ -0,0 +1,18 @@
-lib templo:3.0.0
-lib hxevents:0.4.0
-lib thx.semver:0.2.2
-lib thx.csv:0.2.0
-lib datetime:3.1.1
-lib hscript:2.3.0
-lib smtpmailer:0.4.0
-lib tink_json:0.9.0
-lib sugoi:git:git@artcode.re:cagette/sugoi.git
-lib record-macros:git:git@artcode.re:cagette/record-macros.git
-lib dbadmin:git:git@artcode.re:cagette/dbadmin.git
-cp common
-cp src
-cp lang/master/tpl
-neko www/index.n
-main App
+26
View File
@@ -0,0 +1,26 @@
-lib templo
-lib hxevents
-lib thx.semver
-lib thx.csv
-lib datetime
-lib hscript
-lib smtpmailer
-lib tink_json
-lib sugoi:git:https://github.com/bablukid/sugoi#master
-lib record-macros:git:https://github.com/HaxeFoundation/record-macros#master
-lib dbadmin:git:https://github.com/ncannasse/dbadmin.git#master
-lib cagette-pro:git:https://bubar@bitbucket.org/bubar/cagette-pro.git#master
-lib cagette-hosted:git:https://bubar@bitbucket.org/bubar/cagette-hosted.git#master
-lib cagette-wholesale-order:git:https://github.com/bablukid/cagette-wholesale-order.git#master
-cp common
-cp src
-cp lang/master/tpl
-D plugins
-neko www/index.n
-main App
+76
View File
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<project version="2">
<!-- Output SWF options -->
<output>
<movie outputType="Application" />
<movie input="" />
<movie path="www\index.n" />
<movie fps="30" />
<movie width="800" />
<movie height="600" />
<movie version="0" />
<movie minorVersion="0" />
<movie platform="Neko" />
<movie background="#FFFFFF" />
</output>
<!-- Other classes to be compiled into your SWF -->
<classpaths>
<class path="common" />
<class path="src" />
<class path="lang\master\tpl" />
</classpaths>
<!-- Build options -->
<build>
<option directives="plugins" />
<option flashStrict="False" />
<option noInlineOnDebug="False" />
<option mainClass="App" />
<option enabledebug="False" />
<option additional="" />
</build>
<!-- haxelib libraries -->
<haxelib>
<library name="templo" />
<library name="thx.semver" />
<library name="thx.csv" />
<library name="hxevents" />
<library name="hscript" />
<library name="record-macros" />
<library name="smtpmailer" />
<library name="datetime" />
<library name="sugoi:git:https://github.com/bablukid/sugoi#i18n" />
<library name="record-macros:git:https://github.com/HaxeFoundation/record-macros#maste" />
<library name="dbadmin:git:https://github.com/ncannasse/dbadmin.git#master" />
<library name="cagette-pro:git:https://bubar@bitbucket.org/bubar/cagette-pro.git#master" />
<library name="cagette-connector:git:https://bubar@bitbucket.org/bubar/cagette-connector.git#master" />
<library name="cagette-wholesale-order:git:https://bubar@bitbucket.org/bubar/cagette-wholesale-order.git#master" />
<library name="cagette-hosted:git:https://bubar@bitbucket.org/bubar/cagette-hosted.git#master" />
</haxelib>
<!-- Class files to compile (other referenced classes will automatically be included) -->
<compileTargets>
<!-- example: <compile path="..." /> -->
</compileTargets>
<!-- Paths to exclude from the Project Explorer tree -->
<hiddenPaths>
<hidden path="project.hxml" />
<hidden path="lang\fr\tmp" />
<hidden path="preLaunch" />
<hidden path="art" />
<hidden path="www\file" />
<hidden path="config.tpl.xml" />
<hidden path="clearFileCache.bat" />
<hidden path="obj" />
</hiddenPaths>
<!-- Executed before build -->
<preBuildCommand />
<!-- Executed after build -->
<postBuildCommand alwaysRun="False" />
<!-- Other project options -->
<options>
<option showHiddenPaths="True" />
<option testMovie="Custom" />
<option testMovieCommand="" />
</options>
<!-- Plugin storage -->
<storage />
</project>
+17
View File
@@ -0,0 +1,17 @@
-dce no
-cp common
-cp js
-lib sugoi:git:https://github.com/bablukid/sugoi#i18n
-lib hxleaflet:git:https://github.com/bablukid/hxleaflet
-lib cagette-hosted
-lib react
-lib react-router-4
-lib modular
-lib tink_json
-D plugins
-D no-deprecation-warnings
-main App
-js www/js/app.js
+13
View File
@@ -0,0 +1,13 @@
-dce no
-cp common
-cp js
-lib sugoi:git:git@artcode.re:cagette/sugoi.git
-lib hxleaflet:git:git@artcode.re:cagette/hxleaflet.git
-lib react:1.4.0
-lib react-router-4:0.2.3
-lib modular:0.10.15
-lib tink_json:0.9.0
-main App
-debug
-js www/js/app.js
-D no-deprecation-warnings
+472
View File
@@ -0,0 +1,472 @@
/**
* Common.hx : Shared entities between server and client
*/
@:keep
typedef OrderSimple = {
products: Array<{
product:ProductInfo,
quantity:Int
}>,
total:Float
}
//A temporary order, waiting for being paid and definitely recorded.
@:keep
typedef OrderInSession = {
products:Array <{
productId:Int,
quantity:Float,
#if !js
?product:db.Product,
?distributionId:Int,
#end
} > ,
?userId:Int,
total:Float, //price to pay
?paymentOp:Int, //payment operation ID
}
@:keep
typedef ProductInfo = {
id : Int,
name : String,
ref : Null<String>,
image : Null<String>,
contractId : Int,
price : Float,
vat : Null<Float>, //VAT rate
vatValue : Null<Float>, //amount of VAT included in the price
contractTax : Null<Float>, //pourcentage de commission défini dans le contrat
contractTaxName : Null<String>, //label pour la commission : ex: "frais divers"
desc : Null<String>,
categories : Null<Array<Int>>, //used in old shop
subcategories : Null<Array<Int>>, //used in new shop
orderable : Bool, //can be currently ordered
stock: Null<Float>, //available stock
hasFloatQt : Bool,
qt:Null<Float>,
unitType:Null<Unit>,
organic:Bool,
variablePrice:Bool,
wholesale:Bool,
active:Bool,
?distributionId:Null<Int>, //in the context of a distrib
}
@:keep
typedef ProductWithQuantity = {
product: ProductInfo,
quantity: Int
}
enum Unit{
Piece;
Kilogram;
Gram;
Litre;
Centilitre;
}
typedef CategoryInfo = {
id:Int,
name:String,
?subcategories:Array<CategoryInfo>
}
/**
* datas used with the "tagger" ajax class
*/
@:keep
typedef TaggerInfos = {
products:Array<{product:ProductInfo,categories:Array<Int>}>,//tagged products
categories : Array<{id:Int,categoryGroupName:String,color:String,tags:Array<{id:Int,name:String}>}>, //groupe de categories + tags
}
/**
* Links in navbars for plugins
*/
typedef Link = {
id:String,
link:String,
name:String,
?icon:String,
}
typedef Block = {
id:String,
title:String,
?icon:String,
html:String
}
typedef UserOrder = {
id:Int,
userId:Int,
userName:String,
?userEmail : String,
?userId2:Int,
?userName2:String,
?userEmail2:String,
//deprecated
?productId:Int,
?productRef:String,
?productName:String,
?productPrice:Float,
?productImage:String,
?productQt:Float,
?productUnit:Unit,
?productHasFloatQt:Bool,
?productHasVariablePrice:Bool,
//new way
?product:ProductInfo,
quantity:Float,
smartQt:String,
subTotal:Float,
fees:Null<Float>,
percentageName:Null<String>,
percentageValue:Null<Float>,
total:Float,
//flags
paid:Bool,
invertSharedOrder:Bool,
canceled:Bool,
canModify:Bool,
contractId:Int,
contractName:String,
}
typedef PlaceInfos = {
id:Int,
name:String,
address1:String,
address2:String,
zipCode:String,
city:String,
latitude:Float,
longitude:Float
}
typedef UserInfo = {
id:Int,
name:String,
email:String,
}
typedef GroupOnMap = {
id:Int,
name:String,
image:String,
place:PlaceInfos,
}
enum OrderFlags {
InvertSharedOrder; //invert order when there is a shared/alternated order
//Canceled; //flag for cancelled orders, qt should always be 0
}
typedef OrderByProduct = {
quantity:Float,
smartQt:String,
pid:Int,
pname:String,
ref:String,
priceHT:Float,
priceTTC:Float,
vat:Float,
totalHT:Float,
totalTTC:Float,
weightOrVolume:String,
};
typedef OrderByEndDate = {date: String,contracts: Array<String>};
/**
Event enum used for plugins.
As in most CMS event systems,
the events (or "triggers") can be caught by plugins
to perform an action or modifiy data carried by the event.
**/
enum Event {
Page(uri:String); //a page is displayed
Nav(nav:Array<Link>, name:String, ?id:Int); //a navigation is displayed, optionnal object id if needed
Blocks(blocks:Array<Block>, name:String, ?context:Dynamic); //HTML blocks that can be displayed on a page
#if sys
SendEmail(message : sugoi.mail.Mail); //an email is sent
NewMember(user:db.User,group:db.Amap); //a new member is added to a group
NewGroup(group:db.Amap, author:db.User); //a new group is created
//Distributions
PreNewDistrib(contract:db.Contract); //when displaying the insert distribution form
NewDistrib(distrib:db.Distribution); //when a new distrinbution is created
PreEditDistrib(distrib:db.Distribution);
EditDistrib(distrib:db.Distribution);
DeleteDistrib(distrib:db.Distribution);
PreNewDistribCycle(cycle:db.DistributionCycle);
NewDistribCycle(cycle:db.DistributionCycle);
MultiDistribEvent(md:MultiDistrib);
//Products
PreNewProduct(contract:db.Contract); //when displaying the insert distribution form
NewProduct(product:db.Product); //when a new product is created
PreEditProduct(product:db.Product);
EditProduct(product:db.Product);
DeleteProduct(product:db.Product);
BatchEnableProducts(data:{pids:Array<Int>,enable:Bool});
ProductInfosEvent(p:ProductInfo,?d:db.Distribution); //when infos about a product are displayed
//Contracts
EditContract(contract:db.Contract,form:sugoi.form.Form);
DuplicateContract(contract:db.Contract);
DeleteContract(contract:db.Contract);
//crons
DailyCron;
HourlyCron;
MinutelyCron;
//orders
MakeOrder(orders:Array<db.UserContract>);
StockMove(order:{product:db.Product, move:Float}); //when a stock is modified
//payments
GetPaymentTypes(data:{types:Array<payment.Payment>});
NewOperation(op:db.Operation);
#end
}
/*
* Product Taxonomy structure
*/
typedef TxpDictionnary = {
products:Map<Int,{id:Int,name:String,category:Int,subCategory:Int}>,
categories:Map<Int,{id:Int,name:String}>,
subCategories:Map<Int,{id:Int,name:String}>,
}
/*
* Tutorials
*/
enum TutoAction {
TAPage(uri:String);
TANext;
}
enum TutoPlacement {
TPTop;
TPBottom;
TPLeft;
TPRight;
}
class TutoDatas {
public static var TUTOS;
#if js
//async
public static function get(tuto:String, callback:Dynamic->Void){
#if !test
sugoi.i18n.Locale.init(App.instance.LANG, function(t:sugoi.i18n.GetText){
App.instance.t = t;
init(t);
var tuto = TUTOS.get(tuto);
callback(tuto);
});
#end
}
#else
//sync
public static function get(tuto:String):{name:String, steps:Array<{element:String,text:String,action:TutoAction,placement:TutoPlacement}>}
{
sugoi.i18n.Locale.init(App.current.getLang());
init(sugoi.i18n.Locale.texts);
return TUTOS.get(tuto);
}
#end
static function init(t:sugoi.i18n.GetText){
TUTOS = [
"intro" => {
name:t._("Guided tour for the coordinator"),
steps:[
{
element:null,
text:t._("<p>In order to better discover Cagette.net, we propose to do a guided tour of the user interface of the software. <br/> You will then have a global overview on the different tools that are available to you.</p><p>You will be able to stop and start again this tutorial whenever you want.</p>"),
action: TANext,
placement : null
},
{
element:"ul.nav.navbar-left",
text:t._( "This part of the navigation bar is visible by all members.</br>It allows to access to the three main pages: <ul><li> The <b>home page</b> which displays orders and the delivery planning.</li><li> On the <b>My account</b> page, you can update your personal information and check your orders history</li><li> On the <b>Farmers</b> page, you can see all farmers and coordinators of the group</ul>"),
action: TANext,
placement : TPBottom
},
{
element:"ul.nav.navbar-right",
text:t._("This part is <b>for coordinators only.</b>Here you will be able to manage the register of members, orders, products, etc.<br/><p>Now click on the <b>Members</b> section</p>"),
action: TAPage("/member"),
placement : TPBottom
},{
element:".article .table td:first",
text:t._("The purpose of this section is to administrate the list of your members.<br/>Every time that you register a new membrer, an account will be created for him/her. Now the member can join you at Cagette.net and order or consult the planning of the deliveries.<p>Now click on a <b>member</b> in the list</p>"),
action: TAPage("/member/view/*"),
placement : TPRight
},{
element:".article:first",
text:t._("This is the page of a member. Here you can : <ul><li>see and change their contact details</li><li>manage the membership fee of your group</li><li>see a summary of their orders</li></ul>"),
action: TANext,
placement : TPRight
},{
element:"ul.nav #contractadmin",
text:t._("Now let's have a look at the <b>contracts</b> section which is very important for coordinators."),
action: TAPage("/contractAdmin"),
placement : TPBottom
},{
element:"#contracts",
text:t._("Here you find the list of <b>contracts</b>.They inculde a start date, a end date, and represent your relationship with a farmer. <br/><p>Here you can manage :<ul><li>the list of products of this farmer</li><li>the orders of members for this farmer</li><li>and plan the delivery schedule</li></ul></p>"),
action: TANext,
placement : TPBottom
},{
element:"#places",
text:t._("Here you can manage the list of <b>delivery places</b>.<br/>Don't forget to key-in the complete address as a map will be displayed based on this address"),
action: TANext,
placement : TPTop
},{
element:"#contracts table .btn:first",
text:t._("Let's look closer at how to manage a contract. <b>Click on this button</b>"),
action: TAPage("/contractAdmin/view/*"),
placement : TPBottom
},{
element:".table.table-bordered:first",
text:t._("Here is a summary of the contract.<br/>There are two types of contracts:<ul><li>Constant contracts: the member commits on buying the same products during the whole duration of the contract</li><li>Variable contracts: the member can choose what he buys for each delivery.</li></ul>"),
action: TANext,
placement : TPRight
},{
element:"#subnav #products",
text:t._("Let's see now the page <b>Products</b>"),
action : TAPage("/contractAdmin/products/*"),
placement:TPRight
},{
element:".article .table",
text:t._("On this page, you can manage the list of products offered by this supplier.<br/>Define at least the name and the price of products. It is also possible to add a description and a picture."),
action: TANext,
placement : TPTop
},{
element:"#subnav #deliveries",
text:t._("Let's see the <b>deliveries</b> page"),
action : TAPage("/contractAdmin/distributions/*"),
placement:TPRight
},{
element:".article .table",
text:t._("Here we can manage the list of deliveries for this supplier.<br/>In the software, a delivery has a date, a start time, and an end time. The location of the delivery must also be defined, by using the list that we have already seen."),
action: TANext,
placement : TPLeft
},{
element:"#subnav #orders",
text:t._("Let's see now the <b>Orders</b> page"),
action : TAPage("/contractAdmin/orders/*"), //can fail if the contract is variable because the uri is different
placement:TPRight
},{
element:".article .table",
text:t._("Here we can manage the list of orders for this supplier.<br/>If you choose to \"open orders\" to members, they will be able to make their orders online themselves.<br/>This page will centralize automatically the orders for this supplier. Otherwise, as a coordinator, you will be able to enter orders on behalf of a member."),
action: TANext,
placement : TPLeft
},{
element:"ul.nav #messages",
text:t._("<p>We have seen the main features related to contracts.</p><p>Let's see the <b>messaging</b> section.</p>"),
action: TAPage("/messages"),
placement : TPBottom
},{
element:null,
text:t._("<p>The messaging section allows you to send e-mails to different lists of members. It is not necessary anymore to maintain a lot of lists of e-mails depending on contracts, as all these lists are automatically generated.</p> <p>E-mails are sent with your e-mail address as sender, so you will receive answers in your own mailbox.</p>"),
action: TANext,
placement : null
},{
element:"ul.nav #amapadmin",
text:t._("Click here now on this page"),
action : TAPage("/amapadmin"),
placement : TPBottom,
},{
element:"#subnav",
text:t._("<p>In this last page, you can configure everything that is related to your group.</p><p>The page <b>Access rights</b> is important as it is where you can define other coordinators among members. They will then be able to manage one or many contracts, send emails, etc.</p>"),
action:TANext,
placement : TPBottom
},{
element:"#footer",
text:t._("<p>This is the last step of this tutorial. I hope that it gave you a good overview of this software.<br/>To go further, do not hesitate to look at the <b>documentation</b>. The link is always available at the bottom of the screen.</p>"),
action:TANext,
placement : TPBottom
}
]
},
];
}
}
/**
* Order Reports
*/
enum OrdersReportGroupOption{
ByMember;
ByProduct;
}
enum OrdersReportFormatOption{
Table;
Csv;
PrintableList; //list de distrib ?
}
//Report Options : should be usable in an URL, an API call...
typedef OrdersReportOptions = {
//time scope
startDate:Date,
endDate:Date,
//formatting
?groupBy:OrdersReportGroupOption, //group order by...
?format:OrdersReportFormatOption, //table , csv ?
//filters :
?groups:Array<Int>,
?contracts:Array<Int>, //which contracts
?distributions:Array<Int>,
}
+73
View File
@@ -0,0 +1,73 @@
package;
import Common;
/**
* Formatting tools used on both client and server side.
*
* @author fbarbut
*/
class Formatting
{
/** smart quantity filter : display easier-to-read quantity when it's floats
*
* 0.33 x Lemon 12kg => 2kg Lemon
*/
public static function smartQt(orderQt:Float,productQt:Float,unit:Unit):String{
return formatNum(orderQt * productQt) + "&nbsp;" + Formatting.unit(unit);
}
public static function formatNum(n:Float):String {
if (n == null) return "";
//arrondi a 2 apres virgule
var out = Std.string(roundTo(n, 2));
//ajout un zéro, 1,8-->1,80
if (out.indexOf(".")!=-1 && out.split(".")[1].length == 1) out = out +"0";
//virgule et pas point
return out.split(".").join(",");
}
/**
* Round a number to r digits after coma.
*/
public static function roundTo(n:Float, r:Int):Float {
return Math.round(n * Math.pow(10,r)) / Math.pow(10,r) ;
}
public static function parseFloat(s:String):Float{
if(s.indexOf(",")>0){
return Std.parseFloat(StringTools.replace(s,",","."));
}else{
return Std.parseFloat(s);
}
}
/**
* Display a unit
* @param u -
*/
public static function unit(u:Unit):String{
/*t = sugoi.i18n.Locale.texts;
if(u==null) return t._("piece||unit of a product)");
return switch(u){
case Kilogram: t._("Kg.||kilogramms");
case Gram: t._("g.||gramms");
case Piece: t._("piece||unit of a product)");
case Litre: t._("L.||liter");
case Centilitre: t._("cl.||centiliter");
}*/
if(u==null) return "pièce(s)";
return switch(u){
case Kilogram: "Kg.";
case Gram: "g.";
case Piece: "pièce(s)";
case Litre: "L.";
case Centilitre:"cl.";
}
}
}
File diff suppressed because it is too large Load Diff
Executable
+262
View File
@@ -0,0 +1,262 @@
//React lib
import react.ReactMacro.jsx;
import react.ReactDOM;
import react.*;
import react.router.*;
//custom components
import react.order.*;
import react.product.*;
import react.store.*;
import react.map.*;
import react.user.*;
//require bootstrap JS since it's bundled with browserify
//@:jsRequire('bootstrap') extern class Bootstrap{}
//@:jsRequire('jquery') extern class JQ extends js.jquery.JQuery{}
class App {
public static var instance : App;
public var LANG : String;
public var currency : String; //currency symbol like &euro; 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="&euro;") {
//singleton
instance = this;
if(lang!=null) this.LANG = lang;
this.currency = currency;
}
/**
* Returns a jquery object like $() in javascript
* @deprecated
*/
public static inline function j(r:Dynamic):js.JQuery {
return new js.JQuery(r);
}
public static inline function jq(r:Dynamic):js.jquery.JQuery{
return new js.jquery.JQuery(r);
}
/**
* The JS App will be available as "_" in the document.
*/
public static function main() {
//untyped js.Browser.window.$ = js.Lib.require("jQuery");
untyped js.Browser.window._ = new App();
}
public function getCart() {
return new ShopCart();
}
public function getTagger(cid:Int ) {
return new Tagger(cid);
}
public function getTuto(name:String, step:Int) {
new Tuto(name,step);
}
/**
* remove method for IE compat
*/
public function remove(el:js.html.Element){
if (el == null) return;
el.parentElement.removeChild(el);
}
public function getVATBox(ttcprice:Float,currency:String,rates:String,vat:Float,formName:String){
var input = js.Browser.document.querySelector('form input[name="${formName}_price"]');
remove( js.Browser.document.querySelector('form input[name="${formName}_vat"]').parentElement.parentElement );
ReactDOM.render(jsx('<$VATBox ttc="$ttcprice" currency="$currency" vatRates="$rates" vat="$vat" formName="$formName"/>'), input.parentElement);
//remove(input);
}
/*public function getProductComposer(){
//js.Browser.document.addEventListener("DOMContentLoaded", function(event) {
//ReactDOM.render(jsx('<$ComposerApp/>'), js.Browser.document.getElementById("app"));
//});
}*/
/**
* Removes the form element and replace it by a react js component
* @param divId
* @param productName
* @param txpProductId
* @param formName
*/
public function getProductInput(divId:String, productName:String, txpProductId:String, formName:String ){
js.Browser.document.addEventListener("DOMContentLoaded", function(event) {
//dirty stuff to remove "real" input, and replace it by the react one
App.j("form input[name='"+formName+"_name']").parent().parent().remove();
App.j("form select[name='" + formName+"_txpProductId']").parent().parent().remove();
if (txpProductId == null) txpProductId = "";
ReactDOM.render(jsx('<$ProductInput productName="$productName" txpProductId="$txpProductId" formName="$formName"/>'), js.Browser.document.getElementById(divId));
});
}
public function initReportHeader(){
ReactDOM.render(jsx('<$ReportHeader />'), js.Browser.document.querySelector('div.reportHeaderContainer'));
}
public function initOrderBox(userId:Int, distributionId:Int, contractId:Int, contractType:Int, date:String, place:String, userName:String, currency:String, hasPayments:Bool,callbackUrl:String){
untyped App.j("#myModal").modal();
var onValidate = function() js.Browser.location.href = callbackUrl;
var node = js.Browser.document.querySelector('#myModal .modal-body');
ReactDOM.unmountComponentAtNode(node); //the previous modal DOM element is still there, so we need to destroy it
ReactDOM.render(jsx('<$OrderBox userId="$userId" distributionId="$distributionId"
contractId="$contractId" contractType="$contractType" date="$date" place="$place" userName="$userName"
onValidate=$onValidate currency=$currency hasPayments=$hasPayments />'),node,postReact);
}
function postReact(){
trace("post react");
haxe.Timer.delay(function(){
untyped jq('[data-toggle="tooltip"]').tooltip();
untyped jq('[data-toggle="popover"]').popover();
},500);
}
public static function roundTo(n:Float, r:Int):Float {
return Math.round(n * Math.pow(10,r)) / Math.pow(10,r) ;
}
/**
* Ajax loads a page and display it in a modal window
* @param url
* @param title
*/
public function overlay(url:String,?title,?large=true) {
if (title != null) title = StringTools.urlDecode(title);
var r = new haxe.Http(url);
r.onData = function(data) {
//setup body and title
var m = App.j("#myModal");
m.find(".modal-body").html(data);
if (title != null) m.find(".modal-title").html(title);
if (!large) m.find(".modal-dialog").removeClass("modal-lg");
untyped App.j('#myModal').modal(); //bootstrap 3 modal window
}
r.request();
}
/**
* Displays a login box
*/
public function loginBox(redirectUrl:String,?message:String,?phoneRequired=false) {
var m = App.j("#myModal");
m.find(".modal-title").html("S'identifier");
m.find(".modal-dialog").removeClass("modal-lg");
untyped m.modal();
ReactDOM.render(jsx('<$LoginBox redirectUrl="$redirectUrl" message=$message phoneRequired="$phoneRequired"/>'), js.Browser.document.querySelector('#myModal .modal-body'));
return false;
}
/**
* Displays a sign up box
*/
public function registerBox(redirectUrl:String,?message:String,?phoneRequired=false) {
var m = App.j("#myModal");
m.find(".modal-title").html("S'inscrire");
m.find(".modal-dialog").removeClass("modal-lg");
untyped m.modal();
ReactDOM.render(jsx('<$RegisterBox redirectUrl="$redirectUrl" message=$message phoneRequired="$phoneRequired"/>'), js.Browser.document.querySelector('#myModal .modal-body'));
return false;
}
public function shop(place:Int, date:String) {
ReactDOM.render(jsx('<$Store date=$date place=$place/>'), js.Browser.document.querySelector('#shop'));
}
public function groupMap(lat:String,lng:String,address:String) {
ReactDOM.render(jsx('<$GroupMapRoot lat="$lat" lng="$lng" address="$address"/>'), js.Browser.document.querySelector('#map'));
}
/**
* Helper to get values of a bunch of checked checkboxes
* @param formSelector
*/
public function getCheckboxesId(formSelector:String):Array<String>{
var out = [];
var checkboxes = js.Browser.document.querySelectorAll(formSelector + " input[type=checkbox]");
for ( input in checkboxes ){
var input : js.html.InputElement = cast input;
if ( input.checked ) out.push(input.value);
}
return out;
}
#if plugins
public function getHostedPlugin(){
return new hosted.js.App();
}
#end
/**
* set up a warning message when leaving the page
*/
public function setWarningOnUnload(active:Bool, ?msg:String){
if (active){
js.Browser.window.addEventListener("beforeunload", warn);
}else{
js.Browser.window.removeEventListener("beforeunload", warn);
}
}
function warn(e:js.html.Event) {
var msg = "Voulez vous vraiment quitter cette page ?";
//js.Browser.window.confirm(msg);
untyped e.returnValue = msg; //Gecko + IE
e.preventDefault();
return msg; //Gecko + Webkit, Safari, Chrome etc.
}
/**
* Anti Doubleclick with btn elements.
* Can be bypassed by adding a .btn-noAntiDoubleClick class
*/
public function antiDoubleClick(){
for( n in js.Browser.document.querySelectorAll(".btn:not(.btn-noAntiDoubleClick)") ){
n.addEventListener("click",function(e:js.html.MouseEvent){
var x = untyped e.target;
x.classList.add("disabled");
haxe.Timer.delay(function(){
x.classList.remove("disabled");
},1000);
});
}
}
}
+445
View File
@@ -0,0 +1,445 @@
import Common;
import js.JQuery;
/**
* JS Shopping Cart
*
* @author fbarbut<francois.barbut@gmail.com>
*/
class ShopCart
{
public var products : Map<Int,ProductInfo>; //product db
public var productsArray : Array<ProductInfo>; //to keep order of products
public var categories : Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>; //categ db
public var pinnedCategories : Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>; //categ db
public var order : OrderInSession;
var loader : JQuery; //ajax loader gif
//for scroll mgmt
var cartTop : Int;
var cartLeft : Int;
var cartWidth : Int;
var jWindow : JQuery;
var cartContainer : JQuery;
var date : String;
var place : Int;
public function new()
{
products = new Map();
productsArray = [];
order = cast { products:[] };
categories = [];
pinnedCategories = [];
}
public function add(pid:Int) {
loader.show();
var q = App.j('#productQt' + pid).val();
var qt = 0.0;
var p = this.products.get(pid);
if (p.hasFloatQt) {
q = StringTools.replace(q, ",", ".");
qt = Std.parseFloat(q);
}else {
qt = Std.parseInt(q);
}
if (qt == null) {
qt = 1;
}
//trace("qté : "+qt);
//add server side
var r = new haxe.Http('/shop/add/$pid/$qt');
r.onData = function(data:String) {
loader.hide();
var d = haxe.Json.parse(data);
if (!d.success) js.Browser.alert("Erreur : "+d);
//add locally
subAdd(pid, qt);
render();
}
r.request();
}
function subAdd(pid, qt:Float ) {
for ( p in order.products) {
if (p.productId == pid) {
p.quantity += qt;
render();
return;
}
}
order.products.push( { productId:pid, quantity:qt } );
}
/**
* Render the shopping cart and total
*/
function render() {
var c = App.j("#cart");
c.empty();
//render items in shopping cart
c.append( Lambda.map(order.products, function( x ) {
var p = this.products.get(x.productId);
if (p == null) {
//the product may have been disabled by an admin
return "";
}
var btn = "<a onClick='cart.remove(" + p.id + ")' class='btn btn-default btn-xs' data-toggle='tooltip' data-placement='top' title='Retirer de la commande'><span class='glyphicon glyphicon-remove'></span></a>&nbsp;";
return "<div class='row'>
<div class = 'order col-md-9' > <b> " + x.quantity + " </b> x " + p.name+" </div>
<div class = 'col-md-3'> "+btn+"</div>
</div>";
}).join("\n") );
//compute total price
var total = 0.0;
for (p in order.products) {
var pinfo = products.get(p.productId);
if (pinfo == null) continue;
total += p.quantity * pinfo.price;
}
var ffilter = new sugoi.form.filters.FloatFilter();
var total = ffilter.filterString(Std.string(App.roundTo(total,2)));
c.append("<div class='total'>TOTAL : " + total + "</div>");
if (order.products.length > 0){
App.instance.setWarningOnUnload(true,"Vous avez une commande en cours. Si vous quittez cette page sans confirmer, votre commande sera perdue.");
}else{
App.instance.setWarningOnUnload(false);
}
}
function findCategoryName(cid:Int):String{
for ( cg in this.categories ){
for (c in cg.categs){
if (cid == c.id) {
return c.name;
}
}
}
for ( cg in this.pinnedCategories ){
for (c in cg.categs){
if (cid == c.id) {
return c.name;
}
}
}
return null;
}
/**
* Dynamically sort products by categories
*/
public function sortProductsBy(){
//store products by groups
var groups = new Map<Int,{name:String,products:Array<ProductInfo>}>();
var pinned = new Map<Int,{name:String,products:Array<ProductInfo>}>();
var firstCategGroup = this.categories[0].categs;
//trace(firstCategGroup);
//trace(pinnedCategories);
var pList = this.productsArray.copy();
//for ( p in pList) trace(p.name+" : " + p.categories);
//trace("----------------");
//sort by categs
for ( p in pList.copy() ){
//trace(p.name+" : " + p.categories);
untyped p.element.remove();
for ( categ in p.categories){
if (Lambda.find(firstCategGroup, function(c) return c.id == categ) != null){
//is in this category group
var g = groups.get(categ);
if ( g == null){
var name = findCategoryName(categ);
g = {name:name,products:[]};
}
g.products.push(p);
//trace("remove " + p.name);
pList.remove(p);
groups.set(categ, g);
}
else{
// is in pinned group ?
var isInPinnedCateg = false;
for ( cg in pinnedCategories){
if (Lambda.find(cg.categs, function(c) return c.id == categ) != null){
isInPinnedCateg = true;
break;
}
}
if (isInPinnedCateg){
var c = pinned.get(categ);
if ( c == null){
var name = findCategoryName(categ);
c = {name:name,products:[]};
}
c.products.push(p);
//trace( "add " + p.name+" in PINNED");
pList.remove(p);
pinned.set(categ, c);
}else{
//not in the selected categ nor in pinned groups
continue;
}
}
}
}
//if some untagged products remain
if (pList.length > 0){
groups.set(0,{name:"Autres",products:pList});
}
//trace("----------------");
//render
var container = App.j(".shop .body");
//render firts "pinned" groups , then "groups"
for ( source in [pinned, groups]){
for (o in source){
if (o.products.length == 0) continue;
container.append("<div class='col-md-12 col-xs-12 col-sm-12 col-lg-12'><div class='catHeader'>" + o.name + "</div></div>");
for ( p in o.products){
//trace("GROUP "+o.name+" : "+p.name);
//if the element has already been inserted, we need to clone it
if (untyped p.element.parent().length == 0){
container.append( untyped p.element );
}else{
var clone = untyped p.element.clone();
container.append( clone );
}
}
}
}
App.j(".product").show();
}
/**
* is shopping cart empty ?
*/
public function isEmpty(){
return order.products.length == 0;
}
/**
* submit cart
*/
public function submit() {
var req = new haxe.Http("/shop/submit");
req.onData = function(d) {
App.instance.setWarningOnUnload(false);
js.Browser.location.href = "/shop/validate/"+place+"/"+date;
}
req.addParameter("data", haxe.Json.stringify(order));
req.request(true);
}
/**
* filter products by category
*/
public function filter(cat:Int) {
//icone sur bouton
App.j(".tag").removeClass("active").children().remove("span");//clean
var bt = App.j("#tag" + cat);
bt.addClass("active").prepend("<span class ='glyphicon glyphicon-ok'></span> ");
//affiche/masque produits
for (p in products) {
if (cat==0 || Lambda.has(p.categories, cat)) {
App.j(".shop .product" + p.id).fadeIn(300);
}else {
App.j(".shop .product" + p.id).fadeOut(300);
}
}
}
/**
* remove a product from cart
* @param pid
*/
public function remove(pid:Int ) {
loader.show();
//add server side
var r = new haxe.Http('/shop/remove/$pid');
r.onData = function(data:String) {
loader.hide();
var d = haxe.Json.parse(data);
if (!d.success) js.Browser.alert("Erreur : "+d);
//remove locally
for ( p in order.products.copy()) {
if (p.productId == pid) {
order.products.remove(p);
render();
return;
}
}
render();
}
r.request();
}
/**
* loads products DB and existing cart in ajax
*/
public function init(place:Int,date:String) {
this.place = place;
this.date = date;
loader = App.j("#cartContainer #loader");
var req = new haxe.Http("/shop/init/"+place+"/"+date);
req.onData = function(data) {
loader.hide();
var data : {
products:Array<ProductInfo>,
categories:Array<{name:String,pinned:Bool,categs:Array<CategoryInfo>}>,
order:OrderInSession } = haxe.Unserializer.run(data);
//populate local categories lists
for ( cg in data.categories){
if (cg.pinned){
pinnedCategories.push(cg);
}else{
categories.push(cg);
}
}
//product DB
for (p in data.products) {
//catch dom element for further usage
untyped p.element = App.j(".product"+p.id);
var id : Int = p.id;
//var id : Int = p.id;
//id = id + 1;
this.products.set(id, p);
this.productsArray.push(p);
//trace(p.name+" : " + p.categories);
}
//existing order
for ( p in data.order.products) {
subAdd(p.productId,p.quantity );
}
render();
sortProductsBy();
}
req.request();
//DISABLED : pb quand le panier est plus haut que l'ecran
//scroll mgmt, only for large screens. Otherwise let the cart on page bottom.
/*if (js.Browser.window.matchMedia("(min-width: 1024px)").matches) {
jWindow = App.j(js.Browser.window);
cartContainer = App.j("#cartContainer");
cartTop = cartContainer.position().top;
cartLeft = cartContainer.position().left;
cartWidth = cartContainer.width();
jWindow.scroll(onScroll);
}*/
}
/**
* keep the cart on top when scrolling
* @param e
*/
public function onScroll(e:Dynamic) {
//cart container top position
if (jWindow.scrollTop() > cartTop) {
//trace("absolute !");
cartContainer.addClass("scrolled");
cartContainer.css('left', Std.string(cartLeft) + "px");
cartContainer.css('top', Std.string(/*cartTop*/10) + "px");
cartContainer.css('width', Std.string(cartWidth) + "px");
}else {
cartContainer.removeClass("scrolled");
cartContainer.css('left',"");
cartContainer.css('top', "");
cartContainer.css('width', "");
}
}
}
Executable
+151
View File
@@ -0,0 +1,151 @@
package ;
import Common;
import js.JQuery;
/**
*
* Tag products with categories
*
* @author fbarbut<francois.barbut@gmail.com>
*/
@:keep
class Tagger
{
var contractId : Int;
var data:TaggerInfos;
var pids : Array<Int>; //selected product Ids
public function new(cid:Int)
{
contractId = cid;
pids = [];
}
public function init() {
var req = new haxe.Http("/product/categorizeInit/"+contractId);
req.onData = function(_data) {
data = haxe.Json.parse(_data);
render();
}
req.request();
}
function render() {
var html = new StringBuf();
html.add("<table class='table'>");
for (p in data.products) {
html.add("<tr class='p"+p.product.id+"'>");
var checked = Lambda.has(pids,p.product.id) ? "checked" : "";
html.add('<td><input type="checkbox" name="p${p.product.id}" $checked/></td>');
html.add("<td>" + p.product.name+"</td>");
var tags = [];
//trace('product tags ${p.categories} from tags ${data.categories}');
for (c in p.categories) {
//trouve le nom du tag
var name = "";
var color = "";
for (gc in data.categories) {
for ( t in gc.tags) {
if (c == t.id) {
name = t.name;
color = gc.color;
break;
}
}
}
//var bt = App.jq("<a>[X]</a>")
tags.push("<span class='tag t"+c+"' style='background-color:"+color+";cursor:pointer;'>"+name+"</span>");
}
html.add("<td class='tags'>"+ tags.join(" ") +"</td>");
html.add("</tr>");
}
html.add("</table>");
App.jq("#tagger").html(html.toString());
App.jq("#tagger .tag").click(function(e) {
var el : js.html.Element = cast e.currentTarget;
//find tag Id
var tid = Std.parseInt(el.getAttribute('class').split(" ")[1].substr(1));
//find product Id
var pid = Std.parseInt(el.parentElement.parentElement.getAttribute('class').substr(1));
//remove element
el.remove();
//datas
remove(tid,pid);
});
}
public function add() {
var tagId = Std.parseInt(App.jq("#tag").val());
if (tagId == 0) js.Browser.alert("Impossible de trouver la catégorie selectionnée");
pids = [];
for ( e in App.jq("#tagger input:checked").elements() ) {
pids.push(Std.parseInt(e.attr("name").substr(1)));
}
if (pids.length == 0) js.Browser.alert("Sélectionnez un produit afin de pouvoir lui attribuer une catégorie");
for (p in pids) {
addTag(tagId, p);
}
render();
}
public function remove(tagId:Int,productId:Int) {
//data
for ( p in data.products) {
if (p.product.id == productId) {
for ( t in p.categories) {
if (t == tagId) p.categories.remove(t);
}
}
}
}
function addTag(tagId:Int, productId:Int) {
//check for doubles
for ( p in data.products) {
if (p.product.id == productId) {
for (t in p.categories) {
if (t == tagId) return;
}
}
}
//data
for ( p in data.products) {
if (p.product.id == productId) {
p.categories.push(tagId);
break;
}
}
}
public function submit() {
var req = new haxe.Http("/product/categorizeSubmit/" + contractId);
req.addParameter("data", haxe.Json.stringify(data));
req.onData = function(_data) {
js.Browser.alert(_data);
}
req.request(true);
}
}
+131
View File
@@ -0,0 +1,131 @@
package;
import Common;
/**
* Tutorial javascript widget
* @author fbarbut<francois.barbut@gmail.com>
*/
class Tuto
{
var name:String;
var step:Int;
static var LAST_ELEMENT :String = null; //last hightlit element
public function new(name:String, step:Int){
this.name = name;
this.step = step;
TutoDatas.get(name, init);
}
/**
* asyn init
* @param tuto
*/
function init(tuto)
{
var s = tuto.steps[step];
//close previous popovers
var p = App.jq(".popover");
untyped p.popover('hide');
var t = App.instance.t;
if (t == null) trace("Gettext translator is null");
if (s == null) {
/**
* tutorial is finished : display a modal window
*/
var m = App.jq('#myModal');
untyped m.modal('show');
m.addClass("help");
m.find(".modal-header").html("<span class='glyphicon glyphicon-hand-right'></span> "+tuto.name);
m.find(".modal-body").html("<span class='glyphicon glyphicon-ok'></span> "+t._("This tutorial is over."));
var bt = App.jq("<a class='btn btn-default'><span class='glyphicon glyphicon-chevron-right'></span> "+t._("Come back to tutorials page")+"</a>");
bt.click(function(?_) {
untyped m.modal('hide');
js.Browser.location.href = "/contract?stopTuto=1";
});
m.find(".modal-footer").append(bt);
m.find(".modal-dialog").removeClass("modal-lg"); //small window pls
}else if (s.element == null) {
/**
* no element, make a modal window (usually its the first step of the tutorial)
*/
var m = App.jq('#myModal');
untyped m.modal('show');
m.addClass("help");
m.find(".modal-body").html(s.text);
m.find(".modal-header").html("<span class='glyphicon glyphicon-hand-right'></span> "+tuto.name);
var bt = App.jq("<a class='btn btn-default'><span class='glyphicon glyphicon-chevron-right'></span> "+t._("OK")+"</a>");
bt.click(function(?_) {
untyped m.modal('hide');
new Tuto(name, step + 1);
});
m.find(".modal-footer").append(bt);
m.find(".modal-dialog").removeClass("modal-lg"); //small window pls
}else {
//prepare Bootstrap "popover"
var x = App.jq(s.element).first().attr("title", tuto.name+" <div class='pull-right'>"+(step+1)+"/"+tuto.steps.length+"</div>");
var text = "<p>" + s.text + "</p>";
var bt = null;
switch(s.action) {
case TANext :
bt = App.jq("<p><a class='btn btn-default btn-sm'><span class='glyphicon glyphicon-chevron-right'></span> "+t._("Next")+"</a></p>");
bt.click(function(?_) {
//untyped m.modal('hide');
new Tuto(name, step + 1);
if(LAST_ELEMENT!=null) App.jq(s.element).removeClass("highlight");
});
default:
}
//configure and open popover
var p = switch(s.placement) {
case TPTop: "top";
case TPBottom : "bottom";
case TPLeft : "left";
case TPRight : "right";
default : null;
}
var options = { container:"body", content:text, html:true , placement:p};
untyped x.popover(options).popover('show');
//add a footer
var footer = App.jq("<div class='footer'><div class='pull-left'></div><div class='pull-right'></div></div>");
if (bt != null) footer.find(".pull-right").append(bt);
footer.find(".pull-left").append(makeCloseButton(t._('Stop')));
App.jq(".popover .popover-content").append(footer);
//highlight
App.jq(s.element).first().addClass("highlight");
LAST_ELEMENT = s.element;
}
}
/**
* Make a "close" bt
*/
function makeCloseButton(?text) {
var bt = App.jq("<a class='btn btn-default btn-sm'><span class='glyphicon glyphicon-remove'></span> "+(text==null?"":text)+"</a>");
bt.click(function(?_) {
js.Browser.location.href = "/contract?stopTuto=1";
});
return bt;
}
}
+28
View File
@@ -0,0 +1,28 @@
//
// npm dependencies library
//
(function(scope) {
'use-strict';
scope.__registry__ = Object.assign({}, scope.__registry__, {
//
// list npm modules required in Haxe
//
'bootstrap': require('bootstrap'),
'react': require('react'),
'react-dom': require('react-dom'),
'react-bootstrap-typeahead': require('react-bootstrap-typeahead'),
'react-datetime': require('react-datetime'),
'leaflet': require('leaflet'),
'react-leaflet': require('react-leaflet'),
'react-places-autocomplete': require('react-places-autocomplete'),
'geolib': require('geolib'),
'react-router':require('react-router'),
'react-router-dom':require('react-router-dom'),
});
if (process.env.NODE_ENV !== 'production') {
// enable React hot-reload
require('haxe-modular');
}
})(typeof $hx_scope != "undefined" ? $hx_scope : $hx_scope = {});
+7
View File
@@ -0,0 +1,7 @@
package react;
/**
* @doc https://www.npmjs.com/package/react-bootstrap-datetimepicker
*/
@:jsRequire('react-bootstrap-datetimepicker')
extern class DateTimeField extends react.ReactComponent {}
+29
View File
@@ -0,0 +1,29 @@
package react;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
/**
* A Error Div
*/
class Error extends react.ReactComponentOfProps<{error:String}>
{
public function new(props:Dynamic)
{
super(props);
}
override public function render(){
if (props.error == null) return null;
return jsx('<div className="alert alert-danger">
<span className="glyphicon glyphicon-exclamation-sign"></span> ${props.error}
</div>
');
}
}
+26
View File
@@ -0,0 +1,26 @@
package react;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
/**
* A message Div
*/
class Message extends react.ReactComponentOfProps<{message:String}>
{
public function new(props:Dynamic)
{
super(props);
}
override public function render(){
if (props.message == null) return null;
return jsx('<div className="alert alert-warning">
<span className="glyphicon glyphicon glyphicon-info-sign"></span> ${props.message}
</div>
');
}
}
+94
View File
@@ -0,0 +1,94 @@
package react;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
//datepicker broken if called like this //import react.DateTimeField.*;
//@:jsRequire('react-bootstrap-datetimepicker')
//extern class DateTimeField extends react.ReactComponent {}
/**
* @doc https://github.com/YouCanBookMe/react-datetime
*/
@:jsRequire('react-datetime')
extern class DateTime extends react.ReactComponent {}
/**
* ...
* @author fbarbut
*/
class ReportHeader extends react.ReactComponentOfState<OrdersReportOptions>
{
public function new()
{
super();
state = {startDate:null, endDate:null, groupBy:null, contracts:[]};
//load fr locale of moment.js
var moment = js.Lib.require('moment');
js.Lib.require('moment/locale/fr');
}
override public function render(){
return jsx('<div className="reportHeader">
<div className="col-md-3">
<div className="input-group">
<span className="input-group-addon">
<span className="glyphicon glyphicon-calendar"></span>
</span>
<DateTime name="startDate_PROUT" onChange={onDateChange} locale="fr" dateFormat="LLLL" />
</div>
</div>
<div className="col-md-3">
<DateTime name="endDate" onChange={onDateChange} inputFormat="YYYY-MM-DD HH:mm:ss" />
</div>
<div className="col-md-3">
<select className="form-control" onChange={onGroupByChange}>
<option value="ByMember">Par adhérent</option>
<option value="ByProduct">Par Produit</option>
</select>
</div>
<div className="col-md-3">
<a className="btn btn-primary">Afficher</a>
</div>
</div>');
}
function onDateChange(e:js.html.Event){
trace("onDateChange");
//var name :String = untyped e.target.name;
//var value :String = untyped e.target.value;
//trace('$name $value');
trace(e);
//e.preventDefault();
}
/**
* @doc https://facebook.github.io/react/docs/forms.html
*/
function onGroupByChange(e:js.html.Event){
e.preventDefault();
trace("onGRoupByChange");
var name :String = untyped e.target.name;
var value :String = untyped e.target.value;
if (value == "ByMember"){
state.groupBy = ByMember;
}else{
state.groupBy = ByProduct;
}
trace(state);
setState(state);
}
}
+17
View File
@@ -0,0 +1,17 @@
package react;
/**
*
* Use the 1.4.x version. The 2.x version is still buggy
*
* @doc https://www.npmjs.com/package/react-bootstrap-typeahead
*/
@:jsRequire('react-bootstrap-typeahead', 'Typeahead')
extern class Typeahead extends react.ReactComponent{}
/**
* @doc https://github.com/ericgio/react-bootstrap-typeahead/blob/803f61c1c8d0c943106233ed3c9306acc19b5b2b/docs/API.md#asynctypeahead
* Async component is needed when options and searches are managed asynchronously
*/
@:jsRequire('react-bootstrap-typeahead', 'AsyncTypeahead')
extern class AsyncTypeahead extends react.ReactComponent{}
+125
View File
@@ -0,0 +1,125 @@
package react;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
// I need to store also the "input" because of https://stackoverflow.com/questions/29140354/how-to-handle-decimal-values-in-reacts-onchange-event-for-input
typedef VATBoxState = {ht:Float, ttc:Float, vat:Float, htInput:String, ttcInput:String,lastEdited:String};
/**
* A box to manage prices with and without VAT
* @author fbarbut
*/
class VATBox extends react.ReactComponentOfPropsAndState<{ttc:Float,currency:String,vatRates:String,vat:Float,formName:String},VATBoxState>
{
public function new(props)
{
super(props);
//trace(props);
this.state = {
ht : round(props.ttc/(1+props.vat/100)),
htInput : Std.string(round(props.ttc/(1+props.vat/100))),
ttc : round(props.ttc),
ttcInput : Std.string(round(props.ttc)),
vat:props.vat,
lastEdited:null
};
}
override public function render(){
var rates :Array<Float>= props.vatRates.split("|").map(Std.parseFloat);
var options = [for (r in rates) jsx('<option key="$r" value="$r">$r %</option>') ];
var priceInputName = props.formName+"_price";
var vatInputName = props.formName+"_vat";
return jsx('<div>
<div className="row">
<div className="col-md-4 text-center"> Hors taxe </div>
<div className="col-md-4 text-center"> Taux de TVA </div>
<div className="col-md-4 text-center"> TTC </div>
</div>
<div className="row">
<div className="col-md-4">
<div className="input-group">
<input type="text" name="htInput" value="${state.htInput}" className="form-control" onChange={onChange}/>
<div className="input-group-addon">${props.currency}</div>
</div>
</div>
<div className="col-md-4">
<select name="vat" className="form-control" onChange={onChange} defaultValue=${state.vat}>
${options}
</select>
</div>
<div className="col-md-4">
<div className="input-group">
<input type="text" name="ttcInput" value="${state.ttcInput}" className="form-control" onChange={onChange}/>
<div className="input-group-addon">${props.currency}</div>
</div>
</div>
</div>
<input type="hidden" name="$priceInputName" value="${state.ttc}" />
<input type="hidden" name="$vatInputName" value="${state.vat}" />
</div>
');
}
/**
* Recompute prices
*/
function onChange(e:js.html.Event){
e.preventDefault();
var name :String = untyped e.target.name;
var input : String = Std.string(untyped e.target.value);
if (input == null || input == "") input = "0";
input = StringTools.replace(input, ",", ".");
var value : Float = Std.parseFloat(input);
if (value == null) value = 0;
var rate = 1 + (state.vat / 100);
//trace('name:$name - raw:' + untyped e.target.value+' - input:$input - value:$value ');
switch(name){
case "htInput":
this.setState(cast {ht:value , htInput:input , ttc: round(value * rate), ttcInput:round(value * rate) , lastEdited:"htInput"});
case "ttcInput":
this.setState(cast {ht: round(value / rate), htInput : round(value/rate), ttcInput:input , ttc:value , lastEdited:"ttcInput"});
case "vat":
rate = 1 + (value / 100);
if (state.lastEdited == "htInput"){
//compute ttc from ht
this.setState(cast { vat:value, ht:state.ht, htInput:state.ht, ttc:round(state.ht * rate) , ttcInput:round(state.ht * rate)} );
}else{
//compute ht from ttc
this.setState(cast { vat:value, ht: round( state.ttc/rate ), htInput: round( state.ttc/rate ), ttc:state.ttc , ttcInput:state.ttc} );
}
default:
}
}
inline function round(f:Float):Float{
return Math.round(f * 100) / 100;
}
}
+252
View File
@@ -0,0 +1,252 @@
package react.map;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
import leaflet.L;
using Lambda;
/**
* Externs for react-leaflet
* @doc https://react-leaflet.js.org/docs/en/intro.html
*/
@:jsRequire('react-leaflet', 'Map')
extern class LeafMap extends ReactComponent {}
@:jsRequire('react-leaflet', 'TileLayer')
extern class TileLayer extends ReactComponent {}
@:jsRequire('react-leaflet', 'Marker')
extern class Marker extends ReactComponent {}
@:jsRequire('react-leaflet', 'CircleMarker')
extern class CircleMarker extends ReactComponent {}
@:jsRequire('react-leaflet', 'Popup')
extern class Popup extends ReactComponent {}
@:jsRequire('react-leaflet', 'FeatureGroup')
extern class FeatureGroup extends ReactComponent {}
@:jsRequire('react-leaflet', 'LayerGroup')
extern class LayerGroup extends ReactComponent {}
/*
extern class L2 {
static function icon(a:Dynamic):Dynamic;
static function latLng(lat:Float, lng:Float):Dynamic;
}*/
/**
* GroupItem
* @author rcrestey
*/
typedef GroupMapProps = {
var addressCoord:Dynamic;
var groups:Array<GroupOnMap>;
var fetchGroupsInsideBox:Box->Void;
var groupFocusedId:Int;
};
typedef GroupMapState = {
var isFitting:Bool;
var focusedMarker:Dynamic;
};
typedef Box = {
var minLat:Float;
var maxLat:Float;
var minLng:Float;
var maxLng:Float;
};
class GroupMap extends ReactComponentOfPropsAndState<GroupMapProps, GroupMapState> {
static inline var DEFAULT_LAT = 46.52863469527167; // center of France
static inline var DEFAULT_LNG = 2.43896484375; // center of France
static inline var INIT_ZOOM = 6;
static inline var DEFAULT_ZOOM = 13;
var map:Dynamic;
var featureGroup:Dynamic;
var markerMap = new Map<Int,Dynamic>();
var groupIcon = L.icon({
iconUrl: '/img/marker.svg',
iconSize: [40, 40],
iconAnchor: [20, 40],
popupAnchor: [0, -30],
className: 'icon'
});
var homeIcon = L.icon({
iconUrl: '/img/home.svg',
iconSize: [40, 40],
iconAnchor: [20, 20],
popupAnchor: [0, -30],
className: 'icon'
});
function new() {
super();
state = {
isFitting: false,
focusedMarker: null
};
}
function getMap(element:Dynamic):Void {
map = element.leafletElement;
}
function getFeatureGroup(element:Dynamic):Void {
featureGroup = element.leafletElement;
setState({
isFitting: true
}, fitBounds);
}
function getMarker(element:Dynamic, id:Int):Void {
if (element != null)
markerMap.set(id, element.leafletElement);
}
/**
* Call API to get groups in the current bounding box
*/
function getGroups() {
var bounds = map.getBounds();
var southWest = bounds.getSouthWest();
var northEast = bounds.getNorthEast();
props.fetchGroupsInsideBox({
minLat: southWest.lat,
maxLat: northEast.lat,
minLng: southWest.lng,
maxLng: northEast.lng
});
}
function fitBounds() {
map.fitBounds(featureGroup.getBounds(), {
padding: [30, 30]
});
}
function handleMoveEnd() {
if (
props.addressCoord != null &&
!Lambda.empty(props.groups) &&
map.distance(map.getCenter(), props.addressCoord) == 0
)
setState({
isFitting: true
}, fitBounds);
else if (state.isFitting)
setState({
isFitting: false
});
else
getGroups();
}
override public function componentDidMount() {
if (props.addressCoord == null)
getGroups();
}
override public function shouldComponentUpdate(nextProps:GroupMapProps, nextState:GroupMapState) {
if (nextState.focusedMarker != state.focusedMarker)
return false;
return true;
}
override public function componentDidUpdate(prevProps:GroupMapProps, prevState:GroupMapState) {
if (props.groupFocusedId != null) {
if (
prevProps.groupFocusedId != props.groupFocusedId
|| state.focusedMarker == null
) {
if (state.focusedMarker != null)
state.focusedMarker.closePopup();
var focusedMarker = markerMap.get(props.groupFocusedId);
focusedMarker.openPopup();
setState({
focusedMarker: focusedMarker
});
}
}
else if (prevProps.groupFocusedId != null && state.focusedMarker != null) {
state.focusedMarker.closePopup();
setState({
focusedMarker: null
});
}
}
override public function render() {
var center = props.addressCoord == null
? L.latLng(DEFAULT_LAT, DEFAULT_LNG)
: props.addressCoord;
var zoom = props.addressCoord == null
? INIT_ZOOM
: DEFAULT_ZOOM;
return jsx('
<LeafMap
center=${center}
zoom=${zoom}
ref=${getMap}
onMoveEnd=${handleMoveEnd}
>
<TileLayer
attribution="&amp;copy <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors"
url="https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiYnViYXIiLCJhIjoiY2loM2lubmZpMDBwcGtxbHlwdmw0bXRkbCJ9.rfgXPakoGnXZ3wIGA3-1kQ"
id="bubar.cih3inmqd00tjuxm7oc2532l0"
/>
<FeatureGroup ref=${getFeatureGroup}>
${renderGroupMarkers()}
${renderHomeMarker()}
</FeatureGroup>
</LeafMap>
');
}
function renderGroupMarkers() {
var markers = props.groups.map(function(group) {
var coord = [group.place.latitude, group.place.longitude];
function markerGetter(e:Dynamic) {
getMarker(e, group.place.id);
}
var image = group.image==null ? null : jsx('<img className="groupImage img-responsive" src=${group.image}/>');
return jsx('
<Marker
position=${coord}
ref=${markerGetter}
key=${group.place.id}
icon=${groupIcon}
>
<Popup className="popup">
<div>
<a href=${"/group/"+group.id} target="_blank">
$image
<div className="groupName">${group.name}</div>
</a>
</div>
</Popup>
</Marker>
');
});
return jsx('<div>${markers}</div>');
}
function renderHomeMarker() {
if (props.addressCoord != null)
return jsx('<Marker position=${props.addressCoord} icon=${homeIcon} />');
return null;
}
}
+312
View File
@@ -0,0 +1,312 @@
package react.map;
import js.Promise;
import react.ReactComponent;
import react.ReactMacro.jsx;
import utils.HttpUtil;
import leaflet.L;
import Common;
using Lambda;
@:jsRequire('react-places-autocomplete', 'default')
extern class Autocomplete extends ReactComponent {}
@:jsRequire('react-places-autocomplete')
extern class GeoUtil {
static function geocodeByAddress(address:Dynamic):Promise<Dynamic>;
}
@:jsRequire('geolib')
extern class Geolib {
static function getDistance(start:Dynamic, end:Dynamic):Float;
}
/**
* Groups Map
* @author rcrestey
*/
typedef GroupMapRootState = {
var point:Dynamic;
var address:String;
var groups:Array<GroupOnMap>;
var groupFocusedId:Int;
var isInit:Bool;
};
typedef GroupMapRootProps = {
var lat:Float;
var lng:Float;
var address:String;
};
class GroupMapRoot extends ReactComponentOfState<GroupMapRootState>{
static inline var GROUP_MAP_URL = '/api/group/map';
var distanceMap = new Map<Int,Dynamic>();
public function new(props)
{
super(props);
state = {
point: L.latLng(props.lat, props.lng),
address: props.address,
groups: [],
groupFocusedId: null,
isInit: false
};
}
function onChange(address) {
setState({
address: address
});
}
function openPopup(group:Dynamic) {
setState({
groupFocusedId: group.place.id
});
}
function closePopup() {
setState({
groupFocusedId: null
});
}
function geocodeByAddress(address:String):Promise<Dynamic> {
return GeoUtil.geocodeByAddress(address)
.then(function(results) {
var lat = results[0].geometry.location.lat();
var lng = results[0].geometry.location.lng();
return {lat: lat, lng: lng};
});
}
/**
* Call API to find groups at $lat and $lng
* @param lat -
* @param lng -
*/
function fetchGroups(lat:Float, lng:Float) {
HttpUtil.fetch(GROUP_MAP_URL, GET, {lat: lat, lng: lng}, JSON)
.then(function(results) {
setState({
point: L.latLng(lat, lng),
groups: results.groups,
isInit: true
}, fillDistanceMap);
})
.catchError(function(error) {
trace('Error', error);
});
}
/**
* Call API to look for groups in the defined bounding box
*/
var wait : Bool;
function fetchGroupsInsideBox(newBox) {
switch(wait){
case null,false : wait = true;
case true : trace("stop"); return;
}
HttpUtil.fetch(GROUP_MAP_URL, GET, newBox, JSON)
.then(function(results) {
wait = false;
setState({
groups: results.groups
}, fillDistanceMap);
});
/*.catchError(function(error) {
trace('Error', error + " stack:"+haxe.CallStack.toString(haxe.CallStack.exceptionStack())) ;
wait = false;
});*/
}
function getGroupDistance(group:GroupOnMap):Float {
if (state.point == null)
return null;
var start = {
latitude: state.point.lat,
longitude: state.point.lng
};
var end = {
latitude: group.place.latitude,
longitude: group.place.longitude
};
return Geolib.getDistance(start, end);
}
function fillDistanceMap() {
for (group in state.groups) {
distanceMap.set(group.place.id, getGroupDistance(group));
}
orderGroupsByDistance(state.groups);
setState({
groups: state.groups
});
}
function orderGroupsByDistance(groups:Array<GroupOnMap>) {
groups.sort(function(a, b) {
return distanceMap.get(a.place.id) - distanceMap.get(b.place.id);
});
}
function convertDistance(distance:Int):String { // to test
if (distance > 10000)
return Math.floor(distance / 1000) + ' km';
if (distance > 1000)
return Math.floor(distance / 100) / 10 + ' km';
return distance + ' m';
}
function handleSelect(address:String) {
setState({
address: address
});
geocodeByAddress(address)
.then(function(coord) {
fetchGroups(coord.lat, coord.lng);
})
.catchError(function(error) {
trace('Error', error);
});
}
override public function componentDidMount() {
if (state.point != null)
fetchGroups(state.point.lat, state.point.lng);
else if (state.address != '')
handleSelect(state.address);
}
function renderSuggestion(obj:Dynamic) {
return jsx('
<div className="autocomplete-item">
<i className="fa fa-map-marker autocomplete-icon" />
<strong>${obj.formattedSuggestion.mainText}</strong>&nbsp;
<small className="text-muted">${obj.formattedSuggestion.secondaryText}</small>
</div>
');
}
override public function render() {
var inputProps = {
value: state.address,
onChange: onChange
};
var cssClasses = {
root: 'form-group',
input: 'autocomplete-input',
autocompleteContainer: 'autocomplete-results',
};
return jsx('
<div className="group-map">
<div className="row">
<div id="logo" className="col-md-3">&nbsp;</div>
<div className="col-md-9">
<div className="form-group-container">
Trouvez un groupe Cagette près de chez vous &nbsp;
<Autocomplete
inputProps=${inputProps}
onSelect=${handleSelect}
classNames=${cssClasses}
renderSuggestion=${renderSuggestion}
placeHolder="Saisissez votre adresse"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-md-3" id="groupsContainer">${renderGroupList()}</div>
<div className="col-md-9" id="mapContainer">${renderGroupMap()}</div>
</div>
</div>');
}
function renderGroupMap() {
if (!state.isInit)
return null;
return jsx('
<GroupMap
addressCoord=${state.point}
groups=${state.groups}
fetchGroupsInsideBox=${fetchGroupsInsideBox}
groupFocusedId=${state.groupFocusedId}
/>
');
}
function renderGroupList() {
var groups = state.groups.map(function(group) {
return renderGroup(group);
});
return jsx('
<div className="groups">
${groups}
</div>
');
}
/**
* Renders a group in the left list
*/
function renderGroup(group:GroupOnMap) {
var address = [
group.place.address1,
group.place.address2,
[group.place.zipCode, group.place.city].join(" "),
];
var addressBlock = Lambda.array(address.mapi(function(index, element) {
if (element != null){
return jsx('<div key=${index}>$element</div>');
}else{
return null;
}
}));
var distance = null;
if (distanceMap.get(group.place.id) != null)
distance = jsx('<div className="distance">${convertDistance(distanceMap.get(group.place.id))}</div>');
var classNames = ['clickable groupBlock'];
if (group.place.id == state.groupFocusedId)
classNames.push('focused');
var img = if(group.image==null) {
null;
}else{
jsx('<img src="${group.image}" className="img-responsive" />');
}
return jsx('<a target="_blank"
onMouseEnter=${function() { openPopup(group); }}
onMouseLeave=${closePopup}
className=${classNames.join(' ')}
key=${group.place.id}
href=${"/group/"+group.id}
>
$img
<h4>${group.name}</h4>
<div className="address">${addressBlock}</div>
${distance}
</a>');
}
}
+112
View File
@@ -0,0 +1,112 @@
package react.order;
import react.ReactDOM;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
import utils.HttpUtil;
import react.product.ProductSelect;
import react.router.Redirect;
import react.router.Link;
/**
* A box to add an order to a member
* @author fbarbut
*/
class InsertOrder extends react.ReactComponentOfPropsAndState<{contractId:Int,userId:Int,distributionId:Int,onInsert:UserOrder->Void},{products:Array<ProductInfo>,error:String,selected:Int}>
{
public function new(props)
{
super(props);
state = {products:[],error:null,selected:null};
}
override function componentDidMount()
{
//load product list
HttpUtil.fetch("/api/product/get/", GET, {contractId:props.contractId},PLAIN_TEXT)
.then(function(data:String) {
/*var data : {products:Array<ProductInfo>} = haxe.Json.parse(data);
for( p in data.products) {
p.unitType = Type.createEnumIndex(UnitType,untyped p.unitType);
}*/
var data : {products:Array<ProductInfo>} = tink.Json.parse(data);
setState({products:data.products, error:null,selected:null});
}).catchError(function(data) {
var data = Std.string(data);
trace("Error",data);
if(data.substr(0,1)=="{"){
//json error from server
var data : ErrorInfos = haxe.Json.parse(data);
setState(cast {error:data.error.message} );
}else{
//js error
setState(cast {error:data} );
}
});
}
override public function render(){
//redirect to orderBox if a product is selected
var redirect = if(state.selected!=null) jsx('<$Redirect to="/" />') else null;
return jsx('
<div>
$redirect
<h3>Choisissez le produit à ajouter</h3>
<$Link className="btn btn-default" to="/"><span className="glyphicon glyphicon-chevron-left"></span> Retour</$Link>
<$Error error="${state.error}" />
<hr />
<$ProductSelect onSelect=$onSelectProduct products=${state.products} />
</div>
');
}
function onSelectProduct(p:ProductInfo){
var uo : UserOrder = cast {
id:null,
product:p,
quantity:1,
productId:p.id,
productPrice:p.price,
paid:false,
invert:false,
user2:null
};
props.onInsert(uo);
setState(cast {selected:p.id});
//do not insert order now, just warn the OrderBox
/*
//insert order
var data = [{id:null,productId:p.id,qt:1,paid:false,invert:false,user2:null} ];
var req = {
orders:haxe.Json.stringify(data),
distributionId : props.distributionId,
contractId : props.contractId
};
var r = HttpUtil.fetch("/api/order/update/"+props.userId, POST, req, JSON);
r.then(function(d:Dynamic) {
if (Reflect.hasField(d, "error")) {
setState(cast {error:d.error.message});
}else{
//WOOT
//trace("OK");
//go to OrderBox with a redirect
setState(cast {selected:p.id});
}
}).catchError(function(d) {
trace("PROMISE ERROR", d);
setState(cast {error:d.error.message});
});
*/
}
}
+179
View File
@@ -0,0 +1,179 @@
package react.order;
import react.ReactDOM;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
import react.product.Product;
/**
* A User order
* @author fbarbut
*/
class Order extends react.ReactComponentOfPropsAndState<{order:UserOrder,onUpdate:UserOrder->Void,parentBox:react.order.OrderBox},{order:UserOrder,inputValue:String}>
{
var hasPayments :Bool;
var currency : String;
public function new(props)
{
super(props);
state = {order:props.order,inputValue:null};
hasPayments = props.parentBox.props.hasPayments;
currency = props.parentBox.props.currency;
if (state.order.productUnit == null) state.order.productUnit = Piece;
if (state.order.productQt == null) state.order.productQt = 1;
state.inputValue = if ( isSmartQtInput(state.order) ){
Std.string(round(state.order.quantity * state.order.productQt));
}else{
Std.string(state.order.quantity);
}
}
override public function render(){
var o = state.order;
/*var unit = if (o.productHasFloatQt || o.productHasVariablePrice){
jsx('<div className="col-md-1">${Formatting.unit(o.productUnit)}</div>');
}else{
jsx('<div className="col-md-1"></div>');
}*/
/*
//use smart qt only if hasFloatQt
var productName = if (o.productHasFloatQt || o.productHasVariablePrice){
jsx('<div className="col-md-3">${o.productName}</div>');
}else{
jsx('<div className="col-md-3">${o.productName} ${o.productQt} ${Formatting.unit(o.productUnit)}</div>');
}
*/
/*var productName = if (o.productHasFloatQt || o.productHasVariablePrice){
jsx('<div className="col-md-3">${o.productName}</div>');*/
var input = if (isSmartQtInput(o)){
jsx('<div className="input-group">
<input type="text" className="form-control input-sm text-right" value="${state.inputValue}" onChange=${onChange} onKeyPress=${onKeyPress}/>
<div className="input-group-addon">${Formatting.unit(o.productUnit)}</div>
</div>');
}else{
jsx('<div className="input-group">
<input type="text" className="form-control input-sm text-right" value="${state.inputValue}" onChange=${onChange} onKeyPress=${onKeyPress}/>
</div>');
}
var alternated = if(props.parentBox.props.contractType==0 && props.parentBox.state.users!=null){
//constant orders
var options = props.parentBox.state.users.map(function(x) return jsx('<option key=${x.id} value=${x.id}>${x.name}</option>') );
var checkbox = if(o.invertSharedOrder){
jsx('<input data-toggle="tooltip" title="Inverser l\'alternance" checked="checked" type="checkbox" value="1" onChange=$onChangeInvert />');
}else{
jsx('<input data-toggle="tooltip" title="Inverser l\'alternance" type="checkbox" value="1" onChange=$onChangeInvert />');
}
jsx('<div>
<select className="form-control input-sm" style=${{width:"150px",display:"inline-block"}} onChange=${onChangeUser2} value=${o.userId2}>
<option value="0">-</option>
$options
</select>
$checkbox
</div>');
}else{
null;
}
return jsx('<div className="productOrder row">
<div className="col-md-4">
<$Product productInfo=${o.product} />
</div>
<div className="col-md-1 ref">
${o.productRef}
</div>
<div className="col-md-1">
${round(o.quantity * o.productPrice)}&nbsp;${currency}
</div>
<div className="col-md-2">
$input
${makeInfos()}
</div>
${paidInput()}
<div className="col-md-3">$alternated</div>
</div>');
}
function round(f){
return Formatting.formatNum(f);
}
function paidInput(){
if(hasPayments) return null;
if(state.order.paid){
return jsx('<div className="col-md-1"><input type="checkbox" name="paid" value="1" checked="checked" onChange=${onChangePaid} /></div>');
}else{
return jsx('<div className="col-md-1"><input type="checkbox" name="paid" value="1" onChange=${onChangePaid} /></div>');
}
}
function makeInfos(){
var o = state.order;
return if ( isSmartQtInput(o) ){
jsx('<div className="infos">
<b> ${round(o.quantity)} </b> x <b>${o.productQt} ${Formatting.unit(o.productUnit)}</b > ${o.productName}
</div>');
}else{
null;
}
}
function isSmartQtInput(o:UserOrder):Bool{
return o.product.hasFloatQt || o.product.variablePrice || o.product.wholesale;
}
function onChange(e:js.html.Event){
e.preventDefault();
var value :String = untyped (e.target.value == "") ? "0" : e.target.value;
state.inputValue = value;
var v = Formatting.parseFloat(value);
var o = state.order;
if ( isSmartQtInput(o) ){
//the value is a smart qt, so we need re-compute the quantity
o.quantity = v / o.productQt;
}else{
o.quantity = v;
}
this.setState(state);
if (props.onUpdate != null) props.onUpdate(state.order);
}
function onChangePaid(e:js.html.Event){
state.order.paid = untyped e.target.checked;
this.setState(state);
if (props.onUpdate != null) props.onUpdate(state.order);
}
function onChangeInvert(e:js.html.Event){
state.order.invertSharedOrder = untyped e.target.checked;
this.setState(state);
if (props.onUpdate != null) props.onUpdate(state.order);
}
function onChangeUser2(e:js.html.Event){
var v = Std.parseInt(untyped e.target.value);
state.order.userId2 = v==0 ? null : v;
this.setState(state);
if (props.onUpdate != null) props.onUpdate(state.order);
}
function onKeyPress(event:js.html.KeyboardEvent){
/*if(event.key == 'Enter'){
trace('enter !');
}*/
}
}
+224
View File
@@ -0,0 +1,224 @@
package react.order;
import react.ReactDOM;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
import utils.HttpUtil;
import react.router.HashRouter;
import react.router.Route;
import react.router.Switch;
import react.router.Link;
typedef OrderBoxState = {
orders:Array<UserOrder>,
error:String,
users:Null<Array<UserInfo>>,
};
typedef OrderBoxProps = {
userId:Int,
distributionId:Int,
contractId:Int,
contractType:Int,
date:String,
place:String,
userName:String,
onValidate:Void->Void,
currency:String,
hasPayments:Bool
};
/**
* A box to edit/add orders of a member
* @author fbarbut
*/
class OrderBox extends react.ReactComponentOfPropsAndState<OrderBoxProps,OrderBoxState>
{
public function new(props)
{
super(props);
state = { orders : [], error : null, users:null };
}
override function componentDidMount()
{
//request api avec user + distrib
HttpUtil.fetch("/api/order/get/"+props.userId, GET, {distributionId:props.distributionId,contractId:props.contractId}, PLAIN_TEXT)
.then(function(data:String) {
var data : {orders:Array<UserOrder>} = tink.Json.parse(data);
/*for( o in orders){
//convert ints to enums, enums have been lost in json serialization
o.productUnit = Type.createEnumIndex(Unit, cast o.productUnit );
}*/
setState({orders:data.orders, error:null});
if(props.contractType==0) loadUsers();
}).catchError(function(data) {
var data = Std.string(data);
trace("Error",data);
if(data.substr(0,1)=="{"){
//json error from server
var data : ErrorInfos = haxe.Json.parse(data);
setState( cast {error:data.error.message} );
}else{
//js error
setState( cast {error:data} );
}
});
}
/**
* load user list when contract is constant orders
*/
function loadUsers(){
HttpUtil.fetch("/api/user/getFromGroup/", GET, {}, PLAIN_TEXT)
.then(function(data:String) {
var data : {users:Array<UserInfo>} = tink.Json.parse(data);
setState({users:data.users, error:null});
}).catchError(function(data) {
var data = Std.string(data);
if(data.substr(0,1)=="{"){
//json error from server
var data : ErrorInfos = haxe.Json.parse(data);
setState( cast {error:data.error.message} );
}else{
//js error
setState( cast {error:data} );
}
});
}
override public function render(){
//edit orders
var renderOrders = this.state.orders.map(function(o){
var k :String = if(o.id!=null) {
Std.string(o.id);
} else {
o.productId+"-"+Std.random(99999);
};
return jsx('<$Order key="$k" order="$o" onUpdate=$onUpdate parentBox=${this} />') ;
} );
var delivery = if(props.date==null){
null;
}else{
jsx('<p>Pour la livraison du <b>${props.date}</b> à <b>${props.place}</b></p>');
}
var renderOrderBox = function() return jsx('
<div onKeyPress=${onKeyPress}>
<h3>Commandes de ${props.userName}</h3>
$delivery
<$Error error="${state.error}" />
<hr/>
<div className="row tableHeader">
<div className="col-md-4">Produit</div>
<div className="col-md-1">Ref.</div>
<div className="col-md-1">Prix</div>
<div className="col-md-2">Qté</div>
<div className="col-md-1">Payé</div>
<div className="col-md-3">Alterné avec</div>
</div>
${renderOrders}
<div>
<a onClick=${onClick} className="btn btn-primary">
<span className="glyphicon glyphicon-chevron-right"></span> Valider
</a>
&nbsp;
<$Link className="btn btn-default" to="/insert"><span className="glyphicon glyphicon-plus-sign"></span> Ajouter un produit</$Link>
</div>
</div>
');
var onProductSelected = function(uo:UserOrder){
var existingOrder = Lambda.find(state.orders,function(x) return x.productId==uo.productId );
if(existingOrder!=null){
existingOrder.quantity += uo.quantity;
this.setState(this.state);
}else{
this.state.orders.push(uo);
this.setState(this.state);
}
};
//insert product box
var renderInsertBox = function(){
return jsx('<$InsertOrder contractId="${props.contractId}" userId="${props.userId}" distributionId="${props.distributionId}" onInsert=$onProductSelected/>');
}
return jsx('<$HashRouter>
<$Switch>
<$Route path="/" exact=$true render=$renderOrderBox />
<$Route path="/insert" exact=$true render=$renderInsertBox />
</$Switch>
</$HashRouter>');
}
/**
* called when an order is updated
*/
function onUpdate(data:UserOrder){
/*trace("ON UPDATE : " + data);
for ( o in state.orders){
if (o.id == data.id) {
o.quantity = data.quantity;
o.paid = data.paid;
break;
}
}
setState(this.state);*/
}
/**
* submit updated orders to the API
*/
function onClick(?_){
var data = new Array<{id:Int,productId:Int,qt:Float,paid:Bool,invertSharedOrder:Bool,userId2:Int}>();
for ( o in state.orders) data.push({id:o.id, productId : o.productId, qt: o.quantity, paid : o.paid, invertSharedOrder:o.invertSharedOrder, userId2:o.userId2});
var req = { orders:data };
var p = HttpUtil.fetch("/api/order/update/"+props.userId+"?distributionId="+props.distributionId+"&contractId="+props.contractId, POST, req, JSON);
p.then(function(data:Dynamic) {
//WOOT
if (props.onValidate != null) props.onValidate();
}).catchError(function(data) {
var data = Std.string(data);
trace("Error",data);
if(data.substr(0,1)=="{"){
//json error from server
var data : ErrorInfos = haxe.Json.parse(data);
setState( cast {error:data.error.message} );
}else{
//js error
setState( cast {error:data} );
}
});
}
function onKeyPress(e:js.html.KeyboardEvent){
if(e.key=="Enter") onClick();
}
}
+104
View File
@@ -0,0 +1,104 @@
package react;
import api.react.ReactMacro.jsx;
import js.html.InputElement;
import Common;
typedef ComposerAppState = {
products:Array<{id:Int,name:String,qt:Float,unit:UnitType}>
}
typedef ComposerAppRefs = {
pi:ProductInput,
productContainer:js.html.DivElement,
qt:InputElement,
unit:js.html.SelectElement,
}
/**
* Composite product composer
*
*/
class ComposerApp extends ReactComponentOfStateAndRefs<ComposerAppState, ComposerAppRefs>
{
/*var products:Array<{id:Int,name:String,?qt:Float,?unit:UnitType}>;
public function new(props:Dynamic)
{
super(props);
products = [{id:1,name:"pipo"},{id:2,name:"Loclac"}];
}
override public function render(){
return jsx('
<div className="ComposerApp" style={{margin:"10px"}} >
<div className="form-inline">
<ProductInput ref="pi"/>
<input ref="qt" onChange="$onChange" className="form-control" type="text" name="qt" placeholder="Quantité" />
<select ref="unit" className="form-control" name="unit">
${getUnits()}
</select>
<a className="btn btn-primary" onClick=$addItem>
<span className="glyphicon glyphicon-plus"></span> Ajouter
</a>
</div>
<div className="container" ref="productContainer">
${createChildren()}
</div>
</div>
');
}
function onChange(){
}
function getUnits(){
var out = [];
for ( c in Unit.createAll()){
out.push(jsx( '<option value="{c.getIndex()}"> {Std.string(c)} </option>'));
}
return out;
}
function createChildren()
{
return [for (p in products) jsx('<ProductComp key={p.id} name={p.name} qt={p.qt} unit={p.unit}/>')];
}
function addItem(){
var text :String = refs.pi.refs.input.value;
if (text.length > 0)
{
trace("add " + text);
trace("qt " + this.refs.qt.value);
trace("unit " + this.refs.unit.selectedIndex);
var qt = Std.parseFloat(this.refs.qt.value);
var unit = UnitType.createByIndex(this.refs.unit.selectedIndex);
var id = Std.random(999);
products.push( {id:id, name:text,qt:qt,unit:unit});
setState({products:[{id:id,name:text, qt:qt, unit:unit}]});
//this.forceUpdate();
}
}*/
}
+39
View File
@@ -0,0 +1,39 @@
package react.product;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
/**
* A Product
* @author fbarbut
*/
class Product extends react.ReactComponentOfProps<{productInfo:ProductInfo}>
{
public function new(props)
{
super(props);
}
override public function render(){
var p :ProductInfo = props.productInfo;
//convert int to enum
//p.unitType = Type.createEnumIndex(Common.Unit,cast p.unit);
//var unit = ;
var imgStyle = {width:'64px',height:'64px','backgroundImage':'url("${p.image}")'};
var divStyle = p.active ? {} : {opacity: 0.4};
return jsx('<div className="product row" style=$divStyle>
<div className="col-md-4">
<div src="${p.image}" className="productImg" style=$imgStyle/>
</div>
<div className="col-md-8">
<strong>${p.name}</strong> ${p.qt} ${Formatting.unit(p.unitType)}<br/>
${p.price} &euro;
</div>
</div>');
}
}
+168
View File
@@ -0,0 +1,168 @@
package react.product;
import react.ReactDOM;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
import react.Typeahead;
typedef ProductInputProps = {
formName:String,
txpProductId:Int,
productName:String,
}
typedef ProductInputState = {
txpProductId:Int,
productName:String,
categoryId:Int,
breadcrumb:String,
}
/**
* Product Text Input with autocompletion
*
* @author fbarbut
*/
class ProductInput extends react.ReactComponentOfPropsAndState<ProductInputProps,ProductInputState>
{
public static var DICO : TxpDictionnary = null;
var options : Array<{id:Int,label:String}>;
public function new(props:ProductInputProps)
{
super(props);
options = [];
this.state = {
txpProductId : props.txpProductId,
productName : props.productName,
categoryId : 0,
breadcrumb : ""
};
}
override public function render(){
var inputName :String = props.formName+"_name";
var txpProductInputName :String = props.formName+"_txpProductId";
return jsx('
<div className="row">
<div className="col-md-8">
<AsyncTypeahead
placeholder="Saisissez un nom de produit"
options=$options
onSearch=$onSearch
minLength={3}
style={{width:"350px"}}
onChange=$onChange
onInputChange=$onInputChange
selected={["${state.productName}"]}
isLoading=$true
/>
<div className = "txpProduct" > ${state.breadcrumb}</div>
<input className="txpProduct" type="hidden" name="$txpProductInputName" value="${state.txpProductId}" />
<input className="txpProduct" type="hidden" name="$inputName" value="${state.productName}" />
</div>
<div className="col-md-4">
<img ref="image" className="img-thumbnail" />
</div>
</div>
');
}
/**
* Called when typing is stopped
* @param o
*/
function onSearch(o){
//trace("on search : "+o);
}
/**
* Each time a single letter change in the input
* @param input
*/
function onInputChange(input:String){
trace('on input change $input');
this.setState({productName:input});
}
/**
* Called when an item is selected in suggestions
*/
function onChange(selection:Array<{label:String,id:Int}>){
if (selection == null || selection.length == 0) return;
trace("on change "+selection[0]);
var product = Lambda.find(DICO.products, function(x) return x.id == selection[0].id);
setTaxo(product);
this.setState({productName:selection[0].label});
}
/**
* init typeahead auto-completion features when component is mounted
*/
override function componentDidMount(){
//get dictionnary
if (DICO == null){
var r = new haxe.Http("/product/getTaxo");
r.onData = function(data){
//load dico
DICO = haxe.Unserializer.run(data);
for ( p in DICO.products){
options.push({label:p.name,id:p.id});
}
//default values of input
if (props.txpProductId != null){
var txp = Lambda.find(DICO.products, function(x) return x.id == props.txpProductId);
setTaxo(txp);
}
};
r.request();
}
}
function setTaxo(txp:{id:Int, name:String, category:Int, subCategory:Int}){
if (txp == null) return;
//trace(txp);
this.setState({
categoryId:txp.category,
txpProductId:txp.id,
breadcrumb:getBreadcrumb(txp)/*,
productName:product.name //do not override product name ! */
});
this.refs.image.src="/img/taxo/cat"+txp.category+".png";
}
/**
* generate string like "fruits & vegetables / vegetables / carrots"
* @param name
*/
function getBreadcrumb(product){
//cat
var str = DICO.categories.get(product.category).name;
if (product.subCategory != null){
str += " / " + DICO.subCategories.get(product.subCategory).name;
}
str += " / " + product.name;
return str;
}
}
+41
View File
@@ -0,0 +1,41 @@
package react.product;
import react.ReactDOM;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
import utils.HttpUtil;
/**
* A Product selector
* @author fbarbut
*/
class ProductSelect extends react.ReactComponentOfPropsAndState<{onSelect:ProductInfo->Void,products:Array<ProductInfo>},{selected:Int}>
{
public function new(props)
{
super(props);
state = { selected : null };
}
override public function render(){
var products = props.products.map(function(info){
//var selector = info.id==state.selected ? jsx(''):jsx('<div className="clickable"><$Product productInfo=$info /></div>');
return jsx('<div key=${info.id} className="col-md-6" onClick=${onClick.bind(info.id)}>
<div className="clickable"><$Product productInfo=$info /></div>
</div>');
});
return jsx('<div className="productSelect">${products}</div>');
}
function onClick(i:Int){
this.setState(cast {selected:i});
if(props.onSelect!=null){
var p = Lambda.find(props.products,function(x) return x.id==i);
props.onSelect(p);
}
}
}
+103
View File
@@ -0,0 +1,103 @@
package react.store;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
typedef CartProps = {
var order:OrderSimple;
var addToCart:ProductInfo -> Int -> Void;
var removeFromCart:ProductInfo -> ?Int -> Void;
var submitOrder:OrderSimple -> Void;
};
class Cart extends react.ReactComponentOfProps<CartProps>
{
function addToCart(product:ProductInfo, quantity:Int):Void {
props.addToCart(product, quantity);
}
function removeFromCart(product:ProductInfo, quantity:Int):Void {
props.removeFromCart(product, quantity);
}
function removeAllFromCart(product:ProductInfo):Void {
props.removeFromCart(product);
}
function submitOrder():Void {
props.submitOrder(props.order);
}
override public function render(){
return jsx('
<div className="cart">
<h3>Ma Commande</h3>
${renderProducts()}
${renderFooter()}
</div>
');
}
function renderProducts() {
var productsToOrder = props.order.products.map(function(product:ProductWithQuantity) {
var quantity = product.quantity;
var product = product.product;
return jsx('
<div className="product-to-order" key=${product.name}>
<div>${product.name}</div>
<div>$quantity</div>
<div className="cart-action-buttons">
<div onClick=${function(){
this.addToCart(product, 1);
}}>
+
</div>
<div onClick=${function(){
this.removeFromCart(product, 1);
}}>
-
</div>
<div onClick=${function(){
this.removeAllFromCart(product);
}}>
x
</div>
</div>
</div>
');
});
return jsx('
<div className="products-to-order">
${productsToOrder}
</div>
');
}
function renderFooter() {
var buttonClasses = ["order-button"];
var submit = submitOrder;
if (props.order.products.length == 0) {
buttonClasses.push("order-button--disabled");
submit = null;
}
return jsx('
<div className="cart-footer">
<div className="total">
Total
<div>${props.order.total} </div>
</div>
<div className=${buttonClasses.join(" ")} onClick=$submit>Commander</div>
</div>
');
}
}
+45
View File
@@ -0,0 +1,45 @@
package react.store;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
using Lambda;
typedef FiltersProps = {
var categories:Array<CategoryInfo>;
var filters:Array<String>;
var toggleFilter:String -> Void;
};
class Filters extends react.ReactComponentOfProps<FiltersProps>
{
override public function render(){
return jsx('
<div className="filters">
<h3>Filtres</h3>
${renderFilters()}
</div>
');
}
function renderFilters() {
return props.categories.map(function(category) {
var classNames = ["filter"];
if (props.filters.has(category.name))
classNames.push("active");
return jsx('
<div
className=${classNames.join(" ")}
key=${category.id}
onClick=${function(){
props.toggleFilter(category.name);
}}
>
${category.name}
</div>
');
});
}
}
+65
View File
@@ -0,0 +1,65 @@
package react.store;
import js.Browser.window;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
typedef ProductProps = {
var product:ProductInfo;
var addToCart:ProductInfo -> Int -> Void;
};
typedef ProductState = {
var quantity:Int;
};
class Product extends react.ReactComponentOfPropsAndState<ProductProps, ProductState>
{
static inline var OVERLAY_URL = '/shop/productInfo';
static inline var IMAGE_WIDTH = 120;
static inline var IMAGE_HEIGHT = 120;
public function new() {
super();
state = {
quantity: 1
};
}
function openOverlay() {
untyped window._.overlay('$OVERLAY_URL/${props.product.id}', props.product.name);
}
function updateQuantity(event:Dynamic) {
var quantity = Std.parseInt(event.target.value);
if (Std.is(quantity, Int) && quantity > 0)
setState({
quantity: Std.int(quantity)
});
}
function addToCart() {
props.addToCart(props.product, state.quantity);
}
override public function render(){
var product = props.product;
return jsx('
<div className="product">
<img src=${product.image} width=${IMAGE_WIDTH+'px'} height=${IMAGE_HEIGHT+'px'} alt={product.name} />
<div className="body">
<a onClick=$openOverlay>
${product.name}
</a>
<div>${product.price} </div>
<input type="number" value=${state.quantity} onChange=$updateQuantity />
<div className="button" onClick=$addToCart>Ajouter</div>
</div>
</div>
');
}
}
+73
View File
@@ -0,0 +1,73 @@
package react.store;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
using Lambda;
typedef ProductListProps = {
var categories:Array<CategoryInfo>;
var productsBySubcategoryIdMap:Map<Int, Array<ProductInfo>>;
var filters:Array<String>;
var addToCart:ProductInfo -> Int -> Void;
};
class ProductList extends react.ReactComponentOfProps<ProductListProps>
{
override public function render(){
return jsx('
<div className="categories">
${renderCategories()}
</div>
');
}
function renderCategories() {
return props.categories.map(function(category) {
if (!props.filters.has(category.name))
return null;
return jsx('
<div className="category" key=${category.name}>
<h2>${category.name}</h2>
<div className="subCategories">
${renderSubCategories(category)}
</div>
</div>
');
});
}
function renderSubCategories(category) {
var subCategories = category.subcategories.map(function(category) {
if (!props.productsBySubcategoryIdMap.exists(category.id))
return jsx('<div key=${category.name}>Loading...</div>');
var products = props.productsBySubcategoryIdMap.get(category.id);
return jsx('
<div className="sub-category" key=${category.name}>
<h3>${category.name}</h3>
<div className="products">
${renderProducts(products)}
</div>
</div>
');
});
return jsx('
<div className="sub-categories">
$subCategories
</div>
');
}
function renderProducts(products) {
return products.map(function(product) {
return jsx('
<Product product=${product} key=${product.id} addToCart=${props.addToCart}/>
');
});
}
}
+229
View File
@@ -0,0 +1,229 @@
package react.store;
import react.ReactComponent;
import react.ReactMacro.jsx;
import haxe.Json;
using Lambda;
import Common;
import utils.CartUtils;
import utils.HttpUtil;
typedef StoreProps = {
var place:Int;
var date:String;
};
typedef StoreState = {
var place:PlaceInfos;
var orderByEndDates:Array<OrderByEndDate>;
var categories:Array<CategoryInfo>;
var productsBySubcategoryIdMap:Map<Int, Array<ProductInfo>>;
var order:OrderSimple;
var filters:Array<String>;
};
class Store extends react.ReactComponentOfPropsAndState<StoreProps, StoreState>
{
static inline var CATEGORY_URL = '/api/shop/categories';
static inline var PRODUCT_URL = '/api/shop/products';
static inline var INIT_URL = '/api/shop/init';
static inline var VIEW_URL = '/place/view';
public function new() {
super();
state = {
place: null,
orderByEndDates: [],
categories: [],
filters: [],
productsBySubcategoryIdMap: new Map(),
order: {
products: [],
total: 0
}
};
}
override function componentDidMount() {
var categoriesRequest = HttpUtil.fetch(CATEGORY_URL, GET, {date: props.date, place: props.place}, JSON);
var initRequest = HttpUtil.fetch(INIT_URL, GET, {date: props.date, place: props.place}, JSON);
initRequest.then(function(infos:Dynamic) {
setState({
place: infos.place,
orderByEndDates: infos.orderEndDates
});
})
.catchError(function(error) {
trace("ERROR", error);
});
categoriesRequest.then(function(categories:Dynamic) {
var categories:Array<CategoryInfo> = categories.categories;
var subCategories = [];
for (category in categories) {
subCategories = subCategories.concat(category.subcategories);
}
setState({
categories: categories,
filters: categories.map(function(category) {
return category.name;
})
});
subCategories.map(function(category:CategoryInfo) {
return HttpUtil.fetch(PRODUCT_URL, GET, {date: props.date, place: props.place, subcategory: category.id}, JSON)
.then(function(result) {
var productsBySubcategoryIdMapCopy = [
for (key in state.productsBySubcategoryIdMap.keys())
key => state.productsBySubcategoryIdMap.get(key)
];
productsBySubcategoryIdMapCopy.set(category.id, result.products);
setState({
productsBySubcategoryIdMap: productsBySubcategoryIdMapCopy
});
});
});
})
.catchError(function(error) {
trace("ERROR", error);
});
}
function toggleFilter(category:String) {
var filters = state.filters.copy();
if (state.filters.find(function(categoryInFilter) {
return category == categoryInFilter;
}) != null)
filters.remove(category);
else
filters.push(category);
if (filters.length == 0)
filters = state.categories.map(function(category) {
return category.name;
});
setState({
filters: filters
});
}
function addToCart(productToAdd:ProductInfo, quantity:Int):Void {
setState({
order: CartUtils.addToCart(state.order, productToAdd, quantity)
});
}
function removeFromCart(productToRemove:ProductInfo, ?quantity:Int):Void {
setState({
order: CartUtils.removeFromCart(state.order, productToRemove, quantity)
});
}
function submitOrder(order:OrderSimple) {
var orderInSession = {
total: order.total,
products: order.products.map(function(p:ProductWithQuantity){
return {
productId: p.product.id,
quantity: p.quantity
};
})
}
trace('Order', orderInSession);
}
override public function render(){
return jsx('
<div className="shop">
${renderHeader()}
<ProductList
categories=${state.categories}
productsBySubcategoryIdMap=${state.productsBySubcategoryIdMap}
filters=${state.filters}
addToCart=$addToCart
/>
<Filters
categories=${state.categories}
filters=${state.filters}
toggleFilter=$toggleFilter
/>
<Cart
order=${state.order}
addToCart=$addToCart
removeFromCart=$removeFromCart
submitOrder=$submitOrder
/>
</div>
');
}
function renderHeader() {
if (state.orderByEndDates == null || state.orderByEndDates.length == 0)
return null;
var endDates;
if (state.orderByEndDates.length == 1) {
var orderEndDate = state.orderByEndDates[0].date;
endDates = [jsx('<div key=$orderEndDate>La commande fermera le $orderEndDate</div>')];
}
else {
endDates = state.orderByEndDates.map(function(order) {
if (order.contracts.length == 1)
return jsx('
<div key=${order.date}>
La commande ${order.contracts[0]} fermera le: ${order.date}
</div>
');
return jsx('
<div key=${order.date}>
Les autres commandes fermeront: ${order.date}
</div>
');
});
}
var viewUrl = '$VIEW_URL/${props.place}';
var addressBlock = Lambda.array([
state.place.address1,
state.place.address2,
[state.place.zipCode, state.place.city].join(" "),
].mapi(function(index, element) {
if (element == null)
return null;
return jsx('<div className="address" key=$index>$element</div>');
}));
return jsx('
<div className="shop-header">
<div>
<div className="shop-distribution">
Distribution le ${props.date}
</div>
<div className="shop-order-ends">
$endDates
</div>
</div>
<div className="shop-place">
<span className="info">
<span className="glyphicon glyphicon-map-marker"></span>
<a href=$viewUrl>${state.place.name}</a>
</span>
<div>
$addressBlock
</div>
</div>
</div>
');
}
}
+139
View File
@@ -0,0 +1,139 @@
package react.user;
import react.ReactComponent;
import react.ReactMacro.jsx;
import Common;
typedef LoginBoxProps = {
redirectUrl:String,
message:String,
?phoneRequired:Bool
}
typedef LoginBoxState = {
email:String,
password:String,
error:String
}
/**
* Login Box
* @author fbarbut
*/
class LoginBox extends react.ReactComponentOfPropsAndState<LoginBoxProps,LoginBoxState>
{
public function new(props:LoginBoxProps)
{
if (props.redirectUrl == null) props.redirectUrl = "/";
if (props.message == "") props.message = null;
super(props);
this.state = {email:"", password:"", error:null};
}
function setError(err:String){
this.setState(cast {error:err});
}
override public function render(){
return jsx('<div onKeyPress=$onKeyPress>
<$Error error="${state.error}" />
<$Message message="${props.message}" />
<form action="" method="post" className="form-horizontal">
<div className="form-group">
<label htmlFor="email" className="col-sm-4 control-label">Email : </label>
<div className="col-sm-8">
<input id="email" className="form-control" type="text" name="email" value="${state.email}" required="1" onChange={onChange}/>
</div>
</div>
<div className="form-group">
<label htmlFor="password" className="col-sm-4 control-label">Mot de passe : </label>
<div className="col-sm-8">
<input id="password" type="password" name="password" value="${state.password}" className="form-control" required="1" onChange={onChange}/>
</div>
</div>
<p className="text-center">
<a onClick={submit} className="btn btn-primary btn-lg" ><span className="glyphicon glyphicon-user"></span> S\'identifier</a>
<br/>
<br/>
<a href="/user/forgottenPassword">Mot de passe oublié ?</a>
</p>
</form>
<!--
<hr/>
<p className="text-center">
<b>C\'est votre première visite sur Cagette.net ?</b>&nbsp;&nbsp;
<a onClick={registerBox} className="btn btn-default"><span className="glyphicon glyphicon-chevron-right"></span> S\'inscrire</a>
</p>
-->
</div>');
}
/**
* @doc https://facebook.github.io/react/docs/forms.html
*/
function onChange(e:js.html.Event){
e.preventDefault();
var name :String = untyped e.target.name;
var value :String = untyped /*(e.target.value == "") ? null :*/ e.target.value;
Reflect.setField(state, name, value);
this.setState(this.state);
}
/**
* displays a registerBox
*/
public function registerBox(){
var body = js.Browser.document.querySelector('#myModal .modal-body');
ReactDOM.unmountComponentAtNode( body );
js.Browser.document.querySelector("#myModal .modal-title").innerHTML = "Inscription";
ReactDOM.render(jsx('<$RegisterBox redirectUrl="${props.redirectUrl}" phoneRequired="${props.phoneRequired}"/>'), body );
}
public function submit(?e:js.html.Event){
if (state.email == ""){
setError("Veuillez saisir votre email");
return;
}
if (state.password == ""){
setError("Veuillez saisir votre mot de passe");
return;
}
//lock button
var el: js.html.Element = null;
if(e!=null){
el = cast e.target;
el.classList.add("disabled");
}
var req = new haxe.Http("/api/user/login");
req.addParameter("email", state.email);
req.addParameter("password", state.password);
req.addParameter("redirecturl", props.redirectUrl);
req.onData = req.onError = function(d){
var d = req.responseData;
if(e!=null) el.classList.remove("disabled");
var d = haxe.Json.parse(d);
if (Reflect.hasField(d, "error")) setError(d.error.message);
if (Reflect.hasField(d, "success")) js.Browser.window.location.href = props.redirectUrl;
}
req.request(true);
}
function onKeyPress(e:js.html.KeyboardEvent){
if(e.key=="Enter") submit();
}
}
+165
View File
@@ -0,0 +1,165 @@
package react.user;
import react.ReactDOM;
import react.ReactComponent;
import react.ReactMacro.jsx;
typedef RegisterBoxState = {firstName:String, lastName:String, email:String, password:String, error:String, phone:String};
typedef RegisterBoxProps = {redirectUrl:String,message:String,phoneRequired:Bool};
/**
* Registration box ( sign up )
* @author fbarbut
*/
class RegisterBox extends react.ReactComponentOfPropsAndState<RegisterBoxProps,RegisterBoxState>
{
public function new(props:RegisterBoxProps)
{
if (props.redirectUrl == null) props.redirectUrl = "/";
super(props);
this.state = {firstName:"",lastName:"",email:"",password:"",error:null,phone:""};
}
override public function render(){
//tips for conditionnal rendering : https://github.com/massiveinteractive/haxe-react#gotchas
var phone = null;
if (props.phoneRequired){
phone = jsx('<div className="form-group">
<label htmlFor="phone" className="col-sm-4 control-label">Téléphone : </label>
<div className="col-sm-8">
<input id="phone" type="text" className="form-control" name="phone" value="${state.phone}" onChange={onChange} />
</div>
</div>');
}
return jsx('
<div>
<$Error error="${state.error}" />
<$Message message="${props.message}" />
<form action="" method="post" className="form-horizontal">
<div className="form-group">
<label htmlFor="firstName" className="col-sm-4 control-label">Prénom : </label>
<div className="col-sm-8">
<input id="firstName" type="text" name="firstName" value="${state.firstName}" className="form-control" onChange={onChange}/>
</div>
</div>
<div className="form-group">
<label htmlFor="lastName" className="col-sm-4 control-label">Nom : </label>
<div className="col-sm-8">
<input id="lastName" type="text" name="lastName" value="${state.lastName}" className="form-control" onChange={onChange}/>
</div>
</div>
<div className="form-group">
<label htmlFor="email" className="col-sm-4 control-label">Email : </label>
<div className="col-sm-8">
<input id="email" type="text" className="form-control" name="email" value="${state.email}" onChange={onChange} />
</div>
</div>
${phone}
<div className="form-group">
<label htmlFor="password" className="col-sm-4 control-label">Mot de passe : </label>
<div className="col-sm-8">
<input id="password" type="password" name="password" value="${state.password}" className="form-control" onChange={onChange}/>
</div>
</div>
<p className="text-center">
<a onClick={submit} className="btn btn-primary btn-lg" ><span className="glyphicon glyphicon-chevron-right"></span> Inscription</a>
</p>
</form>
<hr/>
<p className="text-center">
<b>Déjà inscrit ? </b>
<a onClick={loginBox} className="btn btn-default"><span className="glyphicon glyphicon-user"></span> Connectez-vous ici</a>
</p>
</div>
');
}
/**
* @doc https://facebook.github.io/react/docs/forms.html
*/
function onChange(e:js.html.Event){
e.preventDefault();
var name :String = untyped e.target.name;
var value :String = untyped e.target.value;
//trace('onChange : $name = $value');
Reflect.setField(state, name, value);
this.setState(this.state);
}
/**
* displays a login box
*/
public function loginBox(){
var body = js.Browser.document.querySelector('#myModal .modal-body');
ReactDOM.unmountComponentAtNode( body );
js.Browser.document.querySelector("#myModal .modal-title").innerHTML = "Connexion";
ReactDOM.render(jsx('<$LoginBox redirectUrl="${props.redirectUrl}" />'), body );
}
public function submit(e:js.html.Event ){
if (state.email == ""){
setError("Veuillez saisir votre email");
return;
}
if (state.password == ""){
setError("Veuillez saisir un mot de passe");
return;
}
if (state.firstName == ""){
setError("Veuillez saisir votre prénom");
return;
}
if (state.lastName == ""){
setError("Veuillez saisir votre nom de famille");
return;
}
if (state.phone == "" && props.phoneRequired){
setError("Veuillez saisir votre numéro de téléphone");
return;
}
//lock button
var el : js.html.Element = cast e.target;
el.classList.add("disabled");
var req = new haxe.Http("/api/user/register");
req.addParameter("firstName", state.firstName);
req.addParameter("lastName", state.lastName);
req.addParameter("email", state.email);
req.addParameter("password", state.password);
req.addParameter("redirecturl", props.redirectUrl);
if(props.phoneRequired) req.addParameter("phone", state.phone);
req.onData = req.onError = function(d){
var d = req.responseData;
el.classList.remove("disabled");
var d = haxe.Json.parse(d);
if (Reflect.hasField(d, "error")) setError(d.error.message);
if (Reflect.hasField(d, "success")) js.Browser.window.location.href = props.redirectUrl;
}
req.request(true);
}
function setError(err:String){
this.setState(cast {error:err});
}
}
+20
View File
@@ -0,0 +1,20 @@
package test;
import js.Browser;
import utest.Runner;
import utest.ui.Report;
import test.utils.TestCartUtils;
class TestAll
{
public static function main()
{
var runner = new Runner();
// Utils
runner.addCase(new TestCartUtils());
Report.create(runner);
runner.run();
}
}
+44
View File
@@ -0,0 +1,44 @@
[
{
"contractTaxName": null,
"desc": "Des grattons fait avec du cochon gascon, élevé en plein air et selon les préceptes de l'agriculture biologique. Meilleur que des rillettes!",
"vatValue": 0.234597156398104,
"price": 4.5,
"name": "Gratton de porc noir",
"type": 0,
"contractTax": null,
"contractId": 4672,
"orderable": true,
"hasFloatQt": false,
"id": 56610,
"qt": 190,
"unitType": 2,
"ref": "GRA-1",
"vat": 5.5,
"categories": null,
"stock": null,
"organic": false,
"image": "/img/taxo/cat10.png"
},
{
"contractTaxName": null,
"desc": "Un pâté savoureux qui se mange sans modération! Date de péremption : 1 Avril 2019 Lot : L16AV01P Poids : 200gr Ingrédients : Viande de porc issue de lagriculture biologique, Sel, Poivre",
"vatValue": 0.297156398104265,
"price": 5.7,
"name": "Pâté de porc noir",
"type": 0,
"contractTax": null,
"contractId": 4672,
"orderable": true,
"hasFloatQt": false,
"id": 56611,
"qt": 1,
"unitType": 0,
"ref": "PAPN-1",
"vat": 5.5,
"categories": null,
"stock": null,
"organic": false,
"image": "/file/2521_6a77c954191e5a2d8cd5a5d8cade4f6b.PNG"
}
]
+58
View File
@@ -0,0 +1,58 @@
package test.utils;
import sys.io.File.getContent;
import haxe.Json.parse;
import utest.Assert;
import utils.CartUtils;
import Common;
class TestCartUtils
{
static var products:Array<ProductInfo>;
public function new() {
var http = new haxe.Http("localhost/js/test/mocks.json?format=json");
products = parse(getContent("js/test/mocks.json"));
}
public function testAddToCart()
{
var order = {
products: [{
product: products[0],
quantity: 1
}],
total: products[0].price
};
var newProduct = products[1];
order = CartUtils.addToCart(order, newProduct, 1);
Assert.equals(order.products.length, 2);
Assert.equals(order.total, products[0].price + products[1].price);
order = CartUtils.addToCart(order, newProduct, 3);
Assert.equals(order.products[1].quantity, 4);
Assert.equals(order.total, products[0].price + 4 * products[1].price);
}
public function testRemoveFromCart()
{
var order = {
products: [{
product: products[0],
quantity: 5
}, {
product: products[1],
quantity: 3
}],
total: 5 * products[0].price + 3 * products[1].price
};
order = CartUtils.removeFromCart(order, products[0], 2);
Assert.equals(order.products[0].quantity, 3);
Assert.equals(order.total, 3 * products[0].price + 3 * products[1].price);
order = CartUtils.removeFromCart(order, products[1]);
Assert.equals(order.products.length, 1);
Assert.equals(order.total, 3 * products[0].price);
}
}
+64
View File
@@ -0,0 +1,64 @@
package utils;
import Math;
using Lambda;
import Common;
class CartUtils {
public static function addToCart(order:OrderSimple, productToAdd:ProductInfo, quantity:Int):OrderSimple {
var products = order.products.copy();
var total = order.total;
var existingProduct = products.find(function(p) {
return p.product.id == productToAdd.id;
});
if (existingProduct == null)
products.push({
product: productToAdd,
quantity: quantity
});
else
existingProduct.quantity += quantity;
total += quantity * productToAdd.price;
total = Math.round(total * 100) / 100; // to avoid calculation errors
return {
products: products,
total: total
};
}
public static function removeFromCart(order:OrderSimple, productToRemove:ProductInfo, ?quantity:Int):OrderSimple {
var products = order.products.copy();
var total = order.total;
var existingProduct = products.find(function(p) {
return p.product.id == productToRemove.id;
});
if (quantity == null)
quantity = existingProduct.quantity;
if (existingProduct == null)
throw "Can't remove a non existing product";
else if (quantity >= existingProduct.quantity)
products.remove(existingProduct)
else
existingProduct.quantity -= quantity;
if (products.length == 0)
total = 0;
else {
total -= Math.min(quantity, existingProduct.quantity) * productToRemove.price;
total = Math.round(total * 100) / 100; // to avoid calculation errors
}
return {
products: products,
total: total
};
}
}
+115
View File
@@ -0,0 +1,115 @@
package utils;
import haxe.Json;
import js.Promise;
import js.html.XMLHttpRequest;
@:enum abstract HttpMethod(String) to String {
var POST = 'POST';
var GET = 'GET';
var HEAD = 'HEAD';
var PUT = 'PUT';
var DELETE = 'DELETE';
var TRACE = 'TRACE';
var OPTIONS = 'OPTIONS';
var CONNECT = 'CONNECT';
var PATCH = 'GET';
}
@:enum abstract FetchFormat(String) from String to String {
var PLAIN_TEXT = "text/plain";
var JSON = "application/json";
}
//json version of a tink.core.Error
typedef ErrorInfos = {error:{code:Int,message:String,stack:String}}
/**
* Manage HTTP request to a REST API.
*
* POST requests can only have a single JSON object (payload)
*/
class HttpUtil
{
static public function fetch(
url: String,
?method: HttpMethod = GET,
?params: Dynamic = null,
?accept: FetchFormat = PLAIN_TEXT,
?contentType: String = JSON
): Promise<Dynamic> {
return new Promise(function(resolve: Dynamic->Void, reject) {
var data: String = null;
if (params != null)
{
if (params.body != null){
data = Json.stringify(params.body);
} else if(method==POST) {
data = Json.stringify(params);
} else {
url += (url.indexOf('?') > -1) ? '&' : '?';
url += objToString(params);
}
}
var http = new XMLHttpRequest();
http.open(method, url, true);
if (contentType != null && contentType.length > 0)
http.setRequestHeader("Content-type", contentType);
if (accept != null)
http.setRequestHeader("Accept", accept);
http.onreadystatechange = function() {
//trace("readystate",http.readyState, http.status);
if (http.readyState == 4){
switch (http.status){
case 200:
switch (accept){
case JSON:
try {
var json = Json.parse(http.responseText);
resolve(json);
} catch (err: Dynamic){
reject(err);
}
default:
resolve(http.responseText);
}
case 204:
resolve(true);
default:
reject(http.responseText);
}
}
};
http.send(data);
});
}
static public function objToString(obj: Dynamic): String
{
var str = "";
var cpt = 0;
for (key in Reflect.fields(obj))
{
var value: Dynamic = Reflect.field(obj, key);
if (value == null) continue;
if (cpt++ > 0)
str += "&";
if (Std.is(value, Array) && value.length > 0)
str += '$key=${value.join(";")}';
else if (Std.string(value) != "") // String / Int / Float
str += '$key=${StringTools.trim(Std.string(value))}';
}
return str;
}
}
+1
View File
@@ -0,0 +1 @@
This is where the compiled templates are.
+1
View File
@@ -0,0 +1 @@
This is where the compiled templates are.
+1
View File
@@ -0,0 +1 @@
This is where the compiled templates are.
+105
View File
@@ -0,0 +1,105 @@
::use 'design.mtt'::
<div class="col-md-3">
<h2>Coordonnées</h2>
<div class="article">
<div style="font-weight:bold;font-size:120%;">::member.firstName:: ::member.lastName::
::if member.firstName2!=null && member.lastName2!=null::
& ::member.firstName2:: ::member.lastName2::
::end::</div>
$$nullSafe(::member.address1::)<br/>
$$nullSafe(::member.address2::)<br/>
$$nullSafe(::member.zipCode::)
$$nullSafe(::member.city::)<br/>
<br/>
::if member.phone!=null::
Tel : ::member.phone::<br/>
::end::
::if member.email!=null::
E-mail : <a href="mailto: ::member.email::">::member.email::</a><br/>
::end::
::if member.email2!=null::
E-mail : <a href="mailto: ::member.email2::">::member.email2::</a><br/>
::end::
</div>
</div>
<div class="col-md-9">
<h2>::_("Payments")::</h2>
<div class="article">
<table class="table table-condensed">
<!-- total -->
<tr style="font-size:1.3em;">
<td colspan="2"></td>
<td class="text-right">::_("Balance")::&nbsp;:</td>
::if balance<0::
::set color = "C00"::
::else::
::set color = "0C0"::
::end::
<td style="color:#::color::;">
::if balance<0::
-
::end::
</td>
<td style="color:#::color::;">
::Math.abs(balance)::&nbsp;::currency()::
</td>
<td colspan="2">
</td>
</tr>
::foreach t rb.current()::
<tr>
$$operation(::t::)
</tr>
::end::
<tr >
<th colspan="5" class="text-center">
<ul class="pagination">
::if rb.paginationStartPage != 1::
<li>
<a href="?page=::rb.paginationStartPage-1::">
<span class="glyphicon glyphicon-backward"></span>
</a>
</li>
::end::
::foreach i loopList(rb.paginationStartPage,rb.paginationEndPage)::
::if rb.page==i::
<li class="active" ><a href="?page=::i::">::i::</a></li>
::else::
<li><a href="?page=::i::">::i::</a></li>
::end::
::end::
::if rb.paginationEndPage < rb.pages + 1::
<li>
<a href="?page=::rb.paginationEndPage::">
<span class="glyphicon glyphicon-forward"></span>
</a>
</li>
::end::
</ul>
</th>
</tr>
</table>
</div>
</div>
::end::
+53
View File
@@ -0,0 +1,53 @@
::use 'admin/design.mtt'::
<div id="content" class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h1>H1 Title</h1>
<h2>H2 Title</h2>
<h3>H3 Title</h3>
<h4>H4 Title</h4>
</div>
</div>
<div class="panel panel-default">
<div class="panel-body">
<h2>
Titre H2
</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Donec ullamcorper venenatis augue, vitae tempor quam viverra at.
Sed faucibus ipsum sit amet turpis consectetur finibus. Nulla facilisi.
Vivamus consequat vestibulum blandit. Nunc laoreet tellus ac lorem commodo hendrerit.
</p>
<p>
Nulla gravida, libero et scelerisque convallis, nibh nisi ultrices nulla, sed faucibus sapien augue et turpis.
Nunc quis tempor dolor. Duis elementum nisl eu dolor malesuada convallis.
Sed id tellus euismod, eleifend neque ac, iaculis odio. Cras euismod odio quis elit elementum rhoncus.
Mauris in elementum ex, ut scelerisque leo. Suspendisse viverra mi non neque vulputate, in fermentum sapien sodales.
Sed in laoreet ante. Suspendisse non velit sit amet lectus consequat ornare sit amet quis ipsum.
</p>
<div class="well text-center">
Nulla gravida, libero et scelerisque convallis, nibh nisi ultrices nulla, sed faucibus sapien augue et turpis.
Nunc quis tempor dolor. Duis elementum nisl eu dolor malesuada convallis.
<br/>
<a href="#" class="btn btn-primary">Big Button</a>
<a href="#" class="btn btn-default">Big Button</a>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
</div>
::end::
+10
View File
@@ -0,0 +1,10 @@
::use 'admin/design.mtt'::
<h3>Console d'administration de Cagette.net</h3>
<p>
::hDate(now)::
</p>
::end::
+36
View File
@@ -0,0 +1,36 @@
::use 'design.mtt'::
<div class="col-md-3">
<div class="panel panel-warning">
<div class="panel-heading">
<span class="panel-title"><a href="/admin">Administration</a></span>
</div>
<ul class="nav nav-tabs nav-stacked" >
<li><a href="/db">Base de données</a> </li>
<li><a href="/admin/taxo">Dictionnaire de produits</a> </li>
<li><a href="/admin/errors">Errors</a> </li>
<li><a href="/admin/smtp">Conf. Email</a> </li>
<li><a href="/admin/emails">Emails</a> </li>
<!--<li><a href="/admin/plugins">Plugins</a> </li>-->
::if nav!=null::
::foreach i nav::
<li><a href="::i.link::">::i.name::</a> </li>
::end::
::end::
</ul>
</div>
</div>
<div class="col-md-9">
<div class="article">
::raw __content__::
</div>
</div>
::end::
+50
View File
@@ -0,0 +1,50 @@
::use 'admin/design.mtt'::
<div class="span12">
<div class="article">
<h1>Emails</h1>
<p>::_("Emails are temporarily stored in a buffer, waiting to be sent by the cron")::</p>
<p>
<h3>::num:: unsent emails</h3>
</p>
::set url = "/admin/emails"::
$$browse(::browser::,::url::)
<table class="table table-bordered table-striped table-condensed table-hover">
<tr>
<th>id</th>
<th>date</th>
<th>tries</th>
<th>title</th>
<th>html</th>
<th>status</th>
</tr>
<tr ::repeat e browser.current()::>
<td>::e.id::</td>
<td>::e.cdate::</td>
::if(e.tries==0)::
<td class="warning">::e.tries::</td>
::else::
<td class="danger">::e.tries::</td>
::end::
<td>::e.title::</td>
<td>
::if e.htmlBody != null::
<a href="#" onclick="$(this).next().toggle(); return false;">[show]</a>
<div style="display:none;">
::raw e.htmlBody::
</div>
::end::
</td>
<td>::e.rawStatus::</td>
</tr>
</table>
$$browse(::browser::,::url::)
</div>
</div>
::end::
+100
View File
@@ -0,0 +1,100 @@
::use 'admin/design.mtt'::
<div class="span12">
<div class="article">
<h1>Errors</h1>
<div class="box">
<form action="/admin/errors" method="GET">
User :
<input type="text" name="user" class="tid_userSearch" tid_optional="1" ::attr tid_value if(u!=null) u.name+"/"+u.id::/><br/>
Error :
<input type="text" name="like" value="::like::" /><br/>
<input type="submit" value="Search" class="btn btn-default btn-sm"/>
</form>
</div>
<div class="box">
Date serveur : ::now::<br/>
<a href="/admin/errors?empty=1" class="btn btn-default btn-sm" $$confirm()>Vider la table</a>
</div>
<script type="text/javascript" src="/js/rgraph/RGraph.common.core.js" ></script>
<script type="text/javascript" src="/js/rgraph/RGraph.bar.js" ></script>
<canvas id="graph" width="900" height="150">[No canvas support]</canvas>
<script>
window.onload = function ()
{
var datas = [];
var labels = [];
::foreach e errorsStats::
datas.push(::e.c::);
labels.push( '::e.day::' );
::end::
var bar = new RGraph.Bar('graph', datas);
bar.Set('chart.labels', labels);
bar.Set('chart.colors', ['#C00']);
bar.Set('chart.title', 'Last errors');
bar.Set('chart.labels.above', true);
bar.Set('chart.background.barcolor1', 'white');
bar.Set('chart.background.barcolor2', 'white');
bar.Set('chart.background.grid', true);
bar.Set('chart.gutter.left', 60);
bar.Set('chart.gutter.bottom', 40);
bar.Set('chart.text.angle', 45);
bar.Draw();
}
</script>
::set url = "/admin/errors"::
$$browse(::browser::,::url::)
<table class="table table-bordered table-striped table-condensed table-hover">
<tr>
<th>id</th>
<th>date</th>
<th>Error</th>
<th>infos</th>
<th>user id</th>
<th>URL</th>
</tr>
<tr ::repeat r browser.current()::>
<td>::r.id::</td>
<td>::r.date::</td>
::set e = r.error.split('Called')::
<td>
::e.shift()::
::if e.length > 0::
<a href="#" onclick="$(this).next().toggle(); return false;">[stack]</a>
<div style="display:none;">
<pre>Called::e.join("Called")::</pre>
</div>
::end::
</td>
<td>
<a href="#" onclick="$(this).next().toggle(); return false;">[infos]</a>
<div style="display:none;">
<b>IP: </b>::r.ip::<br/>
<b>User-Agent: </b>::r.userAgent::
</div>
</td>
<td>
<span ::cond r.uid!=null::>
<a href="/p/hosted/user/::r.uid::">::r._user.getName()::</a>
</span>
</td>
<td>::r.url::</td>
</tr>
</table>
$$browse(::browser::,::url::)
</div>
</div>
::end::
+56
View File
@@ -0,0 +1,56 @@
::use 'admin/design.mtt'::
<h3>::key::</h3>
<p>
Du ::hDate(from):: au ::hDate(to)::<br/>
Total : <b>::total::</b><br/>
Moyenne : <b>::averageValue::</b>
</p>
<p>
<script type="text/javascript" src="/js/rgraph/RGraph.common.core.js" ></script>
<script type="text/javascript" src="/js/rgraph/RGraph.bar.js" ></script>
<canvas id="graph" width="900" height="150">[No canvas support]</canvas>
<script>
window.onload = function ()
{
var datas = [];
var labels = [];
::foreach d data::
datas.push(::d.value::);
labels.push( '::d.date.toString().substr(0,10)::' );
::end::
var bar = new RGraph.Bar('graph', datas);
bar.Set('chart.labels', labels);
bar.Set('chart.colors', ['#CC0']);
bar.Set('chart.title', '::key::');
bar.Set('chart.labels.above', true);
bar.Set('chart.background.barcolor1', 'white');
bar.Set('chart.background.barcolor2', 'white');
bar.Set('chart.background.grid', true);
bar.Set('chart.gutter.left', 60);
bar.Set('chart.gutter.bottom', 40);
bar.Set('chart.text.angle', 45);
bar.Draw();
}
</script>
</p>
<p>
<table class="table table-bordered table-striped">
::foreach d data::
<tr>
<td>::hDate(d.date)::</td>
<td>::d.value::</td>
</tr>
::end::
</table>
</p>
::end::
+33
View File
@@ -0,0 +1,33 @@
::use 'admin/design.mtt'::
<h2>Plugins</h2>
<p>
L'installation des plugins se fait sur la machine de développement.
</p>
<table class="table table-bordered">
::foreach p plugins::
<tr>
<th>Nom</th>
<th>Installé</th>
</tr>
<tr>
<td>::p.name::</td>
<td>
::set i = p.isInstalled()::
$$bool(::i::)
::if i==false::
<a href="/admin/plugins/install/::p.getName()::" class="btn btn-default">Installer</a>
::end::
</td>
<td>
</td>
</tr>
::end::
</table>
::end::
+31
View File
@@ -0,0 +1,31 @@
::use 'admin/design.mtt'::
<h2>Dictionnaire de produits</h2>
<p>
Cagette.net inclut maintenant un dictionnaire de produits qui permet d'identifier
facilement les produits que vous saisissez.
</p>
<hr/>
::foreach c categ::
<img src="/img/taxo/cat::c.id::.png" style="float:right;width:100px;border:1px solid #666;" />
<h4>#::c.id:: <a href="/db/TxpCategory/edit/::c.id::" target="_blank">::c.name::</a></h4>
::foreach sc c.getSubCategories()::
<p style="margin-left:12px;">
<b><a href="/db/TxpSubCategory/edit/::sc.id::" target="_blank">::sc.name::</a></b>
<br/>
::foreach p sc.getProducts()::
<a href="/db/TxpProduct/edit/::p.id::" target="_blank">::p.name::</a>,
::end::
</p>
::end::
::end::
::end::
+78
View File
@@ -0,0 +1,78 @@
::use 'design.mtt'::
<div class="col-md-3">
<div class="block">
<h4>
::user._amap.name::
</h4>
::if user._amap._image!=null::
::if user._amap.extUrl!=null && user._amap.extUrl!=""::
<a href="::user._amap.extUrl::"><img src="::file(user._amap._image)::" style="margin:auto;display: block;width: 100%;" class="thumbnail" /><br/></a>
::else::
<img src="::file(user._amap._image)::" style="margin:auto;display: block;width: 100%;" class="thumbnail" /><br/>
::end::
::end::
::raw nl2br(user.getAmap().txtIntro)::
<br/>
::if user.isAmapManager()::
$$edit(::_("Edit")::,amap/edit)
::end::
</div>
<div class="block">
<b>::_("Main contact"):: :</b><br/>
$$contact(::user.getAmap()._contact::)
</div>
</div>
<div class="col-md-9">
<div class="row">
::foreach c contracts::
<div class="col-md-12">
<div class="article">
<div class="row">
<div class="col-md-3 text-center">
<div ::cond c._vendor._image!=null:: style="background-image:url('::file(c._vendor._image)::');width:100px;height:100px;" class="productImg"/>
<p>
<b>::c._vendor.name::</b><br/>
::c._vendor.city:: (::c._vendor.zipCode::)
</p>
</div>
<div class="col-md-6">
<h4><a href="/contract/view/::c.id::">::c.name::</a></h4>
<div style="font-size:0;">
::foreach p c.getProductsPreview(5)::
<div data-toggle="tooltip" data-placement="top" title="::p.name::" style="background-image:url('::p.getImage()::')" class="productImg"/>
::end::
</div>
</div>
<div class="col-md-3">
::if c._contact!=null::
<b>::_("Coordinator"):: :</b>
<br/>
$$contact(::c._contact::)
::end::
</div>
</div>
</div>
</div>
::end::
</div>
</div>
::end::
+32
View File
@@ -0,0 +1,32 @@
::use 'amapadmin/design.mtt'::
<h2>::_("Choose a logo")::</h2>
<div class="row">
<div class="col-md-6">
<form action="" method="POST" enctype="multipart/form-data">
<input name="image" type="file" class="btn btn-default"/>
<br/>
::_("Format : JPG, GIF or PNG"):: <br/>
<br/>
<input type="submit" value="::_("Send")::" class="btn btn-primary" />
</form>
<br/>
</div>
<div class="col-md-6" ::cond image!=null:: >
<img src="::file(image)::" class="img-thumbnail" />
</div>
</div>
::end::
+50
View File
@@ -0,0 +1,50 @@
::use 'amapadmin/design.mtt'::
<h3>::user._amap.name::</h3>
<div class="row">
<div class="col-md-6">
<p>
<strong>::membersNum::</strong> ::_("members")::.<br/>
<strong>::contractsNum::</strong> ::_("contracts in progress")::.<br/>
<hr/>
$$edit(::_("Edit properties")::,/amap/edit)
<a href="/amapadmin/addimage/" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-picture"></span>&nbsp;::_("Choose a logo")::</a>
</p>
<hr/>
<p>
::raw _("Your group has a public page, You can use it as a micro-website.<br/>Copy-paste the address of this page and share it !")::
<br/>
</p>
<p class="linkToShare">
<a href="http://::HOST::/group/::user._amap.id::">
http://::HOST::/group/::user._amap.id::
</a>
</p>
<p>
<a href="/group/::user._amap.id::" class="btn btn-default"><span class="glyphicon glyphicon-chevron-right"></span>&nbsp; ::_("Display")::</a>
</p>
</div>
<div class="col-md-6">
::if user._amap._image!=null::
<img src="::file(user._amap._image)::" style="width: 100%;" class="thumbnail" />
::end::
</div>
</div>
::end::
+39
View File
@@ -0,0 +1,39 @@
::use 'design.mtt'::
<div class="col-md-3">
<div class="panel panel-warning">
<div class="panel-heading">
<span class="panel-title"><a href="/amapadmin">::_("My group")::</a></span>
</div>
<ul id="subnav" class="nav nav-tabs nav-stacked" >
<li><a href="/amapadmin/rights/">::_("Access rights")::</a></li>
<li><a href="/amapadmin/vatRates/">::_("VAT rates")::</a></li>
<li><a href="/amapadmin/categories/">::_("Categories")::</a></li>
<li><a href="/amapadmin/currency/">::_("Currency")::</a></li>
::if nav!=null::
::foreach i nav::
<li><a href="::i.link::">::i.name::</a> </li>
::end::
::end::
</ul>
</div>
</div>
<div class="col-md-9">
<div class="article">
::raw __content__::
</div>
</div>
::end::
+40
View File
@@ -0,0 +1,40 @@
::use 'amapadmin/design.mtt'::
<h3>::_("Management of rights and access")::</h3>
$$insert(::_("Add a right")::,/amapadmin/editRight)
<table class="table table-bordered">
<tr>
<th>::_("Name")::</th>
<th>::_("Rights")::</th>
<th>::_("Actions")::</th>
</tr>
::foreach u users::
<tr>
<td>
::u._user.getName()::
</td>
<td>
<ul ::cond u._rights!=null::>
::foreach r u._rights::
<li>::u.getRightName(r)::</li>
::end::
</ul>
</td>
<td>
$$insert(::_("Modify")::,/amapadmin/editRight/::u.userId::)
</td>
</tr>
::end::
</table>
::end::
+129
View File
@@ -0,0 +1,129 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="::LANG::">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" xml:lang="::LANG::" lang="::LANG::" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
::if pageTitle!=null::
::pageTitle::
::else::
::NAME::
::end::
</title>
<!-- Cagette CSS -->
<link rel="stylesheet" type="text/css" href="/css/::getVariable('css')::/style.css"/>
<!--
<link rel="stylesheet" type="text/css" href="/css/::getVariable('css')::/shop.css"/>
<link rel="stylesheet" type="text/css" href="/css/::getVariable('css')::/map.css"/>-->
<!-- JQUERY -->
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<!-- BOOTSTRAP -->
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<!-- bootstrap datepicker -->
<script type="text/javascript" src="/bower_components/moment/min/moment.min.js"></script>
<script type="text/javascript" src="/bower_components/moment/locale/fr.js"></script>
<script type="text/javascript" src="/bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js"></script>
<link rel="stylesheet" href="/bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css" />
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"
integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ=="
crossorigin=""/>
<!-- typeahead
<script type="text/javascript" src="/bower_components/typeahead.js/dist/typeahead.jquery.js"></script> -->
<!-- font awesome -->
<script src="https://use.fontawesome.com/97d97e0dab.js"></script>
<!-- polyfill for IE11 : Object.assign() doesnt exists in IE11 , error in libs.js -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
<!-- Haxe JS + libs bundled with browserify -->
<script type="text/javascript" src="/js/::getVariable('js')::/libs.js"></script>
<script type="text/javascript" src="/js/::getVariable('js')::/app.js"></script>
</head>
::if container==null:: ::set container="container":: ::end::
<body class="::container::">
::if DEBUG::
<div style="position: absolute;background: #666;padding: 8px;left: 0;color: #FF0;">DEBUG ::session.lang::</div>
::end::
::if HOST.substr(0,3)=="pp."::
<div style="position: absolute;background: #F60;padding: 8px;left: 0;color: #FF0;">PREPROD</div>
::end::
<!-- Modal window -->
<div class="modal" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h2 class="modal-title" id="myModalLabel"></h2>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer" style="clear:both;">
<button type="button" class="btn btn-default" data-dismiss="modal">
<span class='glyphicon glyphicon-remove'></span> ::_("Close")::
</button>
</div>
</div>
</div>
</div>
::raw __content__::
<script type="text/javascript">
//Google Analytics
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-17932663-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
$(function () {
//enable BS3 Tooltips & popovers
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="popover"]').popover();
//init lang in js App
_.LANG = "::session.lang::";
//tutorials
::if tuto!=null::
_.getTuto("::tuto.name::",::tuto.step::);
::end::
_.antiDoubleClick();
})
//tracking 24x7
var rumMOKey='1d36c37d438ba069db210c73cb74308d';
(function(){
if(window.performance && window.performance.timing && window.performance.navigation) {
var site24x7_rum_beacon=document.createElement('script');
site24x7_rum_beacon.async=true;
site24x7_rum_beacon.setAttribute('src','//static.site24x7rum.com/beacon/site24x7rum-min.js?appKey='+rumMOKey);
document.getElementsByTagName('head')[0].appendChild(site24x7_rum_beacon);
}
})(window)
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!-- used by the connector plugin -->
<!-- the modal window html is already in the default design -->
<script>
$("#myModal").find(".modal-body").html("::raw html::").removeClass("modal-lg");
$("#myModal").find(".modal-title").html("::raw title::");
::if btn!=null::
$("#myModal").find(".modal-footer").html("::raw btn::");
::end::
$("#myModal").modal();
</script>
+12
View File
@@ -0,0 +1,12 @@
<div class="block">
::if user!=null::
::_("Hello"):: ::user.getName()::<br/>
<a href="/user/logout" class="btn btn-default btn-sm">::_("Log out")::</a>
::else::
::raw _("You already have an account?<br/>
Click here to log in")::<br/>
<a href="/user/login" class="btn btn-default btn-sm btn-primary">::_("Log in")::</a>
::end::
</div>
+48
View File
@@ -0,0 +1,48 @@
::use 'amapadmin/design.mtt'::
<h2>::_("Categories")::</h2>
<p>
::raw _("Categories allow to sort your products per category, for example \"vegetables, meet, backery, etc.\"<br/> It is possible to create many groups of categories in order to sort products as we wish.")::
</p>
<p ::cond groups.length==0 ::>
::_("You currently have no category. Would you like to create the list of categories by default ?")::
<a href="/amapadmin/categories/generate" class="btn btn-default">::_("Create the list of default categories")::</a>
</p>
::foreach g groups::
<table class="table table-bordered">
<tr>
<th>
Groupe "::g.name::"
</th>
<th>
<a href="/amapadmin/categories/editGroup/::g.id::" class="btn btn-default btn-xs">::_("Modify group")::</a>
<a href="/amapadmin/categories/deleteGroup/::g.id::?token=::token::" class="btn btn-default btn-xs" $$confirm()>::_("Remove group")::</a>
</th>
</tr>
::foreach c g.getCategories()::
<tr>
<td>
<span class="tag" style="background: ::c.getColor()::;">::c.name::</span>
</td>
<td>
<a href="/amapadmin/categories/edit/::c.id::" class="btn btn-default btn-xs">::_("Modify")::</a>
<a href="/amapadmin/categories/delete/::c.id::?token=::token::" class="btn btn-default btn-xs" $$confirm() >::_("Remove")::</a>
</td>
</tr>
::end::
<tr>
<td>
<a href="/amapadmin/categories/insert/::g.id::" class="btn btn-default btn-xs">::_("Add a category in this group")::</a>
</td>
<td></td>
</tr>
</table>
::end::
<a href="/amapadmin/categories/insertGroup" class="btn btn-default">::_("Add a group")::</a>
::end::
+260
View File
@@ -0,0 +1,260 @@
::use 'design.mtt'::
<div class="col-md-8">
<h2>::_("Last orders")::</h2>
::if constOrders.length>0::
<div class="article">
<!-- CONSTANT ORDERS -->
<h3>::_("CSA Contracts")::</h3>
::foreach c constOrders::
<h4>::c.contract.name::</h4>
<table class="table table-bordered table-condensed" >
<tr>
<th>::_("Qty||short version for quantity")::</th>
<th>::_("Product")::</th>
<th><a href="#" data-toggle="tooltip" data-placement="top" title="::_('Unit price including taxes')::">::_("U.P||short version of price per unit")::</a></th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Paid")::</th>
</tr>
::set total = 0::
::foreach m c.orders::
<tr>
<td>
::raw m.smartQt::
</td>
<td>
<a href="#" onclick="_.overlay('/shop/productInfo/::m.productId::','::m.productName::')">
<img src="::m.productImage::" style="width:32px;height:32px;" />
::m.productName::
</a>
::if m.userName2 != null::
<br /><span class="glyphicon glyphicon-refresh"></span> ::_("Order alternated with")::
::if user.id == m.userId::
::m.userName2::
::else::
::m.userName::
::end::
::end::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.subTotal)::&nbsp;::currency()::
</td>
<td>
<!-- frais -->
::if m.percentageValue!=null::
<a href="#" data-toggle="tooltip" data-placement="top" title="::m.percentageName:: : ::m.percentageValue:: %">
::formatNum(m.fees)::&nbsp;::currency()::
</a>
::end::
</td>
<td>
<!-- total -->
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
</td>
<td>
::if m.paid==true::
<span style="color:#00AA00;">::_("Paid")::</span>
::else::
<span style="color:#DD0000;">::_("Unpaid")::</span>
::end::
</td>
</tr>
::end::
<tr>
<td colspan="7" class="text-right">
<b>Total :
::formatNum(total)::&nbsp;::currency()::</b>
::set d = c.contract.getDistribs(false,null).length::
( soit ::formatNum(d*total)::&nbsp;::currency():: pour ::d:: livraisons )
</td>
</tr>
</table>
::end::
</div>
::end::
<!-- VARYING ORDERS -->
::if count(varOrders)>0::
<div class="article">
::foreach d varOrders::
<h4>::_("Order delivered on"):: ::hDate(d.date)::</h4>
<table class="table table-bordered table-condensed" >
<tr>
<th>::_("Qty||short version for quantity")::</th>
<th>::_("Product")::</th>
<th><a href="#" data-toggle="tooltip" data-placement="top" title="::_('Unit price including taxes')::">::_("U.P||short version of price per unit")::</a></th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Paid")::</th>
</tr>
::set total = 0::
::foreach o d.orders::
<tr>
<td>
::raw o.smartQt::
</td>
<td>
<a href="#" onclick="_.overlay('/shop/productInfo/::o.productId::','::o.productName::')">
<img src="::o.productImage::" style="width:32px;height:32px;" />
::o.productName::
</a>
</td>
<td>
::formatNum(o.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(o.subTotal)::&nbsp;::currency()::
</td>
<td>
$$nullSafe(::formatNum(o.fees)::)
</td>
<td>
::formatNum(o.total)::&nbsp;::currency()::
::set total = total + o.total::
</td>
<td>
::if o.paid==true::
<span style="color:#00AA00;">::_("Paid")::</span>
::else::
<span style="color:#DD0000;">::_("Unpaid")::</span>
::end::
</td>
</tr>
::end::
<tr>
<td colspan="4">
::if !user._amap.hasPayments()::
$$edit(Modifier cette commande,/contract/editOrderByDate/::d.date::)
::end::
</td>
<td>TOTAL</td>
<td><b>::formatNum(total)::&nbsp;::currency()::</b></td>
</tr>
</table>
::end::
</div>
::end::
</div>
<div class="col-md-4">
<h3>::_("Profile")::</h3>
<div class="article">
<p>
$$contact(::user::)
</p>
<div class="pull-right">
<a href="/account/quit?token=::token::" class="btn btn-default btn-sm" $$confirm(::_("Do you really want to quit this group ? You won't be a member anymore: you won't receive any new message and won't be able to see your previous orders in this group.")::)>
<span class="glyphicon glyphicon-remove"></span>
::_("Leave this group")::
</a>
</div>
$$edit(::_("Edit")::,/account/edit)
</div>
::if user.getAmap().hasPayments()::
<h3>::_("Payments")::</h3>
<div class="article">
<span style="font-size:1.3em;">
::_("Balance"):: :
::if userAmap.balance<0::
::set color = "C00"::
::else::
::set color = "0C0"::
::end::
<span style="color:#::color::;">
::userAmap.balance:: ::currency()::
</span>
</span>
<span style="margin-left:30px;">
<a href="/account/payments" class="btn btn-default btn-small">
<i class="fa fa-credit-card" aria-hidden="true"></i>
::_("Payments details")::
</a>
</span>
</div>
::end::
::if user.isAmapManager()::
<h3>::_("Tutorials")::</h3>
<div class="article">
<table class="table">
::foreach t tutos::
<tr>
<td>::t.name::</td>
<td>
::if t.completion==null::
<a href="/contract/?startTuto=::t.key::" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-play"></span> ::_("Start")::
</a>
::else::
<a href="/contract/?stopTuto=::t.key::" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-remove"></span> ::_("Stop")::
</a>
::end::
</td>
</tr>
::end::
</table>
::if stopTuto::
<div id="stopTuto" data-toggle="popover" title='::_("Tutorial stopped")::' data-placement="left" data-content="::_("You'll be able to restart it here.")::" ></div>
<script>
$(function(){
setTimeout(function() { $("#stopTuto").popover("show"); }, 1000);
});
</script>
::end::
</div>
::end::
<h3>::_("Language")::</h3>
<div class="article">
::_("Display the interface in another language"):: (beta)
:
<ul>
::raw langLinks::
</ul>
<p>
::_("Current language is")::
<b>
::langText::
</b>
</p>
</div>
</div>
::end::
+37
View File
@@ -0,0 +1,37 @@
::use 'design.mtt'::
<div class="col-md-12"><h2>::_("Choose a type of contract:")::</h2></div>
<div class="col-md-6">
<div class="article">
<div style="text-align:center;margin-bottom:12px;">
<a href="/contract/insert/0" class="btn btn-default btn-lg">::raw _("Create a <b>CSA</b> contract")::</a>
</div>
<p>
::_("It is a contract with a duration of one year or for one season. Products are always the same at each delivery")::
</p>
<p>
<i>::raw _("Example: there is a contract for vegetables that is 1 year long. Jean takes a basket that costs 10&euro; per delivery, and Brigitte takes a basket that costs 15&euro; per delivery.")::</i>
</p>
</div>
</div>
<div class="col-md-6">
<div class="article">
<div style="text-align:center;margin-bottom:12px;">
<a href="/contract/insert/1" class="btn btn-default btn-lg">::raw _("Create a contract with <b>variable orders</b>")::</a>
</div>
<p>
::_("It is a contract in which members can change their orders at each delivery. There is a different order for each delivery.")::
</p>
<p>
<i>::raw _("Example: Jean orders a basket that costs 10&euro; for the first delivery, then nothing for the second delivery, then a basket for 20&euro; etc.")::</i>
</p>
</div>
</div>
::end::
+139
View File
@@ -0,0 +1,139 @@
::use 'design.mtt'::
<div id="content" class="col-md-8">
<div class="article">
<h2>::_("Order"):: ::c.name::</h2>
::if distribution!=null::
::_("For the delivery of the"):: <b>::hDate(distribution.date)::</b>
::end::
<p>
::raw _("As long as orders are open, you can come back on this page and modify your order.<br/>You can check any time your current orders on the following page <a href='/contract'>my account</a>.")::
</p>
<form name="order" id="order" method="POST" action="">
<input type="hidden" name="token" value="::token::"/>
::if distribution!=null::
<input type="hidden" name="distribution" value="::distribution.id::"/>
::end::
<table class="table table-bordered">
<tr>
<td></td>
<th>::_("Product")::</th>
<th>::_("Price")::</th>
::foreach d userOrders::
::if d.distrib==null::
<th>::_("Qty")::</th>
::else::
<th>::dDate(d.distrib.date)::</th>
::end::
::end::
</tr>
::if userOrders.length>0::
::foreach uo userOrders[0].datas::
<tr>
<td>
<img src="::uo.product.getImage()::" style="width:32px;height:32px;" />
</td>
<td>
<span ::cond uo.product.organic:: title ="Agriculture biologique" data-toggle="tooltip" data-placement="bottom">
<img src="/img/AB.png"/>&nbsp;
</span>
<a href="#" onclick="_.overlay('/shop/productInfo/::uo.product.id::','::escapeJS(uo.product.getName())::')">
::uo.product.getName()::
</a>
</td>
<td>
::set price = uo.product.price + uo.product._contract.computeFees(uo.product.price)::
::formatNum(price)::&nbsp;::currency()::
</td>
::set i = repeat.uo.index::
::foreach d userOrders::
<td>
::set o = d.datas[i]::
::if o.order==null::
::set q = ""::
::else::
::set q = o.order.quantity::
::end::
::if d.distrib==null::
<!-- contrat amap-->
<input type="text" class="form-control" name="::"d-p"+o.product.id::" id="product::o.product.id::" value="::q::" />
::else::
<input type="text" class="form-control" name="::"d"+d.distrib.id+"-p"+o.product.id::" id="product::o.product.id::" value="::q::" />
::end::
</td>
::end::
</tr>
::end::
::else::
<tr>
<td colspan="4">::_("There is currently no open order")::</td>
</tr>
::end::
<tr>
<th colspan="3" class="text-right">Total</th>
::foreach d userOrders::
<th>
::set total=0::
::foreach o d.datas::
::if o.order!=null::
::set q = o.order.quantity::
::set price = o.product.price + o.product._contract.computeFees(o.product.price)::
::set total = total+(q*price)::
::end::
::end::
::formatNum(total)::&nbsp;::currency()::
</th>
::end::
</tr>
</table>
<i>::_("Tip: to remove an order please key-in the quantity 0")::</i><br/><br/>
<input type="submit" class="btn btn-primary" value="::_("Validate the order")::" name="submit" id="submit" />
</form>
</div>
</div>
<div class="col-md-4">
<div class="article">
::_("Supplier:"):: <b>::c._vendor.name::</b>
<br/>
<br/>
::_("Contact in charge:"):: $$contact(::c._contact::)
</div>
</div>
::end::
+137
View File
@@ -0,0 +1,137 @@
::use 'design.mtt'::
<div id="content" class="col-md-8">
<div class="article">
<h2>::_("Order"):: ::c.name::</h2>
<!--::if distribution!=null::
::_("For the delivery of the"):: <b>::hDate(distribution.date)::</b>
::end::
<hr/>-->
<p>
::raw _("Key-in here your orders in advance for this supplier.<br/>You can check your current orders any time on the following page <a href=\"/contract\">my account</a>.<br/>Tip: to remove an order please key-in the quantity 0")::
</p>
<form name="order" id="order" method="POST" action="">
<input type="hidden" name="token" value="::token::"/>
<!--::if distribution!=null::
<input type="hidden" name="distribution" value="::distribution.id::"/>
::end::-->
<table class="table table-bordered table-striped" style="background:white;" >
<tr>
<td></td>
<th>::_("Product")::</th>
<th>::_("Price")::</th>
::foreach d userOrders::
::if d.distrib==null::
<th>::_("Qty")::</th>
::else::
<th class="text-center">::dDate(d.distrib.date)::</th>
::end::
::end::
</tr>
::if userOrders.length>0::
::foreach uo userOrders[0].datas::
<tr>
<td>
<img src="::uo.product.getImage()::" style="width:32px;height:32px;" />
</td>
<td>
<span ::cond uo.product.organic:: title ="Agriculture biologique" data-toggle="tooltip" data-placement="bottom">
<img src="/img/AB.png"/>&nbsp;
</span>
<a href="#" onclick="_.overlay('/shop/productInfo/::uo.product.id::','::escapeJS(uo.product.getName())::')">
::uo.product.getName()::
</a>
</td>
<td>
::set price = uo.product.price + uo.product._contract.computeFees(uo.product.price)::
::formatNum(price)::&nbsp;::currency()::
</td>
::set i = repeat.uo.index::
::foreach d userOrders::
<td class="text-center">
::set o = d.datas[i]::
::if o.order==null::
::if d.distrib==null::
<!-- contrat amap-->
<input type="text" class="form-control" name="::"d-p"+o.product.id::" id="product::o.product.id::" value="" />
::else::
<input type="text" class="form-control" name="::"d"+d.distrib.id+"-p"+o.product.id::" id="product::o.product.id::" value="" />
::end::
::else::
::o.order.quantity::
::end::
</td>
::end::
</tr>
::end::
::else::
<tr>
<td colspan="4">::_("There is currently no open order")::</td>
</tr>
::end::
<tr>
<th colspan="3" class="text-right">Total</th>
::foreach d userOrders::
<th>
::set total=0::
::foreach o d.datas::
::if o.order!=null::
::set q = o.order.quantity::
::set price = o.product.price + o.product._contract.computeFees(o.product.price)::
::set total = total+(q*price)::
::end::
::end::
::formatNum(total)::&nbsp;::currency()::
</th>
::end::
</tr>
</table>
<input type="submit" class="btn btn-primary" value="::_("Validate the order")::" name="submit" id="submit" />
</form>
</div>
</div>
<div class="col-md-4">
<div class="article">
<p>
::_("Supplier:"):: <b>::c._vendor.name::</b>
</p>
<p>
::_("Contact in charge:"):: $$contact(::c._contact::)
</p>
</div>
</div>
::end::
+75
View File
@@ -0,0 +1,75 @@
::use 'design.mtt'::
<div id="content" class="col-md-8">
<div class="article">
<h2>
::__("Modify the order delivered on ::date::",{date:hDate(date)})::
</h2>
<p>
::raw _("You can modify here an existing order. If you would like to remove a product, just key-in the quantity 0.<br/>You may not be able to modify all elements as the order could be closed for some suppliers")::
</p>
<form name="order" id="order" method="POST" action="">
<input type="hidden" name="token" value="::token::"/>
<table class="table table-bordered">
<tr>
<th>::_("Product")::</th>
<th>::_("U.P.")::</th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Quantity")::</th>
</tr>
::set total = 0::
::foreach o orders::
<tr>
<td>
<a href="#" onclick="_.overlay('/shop/productInfo/::o.productId::')">
<img src="::o.productImage::" style="width:32px;height:32px;" />
::o.quantity:: x ::o.productName::
</a>
</td>
<td>
::formatNum(o.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(o.subTotal)::&nbsp;::currency()::
</td>
<td>
$$nullSafe(::formatNum(o.fees)::)
</td>
<td>
::formatNum(o.total)::&nbsp;::currency()::
</td>
<td>
::if o.canModify::
<input type="text" class="form-control" name="product::o.productId::" id="product::o.productId::" value="::o.quantity::" />
::else::
<span style="color:#AAA">::_("Order closed or already paid")::</span>
::end::
</td>
::set total = total + o.total::
</tr>
::end::
<tr>
<th colspan="3"></th>
<th>::_("TOTAL")::</th>
<th>::roundTo(total,2)::::currency()::</th>
<th></th>
</tr>
</table>
<input type="submit" class="btn btn-primary" value="::_("Update the order")::" name="submit" id="submit" />
</form>
</div>
</div>
::end::
+137
View File
@@ -0,0 +1,137 @@
::use 'design.mtt'::
<div class="col-md-9">
<div class="article">
<h2>::c.name::</h2>
<div class="row">
<div class="col-md-12">
::if c.description!=null::
::raw nl2br(c.description)::
::end::
</div>
</div>
<hr/>
<div class="row">
::foreach p c.getProducts(true)::
<div class="col-md-4" id="product::p.id::">
<table class="product">
<tr>
<td style="vertical-align:top;">
<a onclick="_.overlay('/shop/productInfo/::p.id::','::escapeJS(p.getName())::')" >
<!--<img src="::p.getImage()::" style="width:64px;height:64px;" /> -->
<div style="background-image:url('::p.getImage()::')" class="productImg"/>
</a>
</td>
<td>
<h4>
<a onclick="_.overlay('/shop/productInfo/::p.id::','::escapeJS(p.getName())::')" href="#">::p.getName()::</a>
</h4>
<span class="price">::formatNum(p.getPrice())::&nbsp;::currency()::</span>
</td>
</tr>
</table>
</div>
::end::
</div>
<hr/>
<div class="row">
::set vendor = c._vendor::
<div class="col-md-3" ::cond vendor._image!=null::>
<img src="::file(vendor._image)::" class="img-thumbnail" />
</div>
<div class="col-md-5">
<h4>::vendor.name::</h4>
<b>::vendor.city::</b> (::vendor.zipCode::)
<p ::cond vendor.desc!=null:: style="font-size: 13px;">
::raw nl2br(vendor.desc)::
</p>
::if vendor.linkUrl!=null::
<p>
::if vendor.linkText!=null::
<a href="::vendor.linkUrl::" target="_blank" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-chevron-right"></span> ::vendor.linkText::</a>
::else::
<a href="::vendor.linkUrl::" target="_blank" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-chevron-right"></span> ::_("Read more")::</a>
::end::
</p>
::end::
</div>
<div class="col-md-4">
<h4>::_("Coordinator")::</h4>
::if c._contact!=null::
<b>::c._contact.firstName:: ::c._contact.lastName::</b><br/>
<span ::cond c._contact.email!=null::><span class="glyphicon glyphicon-envelope"></span> <a href="mailto: ::c._contact.email::">::c._contact.email::</a><br/></span>
<span ::cond c._contact.phone!=null::><span class="glyphicon glyphicon-phone-alt"></span> ::c._contact.phone::<br/></span>
::end::
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="article">
<h3>::_("Next distributions")::</h3>
::set distribs = c.getDistribs(true,null)::
<ul style="max-height:600px;overflow-y:auto;">
::foreach d distribs::
<li>
<b>::dDate(d.date)::</b>
<br/>::d._place.name::
<p ::cond d.canOrderNow()::>
::if user._amap.hasShopMode()::
<a href="/shop/::d._place.id::/::d.date.toString().substr(0,10)::" class="btn btn-primary"><span class="glyphicon glyphicon-chevron-right"></span> ::_("Order")::</a>
::end::
</p>
</li>
::end::
</ul>
::if distribs.length==0::
<p>
::_("No planned distributions.")::
</p>
::else::
<p ::cond !user._amap.hasShopMode()::>
<a href="/contract/order/::c.id::" class="btn btn-primary"><span class="glyphicon glyphicon-chevron-right"></span> ::_("Order")::</a>
</p>
::end::
<hr/>
<p>
<a style="font-size:12px;" href='/contract'>::_("You can check your previous orders in 'my account' section")::</a>
</p>
</div>
</div>
::end::
@@ -0,0 +1,56 @@
::use 'design.mtt'::
<!--
<style>
.calDiv{
width:14.28%;
background:grey;
padding:2px;
height:50px;
border:solid 1px black;
display:inline-block;
}
</style>
<div class="col-md-12">
<div class="article">
<h2>::_("Calendar")::</h2>
<div class="calDiv"></div><div class="calDiv"></div><div class="calDiv"></div><div class="calDiv"></div><div class="calDiv"></div><div class="calDiv"></div><div class="calDiv"></div>
</div>
</div>
-->
<div class="col-md-12">
<div class="article">
<h2>Calendrier</h2>
<div style="-webkit-column-count: 3;-moz-column-count: 3;column-count: 3;">
::foreach c calendar::
::if c.d.getTime() < now::
::set col="0.3"::
::else::
::set col="1"::
::end::
<div style="opacity: ::col::;margin-bottom:8px;">
::hDate(c.d)::<br/>
::foreach e c.events::
<div style="margin-left:16px;margin-right:16px;width:12px;height:8px;display:inline-block;background: ::intToHex(e.color,6)::;"></div><b> ::e.name:: </b><br/>
::end::
</div>
::end::
</div>
</div>
</div>
::end::
@@ -0,0 +1,57 @@
::use 'design.mtt'::
::set c = contract::
<div class="col-md-3">
<div class="block">
<ul>
<li><a href="/contractAdmin/products/::c.id::">::_("Products")::</a></li>
<li><a href="/contractAdmin/distributions/::c.id::">::_("Deliveries")::</a></li>
<li><a href="/contractAdmin/orders/::c.id::">::_("Orders")::</a></li>
</ul>
</div>
</div>
<div class="col-md-9">
<div class="article">
<span class="title" style="margin-bottom: 18px;display: block;">::c.name:: - ::c._vendor.name::</span>
<div class="container">
<div class="span4">
<b>Produits</b>
<table class="table">
::foreach p c.getProducts()::
<tr>
<td>::p.name::</td>
<td>$$edit(modifier,/product/edit/::p.id::)</td>
</tr>
::end::
</table>
$$insert(Nouveau produit,/product/insert)
</div>
<div class="span4">
<b>::_("Deliveries")::</b>
<table class="table">
::foreach d c.getDistribs()::
<tr>
<td>::d.date.toString()::</td>
<td>$$edit(modifier,/distribution/edit/::d.id::) </td>
</tr>
::end::
</table>
$$insert(Nouvelle livraison,/distribution/insert)
</div>
</div>
<hr/>
</div>
</div>
::end::
+149
View File
@@ -0,0 +1,149 @@
::use 'design.mtt'::
<div class="col-md-12">
<!-- Distributions to validate -->
<div class="article text-center" ::cond multidistribs.length>0:: style="max-height:400px;overflow-y: auto;">
<h2>::_("Distributions to validate")::</h2>
<p>::_("You need to validate these distributions which took place recently")::</p>
::foreach m multidistribs::
<p>
::if(!m.isConfirmed())::
<a href="/distribution/validate/::m.getDate().toString().substr(0,10)::/::m.getPlace().id::" class="btn btn-primary">
::dDate(m.getDate())::, ::m.getPlace().name::
</a>
::if (isAdmin)::
<a href="/distribution/autovalidate/::m.getDate().toString().substr(0,10)::/::m.getPlace().id::" class="btn btn-danger">Admin : Autovalidate</a>
::end::
::elseif(isAdmin)::
<a class="btn btn-default disabled">
<span class="glyphicon glyphicon-ok"></span> ::dDate(m.getDate())::, ::m.getPlace().name::
</a>
<a href="/distribution/unvalidate/::m.getDate().toString().substr(0,10)::/::m.getPlace().id::" class="btn btn-danger">Admin : Unvalidate</a>
::end::
</p>
::end::
</div>
<div class="article" id="contracts">
<h2>
::if getParam("old")::
::_("Old contracts")::
::else::
::_("Contracts")::
::end::
</h2>
<table class="table">
::foreach c contracts::
::if c.endDate!=null && c.endDate.getTime() < Date.now().getTime()::
::set class = "disabled"::
::elseif c.startDate!=null && c.startDate.getTime() > Date.now().getTime()::
::set class = "disabled"::
::else::
::set class = ""::
::end::
<tr class="::class::" >
<td>
::if c._vendor._image!=null::
<div class="productImg" style="background-image:url('::file(c._vendor._image)::');width:48px;height:48px;" ></div>
::end::
</td>
<td>::c._vendor.name::</td>
<td>
<b>::c.name::</b>
</td>
<td style="font-size:0.9em;">
du ::hDate(c.startDate)::<br/>
au ::hDate(c.endDate)::
</td>
<td>
<div class="btn-group" role="group">
$$edit(::_("Manage")::,/contractAdmin/view/::c.id::)
::if user.canManageAllContracts()::
$$delete(::_("Del.||short version of delete")::,/contract/delete/::c.id::?token=::token::)
::end::
</div>
</td>
</tr>
::end::
</table>
::if user.canManageAllContracts()::
$$insert(::_("New contract")::,/contract/insert/)
::end::
<a href="/contractAdmin?old=1" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-folder-close"></span> ::_("Old contracts")::</a>
<a href="/contractAdmin/calendar" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-calendar"></span> ::_("Calendar")::</a>
<div class="dropdown" style="display:inline-block;">
<button class="btn btn-default btn-sm dropdown-toggle btn-noAntiDoubleClick" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
::_("Global orders view")::
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a href="/contractAdmin/ordersByDate"><span class="glyphicon glyphicon-list"></span> ::_("Same day")::</a></li>
<li><a href="/contractAdmin/ordersByTimeFrame"><span class="glyphicon glyphicon-list"></span> ::_("Timeframe")::</a></li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="article" id="vendors">
<h2>::_("Farmers")::</h2>
<table class="table">
::foreach v vendors::
<tr>
<td>
::if v._image!=null::
<div class="productImg" style="background-image:url('::file(v._image)::');width:48px;height:48px;"></div>
::end::
</td>
<td><b>::v.name::</b></td>
<td>
<div class="btn-group" role="group">
$$edit(::_("Edit")::,/vendor/edit/::v.id::)
<a href="/vendor/addImage/::v.id::" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-picture"></span> ::_("Picture")::</a>
::if user.isAmapManager():: $$delete(,/vendor/delete/::v.id::?token=::token::) ::end::
</div>
</td>
</tr>
::end::
</table>
$$insert(::_("New farmer")::,/vendor/insert/)
</div>
</div>
<div class="col-md-6">
<div class="article" id="places">
<h2>::_("Delivery places")::</h2>
<table class="table">
::foreach p places::
<tr>
<td><b>::p.name::</b></td>
<td>
$$edit(::_("Edit")::,/place/edit/::p.id::)
::if user.isAmapManager():: $$delete(::_("Del.||short version for delete")::,/place/delete/::p.id::?token=::token::) ::end::
</td>
</tr>
::end::
</table>
$$insert(::_("New delivery place")::,/place/insert/)
</div>
</div>
::end::
@@ -0,0 +1,82 @@
::use 'contractadmin/design.mtt'::
<h2>::_("Distributions")::</h2>
<table class="table">
::foreach d deliveries::
::if d.date!=null && d.date.getTime() < Date.now().getTime()::
::set style = "opacity:0.5;"::
::else::
::set style = ""::
::end::
<tr style="::style::" >
<td>
::if d.distributionCycleId!=null::
<img src="/img/icons/arrow_refresh.png" style="opacity:0.5;" />
::end::
::set da = getDate(d.date)::
::if d.end!=null::
::set e = getDate(d.end)::
::end::
::da.dow:: ::da.d:: ::da.m:: de ::da.h+":"+da.i::
::if d.end!=null::
à ::e.h+":"+e.i::
::end::
</td>
<td>
::d._place.name::
</td>
<td>
<div class="btn-group">
$$edit(::_("Edit")::,/distribution/edit/::d.id::)
$$delete(::_("Delete")::,/distribution/delete/::d.id::)
<a href="/distribution/list/::d.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-print"></span>&nbsp;::_("Distribution list")::</a>
</div>
</td>
</tr>
::end::
</table>
<table ::cond cycles.length>0:: class="table">
<tr>
<th>::_("Distribution cycle")::</th>
<th></th>
</tr>
::foreach c cycles::
<tr>
<td><b>
::switch c.cycleType::
::case:: ::_("Weekly")::
::case:: ::_("Monthly")::
::case:: ::_("Every 2 weeks")::
::case:: ::_("Every 3 weeks")::
::end::
</b>
: Du ::hDate(c.startDate):: au ::hDate(c.endDate)::
</td>
<td>
$$delete(::_("Del.")::,::"/distribution/deleteCycle/"+c.id::)
</td>
</tr>
::end::
</table>
$$insert(::_("One time distribution")::,/distribution/insert/::c.id::)
$$insert(::_("Distribution cycle")::,/distribution/insertCycle/::c.id::)
<a href="/contractAdmin/distributionp/::c.id::" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-user"></span> ::_("Distributions engagement")::
</a>
<a href="/contractAdmin/distributions/::c.id::?old=1" class="btn btn-default btn-sm">
::_("Old distributions")::
</a>
::end::
+44
View File
@@ -0,0 +1,44 @@
::use 'design.mtt'::
<div class="col-md-12">
<h2>::c.name:: - ::c._vendor.name::</h2>
</div>
<div class="col-md-3">
<div class="article">
<ul id="subnav" class="nav nav-pills nav-stacked">
<li id="default" ::attr class if(nav[1]=="view") 'active'::>
<a href="/contractAdmin/view/::c.id::">::_("Summary")::</a></li>
<li id="edit">
<a href="/contract/edit/::c.id::">::_("Edit contract")::</a></li>
<li id="products" ::attr class if(nav[1]=="products") 'active'::>
<a href="/contractAdmin/products/::c.id::">::_("Products")::</a></li>
<li id="deliveries" ::attr class if(nav[1]=="distributions") 'active'::>
<a href="/contractAdmin/distributions/::c.id::">::_("Distributions")::</a></li>
<li id="orders" ::attr class if(nav[1]=="orders") 'active'::>
<a href="/contractAdmin/orders/::c.id::">::_("Orders")::</a></li>
<li id="stats">
<a href="/contractAdmin/stats/::c.id::">::_("Stats")::</a></li>
::if navbar!=null::
<hr/>
::foreach i navbar::
<li ::attr class if(nav[1]==i.id) 'active'::>
<a href="::i.link::">
<span ::cond i.icon:: class="glyphicon glyphicon-::i.icon::" ></span>
::i.name::</a>
</li>
::end::
::end::
</ul>
</div>
</div>
<div class="col-md-9">
<div class="article">
::raw __content__::
</div>
</div>
::end::
@@ -0,0 +1,60 @@
::use 'contractadmin/design.mtt'::
<h2>::_("Participate to deliveries")::</h2>
<div>
::_("This page allows to see if members have participated to deliveries enough.")::
</div>
<div style="margin:16px 0 16px 0;text-align:center;" class="alert-warning">
<strong>::raw __("::distribs:: deliveries requiring ::distributorNum:: person(s)",{distribs:distribs,distributorNum:distributorNum})::,<br/>
::users:: ::_("person(s) using this contract"):: <br/>
= ::num:: ::_("participations to deliveries to do per family.")::</strong>
</div>
<table class="table table-bordered">
<tr>
<th class="col-md-5">::_("Suscriber")::</th>
<th class="col-md-5">::raw _("Number of attendances to deliveries")::</th>
<th class="col-md-2"></th>
</tr>
::foreach p participations::
<tr>
<td>
::p.user.getCoupleName()::
</td>
<td>
::p.count::
</td>
<td>
::if p.count>=numRounded::
<img src="/img/icons/accept.png" /> OK
::else::
<img src="/img/icons/stop.png" /> ::_("Insufficient")::
::end::
</td>
</tr>
::end::
</table>
::if(extParticipations.length>0)::
<hr/>
<table class="table table-bordered">
<tr>
<th class="col-md-5">::_("Member without order in this contract")::</th>
<th class="col-md-5">::raw _("Number of attendances to deliveries")::</th>
<th class="col-md-2"></th>
</tr>
::foreach p extParticipations::
<tr>
<td>::p.user.getCoupleName()::</td>
<td>::p.count::</td>
<td></td>
</tr>
::end::
</table>
::end::
::end::
+148
View File
@@ -0,0 +1,148 @@
::use 'contractadmin/design.mtt'::
::if u==null::
<h3>::_("Key-in an order")::</h3>
::else::
<h3>::_("Key-in an order for"):: ::u.getCoupleName()::</h3>
::end::
::if distribution!=null::
<p>::_("For the delivery of the"):: <b>::hDate(distribution.date)::</b></p>
::end::
<form name="order" id="order" method="POST" action="" class="form-horizontal">
<input type="hidden" name="token" value="::token::"/>
::if distribution!=null::
<input type="hidden" name="distribution" value="::distribution.id::" />
::end::
::if u==null ::
<div class="form-group">
<label for="user" class="control-label col-md-2 text-left">::_("Member:")::</label>
<div class="col-md-10">
<select name="user" id="user" class="form-control">
::foreach uu users::
<option value="::uu.value::">::uu.label::</option>
::end::
</select>
</div>
</div>
::end::
<br/>
<table class="table table-bordered">
<tr>
<th></th>
<th>::_("Product")::</th>
<th>::_("Price")::</th>
<th>::_("Quantity")::</th>
<th ::cond !user._amap.hasPayments()::>::_("Status")::</th>
::if distribution==null::<th>::_("Alternately with :")::</th>::end::
</tr>
::set total = 0::
::foreach uo userOrders::
<tr>
<td>
::if uo.product.active==false::
<img src="::uo.product.getImage()::" style="width:32px;height:32px;opacity:0.3;" />
::else::
<img src="::uo.product.getImage()::" style="width:32px;height:32px;" />
::end::
</td>
<td>
::if uo.product.active==false::
<span style="color:#AAA;">::uo.product.getName():: ::_("(inactive)")::</span>
::else::
::uo.product.getName()::
::end::
</td>
<td>
::formatNum(uo.product.price)::&nbsp;::currency()::
</td>
<td>
::if uo.order==null::
::set q = 0::
::else::
::set q = uo.order.quantity::
::end::
::set total = total+(q*uo.product.price)::
<input type="text" class="form-control input-sm" name="product::uo.product.id::" id="product::uo.product.id::" value="::q::" />
</td>
<!-- "paid" checkbox only if payments are not enabled -->
<td class="paid" ::cond !user._amap.hasPayments()::>
::_("Paid:")::
::if uo.order!=null::
<input type="checkbox" name="paid::uo.product.id::" value="1" ::attr checked (uo.order.paid==true ):: />
::else::
<input type="checkbox" name="paid::uo.product.id::" value="1" />
::end::
</td>
<td ::cond distribution==null::>
<select name="user2::uo.product.id::" class="form-control input-sm" style="width:150px;display:inline-block;">
<option value="0">-</option>
::foreach uu users::
<option value="::uu.value::" ::attr selected (uo.order!=null && uo.order._user2!=null && uo.order._user2.id==uu.value)::>::uu.label::</option>
::end::
</select>
<input data-toggle="tooltip" title="::_("Reverse the alternation")::" type="checkbox" name="invert::uo.product.id::" value="1" ::attr checked (uo.order!=null && uo.order.hasInvertSharedOrder()==true ):: />
</td>
</tr>
::end::
::set extra = 0::
::if c.hasPercentageOnOrders()::
<tr>
<th></th>
<th>::_("Total order")::</th>
<th>::total::::currency()::</th>
<th></th>
<th></th>
</tr>
<tr>
<th></th>
<td>::c.percentageName:: (::c.percentageValue::%)</td>
::set extra = total*(c.percentageValue/100)::
<td>::extra::::currency()::</td>
<th></th>
<th></th>
</tr>
::end::
<tr>
<th></th>
<th>::_("Total")::</th>
<th>::total+extra::::currency()::</th>
<th></th>
<th ::cond !user._amap.hasPayments()::>
<a href="#" id="checkButton">::_("Check all")::</a>
<script language="javascript">
var globalCheck = true;
var checkEverything = function(e){
var boxes = $("td.paid :checkbox");
boxes.attr("checked",globalCheck);
globalCheck=!globalCheck;
e.preventDefault();
};
$("#checkButton").click(checkEverything);
</script>
</th>
</tr>
</table>
<input type="submit" class="btn btn-primary" value="::_("Validate the order")::" name="submit" id="submit" />
</form>
::end::
+214
View File
@@ -0,0 +1,214 @@
::use 'contractadmin/design.mtt'::
<h3>::_("Orders")::</h3>
::if distribution!=null::
::_("Delivery of the"):: <b>::hDate(distribution.date)::</b>
::end::
<div id="orderBox"></div><!-- for react -->
<div style="margin:8px 0;">
::if distribution!=null::
<a href="/contractAdmin/edit/::c.id::?d=::distribution.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-plus-sign"></span> ::_("New order")::</a>
::else::
<a href="/contractAdmin/edit/::c.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-plus-sign"></span> ::_("New order")::</a>
::end::
::if distribution!=null::
$$export(/contractAdmin/orders/::c.id::?csv=1&d=::distribution.id::)
<a href="/contractAdmin/ordersByProduct/::c.id::?d=::distribution.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-list"></span>
::_("Summary by products")::
</a>
<div class="dropdown" style="display:inline-block;">
<button class="btn btn-default btn-sm dropdown-toggle btn-noAntiDoubleClick" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-print"></span> ::_("Attendance sheet")::
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a href="/distribution/list/::distribution.id::"> ::_("Standard")::</a></li>
<li><a href="/distribution/listByProductUser/::distribution.id::"> ::_("By Product-User")::</a></li>
</ul>
</div>
::else::
$$export(/contractAdmin/orders/::c.id::?csv=1)
<a href="/contractAdmin/ordersByProduct/::c.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-list"></span>
::_("Summary by products")::</a>
::end::
</div>
::if disabledProducts > 0::
$$alert( ::raw __("Be careful, there are <b>::disabledProducts::</b> order(s) containing <b>disabled products</b>.",{disabledProducts:disabledProducts}):: )
::end::
<style>
td.userRow{
font-size:120%;
color:#e7e7e7;
}
td.userRow a{ color:white; }
</style>
::set total = 0::
::set totalAdh = 0::
<table class="table table-bordered table-hover table-condensed" style="width:100%;">
::set name = ""::
::set name2 = ""::
::foreach m orders::
::if m.userName != name || m.userName2 != name2::
<tr style="background-color:#666;">
<td colspan="7" style="font-size:120%;color:#e7e7e7;">
::if distribution!=null::
::set basket = getBasket(m.userId,distribution._place.id,distribution.date)::
<div class="basketNumber" ::cond basket!=null:: >
<i class="fa fa-shopping-basket" aria-hidden="true"></i> N°::basket.num::
</div>
::end::
::m.userName::
::if m.userName2!=null::
<span class="glyphicon glyphicon-refresh"></span> ::_("alternated with"):: ::m.userName2::
::end::
::set name = m.userName ::
::set name2 = m.userName2 ::
::set totalAdh = totalAdh+1::
</td>
<td>
<!--
::if distribution!=null::
<a href="/contractAdmin/edit/::c.id::/::m.userId::?d=::distribution.id::" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-edit"></span>
::_("Edit")::</a>
::else::
<a href="/contractAdmin/edit/::c.id::/::m.userId::" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-edit"></span>
::_("Edit")::</a>
::end::
-->
::if distribution!=null::
<a class="btn btn-default btn-xs"
onclick="_.initOrderBox(::m.userId::,::distribution.id::,::distribution._contract.id::,::distribution._contract.type::,'::escapeJS(hDate(distribution.date))::','::escapeJS(distribution._place.toString())::','::escapeJS(m.userName)::','::user.getAmap().currency::',::user.getAmap().hasPayments()::,'/contractAdmin/orders/::c.id::?d=::distribution.id::')">
<span class="glyphicon glyphicon-edit"></span>
::_("Edit")::
</a>
::else::
<a class="btn btn-default btn-xs"
onclick="_.initOrderBox(::m.userId::, null, ::c.id::,::c.type::, null, null, '::escapeJS(m.userName)::','::user.getAmap().currency::',::user.getAmap().hasPayments()::,'/contractAdmin/orders/::c.id::')">
<span class="glyphicon glyphicon-edit"></span>
::_("Edit")::
</a>
::end::
</td>
</tr>
<tr>
<th>::_("Prod. ID")::</th>
<th>::_("Qty")::</th>
<th>::_("Product")::</th>
<th><a href="#" data-toggle="tooltip" data-placement="top" title="::_("Unit price including taxes")::">::_("U.P||price per unit")::</a></th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Paid")::</th>
</tr>
::end::
::if m.disabled==true::
::set class="danger"::
::else::
::set class=""::
::end::
<tr class="::class::">
<td class="ref">
$$nullSafe(::m.productRef::)
</td>
<td>
::if(m.quantity==0 && m.canceled)::
<span style="color:#AAA">::_("Canceled")::</span>
::else::
::raw m.smartQt::
::end::
</td>
<td>
::m.productName::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.subTotal)::&nbsp;::currency()::
</td>
<td>
<!-- frais -->
::if m.percentageValue!=null::
<a href="#" data-toggle="tooltip" data-placement="top" title="::m.percentageName:: : ::m.percentageValue:: %">
::formatNum(m.fees)::&nbsp;::currency()::
</a>
::end::
</td>
<td>
<!-- total -->
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
</td>
<td>
::if !m.canceled && m.quantity!=0::
::if m.paid==true::
<span style="color:#00AA00;">::_("Paid")::</span>
::else::
<span style="color:#DD0000;">::_("Not paid")::</span>
::end::
::end::
::if(m.quantity==0)::
&nbsp;&nbsp;&nbsp;&nbsp;
::if distribution==null::
<a href="/contractAdmin/orders/::c.id::?delete=::m.id::&token=::token::" $$confirm(::__("Do you really want to delete this order row for the product \"::productName::\" ?",{productName:m.productName})::) class="btn btn-danger btn-xs" >
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
::_("Delete")::
</a>
::else::
<a href="/contractAdmin/orders/::c.id::?d=::distribution.id::&delete=::m.id::&token=::token::" $$confirm(::__("Do you really want to delete this order row for the product \"::productName::\" ?",{productName:m.productName})::) class="btn btn-danger btn-xs" >
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
::_("Delete")::
</a>
::end::
::end::
</td>
</tr>
::end::
<tr style="background:#DDD;">
<th colspan="6">::_("Total")::</th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
<th></th>
</tr>
</table>
<div style="margin-top:18px;margin-bottom:18px;">
::raw __("<b>::totalAdh::</b> member(s) in this distribution.",{totalAdh:totalAdh})::
</div>
::end::
@@ -0,0 +1,172 @@
::use 'design.mtt'::
<div class="col-md-12">
<div class="article">
<div class="text-center">
<h3>
Distribution du ::hDate(date)::
</h3>
<h4><span class="glyphicon glyphicon-map-marker"></span>::place::</h4>
<br/>
<!--::if(confirmed)::
::_("Validated")::
::else::
::_("Not validated")::
::end::-->
</div>
<div class="text-center">
<a href="/distribution/listByDate/::date.toString().substr(0,10)::/::place.id::/csv" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-list-alt"></span> ::_("CSV Export"):: </a>
<a href="/distribution/listByDate/::date.toString().substr(0,10)::/::place.id::" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-print"></span> ::_("Attendance sheet"):: </a>
::if ctotal::
<a href="/contractAdmin/ordersByDate/::date.toString().substr(0,10)::/::place.id::" class="btn btn-default btn-sm">::_("Totals per member")::</a>
::else::
<a href="/contractAdmin/ordersByDate/::date.toString().substr(0,10)::/::place.id::?ctotal=1" class="btn btn-default btn-sm">::_("Sub-totals per contract")::</a>
::end::
<a href="/contractAdmin/vendorsByDate/::date.toString().substr(0,10)::/::place.id::" class="btn btn-default btn-sm">::_("Totals per supplier")::</a>
</div>
<hr/>
<table class="table table-bordered table-hover table-condensed" style="width:100%;">
::set total = 0:: <!-- big total -->
::set subtotal = 0:: <!-- total per user -->
::set contractTotal = 0:: <!-- total per contract -->
::foreach m orders::
::set i = repeat.m.index::
<!-- name change -->
::if orders[i-1]==null || m.userName != orders[i-1].userName || m.userName2 != orders[i-1].userName2::
<tr style="background-color:#666;">
<td colspan="9" style="font-size:120%;color:#e7e7e7;">
::set basket = getBasket(m.userId,place.id,date)::
<div class="basketNumber" ::cond basket!=null:: >
<i class="fa fa-shopping-basket" aria-hidden="true"></i> ::_("Num"):: ::basket.num::
</div>
::m.userName::
::if m.userName2!=null::
<span class="glyphicon glyphicon-refresh"></span> ::_("alternately with"):: ::m.userName2::
::end::
::set contractTotal = 0::
</td>
</tr>
<tr>
<th>::_("Contract")::</th>
<th>::_("Qty")::</th>
<th>::_("Ref")::</th>
<th>::_("Product")::</th>
<th><a href="#" data-toggle="tooltip" data-placement="top" title="::_("Unit price incl. VAT")::">::_("U.P.")::</a></th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Paid")::</th>
</tr>
::end::
<!-- 1 order line -->
<tr>
<td>
<a href="/contractAdmin/view/::m.contractId::">
::short(m.contractName,40)::
</a>
</td>
<td>
::if(m.quantity==0 && m.canceled)::
<span style="color:#AAA">::_("Canceled")::</span>
::else::
::raw m.smartQt::
::end::
</td>
<td class="ref">
$$nullSafe(::m.productRef::)
</td>
<td>
::short(m.productName,40)::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.subTotal)::&nbsp;::currency()::
</td>
<td>
<!-- fees -->
::if m.percentageValue!=null::
<a href="#" data-toggle="tooltip" data-placement="top" title="::m.percentageName:: : ::m.percentageValue:: %">
::formatNum(m.fees)::&nbsp;::currency()::
</a>
::end::
</td>
<td>
<!-- total -->
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
::set subtotal = subtotal + m.total::
::set contractTotal = contractTotal + m.total::
</td>
<td>
::if m.paid==true::
<span style="color:#00AA00;">::_("Paid")::</span>
::else::
<span style="color:#DD0000;">::_("Not paid")::</span>
::end::
</td>
</tr>
<!-- contract total -->
::if ctotal && (orders[i+1]==null || m.contractName != orders[i+1].contractName || m.userName != orders[i+1].userName )::
<tr style="background:#DDD;">
<th colspan="7" class="text-right">Total ::m.contractName::</th>
<th>::formatNum(contractTotal)::&nbsp;::currency()::</th>
<th></th>
::set contractTotal = 0::
</tr>
::end::
<!-- member total -->
::if (orders[i+1]==null || m.userName != orders[i+1].userName) ::
::if subtotal!=0::
<tr style="border-top:2px solid #AAA;">
<th colspan="7" class="text-right">::_("Total member")::</th>
<th>::formatNum(subtotal)::&nbsp;::currency()::</th>
<th></th>
::set subtotal = 0::
</tr>
::end::
::end::
<!-- end orders loop -->
::end::
<!-- big total -->
<tr style="background:#CCC;font-size:1.3em;">
<th colspan="7" class="text-right">::_("Total of all orders:"):: </th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
<th></th>
</tr>
</table>
</div>
</div>
::end::
@@ -0,0 +1,70 @@
::use 'contractadmin/design.mtt'::
<h3>::_("Summary table per product")::</h3>
::if distribution!=null::
<p>
::set date = hDate(distribution.date)::
::raw __("Delivery of the <b>::date::</b>",{date:date})::
</p>
::end::
<p>
::_("Be careful, if the price of the product was modified during the sale, every order of a member is taken into account with the price as it was at the time of the creation of the order")::
</p>
::set total = 0::
<table class="table table-bordered table-hover" style="width:100%;">
<tr>
<th>::_("Weight/Vol.")::</th>
<th>::_("Quantities")::</th>
<th>::_("Product")::</th>
<th>::_("Reference")::</th>
<th>::_("Unit Price incl. VAT")::</th>
<th>::_("Total")::</th>
</tr>
::foreach m orders::
<tr>
<td>
::raw m.weightOrVolume::
</td>
<td>
::formatNum(m.quantity)::
</td>
<td>
::m.pname::
</td>
<td class="ref">
$$nullSafe(::m.ref::)
</td>
<td>
::formatNum(m.priceTTC)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.totalTTC)::&nbsp;::currency()::
::set total = total + m.totalTTC::
</td>
</tr>
::end::
<tr style="background:#DDD;">
<th colspan="5">Total</th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
</tr>
</table>
::if distribution!=null::
$$export(/contractAdmin/ordersByProduct/::c.id::?csv=1&d=::distribution.id::)
<a href="/contractAdmin/ordersByProductList/::c.id::?d=::distribution.id::" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-list"></span> ::_("Order form")::</a>
::else::
$$export(/contractAdmin/ordersByProduct/::c.id::?csv=1)
<a href="/contractAdmin/ordersByProductList/::c.id::" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-list"></span> ::_("Order form")::</a>
::end::
::end::
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Bon de commande</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<table class="table table-bordered table-hover" style="width:100%;">
<tr>
<td>
::if group._image!=null::
<img src="::file(group._image)::" style="margin:auto;height:150px;" class="thumbnail" />
::end::
</td>
<td>
<h1>::group.name::</h1>
::if distribution!=null::
<h2>Livraison du <b>::hDate(distribution.date)::</b></h2>
::end::
<h2>::c.name:: - ::c._vendor.name::</h2>
</td>
<td>
::set cv = c._vendor::
::if cv._image!=null::
<img src="::file(cv._image)::" class="thumbnail" style="margin:auto;height:150px;" />
::end::
</td>
</tr>
</table>
<p>
::set cc = c._contact::
::set cv = c._vendor::
<b>Responsable contrat : </b> ::cc.firstName:: ::cc.lastName::, ::cc.email::,
::if cc.phone!=null :: ::cc.phone:: ::end::
</p>
<p>
<b>Producteur : </b>::cv.name:: ,
::if cv.email!=null :: ::cv.email:: ::end::,
::if cv.phone!=null :: ::cv.phone:: ::end::
</p>
<p>
<h3>::_("Summary table per product")::</h3>
::_("Be careful, if the price of the product was modified during the sale, every order of a member is taken into account with the price as it was at the time of the creation of the order")::
</p>
::set total = 0::
<table class="table table-bordered table-hover" style="width:100%;">
<tr style="border-top: 2px solid #AAA;">
<th>::_("Weight/Vol.")::</th>
<th>::_("Quantities")::</th>
<th>::_("Product")::</th>
<th>::_("Reference")::</th>
<th>::_("Unit price incl. VAT")::</th>
<th>::_("Total")::</th>
</tr>
::foreach m orders::
<tr style="border-top: 2px solid #AAA;">
<td>
::raw m.weightOrVolume::
</td>
<td>
::formatNum(m.quantity)::
</td>
<td>
::m.pname::
</td>
<td>
$$nullSafe(::m.ref::)
</td>
<td>
::formatNum(m.priceTTC)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.totalTTC)::&nbsp;::currency()::
::set total = total + m.totalTTC::
</td>
</tr>
::end::
<tr style="background:#DDD;">
<th colspan="5">::_("Total")::</th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
</tr>
</table>
<p class="hidden"><i>::_("Push \"Control + P\" to print this page.")::</i></p>
</body>
</html>
@@ -0,0 +1,146 @@
::use 'design.mtt'::
<div class="col-md-12">
<div class="article">
<div class="text-center">
<h3>::_("Orders from the"):: ::hDate(from):: ::_("to"):: ::hDate(to)::</h3>
</div>
::set uri = from.toString().substr(0,10)+"/"+to.toString().substr(0,10)::
<div class="text-center">
<!--<a href="/distribution/listByDate//csv" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-list-alt"></span> ::_("CSV Export"):: </a>-->
<a href="/contractAdmin/vendorsByTimeFrame/::uri::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-list"></span> ::_("Totals per supplier")::</a>
<a href="/contractAdmin/ordersByTimeFrame/::uri::?ctotal=1" class="btn btn-default btn-sm">::_("Sub-totals per contract")::</a>
</div>
<hr/>
<table class="table table-bordered table-hover table-condensed" style="width:100%;">
::set total = 0:: <!-- big total -->
::set subtotal = 0:: <!-- total per user -->
::set contractTotal = 0:: <!-- total per contract -->
::foreach m orders::
::set i = repeat.m.index::
<!-- name change -->
::if orders[i-1]==null || m.userName != orders[i-1].userName || m.userName2 != orders[i-1].userName2::
<tr style="background-color:#666;">
<td colspan="9" style="font-size:120%;color:#e7e7e7;">
::m.userName::
::if m.userName2!=null::
<span class="glyphicon glyphicon-refresh"></span> en alternance avec ::m.userName2::
::end::
::set contractTotal = 0::
</td>
</tr>
<tr>
<th>::_("Contract")::</th>
<th>::_("Qty")::</th>
<th>::_("Ref")::</th>
<th>::_("Product")::</th>
<th><a href="#" data-toggle="tooltip" data-placement="top" title="::_("Unit price incl. VAT")::">::_("U.P.")::</a></th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Paid")::</th>
</tr>
::end::
<!-- 1 order line -->
<tr>
<td>
<a href="/contractAdmin/view/::m.contractId::">
::short(m.contractName,40)::
</a>
</td>
<td>
::if(m.quantity==0 && m.canceled)::
::_("Canceled")::
::else::
::raw m.smartQt::
::end::
</td>
<td>
$$nullSafe(::m.productRef::)
</td>
<td>
::short(m.productName,40)::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.subTotal)::&nbsp;::currency()::
</td>
<td>
<!-- fees -->
::if m.percentageValue!=null::
<a href="#" data-toggle="tooltip" data-placement="top" title="::m.percentageName:: : ::m.percentageValue:: %">
::formatNum(m.fees)::&nbsp;::currency()::
</a>
::end::
</td>
<td>
<!-- total -->
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
::set subtotal = subtotal + m.total::
::set contractTotal = contractTotal + m.total::
</td>
<td>
::if m.paid==true::
<span style="color:#00AA00;">::_("Paid")::</span>
::else::
<span style="color:#DD0000;">::_("Not paid")::</span>
::end::
</td>
</tr>
<!-- contract total -->
::if ctotal && (orders[i+1]==null || m.contractName != orders[i+1].contractName || m.userName != orders[i+1].userName )::
<tr style="background:#DDD;">
<th colspan="7" class="text-right">Total ::m.contractName::</th>
<th>::formatNum(contractTotal)::&nbsp;::currency()::</th>
<th></th>
::set contractTotal = 0::
</tr>
::end::
<!-- member total -->
::if (orders[i+1]==null || m.userName != orders[i+1].userName) ::
::if subtotal!=0::
<tr style="border-top:2px solid #AAA;">
<th colspan="7" class="text-right">::_("Total member")::</th>
<th>::formatNum(subtotal)::&nbsp;::currency()::</th>
<th></th>
::set subtotal = 0::
</tr>
::end::
::end::
<!-- end orders loop -->
::end::
<!-- big total -->
<tr style="background:#CCC;font-size:1.3em;">
<th colspan="7" class="text-right">::_("Total of all orders:"):: </th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
<th></th>
</tr>
</table>
</div>
</div>
::end::
+156
View File
@@ -0,0 +1,156 @@
::use 'contractadmin/design.mtt'::
<h3>::_("Products")::</h3>
<div>
<p>
$$insert(::_("New product")::,/product/insert/::c.id::)
<a href="/product/import/::c.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-list-alt"></span> ::_("Import from Excel/CSV")::</a>
::if !c._amap.hasTaxonomy()::
<a ::cond c.type==1:: href="/product/categorize/::c.id::" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-tag"></span> ::_("Categories")::</a>
::end::
</p>
</div>
<script>
//enable checked products
var enableProducts = function(){
var ids = _.getCheckboxesId('form.products');
if(ids.length==0){
alert("::_("You need to select at least one product to perform this action")::");
}else{
var l = document.location;
l.href = l.protocol +"//"+ l.host + l.pathname + "?enable="+ ids.join("|");
}
};
//disable checked products
var disableProducts = function(){
var ids = _.getCheckboxesId('form.products');
if(ids.length==0){
alert("::_("You need to select at least one product to perform this action")::");
}else{
var l = document.location;
l.href = l.protocol +"//"+ l.host + l.pathname + "?disable="+ ids.join("|");
}
};
//check input when click row
/*var clickRow = function(x){
x.children[0].children[0].checked = !x.children[0].children[0].checked;
}*/
//check all
var check = function(){
var inputs = document.querySelectorAll('form.products input');
var value = inputs[0].checked==true;
for ( i of inputs ) {
i.checked = !value;
}
}
</script>
<form method="GET" class="products">
<table class="table table-striped table-hover">
<tr>
<th></th>
<th></th>
<th>::_("Product ID||id or reference of a product")::</th>
<th>::_("Name")::</th>
<th>::_("Price")::</th>
<th></th>
::if c.hasStockManagement()::
<th>::_("Stock")::</th>
::end::
<th></th>
</tr>
::foreach p c.getProducts(false)::
::if p.active==false::
::set opacity=0.4::
::else::
::set opacity=1::
::end::
<tr style="opacity: ::opacity::" onclick="/*clickRow(this)*/">
<td>
<input type="checkbox" name="checkbox" value="::p.id::" />
</td>
<td>
<img src="::p.getImage()::" style="width:64px;height:64px;" />
</td>
<td class="ref">$$nullSafe(::p.ref::)</td>
<td>
<b>::p.name::</b>
::if p._unitType!=null && p.qt!=null::
::p.qt::&nbsp;::unit(p._unitType,false)::
::end::
::if !c._amap.hasTaxonomy()::
<div>
::foreach c p.getCategories()::
<span class='tag' style='background-color: ::c.getColor()::;'>::c.name::</span>
::end::
</div>
::else::
<div class="detail">
::p.getFullCategorization().join(" / ")::
</div>
::end::
<div>
::if p.organic ::<img src="/img/AB.png" style="display:inline-block"/>::end::
::if p.variablePrice ::<img src="/img/weight.png" style="display:inline-block"/>::end::
</div>
</td>
<td>::formatNum(p.price)::&nbsp;::currency()::</td>
<td>
::if c.hasStockManagement() ::
::if p.stock==null::
-
::else::
::if p.stock <= 10::
<span style="color:red;font-weight:bold;">
::p.stock::
</span>
::else::
::p.stock::
::end::
::end::
::end::
</td>
<td>
<div class="btn-group" role="group">
$$edit(::_("Edit")::,/product/edit/::p.id::)
<a href="/product/addImage/::p.id::" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-picture"></span> ::_("Picture")::</a>
$$delete(::_("Del.")::,/product/delete/::p.id::?token=::token::)
</div>
</td>
</tr>
::end::
</table>
</form>
<p>
<div class="btn-group" role="group">
<a onclick="check();" class="btn btn-default btn-sm btn-noAntiDoubleClick"> ::_("Check all")::</a>
</div>
<div class="btn-group" role="group">
<a class="btn btn-default btn-sm" onclick="enableProducts()"><span class="glyphicon glyphicon-check"></span> ::_("Enable")::</a>
<a class="btn btn-default btn-sm" onclick="disableProducts()"><span class="glyphicon glyphicon-unchecked"></span> ::_("Disable")::</a>
</div>
</p>
::end::
@@ -0,0 +1,53 @@
::use 'contractadmin/design.mtt'::
::if distributions.length>0::
<h3>::_("Orders")::</h3>
<table class="table table-bordered table-hover table-striped">
<tr>
<th>::_("Date")::</th>
<th>::_("Location")::</th>
<th></th>
</tr>
::foreach d distributions::
::if d.date!=null && d.date.getTime() < Date.now().getTime()::
::set style = "opacity:0.5;"::
::else::
::set style = ""::
::end::
<tr style="::style::" >
<td>::hDate(d.date)::</td>
<td>::d._place.name::</td>
<td>
<a href="/contractAdmin/orders/::c.id::?d=::d.id::" class="btn btn-default">
<span class="glyphicon glyphicon-list"></span>
::_("Orders")::
</a>
</td>
</tr>
::end::
</table>
::else::
<div class="alert alert-danger">
<b>Aucune distribution prochainement.</b><br/>
<a href="/contractAdmin/distributions/::c.id::">::_("Click here to manage deliveries")::</a>
</div>
::end::
<div>
<a href="?old=1" class="btn btn-default btn-sm">
::_("Old orders")::
</a>
</div>
::end::
+63
View File
@@ -0,0 +1,63 @@
::use 'contractadmin/design.mtt'::
<div>
<a href="/contractAdmin/stats/::c.id::/?stat=0">
::_("Seniority of subscribers")::
</a> |
<a href="/contractAdmin/stats/::c.id::/?stat=1">
::_("Sharing between orders")::
</a><br/>
</div>
::if stat==0::
<h3>
::__("Seniority of subscribers of ::contract::",{contract:c.name})::
</h3>
<table class="table" style="width:50%;">
<tr>
<th>::_("Number")::</th>
<th>::_("Year")::</th>
</tr>
::foreach a anciennete::
<tr>
<td>::a.cnt::</td>
<td>::a.uyear::</td>
</tr>
::end::
</table>
::end::
::if stat==1::
<h3>::_("Sharing between orders of"):: "::c.name::"</h3>
<table class="table">
<tr>
<th>::_("Quantity")::</th>
<th>::_("Product")::</th>
<th>::_("Total price")::</th>
<th>::_("% of orders")::</th>
</tr>
::foreach r repartition::
<tr>
<td>::r.quantity::</td>
<td>::r.name::</td>
<td>::r.quantity*r.price:: ::currency()::</td>
<td>::r.percent:: %</td>
</tr>
::end::
<tr>
<th colspan="2">Total: ::totalQuantity:: ::_("products")::</th>
<th>::totalPrice:: ::currency()::</th>
<th></th>
</tr>
</table>
$$export(/contractAdmin/stats/::c.id::?csv=1&stat=1)
::end::
::end::
+27
View File
@@ -0,0 +1,27 @@
::use 'contractadmin/design.mtt'::
<h3>::_("Products")::</h3>
<table class="table">
::foreach p c.getProducts()::
<tr>
<td>
<img src="::p.getImage()::" style="width:64px;height:64px;" />
</td>
<td>$$nullSafe(::p.ref::)</td>
<td>::p.name::</td>
<td>::p.stock::</td>
<td>
$$edit(modifier,/product/edit/::p.id::)
</td>
</tr>
::end::
</table>
::end::
@@ -0,0 +1,61 @@
::use 'design.mtt'::
<div class="col-md-12">
<div class="article">
<h2>::_("Global view of orders - delivery of"):: ::hDate(date)::</h2>
<hr/>
::foreach o orders::
::set total = 0::
<table class="table table-bordered table-hover table-striped" style="width:100%;">
<tr>
<th colspan="5">
<!--<div class="pull-right">$$export(::"/contractAdmin/ordersByProduct/"+o.contract.id+"?csv=1&d="+o.distrib.id::)</div>-->
<h4>::o.contract.name:: - ::o.contract._vendor.name::</h4>
</th>
</tr>
<tr>
<th>::_("Quantities")::</th>
<th>::_("Product")::</th>
<th>::_("Reference")::</th>
<th>::_("Unit price incl. VAT")::</th>
<th>::_("Total")::</th>
</tr>
::foreach m o.orders::
<tr>
<td class="col-md-1">
::m.quantity::
</td>
<td class="col-md-5">
::m.pname::
</td>
<td class="col-md-3">
$$nullSafe(::m.ref::)
</td>
<td class="col-md-2">
::formatNum(m.priceTTC)::&nbsp;::currency()::
</td>
<td class="col-md-2">
::formatNum(m.totalTTC)::&nbsp;::currency()::
::set total = total + m.totalTTC::
</td>
</tr>
::end::
<tr style="background:#DDD;">
<th colspan="4">Total</th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
</tr>
</table>
::end::
</div>
</div>
::end::
@@ -0,0 +1,68 @@
::use 'design.mtt'::
<div class="col-md-12">
<div class="article">
<h2>::_("Global view of orders - from"):: ::hDate(from):: ::_("to"):: ::hDate(to)::</h2>
<div class="text-center">
$$export(/contractAdmin/vendorsByTimeFrame/::from.toString().substr(0,10)::/::to.toString().substr(0,10)::?csv=1)
</div>
<hr/>
::foreach o orders::
::set total = 0::
<table class="table table-bordered table-hover table-striped" style="width:100%;">
<tr>
<th colspan="6">
<!--<div class="pull-right">$$export(::"/contractAdmin/ordersByProduct/"+o.contract.id+"?csv=1&d="+o.distrib.id::)</div>-->
<h4>::o.contract.name:: - ::o.contract._vendor.name::</h4>
</th>
</tr>
<tr>
<th>::_("Quantities")::</th>
<th>::_("Product")::</th>
<th>::_("Reference")::</th>
<th>::_("Unit price incl. VAT")::</th>
<th>::_("Total HT")::</th>
<th>::_("Total TTC")::</th>
</tr>
::foreach m o.orders::
<tr>
<td class="col-md-1">
::formatNum(m.quantity)::
</td>
<td class="col-md-4">
::m.pname::
</td>
<td class="col-md-1 ref">
$$nullSafe(::m.ref::)
</td>
<td class="col-md-2">
::formatNum(m.priceTTC)::&nbsp;::currency()::
</td>
<td class="col-md-2">
::formatNum(m.totalHT)::&nbsp;::currency()::
</td>
<td class="col-md-2">
::formatNum(m.totalTTC)::&nbsp;::currency()::
</td>
::set total = total + m.totalTTC::
</tr>
::end::
<tr style="background:#DDD;">
<th colspan="5">::_("Total")::</th>
<th>::formatNum(total)::&nbsp;::currency()::</th>
</tr>
</table>
::end::
</div>
</div>
::end::
+70
View File
@@ -0,0 +1,70 @@
::use 'contractadmin/design.mtt'::
<h3>::_("Summary")::</h3>
<table class="table table-bordered" style="width:400px;">
<tr>
<td>
::_("Contract type")::
</td>
<td>
::if contract.type==0::
::_("CSA Contract")::
::else::
::_("Variable order contract")::
::end::
</td>
</tr>
<tr>
<td>::_("Start date")::</td>
<td>::hDate(contract.startDate)::</td>
</tr>
<tr>
<td>::_("End date")::</td>
<td>::hDate(contract.endDate)::</td>
</tr>
</table>
<div>
$$edit(::_("Edit")::,/contract/edit/::c.id::)
::if user.isAmapManager()::
$$edit(::_("Duplicate")::,/contractAdmin/duplicate/::c.id::)
::end::
</div>
<hr/>
<h4>::_("Farmer")::</h4>
<table class="table table-bordered" style="width:400px;">
<tr>
<td>::_("Name")::</td>
<td><b>::contract._vendor.name::</b></td>
</tr>
<tr>
<td>::_("Email")::</td>
<td>$$nullSafe(::contract._vendor.email::)</td>
</tr>
<tr>
<td>::_("Phone")::</td>
<td>$$nullSafe(::contract._vendor.phone::)</td>
</tr>
</table>
<h4><a href="/contractAdmin/products/::c.id::">::_("Products")::</a></h4>
<div style="max-height:400px;overflow:auto;">
<table class="table table-bordered" style="width:400px;">
::foreach p contract.getProducts(true)::
<tr>
<td><img src="::p.getImage()::" style="width:32px;height:32px;" /> </td>
<td>::p.getName()::</td>
<td>::formatNum(p.price)::&nbsp;::currency()::</td>
</tr>
::end::
</table>
</div>
::end::
+194
View File
@@ -0,0 +1,194 @@
::use 'design.mtt'::
<div id="content" class="col-md-8">
<div class="row">
<div id="content" class="col-md-12">
<div class="alert alert-danger">
<span class="glyphicon glyphicon-exclamation-sign"></span>
Hou là là !
</div>
</div>
</div>
<div class="row">
<div id="content" class="col-md-12">
<div class="alert alert-success">
<span class="glyphicon glyphicon-ok"></span>
Todo bem
</div>
</div>
</div>
<div class="article">
<div class="row">
<div class="col-md-6">
<h3>Boutons large</h3>
<button type="button" class="btn btn-lg btn-noAntiDoubleClick btn-default">noAntiDoubleClick</button>
<button type="button" class="btn btn-lg btn-default">Default</button>
<button type="button" class="btn btn-lg btn-primary">Primary</button>
<button type="button" class="btn btn-lg btn-success">Success</button>
<button type="button" class="btn btn-lg btn-info">Info</button>
<button type="button" class="btn btn-lg btn-warning">Warning</button>
<button type="button" class="btn btn-lg btn-danger">Danger</button>
<button type="button" class="btn btn-lg btn-link">Link</button>
</div>
<div class="col-md-6">
<h3>Boutons default</h3>
<button type="button" class="btn btn-default">Default</button>
<!-- Provides extra visual weight and identifies the primary action in a set of buttons -->
<button type="button" class="btn btn-primary">Primary</button>
<!-- Indicates a successful or positive action -->
<button type="button" class="btn btn-success">Success</button>
<!-- Contextual button for informational alert messages -->
<button type="button" class="btn btn-info">Info</button>
<!-- Indicates caution should be taken with this action -->
<button type="button" class="btn btn-warning">Warning</button>
<!-- Indicates a dangerous or potentially negative action -->
<button type="button" class="btn btn-danger">Danger</button>
<!-- Deemphasize a button by making it look like a link while maintaining button behavior -->
<button type="button" class="btn btn-link">Link</button>
</div>
<div class="col-md-6">
<h3>Boutons small</h3>
<button type="button" class="btn btn-sm btn-default">Default</button>
<button type="button" class="btn btn-sm btn-primary">Primary</button>
<button type="button" class="btn btn-sm btn-success">Success</button>
<button type="button" class="btn btn-sm btn-info">Info</button>
<button type="button" class="btn btn-sm btn-warning">Warning</button>
<button type="button" class="btn btn-sm btn-danger">Danger</button>
<button type="button" class="btn btn-sm btn-link">Link</button>
</div>
<div class="col-md-6">
<h3>Boutons XS</h3>
<button type="button" class="btn btn-xs btn-default">Default</button>
<button type="button" class="btn btn-xs btn-primary">Primary</button>
<button type="button" class="btn btn-xs btn-success">Success</button>
<button type="button" class="btn btn-xs btn-info">Info</button>
<button type="button" class="btn btn-xs btn-warning">Warning</button>
<button type="button" class="btn btn-xs btn-danger">Danger</button>
<button type="button" class="btn btn-xs btn-link">Link</button>
</div>
<div class="col-md-6">
<h1>H1 Title</h1>
<h2>H2 Title</h2>
<h3>H3 Title</h3>
<h4>H4 Title</h4>
</div>
<div class="col-md-6">
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">Panel title</h3>
</div>
<div class="panel-body">
<p>
Type : <b>Gratuit jusqu'à 20 foyers</b> ( actuellement 1 foyers dans votre groupe)
</p>
<p>
<button type="button" class="btn btn-primary"><span class="glyphicon glyphicon-chevron-right"></span> Primary</button>
</p>
</div>
</div>
</div>
</div>
<div class="popover fade bottom in" role="tooltip" id="popover63647" style="top: 531px;left: 50%;display: block;">
<div class="arrow" style="left: 50%;"></div>
<h3 class="popover-title">Visite guidée du coordinateur <div class="pull-right">7/21</div></h3>
<div class="popover-content">
<p>Ici se trouve la liste des <b>contrats</b>.
Ils comportent une date de début, une date de fin, et représentent votre relation avec un producteur. <br />
</p>
<p>
C'est ici que vous pourrez gérer :
</p>
<p>
<ul>
<li>la liste de produits de ce producteur</li>
<li>les commandes des adhérents pour ce producteur</li>
<li>planifier les distributions</li>
</ul>
</p>
<div class="footer">
<div class="pull-right">
<p><a class="btn btn-default btn-sm"><span class="glyphicon glyphicon-chevron-right"></span> Suivant</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="block" >
Le Lorem Ipsum est simplement du <strong>faux texte</strong> employé dans <a href="/">la composition</a>
et la mise en page avant impression. Le Lorem Ipsum est le faux texte standard de l'imprimerie depuis les années 1500,
quand un peintre anonyme assembla ensemble des morceaux de texte pour réaliser un livre spécimen de polices de texte.
Il n'a pas fait que survivre cinq siècles, mais s'est aussi adapté à la bureautique informatique, sans que son contenu n'en soit modifié.
</div>
<div class="block">
<h3>Permanences</h3>
<p>N'oubliez pas de vous inscrire pour aider lors des livraisons</p>
<ul>
<li>Liste 1</li>
<li>Liste 2</li>
</ul>
</div>
<div class="alert alert-danger">
<p>
Attention, vous n'avez défini aucun mot de passe pour votre compte !
</p>
<a href="/user/definePassword" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-chevron-right"></span> Définir un mot de passe</a>
</div>
$$today(Mercredi,15,Décembre,19,90,::user._amap._mainPlace::)
$$date(Mercredi,15,Décembre,19,::user._amap._mainPlace::)
</div>
::end::
+269
View File
@@ -0,0 +1,269 @@
::use 'base.mtt'::
::if getParam('__redirect')!=null::
<!-- need to login to go to this page-->
<script>$(function(){
_.loginBox('::raw getParam("__redirect")::');
});</script>
::end::
<div class="row header" style="margin-top:10px;">
<div class="col-md-8">
::set groupName = getCurrentGroup()::
::if groupName!=null && noGroup!=true::
<h1>::groupName::</h1>
::else::
<div style="margin-bottom:22px;" >
<a href="http://www.cagette.net" target="_blank">
<img src="/img/logo3.png" alt="::NAME::" />
</a>
</div>
::end::
</div>
<div class="col-md-4 accountBlock">
::if user != null::
<div class="dropdown pull-right text-right userName" style="margin-top:16px;">
<a class="" style="margin-right:16px;" href="/user/choose?show=1">
<span class="glyphicon glyphicon-chevron-left"></span>::_("Change group")::
</a>
<a class="dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown">
<span class="glyphicon glyphicon-user"></span>
::user.getName()::
::if user.firstName2!=null::
::_("et"):: ::user.lastName2:: ::user.firstName2::
::end::
</a>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu1">
<li><a href="/user/logout"><span class="glyphicon glyphicon-log-out"></span> ::_("Log out")::</a></li>
</ul>
</div>
::else::
<div class="pull-right text-right">
<a href="/user/login" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-user"></span> ::_("Log in")::</a>
<a class="btn btn-default btn-sm" href="/group/map"><span class="glyphicon glyphicon-search"></span> ::_("Look for another group")::</a>
</div>
::end::
</div>
</div>
::if user != null && noGroup!=true::
<nav class="navbar navbar-default" ::cond user.getAmap()!=null::>
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#cagette-navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="collapse navbar-collapse" id="cagette-navbar">
<ul class="nav navbar-nav navbar-left">
<li ::attr class if(category=="home") 'active' ::><a href="/">::_("Home")::</a></li>
<li ::attr class if(category=="contract" ) 'active' ::><a href="/contract">::_("My account")::</a></li>
<li ::attr class if(category=="amap") 'active' ::><a href="/amap">::_("Farmers")::</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li id="member" ::attr class if(category=="members") 'active' ::>
<a href="/member" ::cond user.canAccessMembership()::>::_("Members")::</a></li>
<li id="contractadmin" ::attr class if(category=="contractadmin") 'active' ::>
<a href="/contractAdmin" ::cond user.isContractManager(null)::>::_("Contracts")::</a></li>
<li id="messages" ::attr class if(category=="messages") 'active' ::>
<a href="/messages" ::cond user.canAccessMessages()::>::_("Messaging")::</a></li>
<li id="amapadmin" ::attr class if(category=="amapadmin") 'active' ::>
<a href="/amapadmin" ::cond user.isAmapManager()::>::_("Group")::</a></li>
<li ::cond user.isAdmin():: ::attr class if(category=="admin") 'active' ::>
<a href="/admin" >::_("Admin")::</a></li>
</ul>
</div>
</nav>
::end::
<!-- Notifications -->
<div class="row">
<div id="content" class="col-md-12">
::foreach notification getMessages()::
<div class="alert ::if notification.error:: alert-danger ::else:: alert-success ::end::" ::cond notification::>
::if notification.error::
<span class="glyphicon glyphicon-exclamation-sign"></span>
::else::
<span class="glyphicon glyphicon-ok"></span>
::end::
::raw notification.text::
</div>
::end::
</div>
</div>
<!-- extra notification block for plugins -->
::if extraNotifBlock!=null::
::raw extraNotifBlock::
::end::
<div class="row">
::raw __content__::
</div>
<!--<div class="row">
<div class="col-md-12 alert-danger text-center" style="border-radius: 8px;margin:8px 0;padding:8px 0;">
<span class="glyphicon glyphicon-cog"></span>
Attention : Cagette.net sera indisponible le <b>mardi 7 août de 9:00 à 11:00</b>.
<br/>
Nous déménageons sur un nouveau serveur pour plus de rapidité et de fiabilité.
</div>
</div>-->
<div class="row" id="footer">
<div class="col-md-9" >
<a href="http://www.cagette.net" target="_blank">
<img src="/img/logo.png" alt="::_("Cagette.net")::"/>
</a>
</div>
<div class="col-md-3" >
::_("HELP")::
<ul>
<li>
<a href="http://www.cagette.net/wiki" target="_blank">::_("Documentation")::</a>
</li>
<li>
<a href="https://www.cagette.pro" target="_blank">Formations pour producteurs</a>
</li>
<li>
::if (user!=null && user.isAmapManager()):: <a href="https://cagette.uservoice.com" target="_blank">::_("Propose features")::</a> ::end::
</li>
</ul>
<!--<p style="position: absolute;display: block;top: 96px;width: 700px;">
<b>::raw _("Vous êtes producteur ?")::</b>
::raw _("Découvrez nos formations gratuites pour utiliser <b>Cagette Pro</b> ")::
&nbsp;&nbsp;<a href="http://alilo.fr/formations-producteurs" target="_blank" class="btn btn-xs btn-info">C'est parti !</a>
</p>-->
</div>
<!--
<div class="col-md-3" >
::_("FOLLOW US")::
<ul class="cagsocialmedia">
<li class="cagfb">
<a title="Facebook" href="https://www.facebook.com/cagette" target="_blank"> <i class="fa fa-facebook"></i></a>
</li>
<li class="cagtwitter">
<a title="Twitter" href="https://twitter.com/Cagettenet" target="_blank"> <i class="fa fa-twitter"></i></a>
</li>
<li class="cagyoutube">
<a title="Youtube" href="https://www.youtube.com/channel/UC3cvGxAUrbN9oSZmr1oZEaw" target="_blank"> <i class="fa fa-youtube"></i></a>
</li>
<li style="background-color:#333;">
<a title="Github" href="https://github.com/bablukid/cagette" target="_blank"> <i class="fa fa-github"></i></a>
</li>
</ul>
</div>
-->
<!--
<div class="col-md-3">
::_("CONTACT US")::
<ul>
<li>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="cmd" value="_s-xclick"/>
<input type="hidden" name="hosted_button_id" value="S9KT7FQS7P622"/>
<input type="image" src="https://www.paypalobjects.com/fr_FR/FR/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal, le réflexe sécurité pour payer en ligne"/>
<img alt="" border="0" src="https://www.paypalobjects.com/fr_FR/i/scr/pixel.gif" width="1" height="1"/>
</form>
</li>
</ul>
</div>
-->
</div>
<div class="row" id="subfooter">
<!--
::raw _("Cagette.net is made by <a href='https://www.alilo.fr' target='_blank'><img src='/img/alilo-trans-black.png' /></a>")::<br/>
-->
<span>version ::VERSION::-::getPlatform()::</span>
</div>
<div class="row">
<div class="col-md-12">
<!-- SQL LOG -->
<table class="sqlLog table table-bordered" ::cond sqlLog::>
::foreach r sqlLog::
<tr ::attr class if(r.bad) 'badSql'::>
<td>::r.t::ms</td>
<td>::r.length::</td>
<td><a href="#" onclick="javascript:alert('::r.explain::');return false;">Exp</a></td>
<td><a href="#" onclick="javascript:alert('::r.stack::');return false;">Stack</a></td>
<td>::r.sql::</td>
</tr>
::end::
</table>
</div>
</div>
::if (false && user!=null && user.isAmapManager())::
<script>
// Include the UserVoice JavaScript SDK (only needed once on a page)
UserVoice=window.UserVoice||[];(function(){var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/sayZ1eIZLfSZUa3IwslHCw.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s)})();
//
// UserVoice Javascript SDK developer documentation:
// https://www.uservoice.com/o/javascript-sdk
//
// Set colors
UserVoice.push(['set', {
accent_color: '#6aba2e',
trigger_color: 'white',
trigger_background_color: '#e2753a'
}]);
// Identify the user and pass traits
// To enable, replace sample data with actual user traits and uncomment the line
UserVoice.push(['identify', {
//email: 'john.doe@example.com', // Users email address
//name: 'John Doe', // Users real name
//created_at: 1364406966, // Unix timestamp for the date the user signed up
//id: 123, // Optional: Unique id of the user (if set, this should not change)
//type: 'Owner', // Optional: segment your users by type
//account: {
// id: 123, // Optional: associate multiple users with a single account
// name: 'Acme, Co.', // Account name
// created_at: 1364406966, // Unix timestamp for the date the account was created
// monthly_rate: 9.99, // Decimal; monthly rate of the account
// ltv: 1495.00, // Decimal; lifetime value of the account
// plan: 'Enhanced' // Plan name for the account
//}
}]);
// Add default trigger to the bottom-right corner of the window:
UserVoice.push(['addTrigger', { mode: 'smartvote', trigger_position: 'bottom-right' }]);
// Or, use your own custom trigger:
//UserVoice.push(['addTrigger', '#id', { mode: 'smartvote' }]);
// Autoprompt for Satisfaction and SmartVote (only displayed under certain conditions)
UserVoice.push(['autoprompt', {}]);
</script>
::end::
::end::
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>::_("Attendance sheet")::</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<h2>
::contract._amap.name:: -
::__("Distribution of the ::date::, from ::from:: to ::to::",{date:dDate(distrib.date),from:hHour(distrib.date),to:hHour(distrib.end)})::
</h2>
<h2>
::distrib._contract.name:: - ::distrib._contract._vendor.name::
</h2>
<p>
<span class="glyphicon glyphicon-map-marker"></span><b>::_("Place"):: : </b>
::distrib._place.toString()::, ::distrib._place.getAddress()::
</p>
<p>
::set c = contract._contact::
::set v = contract._vendor::
<b>::_("Contact in charge of the contract") :</b> ::c.firstName:: ::c.lastName::, ::c.email::, ::c.phone::
</p>
<p>
<b>::_("Farmer"):: : </b>
::v.name:: ,
::if v.email!=null :: ::v.email:: ::end::,
::if v.phone!=null :: ::v.phone:: ::end::
</p>
<p>
<b>::_("Members on duty"):: : </b>
::if distrib._distributor1!=null:: ::distrib._distributor1.getName()::, ::end::
::if distrib._distributor2!=null:: ::distrib._distributor2.getName()::, ::end::
::if distrib._distributor3!=null:: ::distrib._distributor3.getName()::, ::end::
::if distrib._distributor4!=null:: ::distrib._distributor4.getName():: ::end::
</p>
<p>::raw nl2br(distrib._contract._amap.txtDistrib)::</p>
<table class="table table-bordered table-hover" style="width:100%;">
::set total = 0:: <!-- big total -->
::set subtotal = 0:: <!-- total per user -->
::foreach m orders::
::set i = repeat.m.index::
<!-- name change -->
::if orders[i-1]==null || m.userName != orders[i-1].userName || m.userName2 != orders[i-1].userName2::
<tr class="name">
<td colspan="8">
<span style="font-size:1.3em;">
::set basket = getBasket(m.userId,distrib._place.id,distrib.date)::
<span ::cond basket!=null::>N°::basket.num:: - </span>
::m.userName::
</span>
::set u = getUser(m.userId)::
::if u !=null && u.phone!=null::
- Tél. $$nullSafe(::u.phone::)
::end::
::if m.userName2 != null::
::_("alternately with")::
<span style="font-size:1.3em;">
::m.userName2::
</span>
::set u = getUser(m.userId2):
::if u !=null::
- Tél. $$nullSafe(::u.phone::)
::end::
::end::
</td>
</tr>
<tr>
<th>::_("Qty")::</th>
<th>::_("Ref")::</th>
<th>::_("Product")::</th>
<th>::_("U.P.")::</th>
<th>::_("Sub-total")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Signature")::</th>
</tr>
::end::
<!-- 1 order line -->
<tr class="order">
<td>
::if(m.quantity==0 && m.canceled)::
::_("Canceled")::
::else::
::raw m.smartQt::
::end::
</td>
<td>
$$nullSafe(::m.productRef::)
</td>
<td>
::m.productName::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::formatNum(m.subTotal)::&nbsp;::currency()::
</td>
<td>
<!-- frais -->
::if m.percentageValue!=null::
::formatNum(m.fees)::&nbsp;::currency()::
::end::
</td>
<td>
<!-- total -->
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
::set subtotal = subtotal + m.total::
</td>
<td style="width:20%;">
</td>
</tr>
<!-- member total -->
::if (orders[i+1]==null || m.userName != orders[i+1].userName || m.userName2 != orders[i+1].userName2) ::
::if subtotal!=0::
<tr>
<td colspan="5"></td>
<td class="total text-right">::_("TOTAL")::</td>
<td class="total">::formatNum(subtotal)::&nbsp;::currency()::</td>
<td></td>
::set subtotal = 0::
</tr>
::end::
::end::
::end::
<!-- big total -->
<tr style="font-size:1.3em;">
<td class="text-right">::_("Total order:"):: </td>
<td>::formatNum(total)::&nbsp;::currency()::</td>
<td colspan="6"></td>
</tr>
</table>
<p class="hidden"><i>::_("Push on \"Control + P\" to print this page.")::</i></p>
</body>
</html>
+252
View File
@@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>::_("Attendance sheet")::</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<h1>::user._amap.name::</h1>
<h2>Livraison du ::hDate(date)::</h2>
<table style="width:100%;">
::set name = ""::
::set name2 = ""::
::set total = 0::
::set productNum = 0::
<tr>
<th>::_("Name")::</th>
<th>::_("Contact details")::</th>
<th>::_("Qty")::</th>
<th>::_("Product")::</th>
<th>::_("U.P.")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Signature")::</th>
</tr>
::foreach m orders::
::if m.userName != name || m.userName2 != name2::
::set class="name"::
::else::
::set class=""::
::end::
<tr class="::class::">
<td>
::if m.userName != name || m.userName2 != name2::
::set basket = getBasket(m.userId,place.id,date)::
<span ::cond basket!=null::>::_("Num"):: ::basket.num:: - </span>
<b>::m.userName::</b>
::if m.userName2 != null::
<br />::_("alternately with"):: <b>::m.userName2::</b>
::end::
::set total = 0::
::set productNum = 0::
::end::
</td>
<td>
::if m.userName != name || m.userName2 != name2::
::set u = getUser(m.userId):
::if u !=null::
$$nullSafe(::u.phone::)
::end::
::if m.userName2 != null::
::set u = getUser(m.userId2):
::if u !=null::
<br />$$nullSafe(::u.phone::)
::end::
::end::
::end::
</td>
::set name = m.userName ::
::set name2 = m.userName2 ::
<td>
::if(m.quantity==0 && m.canceled)::
::_("Canceled")::
::else::
::raw m.smartQt::
::end::
::set productNum = productNum+m.quantity::
</td>
<td>
::m.productName::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::if m.fees!=null::
::formatNum(m.fees)::&nbsp;::currency()::
::end::
</td>
<td>
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
</td>
<td style="width:20%;"></td>
</tr>
<!--TOTAL-->
::set next = orders[repeat.m.index+1]::
::if next==null || next.userName!=name || next.userName2!=name2::
<tr>
<td></td>
<td></td>
<td class="total">::formatNum(productNum)::</td>
<td class="total" colspan="2"></td>
<td class="total">::_("TOTAL")::</td>
<td class="total">::formatNum(total)::&nbsp;::currency()::</td>
<td></td>
</tr>
<!-- PAIEMENTS -->
<tr ::cond user._amap.hasPayments()::>
<!-- PAYMENT CHECK FOR SHOP MODE -->
::if (user._amap.hasShopMode())::
<td></td>
<th>::_("Payment:"):: </th>
<td colspan="6">
<table style="width:100%;border:1px solid #CCC;">
::if(basket!=null)::
::set tpaid=0::
::set op = basket.getOrderOperation(false)::
::if op!=null::
::foreach p op.getRelatedPayments()::
<tr>
<td style="min-width:30px;">
::if(p.pending==true)::
<img src="/img/tick_no.png"/>
::else::
<img src="/img/tick_ok_me.png"/>
::end::
</td>
<td>
::formatNum(p.amount):: ::currency()::
::set tpaid = p.amount+tpaid::
</td>
<td>
::p.name::
</td>
</tr>
::end::
::end::
<!-- bug : 2 vars are the same but == doesnt work -->
::set total = numClean(total)::
::set tpaid = numClean(tpaid)::
::if(total!=tpaid)::
<tr style="background-color:#EEE;">
<td><img src="/img/tick_no.png"/></td>
<td>::formatNum(total-tpaid):: ::currency()::</td>
<td>
::if(total>tpaid)::
::_("To be paid"):: ::formatNum(total-tpaid):: ::currency()::
::else::
::_("Pay back"):: ::formatNum(tpaid-total):: ::currency()::
::end::
</td>
</tr>
::end::
::end::
</table>
</td>
::else::
<!-- PAYMENT CHECK FOR STANDARD MODE -->
<td></td>
::set ua = u.getUserAmap(user._amap)::
::set balance = ua.balance::
<th>::_("Balance of the member:"):: ::ua.balance:: ::currency()::</th>
<td colspan="6">
::set pending = 0::
<table style="width:100%;border:1px solid #CCC;">
<!-- payment to confirm -->
::foreach p ua.getLastOperations(5)::
::if p.type==2 && p.pending==true::
<tr>
<td style="width:30px;">
::if(p.pending==true)::
<img src="/img/tick_no.png"/>
::else::
<img src="/img/tick_ok_me.png"/>
::end::
</td>
<td>
::formatNum(p.amount):: ::currency()::
- ::p.name::
::set pending = pending + p.amount::
</td>
</tr>
::end::
::end::
<!-- need to pay more or cash back -->
::if(balance<0 && balance+pending!=0)::
<tr style="background-color:#EEE;">
<td style="width:30px;"><img src="/img/tick_no.png"/></td>
<td>
::if(balance+pending<0)::
::_("To be paid"):: ::formatNum(abs(balance+pending)):: ::currency():: pour équilibrer le solde
::else::
::_("Pay back"):: ::formatNum(balance+pending):: ::currency():: pour équilibrer le solde
::end::
</td>
</tr>
::end::
</table>
</td>
::end::
</tr>
::end::
::end::
</table>
<p>::raw nl2br(user.getAmap().txtDistrib)::</p>
<p class="hidden">
<i>::_("Push on \"Control + P\" to print this page.")::</i>
</p>
<table class="" ::cond sqlLog::>
::foreach r sqlLog::
<tr ::attr class if(r.bad) 'badSql'::>
<td>::r.t::ms</td>
<td>::r.length::</td>
<td><a href="#" onclick="javascript:alert('::r.explain::');return false;">Exp</a></td>
<td><a href="#" onclick="javascript:alert('::r.stack::');return false;">Stack</a></td>
<td>::r.sql::</td>
</tr>
::end::
</table>
</body>
</html>
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>::_("Attendance sheet")::</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<!-- page head-->
::fill head::
<td >
<h1 style='text-align:right;'>::user._amap.name::</h1>
<h2 style='text-align:right;'>::_("Delivery of the"):: ::hDate(date)::</h2>
</td>
::end::
<!-- table header -->
::fill tableHeader::
<tr>
<th>::_("Qty")::</th>
<th>::_("Product")::</th>
<th>::_("U.P.")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Signature")::</th>
</tr>
::end::
::set name = ""::
::set name2 = ""::
::set total = 0::
::set productNum = 0::
::foreach m orders::
::if m.userName != name || m.userName2 != name2::
::if name != ""::
::raw "</table> <hr />"::
::end::
::raw "<table style='width:100%;'>" ::
<tr>
<td style='width:33%;'>
<b style="font-size:150%;">
::set basket = getBasket(m.userId,place.id,date)::
<span ::cond basket!=null::>::_("Num"):: ::basket.num:: - </span>
::m.userName::
</b>
::set total = 0::
::set productNum = 0::
</td>
<td>
::set u = getUser(m.userId):
::if u!=null && u.phone!=null::
Tél: $$nullSafe(::u.phone::)
::end::
</td>
<td style='text-align:right;'>
<b>::user._amap.name::</b>
</td>
</tr>
<tr>
<td>
::if m.userName2 != null::
::_("alternately with")::<br/>
<b>::m.userName2::</b>
::set total = 0::
::set productNum = 0::
::end::
</td>
<td>
::if m.userName2 != null::
::set u = getUser(m.userId2):
::if u !=null::
$$nullSafe(::u.phone::)
::end::
::end::
</td>
<td style='text-align:right;'>
<b>::_("Delivery of the"):: ::hDate(date)::</b>
</td>
</tr>
::raw "</table>" ::
::raw "<table style='width:100%;'>"::
::raw tableHeader::
::end::
::if m.userName != name || m.userName2 != name2::
::set class="name"::
::else::
::set class=""::
::end::
<tr class="::class::">
<td style="font-size: ::fontRatio::%;border-bottom: 2px solid #AAA;">
::if(m.quantity==0 && m.canceled)::
::_("Canceled")::
::else::
::raw m.smartQt::
::end::
::set productNum = productNum+m.quantity::
</td>
<td style="font-size: ::fontRatio::%;border-bottom: 2px solid #AAA;">
::m.productName::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::if m.fees!=null::
::formatNum(m.fees)::&nbsp;::currency()::
::end::
</td>
<td>
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
</td>
<td style="width:20%;"></td>
</tr>
::set name = m.userName ::
::set name2 = m.userName2 ::
<!--TOTAL-->
::set next = orders[repeat.m.index+1]::
::if next==null || next.userName!=name || next.userName2!=name2::
<tr>
<td class="total">::formatNum(productNum)::</td>
<td class="total" colspan="2"></td>
<td class="total">::_("TOTAL")::</td>
<td class="total" style="font-size: ::fontRatio::%;">::formatNum(total)::&nbsp;::currency()::</td>
<td></td>
</tr>
::end::
::end::
<p>::raw nl2br(user.getAmap().txtDistrib)::</p>
<p class="hidden"><i>::_("Push on \"Control + P\" to print this page.")::</i></p>
<table class="" ::cond sqlLog::>
::foreach r sqlLog::
<tr ::attr class if(r.bad) 'badSql'::>
<td>::r.t::ms</td>
<td>::r.length::</td>
<td><a href="#" onclick="javascript:alert('::r.explain::');return false;">Exp</a></td>
<td><a href="#" onclick="javascript:alert('::r.stack::');return false;">Stack</a></td>
<td>::r.sql::</td>
</tr>
::end::
</table>
</body>
</html>
@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>::_("Attendance sheet")::</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<!-- page head-->
::fill head::
<td >
<h1 style='text-align:right;'>::user._amap.name::</h1>
<h2 style='text-align:right;'>::_("Delivery of the"):: ::hDate(date)::</h2>
</td>
::end::
<!-- table header -->
::fill tableHeader::
<tr>
<th>::_("Contract")::</th>
<th>::_("Qty")::</th>
<th>::_("Product")::</th>
<th>::_("U.P.")::</th>
<th>::_("Fees")::</th>
<th>::_("Total")::</th>
<th>::_("Signature")::</th>
</tr>
::end::
::set name = ""::
::set name2 = ""::
::set total = 0::
::set productNum = 0::
::set currentContract = ""::
::foreach m orders::
::if m.userName != name || m.userName2 != name2::
::if name != ""::
::raw "</table> <hr />"::
::end::
::raw "<table style='width:100%;'>" ::
<tr>
<td style='width:33%;'>
<b style="font-size:150%;">
::set basket = getBasket(m.userId,place.id,date)::
<span ::cond basket!=null::>N°::basket.num:: - </span>
::m.userName::
</b>
::set total = 0::
::set productNum = 0::
</td>
<td>
::set u = getUser(m.userId):
::if u!=null && u.phone!=null::
Tél: $$nullSafe(::u.phone::)
::end::
</td>
<td style='text-align:right;'>
<b>::user._amap.name::</b>
</td>
</tr>
<tr>
<td>
::if m.userName2 != null::
::_("alternately with")::<br/>
<b>::m.userName2::</b>
::set total = 0::
::set productNum = 0::
::end::
</td>
<td>
::if m.userName2 != null::
::set u = getUser(m.userId2):
::if u !=null::
$$nullSafe(::u.phone::)
::end::
::end::
</td>
<td style='text-align:right;'>
<b>::_("Delivery of the"):: ::hDate(date)::</b>
</td>
</tr>
::raw "</table>" ::
::raw "<table style='width:100%;'>"::
::raw tableHeader::
::end::
::if m.userName != name || m.userName2 != name2::
::set class="name"::
::set currentContract = ""::
::else::
::set class=""::
::end::
<tr class="::class::">
::if currentContract != short(m.contractName,40) ::
::if currentContract == "" ::
::set td_style="font-size:" + fontRatio + "%" ::
::else::
::set td_style="font-size:" + fontRatio + "%;border-top: 2px solid #AAA;" ::
::end::
<td style="::td_style::" >
::short(m.contractName,40)::
</td>
::set currentContract = short(m.contractName,40)::
::else::
<td> </td>
::end::
<td style="font-size: ::fontRatio::%;border-bottom: 2px solid #AAA;">
::if(m.quantity==0 && m.canceled)::
::_("Canceled")::
::else::
::raw m.smartQt::
::end::
::set productNum = productNum+m.quantity::
</td>
<td style="font-size: ::fontRatio::%;border-bottom: 2px solid #AAA;">
::m.productName::
</td>
<td>
::formatNum(m.productPrice)::&nbsp;::currency()::
</td>
<td>
::if m.fees!=null::
::formatNum(m.fees)::&nbsp;::currency()::
::end::
</td>
<td>
::formatNum(m.total)::&nbsp;::currency()::
::set total = total + m.total::
</td>
<td style="width:20%;"></td>
</tr>
::set name = m.userName ::
::set name2 = m.userName2 ::
<!--TOTAL-->
::set next = orders[repeat.m.index+1]::
::if next==null || next.userName!=name || next.userName2!=name2::
<tr>
<td style="border-top: 2px solid #AAA;"></td>
<td class="total">::formatNum(productNum)::</td>
<td class="total" colspan="2"></td>
<td class="total">::_("TOTAL")::</td>
<td class="total" style="font-size: ::fontRatio::%;">::formatNum(total)::&nbsp;::currency()::</td>
<td></td>
</tr>
::end::
::end::
<p>::raw nl2br(user.getAmap().txtDistrib)::</p>
<p class="hidden"><i>::_("Push on \"Control + P\" to print this page.")::</i></p>
<table class="" ::cond sqlLog::>
::foreach r sqlLog::
<tr ::attr class if(r.bad) 'badSql'::>
<td>::r.t::ms</td>
<td>::r.length::</td>
<td><a href="#" onclick="javascript:alert('::r.explain::');return false;">Exp</a></td>
<td><a href="#" onclick="javascript:alert('::r.stack::');return false;">Stack</a></td>
<td>::r.sql::</td>
</tr>
::end::
</table>
</body>
</html>
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>::_("Attendance sheet")::</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<h1>::user._amap.name::</h1>
<h2>Livraison du ::hDate(date)::</h2>
<table style="width:100%;">
::set name = ""::
::set name2 = ""::
::set total = 0::
::set productNum = 0::
<tr style="font-size:120%">
<th>::_("Name")::</th>
<th>::_("Contact details")::</th>
<th>::_("Qty")::</th>
<th>::_("Product")::</th>
<th>::_("Signature")::</th>
</tr>
::foreach m orders::
::if m.userName != name || m.userName2 != name2::
::set class="name"::
::else::
::set class=""::
::end::
<tr class="::class::">
<td style="font-size:120%">
::if m.userName != name || m.userName2 != name2::
<b>
::set basket = getBasket(m.userId,place.id,date)::
<span ::cond basket!=null::>N°::basket.num:: - </span>
::m.userName::
</b>
::if m.userName2 != null::
<br />::_("alternately with"):: <b>::m.userName2::</b>
::end::
::set total = 0::
::set productNum = 0::
::end::
</td>
<td>
::if m.userName != name || m.userName2 != name2::
::set u = getUser(m.userId):
::if u !=null::
$$nullSafe(::u.phone::)
::end::
::if m.userName2 != null::
::set u = getUser(m.userId2):
::if u !=null::
<br />$$nullSafe(::u.phone::)
::end::
::end::
::end::
</td>
::set name = m.userName ::
::set name2 = m.userName2 ::
<td>
::if(m.quantity==0 && m.canceled)::
::_("Canceled")::
::else::
::raw m.smartQt::
::end::
::set productNum = productNum+m.quantity::
</td>
<td>
::m.productName::
</td>
<td style="width:20%;"></td>
</tr>
::set next = orders[repeat.m.index+1]::
::end::
</table>
<p>::raw nl2br(user.getAmap().txtDistrib)::</p>
<p class="hidden">
<i>::_("Push on \"Control + P\" to print this page.")::</i>
</p>
<table class="" ::cond sqlLog::>
::foreach r sqlLog::
<tr ::attr class if(r.bad) 'badSql'::>
<td>::r.t::ms</td>
<td>::r.length::</td>
<td><a href="#" onclick="javascript:alert('::r.explain::');return false;">Exp</a></td>
<td><a href="#" onclick="javascript:alert('::r.stack::');return false;">Stack</a></td>
<td>::r.sql::</td>
</tr>
::end::
</table>
</body>
</html>
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>::_("Attendance sheet")::</title>
<link href="/css/print.css" rel="stylesheet"/>
</head>
<body>
<h2>
::contract._amap.name:: -
::__("Distribution of the ::date::, from ::from:: to ::to::",{date:dDate(distrib.date),from:hHour(distrib.date),to:hHour(distrib.end)})::
</h2>
<h2>
::distrib._contract.name:: - ::distrib._contract._vendor.name::
</h2>
<p>
<span class="glyphicon glyphicon-map-marker"></span><b>::_("Place"):: : </b>
::distrib._place.toString()::, ::distrib._place.getAddress()::
</p>
<p>
::set c = contract._contact::
::set v = contract._vendor::
<b>::_("Contact in charge of the contract") :</b> ::c.firstName:: ::c.lastName::, ::c.email::, ::c.phone::
</p>
<p>
<b>::_("Farmer"):: : </b>
::v.name:: ,
::if v.email!=null :: ::v.email:: ::end::,
::if v.phone!=null :: ::v.phone:: ::end::
</p>
<p>
<b>::_("Members on duty"):: : </b>
::if distrib._distributor1!=null:: ::distrib._distributor1.getName()::, ::end::
::if distrib._distributor2!=null:: ::distrib._distributor2.getName()::, ::end::
::if distrib._distributor3!=null:: ::distrib._distributor3.getName()::, ::end::
::if distrib._distributor4!=null:: ::distrib._distributor4.getName():: ::end::
</p>
<table class="table table-bordered table-hover" style="width:100%;">
<tr>
<th></th>
::foreach p products::
<th>
::p.name::<br/>
::p.ref::
</th>
::end::
<th>TOTAL</th>
</tr>
::foreach user users::
<tr>
<th>
::set basket = getBasket(user.id,distrib._place.id,distrib.date)::
<span ::cond basket!=null::>N°::basket.num:: - </span>
::user.getCoupleName()::
</th>
::foreach p products::
<td>
::set o = orders.get(user.id).get(p.id)::
::if(o!=null)::
::raw o.smartQt::
::end::
</td>
::end::
<th>::formatNum(totalByUser(user.id)):: ::currency()::</th>
</tr>
::end::
<tr>
<th>TOTAL</th>
::foreach p products::
<th>
::set q = totalByProduct(p.id)::
::if(p.qt!=null && (p.hasFloatQt || p.variablePrice) )::
::q * p.qt:: ::unit(p._unitType,false)::
::else::
::q::
::end::
</th>
::end::
</tr>
</table>
<p>::raw nl2br(distrib._contract._amap.txtDistrib)::</p>
<p class="hidden"><i>::_("Push on \"Control + P\" to print this page.")::</i></p>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More