diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..2e05db2
--- /dev/null
+++ b/.gitignore
@@ -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
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000..d6a9326
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,340 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 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.
+
diff --git a/Makefile.dist b/Makefile.dist
new file mode 100644
index 0000000..9a121c3
--- /dev/null
+++ b/Makefile.dist
@@ -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/
+
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index 67e863e..422b4dd
--- a/README.md
+++ b/README.md
@@ -1,2 +1,17 @@
-CAGETTE from Artcode
-====================
+# Cagette.net
+Le logiciel libre des circuits courts.
+
+[](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
diff --git a/build/travis-ci-apache b/build/travis-ci-apache
new file mode 100644
index 0000000..c6ebf18
--- /dev/null
+++ b/build/travis-ci-apache
@@ -0,0 +1,11 @@
+
+ ServerName cagette
+ DirectoryIndex index.n
+ DocumentRoot %TRAVIS_BUILD_DIR%/www
+
+
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
\ No newline at end of file
diff --git a/cagette-php.hxml b/cagette-php.hxml
new file mode 100644
index 0000000..28ed845
--- /dev/null
+++ b/cagette-php.hxml
@@ -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
diff --git a/cagette.hxml b/cagette.hxml
new file mode 100644
index 0000000..aecc8ef
--- /dev/null
+++ b/cagette.hxml
@@ -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
diff --git a/cagetteAllPlugins.hxml b/cagetteAllPlugins.hxml
new file mode 100755
index 0000000..6762dc0
--- /dev/null
+++ b/cagetteAllPlugins.hxml
@@ -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
\ No newline at end of file
diff --git a/cagetteAllPlugins.hxproj b/cagetteAllPlugins.hxproj
new file mode 100755
index 0000000..f6f1362
--- /dev/null
+++ b/cagetteAllPlugins.hxproj
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cagetteAllPluginsJs.hxml b/cagetteAllPluginsJs.hxml
new file mode 100644
index 0000000..0e7469f
--- /dev/null
+++ b/cagetteAllPluginsJs.hxml
@@ -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
+
diff --git a/cagetteJs.hxml b/cagetteJs.hxml
new file mode 100644
index 0000000..2484546
--- /dev/null
+++ b/cagetteJs.hxml
@@ -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
diff --git a/common/Common.hx b/common/Common.hx
new file mode 100755
index 0000000..7c1849d
--- /dev/null
+++ b/common/Common.hx
@@ -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,
+ image : Null,
+ contractId : Int,
+ price : Float,
+ vat : Null, //VAT rate
+ vatValue : Null, //amount of VAT included in the price
+ contractTax : Null, //pourcentage de commission défini dans le contrat
+ contractTaxName : Null, //label pour la commission : ex: "frais divers"
+ desc : Null,
+ categories : Null>, //used in old shop
+ subcategories : Null>, //used in new shop
+ orderable : Bool, //can be currently ordered
+ stock: Null, //available stock
+ hasFloatQt : Bool,
+ qt:Null,
+ unitType:Null,
+ organic:Bool,
+ variablePrice:Bool,
+ wholesale:Bool,
+ active:Bool,
+ ?distributionId:Null, //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
+}
+
+/**
+ * datas used with the "tagger" ajax class
+ */
+@:keep
+typedef TaggerInfos = {
+ products:Array<{product:ProductInfo,categories:Array}>,//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,
+ percentageName:Null,
+ percentageValue:Null,
+ 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};
+
+/**
+ 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, name:String, ?id:Int); //a navigation is displayed, optionnal object id if needed
+ Blocks(blocks:Array, 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,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);
+ StockMove(order:{product:db.Product, move:Float}); //when a stock is modified
+
+ //payments
+ GetPaymentTypes(data:{types:Array});
+ NewOperation(op:db.Operation);
+
+ #end
+
+}
+
+
+/*
+ * Product Taxonomy structure
+ */
+typedef TxpDictionnary = {
+ products:Map,
+ categories:Map,
+ subCategories:Map,
+
+}
+
+
+/*
+ * 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._("
In order to better discover Cagette.net, we propose to do a guided tour of the user interface of the software. You will then have a global overview on the different tools that are available to you.
You will be able to stop and start again this tutorial whenever you want.
"),
+ action: TANext,
+ placement : null
+ },
+ {
+ element:"ul.nav.navbar-left",
+ text:t._( "This part of the navigation bar is visible by all members.It allows to access to the three main pages:
The home page which displays orders and the delivery planning.
On the My account page, you can update your personal information and check your orders history
On the Farmers page, you can see all farmers and coordinators of the group
"),
+ action: TANext,
+ placement : TPBottom
+ },
+ {
+ element:"ul.nav.navbar-right",
+ text:t._("This part is for coordinators only.Here you will be able to manage the register of members, orders, products, etc.
Now click on the Members section
"),
+ 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. 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.
Now click on a member in the list
"),
+ action: TAPage("/member/view/*"),
+ placement : TPRight
+ },{
+ element:".article:first",
+ text:t._("This is the page of a member. Here you can :
see and change their contact details
manage the membership fee of your group
see a summary of their orders
"),
+ action: TANext,
+ placement : TPRight
+ },{
+ element:"ul.nav #contractadmin",
+ text:t._("Now let's have a look at the contracts section which is very important for coordinators."),
+ action: TAPage("/contractAdmin"),
+ placement : TPBottom
+ },{
+ element:"#contracts",
+ text:t._("Here you find the list of contracts.They inculde a start date, a end date, and represent your relationship with a farmer.
Here you can manage :
the list of products of this farmer
the orders of members for this farmer
and plan the delivery schedule
"),
+ action: TANext,
+ placement : TPBottom
+
+ },{
+ element:"#places",
+ text:t._("Here you can manage the list of delivery places. 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. Click on this button"),
+ action: TAPage("/contractAdmin/view/*"),
+ placement : TPBottom
+
+ },{
+ element:".table.table-bordered:first",
+ text:t._("Here is a summary of the contract. There are two types of contracts:
Constant contracts: the member commits on buying the same products during the whole duration of the contract
Variable contracts: the member can choose what he buys for each delivery.
"),
+ action: TANext,
+ placement : TPRight
+
+ },{
+ element:"#subnav #products",
+ text:t._("Let's see now the page Products"),
+ 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. 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 deliveries page"),
+ action : TAPage("/contractAdmin/distributions/*"),
+ placement:TPRight
+ },{
+ element:".article .table",
+ text:t._("Here we can manage the list of deliveries for this supplier. 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 Orders 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. If you choose to \"open orders\" to members, they will be able to make their orders online themselves. 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._("
We have seen the main features related to contracts.
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.
E-mails are sent with your e-mail address as sender, so you will receive answers in your own mailbox.
"),
+ 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._("
In this last page, you can configure everything that is related to your group.
The page Access rights 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.
This is the last step of this tutorial. I hope that it gave you a good overview of this software. To go further, do not hesitate to look at the documentation. The link is always available at the bottom of the screen.
";
+ }).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("
TOTAL : " + total + "
");
+
+
+ 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}>();
+ var pinned = new Map}>();
+
+ 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("
" + o.name + "
");
+ 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(" ");
+
+
+ //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,
+ categories:Array<{name:String,pinned:Bool,categs:Array}>,
+ 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', "");
+ }
+
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/js/Tagger.hx b/js/Tagger.hx
new file mode 100755
index 0000000..60a4f37
--- /dev/null
+++ b/js/Tagger.hx
@@ -0,0 +1,151 @@
+package ;
+import Common;
+import js.JQuery;
+/**
+ *
+ * Tag products with categories
+ *
+ * @author fbarbut
+ */
+@:keep
+class Tagger
+{
+
+ var contractId : Int;
+ var data:TaggerInfos;
+ var pids : Array; //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("
");
+ 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("[X]")
+ tags.push(""+name+"");
+ }
+
+ html.add("
"+ tags.join(" ") +"
");
+ html.add("
");
+ }
+ html.add("
");
+ 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);
+
+ }
+
+}
\ No newline at end of file
diff --git a/js/Tuto.hx b/js/Tuto.hx
new file mode 100644
index 0000000..589a841
--- /dev/null
+++ b/js/Tuto.hx
@@ -0,0 +1,131 @@
+package;
+import Common;
+
+/**
+ * Tutorial javascript widget
+ * @author fbarbut
+ */
+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(" "+tuto.name);
+ m.find(".modal-body").html(" "+t._("This tutorial is over."));
+ var bt = App.jq(" "+t._("Come back to tutorials page")+"");
+ 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(" "+tuto.name);
+
+ var bt = App.jq(" "+t._("OK")+"");
+ 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+"
"+(step+1)+"/"+tuto.steps.length+"
");
+ var text = "
" + s.text + "
";
+ var bt = null;
+ switch(s.action) {
+ case TANext :
+
+ bt = App.jq("
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/admin/cssDemo.mtt b/lang/master/tpl/admin/cssDemo.mtt
new file mode 100644
index 0000000..0496fac
--- /dev/null
+++ b/lang/master/tpl/admin/cssDemo.mtt
@@ -0,0 +1,53 @@
+::use 'admin/design.mtt'::
+
+
+
+
+
H1 Title
+
H2 Title
+
H3 Title
+
H4 Title
+
+
+
+
+
+
+ Titre H2
+
+
+ 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.
+
+
+ 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.
+
+
+
+
+ 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.
+
+ Big Button
+ Big Button
+
+
+
+
+
+
+
+
+
Panel heading without title
+
+ Panel content
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/admin/default.mtt b/lang/master/tpl/admin/default.mtt
new file mode 100644
index 0000000..4be3e8d
--- /dev/null
+++ b/lang/master/tpl/admin/default.mtt
@@ -0,0 +1,10 @@
+::use 'admin/design.mtt'::
+
+
+
Console d'administration de Cagette.net
+
+ ::hDate(now)::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/admin/design.mtt b/lang/master/tpl/admin/design.mtt
new file mode 100644
index 0000000..39d894d
--- /dev/null
+++ b/lang/master/tpl/admin/design.mtt
@@ -0,0 +1,36 @@
+::use 'design.mtt'::
+
+
+ ::raw __content__::
+
+
+
+
+
diff --git a/lang/master/tpl/block/modal.mtt b/lang/master/tpl/block/modal.mtt
new file mode 100644
index 0000000..de3cd10
--- /dev/null
+++ b/lang/master/tpl/block/modal.mtt
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/lang/master/tpl/blocks/user.mtt_removeme b/lang/master/tpl/blocks/user.mtt_removeme
new file mode 100644
index 0000000..ba3c5fe
--- /dev/null
+++ b/lang/master/tpl/blocks/user.mtt_removeme
@@ -0,0 +1,12 @@
+
+ ::if user!=null::
+ ::_("Hello"):: ::user.getName()::
+
+ ::_("Log out")::
+ ::else::
+
+ ::raw _("You already have an account?
+ Click here to log in")::
+ ::_("Log in")::
+ ::end::
+
\ No newline at end of file
diff --git a/lang/master/tpl/categories/default.mtt b/lang/master/tpl/categories/default.mtt
new file mode 100644
index 0000000..d732963
--- /dev/null
+++ b/lang/master/tpl/categories/default.mtt
@@ -0,0 +1,48 @@
+::use 'amapadmin/design.mtt'::
+
+
::_("Categories")::
+
+
+ ::raw _("Categories allow to sort your products per category, for example \"vegetables, meet, backery, etc.\" It is possible to create many groups of categories in order to sort products as we wish.")::
+
+::end::
+
+::_("Add a group")::
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/contract/default.mtt b/lang/master/tpl/contract/default.mtt
new file mode 100644
index 0000000..0e192fc
--- /dev/null
+++ b/lang/master/tpl/contract/default.mtt
@@ -0,0 +1,260 @@
+::use 'design.mtt'::
+
+ ::_("It is a contract with a duration of one year or for one season. Products are always the same at each delivery")::
+
+
+ ::raw _("Example: there is a contract for vegetables that is 1 year long. Jean takes a basket that costs 10€ per delivery, and Brigitte takes a basket that costs 15€ per delivery.")::
+
+ ::_("It is a contract in which members can change their orders at each delivery. There is a different order for each delivery.")::
+
+
+ ::raw _("Example: Jean orders a basket that costs 10€ for the first delivery, then nothing for the second delivery, then a basket for 20€ etc.")::
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/contract/order.mtt b/lang/master/tpl/contract/order.mtt
new file mode 100644
index 0000000..e904efc
--- /dev/null
+++ b/lang/master/tpl/contract/order.mtt
@@ -0,0 +1,139 @@
+::use 'design.mtt'::
+
+
+
::_("Order"):: ::c.name::
+
+ ::if distribution!=null::
+ ::_("For the delivery of the"):: ::hDate(distribution.date)::
+ ::end::
+
+
+ ::raw _("As long as orders are open, you can come back on this page and modify your order. You can check any time your current orders on the following page my account.")::
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/contract/orderAndPay.mtt b/lang/master/tpl/contract/orderAndPay.mtt
new file mode 100644
index 0000000..b1ae969
--- /dev/null
+++ b/lang/master/tpl/contract/orderAndPay.mtt
@@ -0,0 +1,137 @@
+::use 'design.mtt'::
+
+
+
::_("Order"):: ::c.name::
+
+
+
+ ::raw _("Key-in here your orders in advance for this supplier. You can check your current orders any time on the following page my account. Tip: to remove an order please key-in the quantity 0")::
+
+
+
+
+
+
+
+
+
+ ::_("Supplier:"):: ::c._vendor.name::
+
+
+ ::_("Contact in charge:"):: $$contact(::c._contact::)
+
+
+
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/contract/orderByDate.mtt b/lang/master/tpl/contract/orderByDate.mtt
new file mode 100644
index 0000000..e7d5a01
--- /dev/null
+++ b/lang/master/tpl/contract/orderByDate.mtt
@@ -0,0 +1,75 @@
+::use 'design.mtt'::
+
+
+
+ ::__("Modify the order delivered on ::date::",{date:hDate(date)})::
+
+
+
+ ::raw _("You can modify here an existing order. If you would like to remove a product, just key-in the quantity 0. You may not be able to modify all elements as the order could be closed for some suppliers")::
+
+
+
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/contract/view.mtt b/lang/master/tpl/contract/view.mtt
new file mode 100644
index 0000000..0ad9a7b
--- /dev/null
+++ b/lang/master/tpl/contract/view.mtt
@@ -0,0 +1,137 @@
+::use 'design.mtt'::
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/contractadmin/distributionp.mtt b/lang/master/tpl/contractadmin/distributionp.mtt
new file mode 100644
index 0000000..e9a59d4
--- /dev/null
+++ b/lang/master/tpl/contractadmin/distributionp.mtt
@@ -0,0 +1,60 @@
+::use 'contractadmin/design.mtt'::
+
::_("Participate to deliveries")::
+
+
+ ::_("This page allows to see if members have participated to deliveries enough.")::
+
+
+
+ ::raw __("::distribs:: deliveries requiring ::distributorNum:: person(s)",{distribs:distribs,distributorNum:distributorNum})::,
+ ::users:: ::_("person(s) using this contract")::
+ = ::num:: ::_("participations to deliveries to do per family.")::
+
+ ::set date = hDate(distribution.date)::
+ ::raw __("Delivery of the ::date::",{date:date})::
+
+ ::end::
+
+
+ ::_("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")::
+
+ ::set total = 0::
+
+
+
::_("Weight/Vol.")::
+
::_("Quantities")::
+
::_("Product")::
+
::_("Reference")::
+
::_("Unit Price incl. VAT")::
+
::_("Total")::
+
+
+ ::foreach m orders::
+
+
+ ::raw m.weightOrVolume::
+
+
+ ::formatNum(m.quantity)::
+
+
+ ::m.pname::
+
+
+ $$nullSafe(::m.ref::)
+
+
+ ::formatNum(m.priceTTC):: ::currency()::
+
+
+ ::formatNum(m.totalTTC):: ::currency()::
+ ::set total = total + m.totalTTC::
+
+ ::_("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")::
+
+ ::set total = 0::
+
+
+
::_("Weight/Vol.")::
+
::_("Quantities")::
+
::_("Product")::
+
::_("Reference")::
+
::_("Unit price incl. VAT")::
+
::_("Total")::
+
+ ::foreach m orders::
+
+
+ ::raw m.weightOrVolume::
+
+
+ ::formatNum(m.quantity)::
+
+
+ ::m.pname::
+
+
+ $$nullSafe(::m.ref::)
+
+
+ ::formatNum(m.priceTTC):: ::currency()::
+
+
+ ::formatNum(m.totalTTC):: ::currency()::
+ ::set total = total + m.totalTTC::
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/cssDemo.mtt b/lang/master/tpl/cssDemo.mtt
new file mode 100644
index 0000000..42ffda0
--- /dev/null
+++ b/lang/master/tpl/cssDemo.mtt
@@ -0,0 +1,194 @@
+::use 'design.mtt'::
+
+
+
+
+
+
+
+ Hou là là !
+
+
+
+
+
+
+
+
+
+
+ Todo bem
+
+
+
+
+
+
+
+
+
+
Boutons large
+
+
+
+
+
+
+
+
+
+
+
+
Boutons default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Boutons small
+
+
+
+
+
+
+
+
+
+
+
Boutons XS
+
+
+
+
+
+
+
+
+
+
+
+
+
H1 Title
+
H2 Title
+
H3 Title
+
H4 Title
+
+
+
+
+
+
+
+
Panel title
+
+
+
+ Type : Gratuit jusqu'à 20 foyers ( actuellement 1 foyers dans votre groupe)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Visite guidée du coordinateur
7/21
+
+
Ici se trouve la liste des contrats.
+ Ils comportent une date de début, une date de fin, et représentent votre relation avec un producteur.
+
+
+ C'est ici que vous pourrez gérer :
+
+
+
+
la liste de produits de ce producteur
+
les commandes des adhérents pour ce producteur
+
planifier les distributions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Le Lorem Ipsum est simplement du faux texte employé dans la composition
+ 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é.
+
+
+
+
+
+
+
Permanences
+
+
N'oubliez pas de vous inscrire pour aider lors des livraisons
+
+
+
Liste 1
+
Liste 2
+
+
+
+
+
+ Attention, vous n'avez défini aucun mot de passe pour votre compte !
+
+ ::contract._amap.name:: -
+ ::__("Distribution of the ::date::, from ::from:: to ::to::",{date:dDate(distrib.date),from:hHour(distrib.date),to:hHour(distrib.end)})::
+
+ ::set c = contract._contact::
+ ::set v = contract._vendor::
+ ::_("Contact in charge of the contract") : ::c.firstName:: ::c.lastName::, ::c.email::, ::c.phone::
+
+ ::contract._amap.name:: -
+ ::__("Distribution of the ::date::, from ::from:: to ::to::",{date:dDate(distrib.date),from:hHour(distrib.date),to:hHour(distrib.end)})::
+
+ ::set c = contract._contact::
+ ::set v = contract._vendor::
+ ::_("Contact in charge of the contract") : ::c.firstName:: ::c.lastName::, ::c.email::, ::c.phone::
+
::_("Planning of deliveries:"):: ::contract.name::
+
+
+ ::raw _("Subscribe now by selecting checkboxes corresponding to dates which are convenient for you. The goal of attendances consists in helping during the delivery of products.")::
+
+
+
+
+
+
+ ::foreach d distribs::
+ ::set date = getDate(d.date)::
+
+
+::end::
diff --git a/lang/master/tpl/empty.mtt b/lang/master/tpl/empty.mtt
new file mode 100644
index 0000000..5f28270
--- /dev/null
+++ b/lang/master/tpl/empty.mtt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lang/master/tpl/error.mtt b/lang/master/tpl/error.mtt
new file mode 100644
index 0000000..5bf07ae
--- /dev/null
+++ b/lang/master/tpl/error.mtt
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
::_("Ouch !")::
+
+ ::_("An error occurred.")::
+
+
+ ::_("If the website is unavailable, please try again in few minutes.")::
+
+ ::_("If this happen repeatedly, please let us know so we can solve the issue.")::
+
+
+
::_("Technical description of the error")::
+
+
::message::
+
+ ::if stack::
+
Stack
+
::stack::
+ ::end::
+
+
+
+
\ No newline at end of file
diff --git a/lang/master/tpl/form.mtt b/lang/master/tpl/form.mtt
new file mode 100644
index 0000000..8f265a8
--- /dev/null
+++ b/lang/master/tpl/form.mtt
@@ -0,0 +1,14 @@
+::use 'design.mtt'::
+
\ No newline at end of file
diff --git a/lang/master/tpl/group/view.mtt b/lang/master/tpl/group/view.mtt
new file mode 100644
index 0000000..1edf17f
--- /dev/null
+++ b/lang/master/tpl/group/view.mtt
@@ -0,0 +1,195 @@
+::use 'base.mtt'::
+
+
+
+ ::raw _("Congratulations, you just created a new group ! As an example, we created a fake farmer and fake products. It will show you how things work together in Cagette.net. Of course, you'll be able to delete them.")::
+
+
+
+ ::raw _("As a starter, we recommand you to follow the guided tour to discover step by step the main sections of the software.")::
+
+
+ ::t.name::
+
+ ::if x==2 && t.pending==true::
+ ::if(t.getPaymentType()=="check")::
+ ::_("Check to be given at the collection of products")::
+ ::elseif(t.getPaymentType()=="cash")::
+ ::_("Cash to be given at the collection of products")::
+ ::elseif(t.getPaymentType()=="transfer")::
+ ::_("This transfer must be validated by a coordinator")::
+ ::else::
+ ::t.name::
+ ::hDate(t.date)::
+ ::end::
+
+ ::else::
+ ::hDate(t.date)::
+ ::end::
+
\ No newline at end of file
diff --git a/lang/master/tpl/mail/forgottenPassword.mtt b/lang/master/tpl/mail/forgottenPassword.mtt
new file mode 100644
index 0000000..08dacd9
--- /dev/null
+++ b/lang/master/tpl/mail/forgottenPassword.mtt
@@ -0,0 +1,16 @@
+:: use 'mail/design.mtt'::
+
+
+ ::_("Be careful, this link is valid only 24 hours for security reasons.")::
+
+
+::end::
+
+
diff --git a/lang/master/tpl/mail/invitation.mtt b/lang/master/tpl/mail/invitation.mtt
new file mode 100644
index 0000000..d6b4f32
--- /dev/null
+++ b/lang/master/tpl/mail/invitation.mtt
@@ -0,0 +1,45 @@
+:: use 'mail/design.mtt'::
+
+
+
+
+
+
+ ::_("Hello"):: $$nullSafe(::name::),
+
+
+
+ ::if groupName!=null::
+
+ ::raw __("As a member of ::groupName::",{groupName:groupName})::
+
+ ::_("you have been pre-registered on Cagette.net")::
+
+ ::else::
+
+ ::_("you have been pre-registered on Cagette.net")::
+
+ ::end::
+
+
+
+ ::_("This software makes the organisation of the group easy by informing you about the next delivery dates, orders and current contracts")::
+
+
+ ::_("In order to finalize the subscription, please choose your password")::
+
+
+ ::_("You will then be able to connect anytime by using your e-mail")::
+ ( ::email:: ::if email2!=null:: ou ::email2:: ::end:: )
+ ::_("and your password on the website"):: ::HOST::
+
+
+ » ::_("Finalize your subscription")::
+
+
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/mail/message.mtt b/lang/master/tpl/mail/message.mtt
new file mode 100644
index 0000000..3a80a5c
--- /dev/null
+++ b/lang/master/tpl/mail/message.mtt
@@ -0,0 +1,8 @@
+:: use 'mail/design.mtt'::
+
+
+::raw text::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/mail/newPasswordConfirmed.mtt b/lang/master/tpl/mail/newPasswordConfirmed.mtt
new file mode 100644
index 0000000..a2f0520
--- /dev/null
+++ b/lang/master/tpl/mail/newPasswordConfirmed.mtt
@@ -0,0 +1,17 @@
+:: use 'mail/design.mtt'::
+
+
+ ::_("Hello"):: ::user.firstName::,
+
+
+ ::__("A new password has been defined for ::site::",{site:NAME})::
+
+ ::raw __("The new password is ::pass::, don't forget it !",{pass:password})::
+
+
+ ::raw __("You can use the following emails to log into your account : ::emails::",{emails:emails})::
+
+
+ ::formatNum(m.total):: ::currency()::
+ ::set total = total + m.total::
+
+
+
+ ::end::
+
+
+
+
Total :
+
::formatNum(total):: ::currency()::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/mail/ordersByProduct.mtt b/lang/master/tpl/mail/ordersByProduct.mtt
new file mode 100644
index 0000000..03904be
--- /dev/null
+++ b/lang/master/tpl/mail/ordersByProduct.mtt
@@ -0,0 +1,69 @@
+::use 'mail/design.mtt'::
+
+
Récapitulatif de commande par produits
+
+
+ Les commandes du contrat ::contract.name:: dont vous êtes coordinateur ont fermé le ::hDate(distribution.orderEndDate)::.
+
+
+
+
+
+ Date de livraison
+
+
+ ::dDate(distribution.date):: de ::hHour(distribution.date):: à ::hHour(distribution.end)::
+
+
+
+
Adresse
+
+ ::distribution._place.getFullAddress()::
+
+
+
+
+
+
+
+
+
+
+
Poids/Vol.
+
Quantités
+
Produit
+
Référence
+
Prix unitaire TTC
+
Total
+
+ ::set total = 0::
+ ::foreach m orders::
+
+
::raw m.weightOrVolume::
+
+ ::raw m.quantity::
+
+
+ ::m.pname::
+
+
+ $$nullSafe(::m.ref::)
+
+
+ ::formatNum(m.priceTTC):: ::currency()::
+
+
+ ::formatNum(m.totalTTC):: ::currency()::
+ ::set total = total + m.totalTTC::
+
+
+ ::end::
+
+
+
+
Total :
+
::formatNum(total):: ::currency()::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/maintain.mtt b/lang/master/tpl/maintain.mtt
new file mode 100644
index 0000000..af51940
--- /dev/null
+++ b/lang/master/tpl/maintain.mtt
@@ -0,0 +1,17 @@
+::use 'design.mtt'::
+
+
+
+ ::_("Maintenance")::
+
+ Le site est actuellement en maintenance.
+ Revenez dans quelques instants.
+
+
+ The website is currently under maintenance.
+ Please come back later.
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/member/default.mtt b/lang/master/tpl/member/default.mtt
new file mode 100644
index 0000000..8ed97b1
--- /dev/null
+++ b/lang/master/tpl/member/default.mtt
@@ -0,0 +1,164 @@
+::use 'design.mtt'::
+
+
+ ::if select!=null::
+
::_("Selection :||show a sublist of members which has been selected")::
+ ::if select=="nocontract"::
+ ::_("Members without any contracts")::
+ ::elseif select=="nomembership"::
+ ::_("Members whose membership should be renewed")::
+ ::elseif select=="newusers"::
+ ::_("Members who has never logged in")::
+ ::elseif select=="contract"::
+ ::_("Members with orders or contracts")::
+ ::end::
+
::_("Before validating this import, please check that this table est correctly filled. If some datas are shifted or are not linked to the right column, please check your CSV file.")::
+
Fill out the table, of modify your existing file to striclty match the columns order.
Once your file is completed, click on \"save as\" in your spreadsheet software, and select \"CSV\" format
Select the following options : separator : coma or dot-coma, encoding : Unicode UTF-8
Save the file
finally, click on \"select a file\" on this page and upload it.
")::
+
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/member/lastMessages.mtt b/lang/master/tpl/member/lastMessages.mtt
new file mode 100644
index 0000000..d465423
--- /dev/null
+++ b/lang/master/tpl/member/lastMessages.mtt
@@ -0,0 +1,30 @@
+
+
+
::_("Date")::
+
::_("Subject")::
+
::_("Status")::
+
+
+ ::foreach m emails::
+
+ ::if m.failure!=null::
+ ::set class = "danger"::
+ ::else::
+ ::set class = "success"::
+ ::end::
+
+
\ No newline at end of file
diff --git a/lang/master/tpl/member/payments.mtt b/lang/master/tpl/member/payments.mtt
new file mode 100644
index 0000000..4e57a59
--- /dev/null
+++ b/lang/master/tpl/member/payments.mtt
@@ -0,0 +1,121 @@
+::use 'design.mtt'::
+
+
+ ::if userAmap.hasValidMembership()::
+ $$bool(true) ::_("Membership up to date")::
+ ::else::
+ $$bool(false) ::_("Membership to renew")::
+ ::end::
+ ::_("for the period||full sentence : Membership up to date for the period 2017-2018"):: ::user._amap.getPeriodName(null)::
+
+ ::_("Manage membership fees")::
+
+
+ ::end::
+
+
+
+
+
+
::_("Current orders")::
+
+ ::set o = userContracts::
+ ::if o.constOrders.length>0::
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/product/categorize.mtt b/lang/master/tpl/product/categorize.mtt
new file mode 100644
index 0000000..e0db06a
--- /dev/null
+++ b/lang/master/tpl/product/categorize.mtt
@@ -0,0 +1,57 @@
+::use 'contractadmin/design.mtt'::
+
+
::_("Categories")::
+
+
+ ::raw _("You can link your products to categories, in order to better organize them in the \"shop mode\". To add categories: select checkboxes of products and then select a category in the list at the bottom of the page. Click then on \"add\". To remove a category: click on it.")::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/product/compose.mtt b/lang/master/tpl/product/compose.mtt
new file mode 100644
index 0000000..82ab5ea
--- /dev/null
+++ b/lang/master/tpl/product/compose.mtt
@@ -0,0 +1,20 @@
+::use 'design.mtt'::
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/product/import.mtt b/lang/master/tpl/product/import.mtt
new file mode 100644
index 0000000..90e75cf
--- /dev/null
+++ b/lang/master/tpl/product/import.mtt
@@ -0,0 +1,111 @@
+::use 'design.mtt'::
+
+
+
+
::_("Import products")::
+
+ ::if step==1::
+
+
+
+ ::elseif step==2::
+
+
Produits à importer
+
+ ::if !csv.isEmpty()::
+
+
+
+
+ ::foreach h csv.getHeaders()::
+
::_(h)::
+ ::end::
+
+
+ ::foreach d csv.getDatas()::
+
+ ::foreach r d::
+
+ ::if r!=null:: ::r:: ::end::
+
+ ::end::
+
+ ::end::
+
+
+ ::_("Warning")::
+
+ ::raw _("Before validating the import, please check if the table above is correctly filled. If data are not in line or not corresponding to the right column, it means that your CSV file is not correct.")::
+
::raw _("Fill correctly this file, or modify your existing file to respect strictly the order of columns")::
+
::raw _("When your file is ready, click on\"Save as\" in your Excel sheet and choose the format CSV")::
+
::raw _("Choose the following options: separation characters: comma, encoding: Unicode UTF8")::
+
::raw _("Save your file")::
+
::raw _("Then click on the button above \"Choose a file\" and send your file")::
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/shop/choose.mtt b/lang/master/tpl/shop/choose.mtt
new file mode 100644
index 0000000..1950aba
--- /dev/null
+++ b/lang/master/tpl/shop/choose.mtt
@@ -0,0 +1,108 @@
+::use 'design.mtt'::
+
+
+
::_("Choose a delivery")::
+
+
+
+ ::if distribs.length==0::
+
+ ::_("There is currently no delivery planned.")::
+
\ No newline at end of file
diff --git a/lang/master/tpl/stats/default.mtt b/lang/master/tpl/stats/default.mtt
new file mode 100644
index 0000000..f087e3d
--- /dev/null
+++ b/lang/master/tpl/stats/default.mtt
@@ -0,0 +1,30 @@
+::use 'design.mtt'::
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/transaction/check.mtt b/lang/master/tpl/transaction/check.mtt
new file mode 100644
index 0000000..7084650
--- /dev/null
+++ b/lang/master/tpl/transaction/check.mtt
@@ -0,0 +1,42 @@
+::use 'design.mtt'::
+
+
+
+
+
+
+ ::_("Your order has been recorded !")::
+
+
+
+
+
+
+
+
+
+ ::_("You chose to pay by check at the product distribution"):: :
+
+
+
+
+ ::raw __("Prepare a check of ::amount:: ::currency:: payable to : ::checkOrder::",{checkOrder:user._amap.checkOrder,amount:formatNum(amount),currency:currency()})::
+
+
+ ::_("Mention this reference at the back of the check in order to facilitate its process:")::
+ ::code::
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/transaction/moneypot.mtt b/lang/master/tpl/transaction/moneypot.mtt
new file mode 100644
index 0000000..c240474
--- /dev/null
+++ b/lang/master/tpl/transaction/moneypot.mtt
@@ -0,0 +1,42 @@
+::use 'design.mtt'::
+
+
+
+
+
+
+ ::_("Your order has been recorded !")::
+
+
+
+
+
+
+
+
+
+ ::_("You chose to pay with your money pot."):: :
+
+
+
+ ::_("Please remember to clear your balance at the time agreed with the coordinator of your group.")::
+
+
+
+ ::__("Your current balance is : ::balance:: ::currency::",{balance: balance,currency: currency() })::
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/transaction/pay.mtt b/lang/master/tpl/transaction/pay.mtt
new file mode 100644
index 0000000..24ac164
--- /dev/null
+++ b/lang/master/tpl/transaction/pay.mtt
@@ -0,0 +1,60 @@
+::use 'design.mtt'::
+
+
+
+
::_("Order payment")::
+
+
+
+
+
+ ::_("Amount to pay"):: : ::formatNum(amount):: ::currency()::
+
+
+ ::_("Your basket has been recorded, please select a payment method to confirm it.")::
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/transaction/transfer.mtt b/lang/master/tpl/transaction/transfer.mtt
new file mode 100644
index 0000000..a12231d
--- /dev/null
+++ b/lang/master/tpl/transaction/transfer.mtt
@@ -0,0 +1,40 @@
+::use 'design.mtt'::
+
+
+
+
+
+ ::_("Your order has been recorded !")::
+
+
+
+
+
+
+
+
+ ::_("You chose to pay by transfer"):: :
+
+
+ ::_("Please make a transfer of")::
+ ::formatNum(amount):: ::currency()::
+
+ ::_("to the following bank account:")::
+ ::_("IBAN:"):: ::user._amap.IBAN::
+
+
+ ::_("Mention this reference in the label of the transfer in order to facilitate its process:")::
+ ::code::
+
+
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/user/definePassword.mtt b/lang/master/tpl/user/definePassword.mtt
new file mode 100644
index 0000000..d2682ab
--- /dev/null
+++ b/lang/master/tpl/user/definePassword.mtt
@@ -0,0 +1,39 @@
+:: use 'design.mtt'::
+
+
::_("Change of password")::
+
+::if error!=null::
+
+ ::error::
+
+::end::
+
+ ::if step==1 ::
+
+ ::_("If you have forgotten your password or if you would like to change it, please key-in below your e-mail address:")::
+
+ ::raw form::
+
+ ::elseif step==2::
+
+ ::_("An e-mail was sent to you to allow you changing your password.")::
+
+ ::elseif step==3::
+
+ ::_("Key-in your new password:")::
+
+ ::raw form::
+
+ ::else::
+
+ ::_("Congratulations, your password have been modified!")::
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/user/register.mtt b/lang/master/tpl/user/register.mtt
new file mode 100644
index 0000000..06e3099
--- /dev/null
+++ b/lang/master/tpl/user/register.mtt
@@ -0,0 +1,11 @@
+::use 'design.mtt'::
+
+
+
+
::_("Sign up!")::
+
+ ::raw form::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/validate/user.mtt b/lang/master/tpl/validate/user.mtt
new file mode 100644
index 0000000..e764b69
--- /dev/null
+++ b/lang/master/tpl/validate/user.mtt
@@ -0,0 +1,284 @@
+::use 'design.mtt'::
+
+
+
+
+
+
+ ::_("Validate the delivery of the"):: ::hDate(date)::
+
+ $$insert(::_("Key-in a refund")::,/validate/::date::/::place.id::/::member.id::/addRefund/)
+ $$insert(::_("Key-in a payment")::,/validate/::date::/::place.id::/::member.id::/addPayment/)
+
+ ::if(op.pending && op.type==2)::
+
+
+ ::_("This payment has been received")::
+
+ $$delete(Suppr.,/validate/::date::/::place.id::/::member.id::/deleteOp/::op.id::?token=::token::)
+ ::end::
+
+
+ ::end::
+
+
+
Solde :
+ ::if balance<0::
+ ::set color = "C00"::
+ ::else::
+ ::set color = "0C0"::
+ ::end::
+
+ ::balance:: ::currency()::
+
+
+
+
+
+
+ $$insert(::_("Key-in a refund")::,/validate/::date::/::place.id::/::member.id::/addRefund/)
+ $$insert(::_("Key-in a payment")::,/validate/::date::/::place.id::/::member.id::/addPayment/)
+
+ ::_("On this page you can manage the list of suppliers of your group.")::
+
+
+
+::end::
\ No newline at end of file
diff --git a/lang/master/tpl/vendor/view.mtt b/lang/master/tpl/vendor/view.mtt
new file mode 100644
index 0000000..7939dde
--- /dev/null
+++ b/lang/master/tpl/vendor/view.mtt
@@ -0,0 +1,22 @@
+::use 'design.mtt'::
+
";
+ i++;
+ }
+ view.langLinks = langLinks;
+ view.langText = langNames[langs.indexOf(app.session.lang)];
+
+ //change account lang
+ if (app.params.exists("lang") && app.user!=null){
+ app.user.lock();
+ app.user.lang = app.params.get("lang");
+ app.user.update();
+ }
+
+ var ua = db.UserAmap.get(app.user, app.user.amap);
+ if (ua == null) throw Error("/", t._("You are not a member of this group"));
+
+ var constOrders = null;
+ var varOrders = new Map>();
+
+ var a = App.current.user.amap;
+ var oneMonthAgo = DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30);
+
+ //constant orders
+ var contracts = db.Contract.manager.search($type == db.Contract.TYPE_CONSTORDERS && $amap == a && $endDate > oneMonthAgo, false);
+ constOrders = [];
+ for ( c in contracts){
+ var orders = app.user.getOrdersFromContracts([c]);
+ if (orders.length == 0) continue;
+ constOrders.push({contract:c, orders:service.OrderService.prepare(orders) });
+ }
+
+ //variable orders, grouped by date
+ var contracts = db.Contract.manager.search($type == db.Contract.TYPE_VARORDER && $amap == a && $endDate > oneMonthAgo, false);
+
+ for (c in contracts) {
+ var ds = c.getDistribs(false);
+ for (d in ds) {
+ //store orders in a stringmap like "2015-01-01" => [order1,order2,...]
+ var k = d.date.toString().substr(0, 10);
+ var orders = app.user.getOrdersFromDistrib(d);
+ if (orders.length > 0) {
+ if (!varOrders.exists(k)) {
+ varOrders.set(k, Lambda.array(orders));
+ }else {
+ var z = varOrders.get(k).concat(Lambda.array(orders));
+ varOrders.set(k, z);
+ }
+ }
+ }
+ }
+
+ //final structure
+ var varOrders2 = new Array<{date:Date,orders:Array}>();
+ for ( k in varOrders.keys()) {
+
+ var d = new Date(k.split("-")[0].parseInt(), k.split("-")[1].parseInt() - 1, k.split("-")[2].parseInt(), 0, 0, 0);
+ var orders = service.OrderService.prepare( Lambda.list(varOrders[k]) );
+
+ varOrders2.push({date:d,orders:orders});
+ }
+
+ //sort by date desc
+ varOrders2.sort(function(b, a) {
+ return Math.round(a.date.getTime()/1000)-Math.round(b.date.getTime()/1000);
+ });
+
+ view.varOrders = varOrders2;
+ view.constOrders = constOrders;
+
+
+ // tutorials
+ if (app.user.isAmapManager()) {
+
+
+ //actions
+ if (app.params.exists('startTuto') ) {
+
+ //start a tuto
+ app.user.lock();
+ var t = app.params.get('startTuto');
+ app.user.tutoState = {name:t,step:0};
+ app.user.update();
+ }
+
+
+ //tuto state
+ var tutos = new Array<{name:String,completion:Float,key:String}>();
+
+ for ( k in Tutorial.all().keys() ) {
+ var t = Tutorial.all().get(k);
+
+ var completion = null;
+ if (app.user.tutoState!=null && app.user.tutoState.name == k) completion = app.user.tutoState.step / t.steps.length;
+
+ tutos.push( { name:t.name, completion:completion , key:k } );
+ }
+
+ view.tutos = tutos;
+ }
+
+ //should be able to stop tuto in any case
+ if (app.params.exists('stopTuto')) {
+ //stopped tuto from a tuto window
+ app.user.lock();
+ app.user.tutoState = null;
+ app.user.update();
+ view.stopTuto = true;
+ }
+
+ checkToken();
+ view.userAmap = ua;
+ }
+
+ /**
+ * Edit a contract
+ */
+ @tpl("form.mtt")
+ function doEdit(c:db.Contract) {
+
+ view.category = 'contractadmin';
+ if (!app.user.isContractManager(c)) throw Error('/', t._("Forbidden action"));
+
+ view.title = t._("Edit contract ::contractName::",{contractName:c.name});
+
+ var group = c.amap;
+ var currentContact = c.contact;
+
+ var form = Form.fromSpod(c);
+ form.removeElement( form.getElement("amapId") );
+ form.removeElement(form.getElement("type"));
+ form.getElement("userId").required = true;
+
+ app.event(EditContract(c,form));
+
+ if (form.checkToken()) {
+ form.toSpod(c);
+ c.amap = group;
+
+ //checks & warnings
+ if (c.hasPercentageOnOrders() && c.percentageValue==null) throw Error("/contract/edit/"+c.id, t._("If you would like to add fees to the order, define a rate (%) and a label."));
+
+ if (c.hasStockManagement()) {
+ for (p in c.getProducts()) {
+ if (p.stock == null) {
+ app.session.addMessage(t._("Warning about management of stock. Please fill the field \"stock\" for all your products"), true);
+ break;
+ }
+ }
+ }
+
+ //no stock mgmt for constant orders
+ if (c.hasStockManagement() && c.type==db.Contract.TYPE_CONSTORDERS) {
+ c.flags.unset(ContractFlags.StockManagement);
+ app.session.addMessage(t._("Managing stock is not available for CSA contracts"), true);
+ }
+
+
+ c.update();
+
+ //update rights
+ if ( c.contact != null && (currentContact==null || c.contact.id!=currentContact.id) ) {
+ var ua = db.UserAmap.get(c.contact, app.user.amap, true);
+ ua.giveRight(ContractAdmin(c.id));
+ ua.giveRight(Messages);
+ ua.giveRight(Membership);
+ ua.update();
+
+ //remove rights to old contact
+ if (currentContact != null) {
+ var x = db.UserAmap.get(currentContact, c.amap, true);
+ if (x != null) {
+ x.removeRight(ContractAdmin(c.id));
+ x.update();
+ }
+ }
+
+ }
+
+ throw Ok("/contractAdmin/view/"+c.id, t._("Contract updated"));
+ }
+
+ view.form = form;
+ }
+
+ @tpl("contract/insertChoose.mtt")
+ function doInsertChoose() {
+ //checkToken();
+
+ }
+
+ /**
+ * Créé un nouveau contrat
+ */
+ @tpl("form.mtt")
+ function doInsert(?type:Int) {
+ if (!app.user.canManageAllContracts()) throw Error('/', t._("Forbidden action"));
+ if (type == null) throw Redirect('/contract/insertChoose');
+
+ view.title = if (type == db.Contract.TYPE_CONSTORDERS)t._("Create a contract with fixed orders") else t._("Create a contract with variable orders");
+
+ var c = new db.Contract();
+
+ var form = Form.fromSpod(c);
+ form.removeElement( form.getElement("amapId") );
+ form.removeElement(form.getElement("type"));
+ form.getElement("userId").required = true;
+
+ if (form.checkToken()) {
+ form.toSpod(c);
+ c.amap = app.user.amap;
+ //trace(app.user.amap);
+ //trace(c.amap);
+ c.type = type;
+ c.insert();
+
+ //right
+ if (c.contact != null) {
+ var ua = db.UserAmap.get(c.contact, app.user.amap, true);
+ ua.giveRight(ContractAdmin(c.id));
+ ua.giveRight(Messages);
+ ua.giveRight(Membership);
+ ua.update();
+ }
+
+ throw Ok("/contractAdmin/view/"+c.id, t._("New contract created"));
+ }
+
+ view.form = form;
+ }
+
+ /**
+ * Delete a contract (... and its products, orders & distributions)
+ */
+ function doDelete(c:db.Contract) {
+
+ if (!app.user.canManageAllContracts()) throw Error("/contractAdmin", t._("You don't have the authorization to remove a contract"));
+
+ if (checkToken()) {
+ c.lock();
+
+ //check if there is orders in this contract
+ var products = c.getProducts();
+
+ var orders = db.UserContract.manager.search($productId in Lambda.map(products, function(p) return p.id));
+ var qt = 0.0;
+ for ( o in orders) qt += o.quantity; //there could be "zero c qt" orders
+ if (qt > 0) {
+ throw Error("/contractAdmin", t._("You cannot delete this contract because some orders are linked to it."));
+ }
+
+ //remove admin rights and delete contract
+ if(c.contact!=null){
+ var ua = db.UserAmap.get(c.contact, c.amap, true);
+ if (ua != null) {
+ ua.removeRight(ContractAdmin(c.id));
+ ua.update();
+ }
+ }
+
+ app.event(DeleteContract(c));
+
+ c.delete();
+ throw Ok("/contractAdmin", t._("Contract deleted"));
+ }
+
+ throw Error("/contractAdmin", t._("Token error"));
+ }
+
+ /**
+ * Make an order by contract ( standard mode )
+ * The form is prepopulated if orders have already been made.
+ *
+ * It should work for constant orders ( will display one column )
+ * or varying orders ( with as many columns as distributions dates )
+ *
+ */
+ @tpl("contract/order.mtt")
+ function doOrder(c:db.Contract ) {
+
+ //checks
+ if (app.user.amap.hasPayments()) throw Redirect("/contract/orderAndPay/" + c.id);
+ if (app.user.amap.hasShopMode()) throw Redirect("/shop");
+ if (!c.isUserOrderAvailable()) throw Error("/", t._("This contract is not opened for orders"));
+
+
+ var distributions = [];
+ // If its a varying contract, we display a column by distribution
+ if (c.type == db.Contract.TYPE_VARORDER) {
+ distributions = db.Distribution.getOpenToOrdersDeliveries(c);
+ }else{
+ distributions = [null];
+ }
+
+ //list of distribs with a list of product and optionnaly an order
+ var userOrders = new Array< {distrib:db.Distribution,datas:Array<{order:db.UserContract,product:db.Product}>} >();
+ var products = c.getProducts();
+
+ if ( c.type == db.Contract.TYPE_VARORDER ){
+
+ for ( d in distributions){
+ var datas = [];
+ for ( p in products) {
+ var ua = { order:null, product:p };
+
+ var order = db.UserContract.manager.select($user == app.user && $productId == p.id && $distributionId==d.id, true);
+
+ if (order != null) ua.order = order;
+ datas.push(ua);
+ }
+
+ userOrders.push({distrib:d,datas:datas});
+ }
+
+ }else{
+
+ var datas = [];
+ for ( p in products) {
+ var ua = { order:null, product:p };
+
+ var order = db.UserContract.manager.select($user == app.user && $productId == p.id, true);
+
+ if (order != null) ua.order = order;
+ datas.push(ua);
+ }
+
+ userOrders.push({distrib:null,datas:datas});
+
+ }
+
+
+ //form check
+ if (checkToken()) {
+
+ //get dsitrib if needed
+ //var distrib : db.Distribution = null;
+ //if (c.type == db.Contract.TYPE_VARORDER) {
+ //distrib = db.Distribution.manager.get(Std.parseInt(app.params.get("distribution")), false);
+ //}
+
+ for (k in app.params.keys()) {
+
+ if (k.substr(0, 1) != "d") continue;
+ var qt = app.params.get(k);
+ if (qt == "") continue;
+
+ var pid = null;
+ var did = null;
+ try{
+ pid = Std.parseInt(k.split("-")[1].substr(1));
+ did = Std.parseInt(k.split("-")[0].substr(1));
+ }catch (e:Dynamic){trace("unable to parse key "+k); }
+
+ //find related element in userOrders
+ var uo = null;
+ for ( x in userOrders){
+ if (x.distrib!=null && x.distrib.id != did) {
+ continue;
+ }else{
+ for (a in x.datas){
+ if (a.product.id == pid){
+ uo = a;
+ break;
+ }
+ }
+ }
+ }
+
+ if (uo == null) throw t._("Could not find the product ::produ:: and delivery ::deliv::", {produ:pid, deliv:did});
+
+ var q = 0.0;
+
+ if (uo.product.hasFloatQt ) {
+ var param = StringTools.replace(qt, ",", ".");
+ q = Std.parseFloat(param);
+ }else {
+ q = Std.parseInt(qt);
+ }
+
+
+ if (uo.order != null) {
+ OrderService.edit(uo.order, q);
+ }else {
+ OrderService.make(app.user, q, uo.product, did);
+ }
+
+ }
+ throw Ok("/contract/order/"+c.id, t._("Your order has been updated"));
+ }
+
+ view.c = view.contract = c;
+ view.userOrders = userOrders;
+ }
+
+
+ /**
+ * Make an order by contract ( standard mode ) + payment process
+ */
+ @tpl("contract/orderAndPay.mtt")
+ function doOrderAndPay(c:db.Contract ) {
+
+ //checks
+ if (!app.user.amap.hasPayments()) throw Redirect("/contract/order/" + c.id);
+ if (app.user.amap.hasShopMode()) throw Redirect("/");
+ if (!c.isUserOrderAvailable()) throw Error("/", t._("This contract is not opened for orders"));
+
+ var distributions = [];
+ /* If its a varying contract, we display a column by distribution*/
+ if (c.type == db.Contract.TYPE_VARORDER) {
+ distributions = db.Distribution.getOpenToOrdersDeliveries(c);
+ }
+
+ //list of distribs with a list of product and optionnaly an order
+ var userOrders = new Array< {distrib:db.Distribution,datas:Array<{order:db.UserContract,product:db.Product}>} >();
+ var products = c.getProducts();
+
+ for ( d in distributions){
+ var datas = [];
+ for ( p in products) {
+ var ua = { order:null, product:p };
+
+ var order : db.UserContract = null;
+ if (c.type == db.Contract.TYPE_VARORDER) {
+ order = db.UserContract.manager.select($user == app.user && $productId == p.id && $distributionId==d.id, true);
+ }else {
+ order = db.UserContract.manager.select($user == app.user && $productId == p.id, true);
+ }
+
+ if (order != null) ua.order = order;
+ datas.push(ua);
+ }
+
+ userOrders.push({distrib:d,datas:datas});
+ }
+
+
+ //form check
+ if (checkToken()) {
+
+ //get distrib if needed
+ var distrib = null;
+ if (c.type == db.Contract.TYPE_VARORDER) {
+ distrib = db.Distribution.manager.get(Std.parseInt(app.params.get("distribution")), false);
+ }
+
+ var orders : OrderInSession = {products:[],userId:app.user.id,total:0};
+
+ for (k in app.params.keys()) {
+
+ if (k.substr(0, 1) != "d") continue;
+ var qt = app.params.get(k);
+ if (qt == "") continue;
+
+ var pid = null;
+ var did = null;
+ try{
+ pid = Std.parseInt(k.split("-")[1].substr(1));
+ did = Std.parseInt(k.split("-")[0].substr(1));
+ }catch (e:Dynamic){
+ trace("unable to parse key "+k);
+ }
+
+ //find related element in userOrders
+ var uo = null;
+ for ( x in userOrders){
+ if (x.distrib!=null && x.distrib.id != did) {
+ continue;
+ }else{
+ for (a in x.datas){
+ if (a.product.id == pid){
+ uo = a;
+ break;
+ }
+ }
+ }
+ }
+
+ if (uo == null) throw t._("Could not find the product ::produ:: and delivery ::deliv::", {produ:pid, deliv:did});
+
+ //quantity
+ var q = 0.0;
+ if (uo.product.hasFloatQt ) {
+ var param = StringTools.replace(qt, ",", ".");
+ q = Std.parseFloat(param);
+ }else {
+ q = Std.parseInt(qt);
+ }
+
+ orders.products.push({productId:pid, quantity:q, distributionId:did});
+
+ var p = db.Product.manager.get(pid, false);
+ orders.total += p.getPrice() * q;
+
+ }
+
+ App.current.session.data.order = orders;
+
+ //Go to payments page
+ if (c.type == db.Contract.TYPE_CONSTORDERS) {
+ throw Ok("/contract/order/"+c.id, t._("Your CSA order has been saved"));
+ }else{
+ throw Ok("/transaction/pay/", t._("In order to save your order, please choose a means of payment."));
+ }
+
+
+
+ }
+
+ view.c = view.contract = c;
+ view.userOrders = userOrders;
+ }
+
+
+ /**
+ * A user edit an order for a multidistrib.
+ */
+ @tpl("contract/orderByDate.mtt")
+ function doEditOrderByDate(date:Date) {
+
+ if (app.user.amap.hasPayments()) {
+ //when payments are active, the user cannot modify his order
+ throw Redirect("/");
+ }
+
+ // cannot edit order if date is in the past
+ if (Date.now().getTime() > date.getTime()) {
+
+ var msg = t._("This delivery has already taken place, you can no longer modify the order.");
+ if (app.user.isContractManager()) msg += t._(" As the manager of the contract you can modify the order from this page: Management of contracts");
+
+ throw Error("/contract", msg);
+ }
+
+ // Il faut regarder le contrat de chaque produit et verifier si le contrat est toujours ouvert à la commande.
+ var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+ var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
+
+ var cids = Lambda.map(app.user.amap.getActiveContracts(true), function(c) return c.id);
+ var distribs = db.Distribution.manager.search(($contractId in cids) && $date >= d1 && $date <=d2 , false);
+ var orders = db.UserContract.manager.search($userId==app.user.id && $distributionId in Lambda.map(distribs,function(d)return d.id) );
+ view.orders = service.OrderService.prepare(orders);
+ view.date = date;
+
+ //form check
+ if (checkToken()) {
+
+ var orders_out = [];
+
+ for (k in app.params.keys()) {
+ var param = app.params.get(k);
+ if (k.substr(0, "product".length) == "product") {
+
+ //trouve le produit dans userOrders
+ var pid = Std.parseInt(k.substr("product".length));
+ var order = Lambda.find(orders, function(uo) return uo.product.id == pid);
+ if (order == null) throw t._("Error, could not find the order");
+
+ var q = 0.0;
+ if (order.product.hasFloatQt ) {
+ param = StringTools.replace(param, ",", ".");
+ q = Std.parseFloat(param);
+ }else {
+ q = Std.parseInt(param);
+ }
+
+ var quantity = Math.abs( q==null?0:q );
+
+ if ( order.distribution.canOrderNow() ) {
+ //met a jour la commande
+ var o = OrderService.edit(order, quantity);
+ if(o!=null) orders_out.push( o );
+ }
+ }
+ }
+
+ app.event(MakeOrder(orders_out));
+
+ throw Ok("/contract", t._("Your order has been updated"));
+ }
+ }
+}
diff --git a/src/controller/ContractAdmin.hx b/src/controller/ContractAdmin.hx
new file mode 100755
index 0000000..8b5e859
--- /dev/null
+++ b/src/controller/ContractAdmin.hx
@@ -0,0 +1,1027 @@
+package controller;
+import db.UserContract;
+import sugoi.form.elements.Checkbox;
+import sugoi.form.elements.Selectbox;
+import sugoi.form.Form;
+import sugoi.form.elements.StringInput;
+import Common;
+import datetime.DateTime;
+using tools.ObjectListTool;
+using tools.DateTool;
+import service.OrderService;
+
+class ContractAdmin extends Controller
+{
+
+ public function new()
+ {
+ super();
+ if (!app.user.isContractManager()) throw Error("/", t._("You don't have the authorization to manage contracts"));
+ view.nav = ["contractadmin"];
+
+ }
+
+ public function sendNav(c){
+ var navbar = new Array();
+ var e = Nav(navbar,"contractAdmin",c.id);
+ app.event(e);
+ view.navbar = e.getParameters()[0];
+ }
+
+ /**
+ * Contract admin main page
+ */
+ @tpl("contractadmin/default.mtt")
+ function doDefault(?args:{old:Bool}) {
+
+ view.nav.push("default");
+
+ var now = Date.now();
+
+ var contracts;
+ if (args != null && args.old) {
+ contracts = db.Contract.manager.search($amap == app.user.amap && $endDate < Date.now() ,{orderBy:-startDate},false);
+ }else {
+ contracts = db.Contract.getActiveContracts(app.user.amap, true, false);
+ }
+
+ //filter if current user is not manager
+ if (!app.user.isAmapManager()) {
+ for ( c in Lambda.array(contracts).copy()) {
+ if(!app.user.canManageContract(c)) contracts.remove(c);
+ }
+ }
+
+ view.contracts = contracts;
+ view.vendors = app.user.amap.getVendors();
+ view.places = app.user.amap.getPlaces();
+ checkToken();
+
+ //Multidistribs to validate
+ if(app.user.isAmapManager() && app.user.amap.hasPayments()){
+ var twoMonthAgo = tools.DateTool.deltaDays(now,-60);
+ var multidistribs = MultiDistrib.getFromTimeRange(app.user.amap,twoMonthAgo,now,db.Contract.TYPE_VARORDER);
+ /*for( md in multidistribs.copy()){
+ if(md.hasOnlyConstantOrders()) multidistribs.remove(md);
+ }*/
+ view.multidistribs = multidistribs;
+
+ }else{
+ view.multidistribs = [];
+ }
+ }
+
+ /**
+ * Manage products
+ */
+ @tpl("contractadmin/products.mtt")
+ function doProducts(contract:db.Contract,?args:{?enable:String,?disable:String}) {
+ view.nav.push("products");
+ sendNav(contract);
+
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("Access forbidden") );
+ view.c = contract;
+
+ //checks
+ if (app.user.amap.hasShopMode() && !app.user.amap.hasTaxonomy() ) {
+ for ( p in contract.getProducts(false)) {
+ if (p.getCategories().length == 0) {
+ app.session.addMessage(t._("Warning, at least one product does not have any category. Click here to add categories", {contractid:contract.id}), true);
+ break;
+ }
+ }
+ }
+
+ //batch enable / disable products
+ if (args != null){
+
+ if (args.disable != null){
+ var pids = Lambda.array(Lambda.map(args.disable.split("|"), function(x) return Std.parseInt(x)));
+ service.ProductService.batchDisableProducts(pids);
+ }
+
+ if (args.enable != null){
+ var pids = Lambda.array(Lambda.map(args.enable.split("|"), function(x) return Std.parseInt(x)));
+ service.ProductService.batchEnableProducts(pids);
+ }
+
+ }
+
+ //generate a token
+ checkToken();
+ }
+
+
+ /**
+ * - hidden page -
+ * copy products from a contract to an other
+ */
+ @admin @tpl("form.mtt")
+ function doCopyProducts(contract:db.Contract) {
+ view.title = t._("Copy products in: ")+contract.name;
+ var form = new Form("copy");
+ var contracts = app.user.amap.getActiveContracts();
+ var contracts = Lambda.map(contracts, function(c) return {key:Std.string(c.id),value:Std.string(c.name) } );
+ form.addElement(new sugoi.form.elements.Selectbox("source", t._("Copy products from: "),Lambda.array(contracts)));
+ form.addElement(new sugoi.form.elements.Checkbox("delete", t._("Delete existing products (all orders will be deleted!)")));
+ if (form.checkToken()) {
+
+ if (form.getValueOf("delete") == "1") {
+ for ( p in contract.getProducts()) {
+ p.lock();
+ p.delete();
+ }
+ }
+
+ var source = db.Contract.manager.get(Std.parseInt(form.getValueOf("source")), false);
+ var prods = source.getProducts();
+ for ( source_p in prods) {
+ var p = new db.Product();
+ p.name = source_p.name;
+ p.price = source_p.price;
+ //p.type = source_p.type;
+ p.contract = contract;
+ p.insert();
+ }
+
+ throw Ok("/contractAdmin/products/" + contract.id, t._("Products copied from ") + source.name);
+
+
+ }
+
+
+ view.form = form;
+ }
+
+ /**
+ * displays a calendar of the current month
+ * with all events ( contracts start and end, deliveries... )
+ */
+ @tpl('contractadmin/calendar.mtt')
+ public function doCalendar() {
+
+ var contracts = db.Contract.getActiveContracts(app.user.amap, true, false);
+
+ //Events of the month in a calendar
+ var cal = Calendar.getMonthViewMap();
+
+ for ( c in contracts) {
+ var start = c.startDate.toString().substr(0,10);
+ var end = c.endDate.toString().substr(0,10);
+ if (cal.exists( start )) {
+ var v = cal.get(start);
+ v.push( { name: t._("Contract start ") + c.name, color:Calendar.COLOR_CONTRACT } );
+ cal.set( start, v );
+ }
+ if (cal.exists( end )) {
+ var v = cal.get(end);
+ v.push( { name: t._("Contract end ") +c.name, color:Calendar.COLOR_CONTRACT } );
+ cal.set( end, v );
+ }
+
+ //deliveries
+ for ( d in c.getDistribs(false)) {
+ var start = d.date.toString().substr(0,10);
+
+ if (cal.exists( start )) {
+ var v = cal.get( start );
+ v.push( { name: t._("Distribution") +" "+d.contract.name, color:Calendar.COLOR_DELIVERY } );
+ cal.set( start, v );
+ }
+
+ if ( d.orderStartDate != null && d.orderStartDate != null ) {
+ var k = d.orderStartDate.toString().substr(0,10);
+ if (cal.exists( k )) {
+ var v = cal.get( k );
+ v.push( { name: t._("Opening of orders ") +d.contract.name, color:Calendar.COLOR_ORDER } );
+ cal.set( k , v );
+ }
+
+ var k = d.orderEndDate.toString().substr(0,10);
+ if (cal.exists( k )) {
+ var v = cal.get( k );
+ v.push( { name: t._("End of orders") +d.contract.name, color:Calendar.COLOR_ORDER } );
+ cal.set( k , v );
+ }
+
+ }
+
+ }
+
+ }
+
+ var n = Date.now();
+ view.now = new Date(n.getFullYear(),n.getMonth(),n.getDate(),0,0,0).getTime();
+ view.calendar = Calendar.mapToArray( cal );
+
+
+ }
+
+ /**
+ * global view on orders within a timeframe
+ */
+ @tpl('contractadmin/ordersByTimeFrame.mtt')
+ function doOrdersByTimeFrame(?from:Date, ?to:Date/*, ?place:db.Place*/){
+
+ if (from == null) {
+
+ var f = new sugoi.form.Form("listBydate", null, sugoi.form.Form.FormMethod.GET);
+
+ var now = DateTime.now();
+ var from = now.snap(Month(Down)).getDate();
+ var to = now.snap(Month(Up)).add(Day(-1)).getDate();
+
+
+ var el = new sugoi.form.elements.DatePicker("from", t._("Start date"), from,true);
+ el.format = 'LL';
+ f.addElement(el);
+
+ var el = new sugoi.form.elements.DatePicker("to", t._("End date"), to,true);
+ el.format = 'LL';
+ f.addElement(el);
+
+ //var places = Lambda.map(app.user.amap.getPlaces(), function(p) return {label:p.name,value:p.id} );
+ //f.addElement(new sugoi.form.elements.IntSelect("placeId", "Lieu", Lambda.array(places),app.user.amap.getMainPlace().id,true));
+
+ view.form = f;
+ view.title = t._("Global view of orders");
+ app.setTemplate("form.mtt");
+
+ if (f.checkToken()) {
+
+ var url = '/contractAdmin/ordersByTimeFrame/' + f.getValueOf("from").toString().substr(0, 10) +"/"+f.getValueOf("to").toString().substr(0, 10);
+ //var p = f.getValueOf("placeId");
+ //if (p != null) url += "/"+p;
+ throw Redirect( url );
+ }
+
+ return;
+
+ }else {
+
+ var d1 = tools.DateTool.setHourMinute(from,0,0);
+ var d2 = tools.DateTool.setHourMinute(to,23,59);
+ var contracts = app.user.amap.getActiveContracts(true);
+ var cconst = [];
+ var cvar = [];
+ for ( c in contracts) {
+ if (c.type == db.Contract.TYPE_CONSTORDERS) cconst.push(c.id);
+ if (c.type == db.Contract.TYPE_VARORDER) cvar.push(c.id);
+ }
+
+ //distribs
+ var vdistribs = db.Distribution.manager.search(($contractId in cvar) && $date >= d1 && $date <= d2 /*&& place.id==$placeId*/, false);
+ var cdistribs = db.Distribution.manager.search(($contractId in cconst) && $date >= d1 && $date <= d2 /*&& place.id==$placeId*/, false);
+
+ if (vdistribs.length == 0 && cdistribs.length == 0) throw Error("/contractAdmin/ordersByDate", t._("There is no delivery at this date"));
+
+ //varying orders
+ var varorders = db.UserContract.manager.search($distributionId in vdistribs.getIds() , { orderBy:userId } );
+
+ //constant orders
+ var constorders = [];
+ for ( d in cdistribs) {
+ var orders2 = db.UserContract.manager.search($productId in d.contract.getProducts().getIds(), { orderBy:userId } );
+ constorders = constorders.concat(Lambda.array(orders2));
+ }
+
+ //merge 2 lists
+ var orders = Lambda.array(varorders).concat(Lambda.array(constorders));
+ var orders = service.OrderService.prepare(Lambda.list(orders));
+
+ view.orders = orders;
+ view.from = from;
+ view.to = to;
+ view.ctotal = app.params.exists("ctotal");
+
+ }
+
+
+
+ }
+
+ /**
+ * Global view on orders in one day
+ *
+ * @param date
+ */
+ @tpl('contractadmin/ordersByDate.mtt')
+ function doOrdersByDate(?date:Date,?place:db.Place){
+ if (date == null) {
+
+ var f = new sugoi.form.Form("listBydate", null, sugoi.form.Form.FormMethod.GET);
+ var el = new sugoi.form.elements.DatePicker("date", t._("Delivery date"), true);
+ el.format = 'LL';
+ f.addElement(el);
+
+ var places = Lambda.map(app.user.amap.getPlaces(), function(p) return {label:p.name,value:p.id} );
+ f.addElement(new sugoi.form.elements.IntSelect("placeId", "Lieu", Lambda.array(places),app.user.amap.getMainPlace().id,true));
+
+ view.form = f;
+ view.title = t._("Global view of orders");
+ view.text = t._("This page allows you to have a global view on orders of all contracts");
+ view.text += t._(" Select a delivery date:");
+ app.setTemplate("form.mtt");
+
+ if (f.checkToken()) {
+
+ var url = '/contractAdmin/ordersByDate/' + f.getValueOf("date").toString().substr(0, 10);
+ var p = f.getValueOf("placeId");
+ if (p != null) url += "/"+p;
+ throw Redirect( url );
+ }
+
+ return;
+
+ }else {
+
+ var d1 = date.setHourMinute(0, 0);
+ var d2 = date.setHourMinute(23,59);
+ var contracts = app.user.amap.getActiveContracts(true);
+ var cconst = [];
+ var cvar = [];
+ for ( c in contracts) {
+ if (c.type == db.Contract.TYPE_CONSTORDERS) cconst.push(c.id);
+ if (c.type == db.Contract.TYPE_VARORDER) cvar.push(c.id);
+ }
+
+ //distribs
+ var vdistribs = db.Distribution.manager.search(($contractId in cvar) && $date >= d1 && $date <= d2 && place.id==$placeId, false);
+ var cdistribs = db.Distribution.manager.search(($contractId in cconst) && $date >= d1 && $date <= d2 && place.id==$placeId, false);
+
+ if (vdistribs.length == 0 && cdistribs.length == 0) throw Error("/contractAdmin/ordersByDate", t._("There is no delivery at this date"));
+
+ //varying orders
+ var varorders = db.UserContract.manager.search($distributionId in vdistribs.getIds() , { orderBy:userId } );
+
+ //constant orders
+ var constorders = [];
+ for ( d in cdistribs) {
+ var orders2 = db.UserContract.manager.search($productId in d.contract.getProducts().getIds(), { orderBy:userId } );
+ constorders = constorders.concat(Lambda.array(orders2));
+ }
+
+ //merge 2 lists
+ var orders = Lambda.array(varorders).concat(Lambda.array(constorders));
+ var orders = service.OrderService.prepare(Lambda.list(orders));
+
+ view.orders = orders;
+ view.date = date;
+ view.place = place;
+ view.ctotal = app.params.exists("ctotal");
+ }
+ }
+
+
+ /**
+ * Global view on orders, producer view
+ */
+ @tpl('contractadmin/vendorsByDate.mtt')
+ function doVendorsByDate(date:Date,place:db.Place){
+
+ var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+ var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
+ var contracts = app.user.amap.getActiveContracts(true);
+ var cids = Lambda.map(contracts, function(c) return c.id);
+
+ //distribs for both types in active contracts
+ var distribs = db.Distribution.manager.search(($contractId in cids) && $date >= d1 && $date <= d2 && $place==place , false);
+
+
+ if ( distribs.length == 0 ) throw Error("/contractAdmin/ordersByDate", t._("There is no delivery at this date"));
+
+ var out = new Map();//key : vendor id
+
+ for (d in distribs){
+ var vid = d.contract.vendor.id;
+ var o = out.get(vid);
+
+ if (o == null){
+ out.set( vid, {contract:d.contract,distrib:d,orders:service.ReportService.getOrdersByProduct( d )});
+ }else{
+
+ //add orders with existing ones
+ for ( x in service.ReportService.getOrdersByProduct( d ) ){
+
+ //find record in existing orders
+ var f : Dynamic = Lambda.find(o.orders, function(a) return a.pid == x.pid);
+ if (f == null){
+ //new product order
+ o.orders.push(x);
+ }else{
+ //increment existing
+ f.quantity += untyped x.quantity;
+ f.total += untyped x.total;
+ }
+ }
+ out.set(vid, o);
+ }
+ }
+
+ view.orders = Lambda.array(out);
+ view.date = date;
+ }
+
+
+ /**
+ * Global view on orders, producer view
+ */
+ @tpl('contractadmin/vendorsByTimeFrame.mtt')
+ function doVendorsByTimeFrame(from:Date,to:Date){
+
+ var d1 = tools.DateTool.setHourMinute(from,0,0);
+ var d2 = tools.DateTool.setHourMinute(to,23,59);
+ var contracts = app.user.amap.getActiveContracts(true);
+ var cids = contracts.getIds();
+
+ //distribs for both types in active contracts
+ var distribs = db.Distribution.manager.search(($contractId in cids) && $date >= d1 && $date <= d2 /*&& $place==place*/, false);
+ if ( distribs.length == 0 ) throw Error("/contractAdmin/", t._("There is no delivery during this period"));
+
+ var out = new Map}>();//key : vendor id
+
+ for (d in distribs){
+ var vid = d.contract.vendor.id;
+ var o = out.get(vid);
+
+ if (o == null){
+ out.set( vid, {contract:d.contract,distrib:d,orders:service.ReportService.getOrdersByProduct(d) });
+ }else{
+
+ //add orders with existing ones
+ for ( x in service.ReportService.getOrdersByProduct(d) ){
+
+ //find record in existing orders
+ var f : OrderByProduct = Lambda.find(o.orders, function(a:OrderByProduct) return a.pid == x.pid);
+ if (f == null){
+ //new product order
+ o.orders.push(x);
+ }else{
+ //increment existing
+ f.quantity += x.quantity;
+ f.totalHT += x.totalHT;
+ f.totalTTC += x.totalTTC;
+ }
+ }
+ out.set(vid, o);
+ }
+ }
+
+ view.orders = Lambda.array(out);
+
+ if ( app.params.exists("csv") ){
+ var totalHT = 0.0;
+ var totalTTC = 0.0;
+
+ var orders = [];
+ for ( x in out){
+ //empty line
+ orders.push({"quantity":null, "pname":null, "ref":null, "priceHT":null, "priceTTC":null, "totalHT":null, "totalTTC":null});
+ orders.push({"quantity":null, "pname":x.contract.vendor.name, "ref":null, "priceHT":null, "priceTTC":null, "totalHT":null, "totalTTC":null});
+
+ for (o in x.orders){
+ if(o.vat==null) o.vat = 0;
+ orders.push({
+ "quantity":view.formatNum(o.quantity),
+ "pname":o.pname,
+ "ref":o.ref,
+ "priceHT":view.formatNum(o.priceTTC / (1 + o.vat / 100) ),
+ "priceTTC":view.formatNum(o.priceTTC),
+ "totalHT":view.formatNum(o.totalHT),
+ "totalTTC":view.formatNum(o.totalTTC)
+ });
+ totalTTC += o.totalTTC;
+ totalHT += o.totalHT;
+ }
+
+ //total line
+ orders.push({
+ "quantity":null,
+ "pname":null,
+ "ref":null,
+ "priceHT":null,
+ "priceTTC":null,
+ "totalHT":view.formatNum(totalHT) + "",
+ "totalTTC":view.formatNum(totalTTC)+""
+ });
+ totalTTC = 0;
+ totalHT = 0;
+
+ }
+ var fileName = t._("Orders from the ::fromDate:: to the ::toDate:: per supplier.csv", {fromDate:from.toString().substr(0, 10), toDate:to.toString().substr(0, 10)});
+ sugoi.tools.Csv.printCsvDataFromObjects(orders, ["quantity", "pname", "ref", "priceHT", "priceTTC", "totalHT","totalTTC"], fileName);
+ return;
+ }
+
+ view.from = from;
+ view.to = to;
+ }
+
+
+ /**
+ * Overview of orders for this contract in backoffice
+ */
+ @tpl("contractadmin/orders.mtt")
+ function doOrders(contract:db.Contract, args:{?d:db.Distribution,?delete:db.UserContract}) {
+ view.nav.push("orders");
+ sendNav(contract);
+
+ //Checking permissions
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ if (contract.type == db.Contract.TYPE_VARORDER && args != null && args.d == null ) {
+ throw Redirect("/contractAdmin/selectDistrib/" + contract.id);
+ }
+
+ //Delete specified order with quantity of zero
+ if (checkToken() && args != null && args.delete != null ) {
+ try {
+ service.OrderService.delete(args.delete);
+ } catch(e:tink.core.Error){
+ throw Error("/contractAdmin/orders/" + contract.id, e.message);
+ }
+ if(args.d!=null){
+ throw Ok("/contractAdmin/orders/" + contract.id + "?d="+args.d.id, t._("The order has been deleted."));
+ }else{
+ throw Ok("/contractAdmin/orders/" + contract.id, t._("The order has been deleted."));
+ }
+
+ }
+
+ var d = null;
+ if (contract.type == db.Contract.TYPE_VARORDER ){
+ view.distribution = args.d;
+ d = args.d;
+ }
+ view.c = contract;
+ var orders = db.UserContract.getOrders(contract, d, app.params.exists("csv"));
+
+ if ( !app.params.exists("csv") ){
+
+ //show orders on disabled products
+ var disabledProducts = 0;
+ for ( o in orders ){
+ if ( !db.Product.manager.get(o.productId, false).active ) {
+ disabledProducts++;
+ Reflect.setField(o, "disabled", true);
+ }
+ }
+
+ view.disabledProducts = disabledProducts;
+ view.orders = orders;
+ }
+
+ }
+
+
+ /**
+ * hidden feature : updates orders by setting current product price.
+ */
+ function doUpdatePrices(contract:db.Contract, args:{?d:db.Distribution}) {
+
+ sendNav(contract);
+
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ if (contract.type == db.Contract.TYPE_VARORDER && args.d == null ) {
+ throw Redirect("/contractAdmin/selectDistrib/" + contract.id);
+ }
+ var d = null;
+ if (contract.type == db.Contract.TYPE_VARORDER ){
+ view.distribution = args.d;
+ d = args.d;
+ }
+
+ for ( o in contract.getOrders(d)){
+ o.lock();
+ o.productPrice = o.product.price;
+ if (contract.hasPercentageOnOrders()){
+ o.feesRate = contract.percentageValue;
+ }
+ o.update();
+
+ }
+ throw Ok("/contractAdmin/orders/"+contract.id+"?d="+args.d.id, t._("Prices are now up to date."));
+ }
+
+ /**
+ * Duplicate a contract
+ */
+ @tpl("form.mtt")
+ function doDuplicate(contract:db.Contract) {
+
+ sendNav(contract);
+ if (!app.user.isAmapManager()) throw Error("/", t._("You do not have the authorization to manage this contract"));
+
+ view.title = "Dupliquer le contrat '"+contract.name+"'";
+ var form = new Form("duplicate");
+
+ form.addElement(new StringInput("name", t._("Name of the new contract"), contract.name.substr(0,50) + " - copy"));
+ form.addElement(new Checkbox("copyProducts", t._("Copy products"),true));
+ form.addElement(new Checkbox("copyDeliveries", t._("Copy deliveries"),true));
+
+ if (form.checkToken()) {
+
+ var nc = new db.Contract();
+ nc.name = form.getValueOf("name");
+ nc.startDate = contract.startDate;
+ nc.endDate = contract.endDate;
+ nc.amap = contract.amap;
+ nc.contact = contract.contact;
+ nc.description = contract.description;
+ nc.distributorNum = contract.distributorNum;
+ nc.flags = contract.flags;
+ nc.type = contract.type;
+ nc.vendor = contract.vendor;
+ nc.percentageName = contract.percentageName;
+ nc.percentageValue = contract.percentageValue;
+ nc.insert();
+
+ //give right to this contract
+ if(contract.contact!=null){
+ var ua = db.UserAmap.get(contract.contact, contract.amap);
+ ua.giveRight(ContractAdmin(nc.id));
+ }
+
+
+ if (form.getValueOf("copyProducts") == true) {
+ var prods = contract.getProducts();
+ for ( source_p in prods) {
+ var p = new db.Product();
+ p.name = source_p.name;
+ p.price = source_p.price;
+ p.contract = nc;
+ p.image = source_p.image;
+ p.desc = source_p.desc;
+ p.ref = source_p.ref;
+ p.stock = source_p.stock;
+ p.vat = source_p.vat;
+ p.organic = source_p.organic;
+ p.txpProduct = source_p.txpProduct;
+ p.unitType = source_p.unitType;
+ p.multiWeight = source_p.multiWeight;
+ p.variablePrice = source_p.variablePrice;
+ p.insert();
+
+ for (source_cat in source_p.getCategories()){
+ var cat = new db.ProductCategory();
+ cat.product = p;
+ cat.category = source_cat;
+ cat.insert();
+ }
+ }
+ }
+
+ if (form.getValueOf("copyDeliveries") == true) {
+ for ( ds in contract.getDistribs()) {
+ var d = new db.Distribution();
+ d.contract = nc;
+ d.date = ds.date;
+ d.distributor1 = ds.distributor1;
+ d.distributor2 = ds.distributor2;
+ d.distributor3 = ds.distributor3;
+ d.distributor4 = ds.distributor4;
+ d.orderStartDate = ds.orderStartDate;
+ d.orderEndDate = ds.orderEndDate;
+ d.end = ds.end;
+ d.place = ds.place;
+ d.insert();
+ }
+ }
+
+ app.event(DuplicateContract(contract));
+
+ throw Ok("/contractAdmin/view/" + nc.id, t._("The contract has been duplicated"));
+ }
+
+ view.form = form;
+ }
+
+
+
+ /**
+ * Orders grouped by product
+ */
+ @tpl("contractadmin/ordersByProduct.mtt")
+ function doOrdersByProduct(contract:db.Contract, args:{?d:db.Distribution}) {
+
+ sendNav(contract);
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ if (contract.type == db.Contract.TYPE_VARORDER && args.d == null ) throw Redirect("/contractAdmin/selectDistrib/" + contract.id);
+
+ var d = args != null ? args.d : null;
+ if (d == null) d = contract.getDistribs(false).first();
+ if (d == null) throw Error("/contractAdmin/orders/"+contract.id,t._("There is no delivery in this contract, please create at least one distribution."));
+
+ var orders = service.ReportService.getOrdersByProduct(d,app.params.exists("csv"));
+ view.orders = orders;
+ view.distribution = d;
+ view.c = contract;
+
+ }
+
+ /**
+ * Purchase order to print
+ */
+ @tpl("contractadmin/ordersByProductList.mtt")
+ function doOrdersByProductList(contract:db.Contract, args:{?d:db.Distribution}) {
+
+ sendNav(contract);
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ if (contract.type == db.Contract.TYPE_VARORDER && args.d == null ) throw Redirect("/contractAdmin/selectDistrib/" + contract.id);
+
+ if (contract.type == db.Contract.TYPE_VARORDER ) view.distribution = args.d;
+ view.c = contract;
+ view.group = contract.amap;
+ var d = args != null ? args.d : null;
+ if (d == null) d = contract.getDistribs(false).first();
+ if (d == null) throw t._("No delivery in this contract");
+
+ var orders = service.ReportService.getOrdersByProduct(d,false);
+ view.orders = orders;
+ }
+
+ /**
+ * Lists deliveries for this contract
+ */
+ @tpl("contractadmin/deliveries.mtt")
+ function doDistributions(contract:db.Contract, ?args: { old:Bool } ) {
+ view.nav.push("distributions");
+ sendNav(contract);
+
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ view.c = contract;
+
+ if (args != null && args.old) {
+ //display also old deliveries
+ view.deliveries = contract.getDistribs(false);
+ }else {
+ view.deliveries = db.Distribution.manager.search($end > DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30) && $contract == contract, { orderBy:date} );
+ }
+
+ view.cycles = db.DistributionCycle.manager.search( $contract==contract && $endDate > Date.now() ,false);
+
+ }
+
+ /**
+ * Attendance to distribution
+ */
+ @tpl("contractadmin/distributionp.mtt")
+ function doDistributionp(contract:db.Contract) {
+ view.nav.push("distributions");
+ sendNav(contract);
+
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+
+ var distribs = contract.getDistribs(false);
+ var userCount = new Map();
+ var suscribers = contract.getUsers();
+ for( u in suscribers) userCount[u.id] = 0;
+ //populate
+
+ var increment = function(user:db.User){
+ if(user==null) return;
+ var x = userCount[user.id];
+ if(x==null){
+ userCount[user.id] = 1;
+ }else{
+ userCount[user.id] = x+1;
+ }
+ }
+
+ for ( d in distribs) {
+ increment(d.distributor1);
+ increment(d.distributor2);
+ increment(d.distributor3);
+ increment(d.distributor4);
+ }
+
+ var out1 = new Array<{user:db.User,count:Int}>();//suscribers
+ var out2 = new Array<{user:db.User,count:Int}>();//not suscribers
+ for( k in userCount.keys()){
+ if(Lambda.find(suscribers,function(s) return s.id==k)!=null){
+ out1.push( { user:db.User.manager.get(k), count:userCount[k] } );
+ }else{
+ out2.push( { user:db.User.manager.get(k), count:userCount[k] } );
+ }
+
+ }
+
+ var num = (distribs.length*contract.distributorNum) / suscribers.length;
+ view.num = Std.string(num).substr(0,4);
+ view.numRounded = Math.round(num);
+ view.users = suscribers.length;
+ view.distributorNum = contract.distributorNum;
+ view.distribs = distribs.length;
+ view.c = contract;
+ view.participations = out1;
+ view.extParticipations = out2;
+ }
+
+ @tpl("contractadmin/view.mtt")
+ function doView(contract:db.Contract) {
+ view.nav.push("view");
+ sendNav(contract);
+
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ view.c = view.contract = contract;
+ }
+
+
+ @tpl("contractadmin/stats.mtt")
+ function doStats(contract:db.Contract, ?args: { stat:Int } ) {
+ sendNav(contract);
+ if (!app.user.canManageContract(contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ view.c = contract;
+
+ if (args == null) args = { stat:0 };
+ view.stat = args.stat;
+ var pids = contract.getProducts().map(function(x) return x.id);
+ switch(args.stat) {
+ case 0 :
+ //ancienneté des amapiens
+
+ if(pids.length==0){
+ view.anciennete = new List();
+ }else{
+ view.anciennete = sys.db.Manager.cnx.request("select YEAR(u.cdate) as uyear ,count(DISTINCT u.id) as cnt from User u, UserContract up where up.userId=u.id and up.productId IN (" + pids.join(",") + ") group by uyear;").results();
+ }
+
+ case 1 :
+ //repartition des commandes
+ var repartition = new List();
+ var total = 0;
+ var totalPrice = 0;
+ if(pids.length!=0){
+ var repartition = sys.db.Manager.cnx.request("select sum(quantity) as quantity,productId,p.name,p.price from UserContract up, Product p where up.productId IN (" + contract.getProducts().map(function(x) return x.id).join(",") + ") and up.productId=p.id group by productId").results();
+ for ( r in repartition) {
+ total += r.quantity;
+ totalPrice += r.price*r.quantity;
+ }
+ for (r in repartition) {
+ Reflect.setField(r, "percent", Math.round((r.quantity/total)*100) );
+ }
+
+ if ( app.params.exists("csv") ){
+ sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(repartition), ["quantity","productId","name","price","percent"], "stats-" + contract.name+".csv");
+ }
+ }
+
+ view.repartition = repartition;
+ view.totalQuantity = total;
+ view.totalPrice = totalPrice;
+
+ }
+
+ }
+
+
+
+ /**
+ * Efface une commande
+ * @param uc
+ */
+ function doDelete(uc:UserContract) {
+ if (!app.user.canManageContract(uc.product.contract)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ uc.lock();
+ uc.delete();
+ throw Ok('/contractAdmin/orders/'+uc.product.contract.id, t._("The contract has been canceled"));
+ }
+
+
+ @tpl("contractadmin/selectDistrib.mtt")
+ function doSelectDistrib(c:db.Contract, ?args:{old:Bool}) {
+ view.nav.push("orders");
+ sendNav(c);
+
+ view.c = c;
+ if (args != null && args.old){
+ view.distributions = c.getDistribs(false);
+ }else{
+ view.distributions = c.getDistribs(true);
+ }
+
+ }
+
+ /**
+ * Edit a user's orders
+ */
+ @tpl("contractadmin/edit.mtt")
+ function doEdit(c:db.Contract, ?user:db.User, args:{?d:db.Distribution}) {
+ view.nav.push("orders");
+ sendNav(c);
+
+ if (!app.user.canManageContract(c)) throw Error("/", t._("You do not have the authorization to manage this contract"));
+ if (args.d != null && args.d.validated) throw Error("/contractAdmin/orders/" + c.id + "?d=" + args.d.id, t._("This delivery has been already validated"));
+
+ view.c = view.contract = c;
+ view.u = user;
+ view.distribution = args.d;
+
+ //need to select a distribution for varying orders contracts
+ if (c.type == db.Contract.TYPE_VARORDER && args.d == null ) {
+
+ throw Redirect("/contractAdmin/orders/" + c.id);
+
+ }else {
+
+ //members of the group
+ view.users = app.user.amap.getMembersFormElementData();
+
+ var userOrders = new Array<{order:db.UserContract,product:db.Product}>();
+ var products = c.getProducts(false);
+
+ for ( p in products) {
+ var ua = { order:null, product:p };
+
+ var order : db.UserContract = null;
+ if (c.type == db.Contract.TYPE_VARORDER) {
+ order = db.UserContract.manager.select($user == user && $productId == p.id && $distributionId==args.d.id, true);
+ }else {
+ order = db.UserContract.manager.select($user == user && $productId == p.id, true);
+ }
+
+ if (order != null) ua.order = order;
+ userOrders.push(ua);
+ }
+
+ //form check
+ if (checkToken()) {
+
+ //it's a new order, the user has been defined in the form.
+ if (user == null) {
+ user = db.User.manager.get(Std.parseInt(app.params.get("user")));
+ if (user == null){
+ var user = app.params.get("user");
+ throw t._("Unable to find user #::num::",{num:user});
+ }
+ if (!user.isMemberOf(app.user.amap)) throw user + " is not member of this group";
+ }
+
+ //get distrib if needed
+ var distrib : db.Distribution = null;
+ if (c.type == db.Contract.TYPE_VARORDER) {
+ distrib = db.Distribution.manager.get(Std.parseInt(app.params.get("distribution")), false);
+ }
+
+ var orders = [];
+
+ for (k in app.params.keys()) {
+ var param = app.params.get(k);
+ if (k.substr(0, "product".length) == "product") {
+
+ //trouve le produit dans userOrders
+ var pid = Std.parseInt(k.substr("product".length));
+ var uo = Lambda.find(userOrders, function(uo) return uo.product.id == pid);
+ if (uo == null) throw t._("Unable to find product ::pid::", {pid:pid});
+
+ //user2 ?
+ var user2 : db.User = null;
+ var invert = false;
+ if (app.params.get("user2" + pid) != null && app.params.get("user2" + pid) != "0") {
+ user2 = db.User.manager.get(Std.parseInt(app.params.get("user2"+pid)));
+ if (user2 == null) {
+ var user = app.params.get("user2");
+ throw t._("Unable to find user #::num::",{num:user});
+ }
+ if (!user2.isMemberOf(app.user.amap)) throw t._("::user:: is not part of this group",{user:user2});
+ if (user.id == user2.id) throw t._("Both selected accounts must be different ones");
+
+ invert = app.params.get("invert" + pid) == "1";
+ }
+
+ //filter quantity
+ var q = 0.0;
+ if (uo.product.hasFloatQt ) {
+ param = StringTools.replace(param, ",", ".");
+ q = Std.parseFloat(param);
+ }else {
+ q = Std.parseInt(param);
+ }
+ if(q==null) continue; //ignore bad qt input
+
+ //record order
+ if (uo.order != null) {
+ //existing record
+ var o = OrderService.edit(uo.order, q, (app.params.get("paid" + pid) == "1"), user2, invert);
+ if (o != null) orders.push(o);
+ }else {
+ //new record
+ var o = OrderService.make(user, q, uo.product, distrib == null ? null : distrib.id, (app.params.get("paid" + pid) == "1"), user2, invert);
+ if (o != null) orders.push(o);
+ }
+ }
+ }
+
+ app.event(MakeOrder(orders));
+ db.Operation.onOrderConfirm(orders);
+
+ if (distrib != null) {
+ throw Ok("/contractAdmin/orders/" + c.id +"?d="+distrib.id, t._("The order has been updated"));
+ }else {
+ throw Ok("/contractAdmin/orders/" + c.id, t._("The order has been updated"));
+ }
+ }
+ view.userOrders = userOrders;
+ }
+ }
+
+}
diff --git a/src/controller/Controller.hx b/src/controller/Controller.hx
new file mode 100755
index 0000000..b590ab2
--- /dev/null
+++ b/src/controller/Controller.hx
@@ -0,0 +1,28 @@
+package controller;
+
+/**
+ * Base Cagette.net Controller
+ * @author fbarbut
+ */
+class Controller extends sugoi.BaseController
+{
+
+ var t: sugoi.i18n.GetText;
+
+ public function new()
+ {
+ super();
+
+ //gettext translator
+ this.t = sugoi.i18n.Locale.texts;
+
+ }
+
+ public function checkIsLogged(){
+ if(app.user==null) {
+ throw new tink.core.Error(t._("You should be logged in to perform this action."));
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/controller/Cron.hx b/src/controller/Cron.hx
new file mode 100755
index 0000000..2eea8b0
--- /dev/null
+++ b/src/controller/Cron.hx
@@ -0,0 +1,477 @@
+package controller;
+import sugoi.db.Cache;
+import sugoi.Web;
+import sugoi.mail.Mail;
+import Common;
+using Lambda;
+using tools.DateTool;
+
+class Cron extends Controller
+{
+
+ public function doDefault()
+ {
+
+ }
+
+ /**
+ * CLI only en prod
+ */
+ function canRun() {
+ if (App.current.user != null && App.current.user.isAdmin()){
+ return true;
+ }else if (App.config.DEBUG) {
+ return true;
+ }else {
+
+ if (Web.isModNeko) {
+ Sys.print("only CLI.");
+ return false;
+ }else {
+ return true;
+ }
+ }
+ }
+
+ public function doMinute() {
+
+ print("Cron.doMinute is called");
+
+ if (!canRun()) return;
+
+ app.event(MinutelyCron);
+
+ sendEmailsfromBuffer();
+
+ }
+
+ /**
+ * Hourly Cron
+ *
+ * this function can be locally tested with `neko index.n cron/hour > cron.log`
+ */
+ public function doHour() {
+
+ app.event(HourlyCron);
+
+ distribNotif(4,db.User.UserFlags.HasEmailNotif4h); //4h before
+ distribNotif(24,db.User.UserFlags.HasEmailNotif24h); //24h before
+ distribNotif(0, db.User.UserFlags.HasEmailNotifOuverture); //on command open
+
+ distribValidationNotif();
+ //sendOrdersByProductWhenOrdersClose();
+ }
+
+
+ public function doDaily() {
+ if (!canRun()) return;
+
+ app.event(DailyCron);
+
+ //ERRORS MONITORING
+ var n = Date.now();
+ var yest24h = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0);
+ var yest0h = DateTools.delta(yest24h, -1000 * 60 * 60 * 24);
+
+ var errors = sugoi.db.Error.manager.search( $date < yest24h && $date > yest0h );
+ if (errors.length > 0) {
+ var report = new StringBuf();
+ report.add("
" + App.config.NAME + " : ERRORS
");
+ for (e in errors) {
+ report.add("
"+e.error + " at URL " + e.url + " ( user : " + (e.user!=null?e.user.toString():"none") + ", IP : " + e.ip + ")
");
+ }
+
+ var m = new Mail();
+ m.setSender(App.config.get("default_email"),"Cagette.net");
+ m.addRecipient(App.config.get("webmaster_email"));
+ m.setSubject(App.config.NAME+" Errors");
+ m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:report.toString() } ) );
+ App.sendMail(m);
+ }
+
+
+ //DEMO CONTRATS deletion after 7 days ( see controller.Group.doCreate() )
+ db.Contract.manager.delete($name == "Contrat AMAP Maraîcher Exemple" && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7));
+ db.Contract.manager.delete($name == "Contrat Poulet Exemple" && $startDate < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 7));
+
+
+ //Old Messages cleaning
+ db.Message.manager.delete($date < DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30 * 6));
+
+ //DB cleaning : I dont know how, but some people have empty string emails...
+ /*for ( u in db.User.manager.search($email == "", true)){
+ u.email = Std.random(9999) + "@cagette.net";
+ u.update();
+ }
+ for ( u in db.User.manager.search($email2 == "", true)){
+ u.email2 = null;
+ u.update();
+ }*/
+
+
+ }
+
+ /**
+ * Send email notifications to users before a distribution
+ * @param hour
+ * @param flag
+ */
+ function distribNotif(hour:Int, flag:db.User.UserFlags) {
+
+ //trouve les distrib qui commencent dans le nombre d'heures demandé
+ //on recherche celles qui commencent jusqu'à une heure avant pour ne pas en rater
+ var from = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * (hour-1));
+ var to = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * hour);
+
+ //if (App.config.DEBUG) from = DateTools.delta(from, 1000.0 * 60 * 60 * 24 * -30);
+
+ // dans le cas HasEmailNotifOuverture la date à prendre est le orderStartDate
+ // et non pas date qui est la date de la distribution
+ var distribs;
+ if ( db.User.UserFlags.HasEmailNotifOuverture == flag )
+ distribs = db.Distribution.manager.search( $orderStartDate >= from && $orderStartDate <= to , false);
+ else
+ distribs = db.Distribution.manager.search( $date >= from && $date <= to , false);
+
+ //Sys.print("distribNotif "+hour+" from "+from+" to "+to+" \n");
+
+ //on s'arrete immédiatement si aucune distibution trouvée
+ if (distribs.length == 0) return;
+
+ //cherche plus tard si on a pas une "grappe" de distrib
+ /*while (true) {
+ var extraDistribs ;
+ if ( db.User.UserFlags.HasEmailNotifOuverture != flag )
+ extraDistribs = db.Distribution.manager.search( $date >= to && $date = to && $orderStartDate 0) {
+ //on fait un tour de plus avec une heure plus tard
+ to = DateTools.delta(h, 1000.0 * 60 * 60);
+ }else {
+ //plus de distribs
+ break;
+ }
+ }*/
+
+ //on vérifie dans le cache du jour que ces distrib n'ont pas deja été traitées lors d'un cron précédent
+ var cacheId = Date.now().toString().substr(0, 10)+Std.string(flag);
+ var dist :Array = sugoi.db.Cache.get(cacheId);
+ if (dist != null) {
+ for (d in Lambda.array(distribs)) {
+ if (Lambda.exists(dist, function(x) return x == d.id)) {
+ // Comment this line in case of local test
+ distribs.remove(d);
+ }
+ }
+ }else {
+ dist = [];
+ }
+
+ //toutes les distribs trouvées ont deja été traitées
+ if (distribs.length == 0) return;
+
+ //stocke cache
+ for (d in distribs) dist.push(d.id);
+ Cache.set(cacheId, dist, 24 * 60 * 60);
+
+ //We have now the distribs we want to notify about.
+ var distribsByContractId = new Map();
+ for (d in distribs) {
+ if (d == null || d.contract==null) continue;
+ distribsByContractId.set(d.contract.id, d);
+ }
+
+ //Boucle sur les distributions pour gerer le cas de plusieurs distributions le même jour sur le même contrat
+ var orders = [];
+ for (d in distribs) {
+ if (d == null || d.contract==null) continue;
+ //get orders for both type of contracts
+ for ( x in d.contract.getOrders(d)) orders.push(x);
+ }
+
+ /*
+ * Group orders by users-group to receive separate emails by groups for the same user.
+ * Map key is $userId-$groupId
+ */
+ var users = new Map ,
+ vendors:Array
+ }>();
+
+ for (o in orders) {
+
+ var x = users.get(o.user.id+"-"+o.product.contract.amap.id);
+ if (x == null) x = {user:o.user,distrib:null,products:[],vendors:[]};
+ x.distrib = distribsByContractId.get(o.product.contract.id);
+ x.products.push(o);
+ users.set(o.user.id+"-"+o.product.contract.amap.id, x);
+ //trace (o.userId+"-"+o.product.contract.amap.id, x);Sys.print(" \n");
+
+ // Prévenir également le deuxième user en cas des commandes alternées
+ if (o.user2 != null) {
+ var x = users.get(o.user2.id+"-"+o.product.contract.amap.id);
+ if (x == null) x = {user:o.user2,distrib:null,products:[],vendors:[]};
+ x.distrib = distribsByContractId.get(o.product.contract.id);
+ x.products.push(o);
+ users.set(o.user2.id+"-"+o.product.contract.amap.id, x);
+ //trace (o.user2.id+"-"+o.product.contract.amap.id, x);Sys.print(" \n");
+ }
+ }
+
+ //remove zero qt orders
+ for( k in users.keys()){
+ var x = users.get(k);
+ var total = 0.0;
+ for( o in x.products) total += o.quantity;
+ if(total==0.0) users.remove(k);
+ }
+
+ // Dans le cas de l'ouverture de commande, ce sont tous les users qu'il faut intégrer
+ if ( db.User.UserFlags.HasEmailNotifOuverture == flag )
+ {
+ for (d in distribs) {
+ var memberList = d.contract.amap.getMembers();
+ for (u in memberList) {
+ var x = users.get(u.id+"-"+d.contract.amap.id);
+ if (x == null) x = {user:u,distrib:null,products:[],vendors:[]};
+ x.distrib = distribsByContractId.get(d.contract.id);
+ x.vendors.push(d.contract.vendor);
+ users.set(u.id+"-"+d.contract.amap.id, x);
+ //print(u.id+"-"+d.contract.amap.id, x);
+ }
+ }
+ }
+
+ for ( u in users) {
+
+ if (u.user.flags.has(flag) ) {
+
+ if (u.user.email != null) {
+ var group = u.distrib.contract.amap;
+ this.t = sugoi.i18n.Locale.init(u.user.lang); //switch to the user language
+
+ var text;
+ if ( db.User.UserFlags.HasEmailNotifOuverture == flag )
+ {
+ //order opening notif
+ text = t._("Opening of orders for the delivery of ::date::", {date:view.hDate(u.distrib.date)});
+ text += " ";
+ text += t._("The following suppliers are involved :");
+ text += "
";
+ for ( v in u.vendors) {
+ text += "
" + v + "
";
+ }
+ text += "
";
+
+ }else{
+ //Distribution notif to the users
+ var d = u.distrib;
+ text = t._("Do not forget the delivery on ::day:: from ::from:: to ::to:: ", {day:view.dDate(d.date),from:view.hHour(d.date),to:view.hHour(d.end)});
+ text += t._("Your products to collect :") + "
";
+ for ( p in u.products) {
+ text += "
"+p.quantity+" x "+p.product.getName();
+ // Gerer le cas des contrats en alternance
+ if (p.user2 != null) {
+ text += " " + t._("alternated with") + " ";
+ if (u.user == p.user)
+ text += p.user2.getCoupleName();
+ else
+ text += p.user.getCoupleName();
+ }
+ text += "
";
+ }
+ text += "
";
+ }
+
+ if (u.distrib.isDistributor(u.user)) {
+ text += t._("Warning: you are in charge of the delivery ! Do not forget to print the attendance sheet.");
+ }
+
+ try{
+ var m = new Mail();
+ m.setSender(App.config.get("default_email"), "Cagette.net");
+ if(group.contact!=null) m.setReplyTo(group.contact.email, group.name);
+ m.addRecipient(u.user.email, u.user.getName());
+ if (u.user.email2 != null) m.addRecipient(u.user.email2);
+ m.setSubject( group.name+" : "+t._("Distribution on ::date::",{date:app.view.hDate(u.distrib.date)}) );
+ m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:text,group:group } ) );
+ App.sendMail(m , u.distrib.contract.amap);
+ }catch (e:Dynamic){
+ app.logError(e); //email could be invalid
+ }
+
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Check if there is a multi-distrib to validate.
+ *
+ * Autovalidate it after 10 days
+ */
+ function distribValidationNotif(){
+
+ var now = Date.now();
+
+ var from = now.setHourMinute( now.getHours(), 0 );
+ var to = now.setHourMinute( now.getHours()+1 , 0);
+
+ var explain = t._("
This step is important in order to:
");
+ explain += t._("
Update orders if delivered quantities are different from ordered quantities
");
+ explain += t._("
Confirm the reception of payments (checks, cash, transfers) in order to mark orders as 'paid'
");
+
+ /*
+ * warn administrator if a distribution just ended
+ */
+ var ds = db.Distribution.manager.search( !$validated && ($end >= from) && ($end < to) , false);
+
+ for ( d in Lambda.array(ds)){
+ if ( d.contract.type != db.Contract.TYPE_VARORDER ){
+ ds.remove(d);
+ }else if ( !d.contract.amap.hasPayments() ){
+ ds.remove(d);
+ }
+ }
+
+ var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
+ var view = App.current.view;
+
+ for ( d in ds ){
+ // var subj = "["+d.contract.amap.name+"] " + t._("Validation of the ::date:: distribution",{date:view.hDate(d.date)});
+ var subj = t._("[::group::] Validation of the ::date:: distribution",{group : d.contract.amap.name , date : view.hDate(d.date)});
+
+ var url = "http://" + App.config.HOST + "/distribution/validate/"+d.date.toString().substr(0,10)+"/"+d.place.id;
+
+ var html = t._("
Your distribution just finished, don't forget to validate it
");
+ html += explain;
+ html += t._("
Click here to validate the distribution (You must be connected to your group Cagette)", {distriburl:url});
+
+ App.quickMail(d.contract.amap.contact.email, subj, html);
+ }
+
+ /*
+ * warn administrator if a distribution ended 3 days ago
+ */
+
+ var from = now.setHourMinute( now.getHours() , 0 ).deltaDays(-3);
+ var to = now.setHourMinute( now.getHours()+1 , 0).deltaDays(-3);
+
+ //warn administrator if a distribution just ended
+ var ds = db.Distribution.manager.search( !$validated && ($end >= from) && ($end < to) , false);
+
+ for ( d in Lambda.array(ds)){
+ if ( d.contract.type != db.Contract.TYPE_VARORDER ){
+ ds.remove(d);
+ }else if ( !d.contract.amap.hasPayments() ){
+ ds.remove(d);
+ }
+ }
+
+ var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
+
+ for ( d in ds ){
+ // var subj = d.contract.amap.name + t._(": Validation of the delivery of the ") + App.current.view.hDate(d.date);
+ var subj = t._("[::group::] Validation of the ::date:: distribution",{group : d.contract.amap.name , date : view.hDate(d.date)});
+
+ var url = "http://" + App.config.HOST + "/distribution/validate/"+d.date.toString().substr(0,10)+"/"+d.place.id;
+
+ var html = t._("
Reminder: you have a delivery to validate.
");
+ html += explain;
+ html += t._("
Click here to validate the delivery (You must be connected to your Cagette group)", {distriburl:url});
+
+ App.quickMail(d.contract.amap.contact.email, subj, html);
+ }
+
+
+ /*
+ * Autovalidate unvalidated distributions after 10 days
+ */
+ var from = now.setHourMinute( now.getHours() , 0 ).deltaDays( 0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT );
+ var to = now.setHourMinute( now.getHours() + 1 , 0).deltaDays( 0 - db.Distribution.DISTRIBUTION_VALIDATION_LIMIT );
+ print('AUTOVALIDATION');
+ print('Find distributions from $from to $to');
+ var ds = db.Distribution.manager.search( !$validated && ($end >= from) && ($end < to) , true);
+ for ( d in Lambda.array(ds)){
+ if ( d.contract.type != db.Contract.TYPE_VARORDER ){
+ ds.remove(d);
+ }else if ( !d.contract.amap.hasPayments() ){
+ ds.remove(d);
+ }
+ }
+ for ( d in ds){
+ print(d.toString());
+
+ service.PaymentService.validateDistribution(d);
+
+ }
+ //email
+ var ds = tools.ObjectListTool.deduplicateDistribsByKey(ds);
+ for ( d in ds ){
+ // var subj = d.contract.amap.name + t._(": Validation of the distribution of the ") + App.current.view.hDate(d.date);
+ var subj = t._("[::group::] Validation of the ::date:: distribution",{group : d.contract.amap.name , date : view.hDate(d.date)});
+ var html = t._("
As you did not validate it manually after 10 days, the delivery of the ::deliveryDate:: has been validated automatically
", {deliveryDate:App.current.view.hDate(d.date)});
+ App.quickMail(d.contract.amap.contact.email, subj, html);
+ }
+
+ }
+
+ /**
+ * Send emails from buffer.
+ *
+ * Warning, if the cron is executed each minute,
+ * you should consider the right amount of emails to send each minute in order to avoid overlaping and getting in concurrency problems.
+ * (like "SELECT * FROM BufferedMail WHERE sdate IS NULL ORDER BY cdate DESC LIMIT 100 FOR UPDATE Lock wait timeout exceeded; try restarting transaction")
+ */
+ function sendEmailsfromBuffer(){
+ print("
Send Emails from Buffer
");
+
+ //send
+ for( e in sugoi.db.BufferedMail.manager.search($sdate==null,{limit:50,orderBy:-cdate},false) ){
+ e.lock();
+ if(e.isSent()) continue;
+
+ print('#${e.id} - ${e.title}');
+ e.finallySend();
+ Sys.sleep(0.1);
+ }
+
+ //delete old emails
+ var threeMonthsAgo = DateTools.delta(Date.now(), -1000.0*60*60*24*30*3);
+ sugoi.db.BufferedMail.manager.delete($cdate < threeMonthsAgo);
+
+ //emails that cannot be sent
+ for( e in sugoi.db.BufferedMail.manager.search($tries>100,{limit:50,orderBy:-cdate},true) ){
+ if(e.sender.email != App.config.get("default_email")){
+ var str = t._("Sorry, the email entitled ::title:: could not be sent.",{title:e.title});
+ App.quickMail(e.sender.email,t._("Email not sent"),str);
+ }
+ e.delete();
+ }
+ }
+
+ /**
+ * Email product report when orders close
+ **/
+ function sendOrdersByProductWhenOrdersClose(){
+
+ var range = tools.DateTool.getLastHourRange();
+ // Sys.println("Time is "+Date.now()+" ");
+ // Sys.println('Find all distributions that have closed in the last hour from ${range.from} to ${range.to} \n ');
+
+ for ( d in db.Distribution.manager.search($orderEndDate >= range.from && $orderEndDate < range.to, false)){
+ service.OrderService.sendOrdersByProductReport(d);
+ }
+
+ }
+
+
+ function print(text){
+ Sys.println( text + " " );
+ }
+}
diff --git a/src/controller/Distribution.hx b/src/controller/Distribution.hx
new file mode 100755
index 0000000..9c52481
--- /dev/null
+++ b/src/controller/Distribution.hx
@@ -0,0 +1,577 @@
+package controller;
+import db.UserContract;
+import sugoi.form.Form;
+import sugoi.form.elements.HourDropDowns;
+using tools.DateTool;
+import Common;
+
+class Distribution extends Controller
+{
+
+
+ /**
+ * Attendance sheet by user-product (single distrib)
+ */
+ @tpl('distribution/list.mtt')
+ function doList(d:db.Distribution) {
+ view.distrib = d;
+ view.place = d.place;
+ view.contract = d.contract;
+ view.orders = service.OrderService.prepare(d.getOrders());
+
+ }
+
+ /**
+ * Attendance sheet by product-user (single distrib)
+ */
+ @tpl('distribution/listByProductUser.mtt')
+ function doListByProductUser(d:db.Distribution) {
+ view.distrib = d;
+ view.place = d.place;
+ view.contract = d.contract;
+ // view.orders = UserContract.prepare(d.getOrders());
+
+ //make a 2 dimensons table : data[userId][productId]
+ //WARNING : BUGS WILL APPEAR if there is many Order line for the same product
+ var data = new Map>();
+ var products = [];
+ var uo = d.getOrders();
+
+ for(o in uo){
+ products.push(o.product);
+ }
+
+ for ( o in service.OrderService.prepare(uo)) {
+
+ var user = data[o.userId];
+ if (user == null) user = new Map();
+ user[o.productId] = o;
+ data[o.userId] = user;
+
+ }
+
+ //products
+ var products = tools.ObjectListTool.deduplicate(products);
+ products.sort(function(b, a) {
+ return (a.name < b.name)?1:-1;
+ });
+ view.products = products;
+
+ //users
+ var users = Lambda.array(d.getUsers());
+ // var usersMap = tools.ObjectListTool.toIdMap(users);
+ users.sort(function(b, a) {
+ return (a.lastName < b.lastName)?1:-1;
+ });
+ view.users = users;
+ // view.usersMap = usersMap;
+
+ view.orders = data;
+
+ //total to pay by user
+ view.totalByUser = function(uid:Int){
+ var total = 0.0;
+ for( o in data[uid]) total+= o.total;
+ return total;
+ }
+
+ //total qty of product
+ view.totalByProduct = function(pid:Int){
+ var total = 0.0;
+ for( uid in data.keys()){
+ var x = data[uid][pid];
+ if(x!=null) total+= x.quantity;
+ }
+ return total;
+ }
+
+ }
+
+ /**
+ * Attendance sheet to print ( mutidistrib )
+ */
+ @tpl('distribution/listByDate.mtt')
+ function doListByDate(date:Date,place:db.Place, ?type:String, ?fontSize:String) {
+
+ if (!app.user.isContractManager()) throw Error('/', t._("Forbidden action"));
+
+ view.place = place;
+
+ if (type == null) {
+
+ //display form
+ var f = new sugoi.form.Form("listBydate", null, sugoi.form.Form.FormMethod.GET);
+ f.addElement(new sugoi.form.elements.RadioGroup("type", "Affichage", [
+ { value:"one", label:t._("One person per page") },
+ { value:"contract", label:t._("One person per page sorted by contract") },
+ { value:"all", label:t._("All") },
+ { value:"allshort", label:t._("All but without prices and totals") },
+ ],"all"));
+ f.addElement(new sugoi.form.elements.RadioGroup("fontSize", "Taille de police", [
+ { value:"S" , label:"S" },
+ { value:"M" , label:"M" },
+ { value:"L" , label:"L" },
+ { value:"XL", label:"XL" },
+ ], "S", "S", false));
+
+ view.form = f;
+ app.setTemplate("form.mtt");
+
+ if (f.checkToken()) {
+ var suburl = f.getValueOf("type")+"/"+f.getValueOf("fontSize");
+ var url = '/distribution/listByDate/' + date.toString().substr(0, 10)+"/"+place.id+"/"+suburl;
+ throw Redirect( url );
+ }
+
+ return;
+
+ }else {
+
+ view.date = date;
+ view.fontRatio = switch(fontSize){
+ case "M" : 125; //100x1.25
+ case "L" : 156; //125x1.25
+ case "XL": 195; //156x1.25
+ default : 100;
+ };
+
+ switch(type) {
+ case "one":
+ app.setTemplate("distribution/listByDateOnePage.mtt");
+ case "allshort" :
+ app.setTemplate("distribution/listByDateShort.mtt");
+ case "contract" :
+ app.setTemplate("distribution/listByDateOnePageContract.mtt");
+ }
+
+ var d1 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
+ var d2 = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
+ var contracts = app.user.amap.getActiveContracts(true);
+ //var cids = Lambda.map(contracts, function(c) return c.id);
+ var cconst = [];
+ var cvar = [];
+ for ( c in contracts) {
+ if (c.type == db.Contract.TYPE_CONSTORDERS) cconst.push(c.id);
+ if (c.type == db.Contract.TYPE_VARORDER) cvar.push(c.id);
+ }
+
+ //commandes variables
+ var distribs = db.Distribution.manager.search(($contractId in cvar) && $date >= d1 && $date <= d2 && $place==place, false);
+ var orders = db.UserContract.manager.search($distributionId in Lambda.map(distribs, function(d) return d.id) , { orderBy:userId } );
+
+ //commandes fixes
+ var distribs = db.Distribution.manager.search(($contractId in cconst) && $date >= d1 && $date <= d2 && $place==place, false);
+ var orders = Lambda.array(orders);
+ for ( d in distribs) {
+ var orders2 = db.UserContract.manager.search($productId in Lambda.map(d.contract.getProducts(), function(d) return d.id) , { orderBy:userId } );
+ orders = orders.concat(Lambda.array(orders2));
+ }
+
+ var orders3 = service.OrderService.prepare(Lambda.list(orders));
+ view.orders = orders3;
+
+ if (type == "csv") {
+ var data = new Array();
+
+ for (o in orders3) {
+ data.push( {
+ "name":o.userName,
+ "productName":o.productName,
+ "price":view.formatNum(o.productPrice),
+ "quantity":o.quantity,
+ "fees":view.formatNum(o.fees),
+ "total":view.formatNum(o.total),
+ "paid":o.paid
+ });
+ }
+
+ sugoi.tools.Csv.printCsvDataFromObjects(data, ["name", "productName", "price", "quantity","fees","total", "paid"],"Export-commandes-"+date.toString().substr(0,10)+"-Cagette");
+ return;
+ }
+ }
+ }
+
+ function doDelete(d:db.Distribution) {
+
+ if (!app.user.isContractManager(d.contract)) throw Error('/', t._("Forbidden action"));
+
+ var contractId = d.contract.id;
+ try {
+ service.DistributionService.delete(d);
+ }
+ catch(e:tink.core.Error){
+ throw Error("/contractAdmin/distributions/" + contractId, e.message);
+ }
+
+ throw Ok("/contractAdmin/distributions/" + contractId, t._("the delivery has been deleted"));
+
+ }
+
+ /**
+ * Edit a distribution
+ */
+ @tpl('form.mtt')
+ function doEdit(d:db.Distribution) {
+ if (!app.user.isContractManager(d.contract)) throw Error('/', t._('Forbidden action') );
+ var contract = d.contract;
+
+ var form = sugoi.form.Form.fromSpod(d);
+ form.removeElement(form.getElement("contractId"));
+ form.removeElement(form.getElement("end"));
+ form.removeElement(form.getElement("distributionCycleId"));
+ var x = new sugoi.form.elements.HourDropDowns("end", t._("End time") ,d.end,true);
+ form.addElement(x, 3);
+
+ if (d.contract.type == db.Contract.TYPE_VARORDER ) {
+ form.addElement(new sugoi.form.elements.DatePicker("orderStartDate", t._("Orders opening date"), d.orderStartDate));
+ form.addElement(new sugoi.form.elements.DatePicker("orderEndDate", t._("Orders closing date"), d.orderEndDate));
+ }
+
+ if (form.isValid()) {
+
+ var orderStartDate = null;
+ var orderEndDate = null;
+
+ try{
+
+ if (d.contract.type == db.Contract.TYPE_VARORDER ) {
+ orderStartDate = form.getValueOf("orderStartDate");
+ orderEndDate = form.getValueOf("orderEndDate");
+ }
+
+ d = service.DistributionService.edit(d,
+ form.getValueOf("date"),
+ form.getValueOf("end"),
+ form.getValueOf("placeId"),
+ form.getValueOf("distributor1Id"),
+ form.getValueOf("distributor2Id"),
+ form.getValueOf("distributor3Id"),
+ form.getValueOf("distributor4Id"),
+ orderStartDate,
+ orderEndDate);
+
+ }
+ catch(e:tink.core.Error){
+ throw Error('/contractAdmin/distributions/' + contract.id,e.message);
+ }
+
+ if (d.date == null) {
+ var msg = t._("The distribution has been proposed to the supplier, please wait for its validation");
+ throw Ok('/contractAdmin/distributions/'+contract.id, msg );
+ }
+ else {
+ throw Ok('/contractAdmin/distributions/'+contract.id, t._("The distribution has been recorded") );
+ }
+
+ }
+ else {
+ app.event(PreEditDistrib(d));
+ }
+
+ view.form = form;
+ view.title = t._("Edit a distribution");
+ }
+
+ @tpl('form.mtt')
+ function doEditCycle(d:db.DistributionCycle) {
+
+ if (!app.user.isContractManager(d.contract)) throw Error('/', 'Action interdite');
+
+ var form = sugoi.form.Form.fromSpod(d);
+ form.removeElement(form.getElement("contractId"));
+
+ if (form.isValid()) {
+ form.toSpod(d); //update model
+ d.update();
+ throw Ok('/contractAdmin/distributions/'+d.contract.id, t._("The delivery is now up to date"));
+ }
+
+ view.form = form;
+ view.title = t._("Modify a delivery");
+ }
+
+ @tpl("form.mtt")
+ public function doInsert(contract:db.Contract) {
+
+ if (!app.user.isContractManager(contract)) throw Error('/', t._('Forbidden action') );
+
+ var d = new db.Distribution();
+ d.place = contract.amap.getMainPlace();
+ var form = sugoi.form.Form.fromSpod(d);
+ form.removeElement(form.getElement("contractId"));
+ form.removeElement(form.getElement("distributionCycleId"));
+ form.removeElement(form.getElement("end"));
+ var x = new sugoi.form.elements.HourDropDowns("end", t._("End time") );
+ form.addElement(x, 3);
+
+ //default values
+ form.getElement("date").value = DateTool.now().deltaDays(30).setHourMinute(19, 0);
+ form.getElement("end").value = DateTool.now().deltaDays(30).setHourMinute(20, 0);
+
+ if (contract.type == db.Contract.TYPE_VARORDER ) {
+ form.addElement(new sugoi.form.elements.DatePicker("orderStartDate", t._("Orders opening date"),DateTool.now().deltaDays(10).setHourMinute(8, 0)));
+ form.addElement(new sugoi.form.elements.DatePicker("orderEndDate", t._("Orders closing date"),DateTool.now().deltaDays(20).setHourMinute(23, 59)));
+ }
+
+ if (form.isValid()) {
+
+ var createdDistrib = null;
+ var orderStartDate = null;
+ var orderEndDate = null;
+
+ try {
+
+ if (contract.type == db.Contract.TYPE_VARORDER ) {
+ orderStartDate = form.getValueOf("orderStartDate");
+ orderEndDate = form.getValueOf("orderEndDate");
+ }
+
+ createdDistrib = service.DistributionService.create(
+ contract,
+ form.getValueOf("date"),
+ form.getValueOf("end"),
+ form.getValueOf("placeId"),
+ form.getValueOf("distributor1Id"),
+ form.getValueOf("distributor2Id"),
+ form.getValueOf("distributor3Id"),
+ form.getValueOf("distributor4Id"),
+ orderStartDate,
+ orderEndDate);
+ }
+ catch(e:tink.core.Error){
+ throw Error('/contractAdmin/distributions/' + contract.id,e.message);
+ }
+
+ if (createdDistrib.date == null) {
+ var html = t._("Your request for a delivery has been sent to ::supplierName::. Be patient, you will receive an e-mail indicating if the request has been validated or refused.", {supplierName:contract.vendor.name});
+ var btn = "OK";
+ App.current.view.extraNotifBlock = App.current.processTemplate("block/modal.mtt",{html:html,title:t._("Distribution request sent"),btn:btn} );
+ } else {
+ throw Ok('/contractAdmin/distributions/'+ createdDistrib.contract.id , t._("The distribution has been recorded") );
+ }
+
+ }else{
+ //event
+ app.event(PreNewDistrib(contract));
+
+ }
+
+ view.form = form;
+ view.title = t._("Create a distribution");
+ }
+
+ /**
+ * create a distribution cycle for a contract
+ */
+ @tpl("form.mtt")
+ public function doInsertCycle(contract:db.Contract) {
+
+ if (!app.user.isContractManager(contract)) throw Error('/', t._("Forbidden action"));
+
+ var dc = new db.DistributionCycle();
+ dc.place = contract.amap.getMainPlace();
+ var form = sugoi.form.Form.fromSpod(dc);
+ form.removeElementByName("contractId");
+
+ form.getElement("startDate").value = DateTool.now();
+ form.getElement("endDate").value = DateTool.now().deltaDays(30);
+
+ //start hour
+ form.removeElementByName("startHour");
+ var x = new HourDropDowns("startHour", t._("Start time"), DateTool.now().setHourMinute( 19, 0) , true);
+ form.addElement(x, 5);
+
+ //end hour
+ form.removeElement(form.getElement("endHour"));
+ var x = new HourDropDowns("endHour", t._("End time"), DateTool.now().setHourMinute(20, 0), true);
+ form.addElement(x, 6);
+
+ if (contract.type == db.Contract.TYPE_VARORDER){
+
+ form.getElement("daysBeforeOrderStart").value = 10;
+ form.getElement("daysBeforeOrderStart").required = true;
+ form.removeElementByName("openingHour");
+ var x = new HourDropDowns("openingHour", t._("Opening time"), DateTool.now().setHourMinute(8, 0) , true);
+ form.addElement(x, 8);
+
+ form.getElement("daysBeforeOrderEnd").value = 2;
+ form.getElement("daysBeforeOrderEnd").required = true;
+ form.removeElementByName("closingHour");
+ var x = new HourDropDowns("closingHour", t._("Closing time"), DateTool.now().setHourMinute(23, 0) , true);
+ form.addElement(x, 10);
+
+ }else{
+
+ form.removeElementByName("daysBeforeOrderStart");
+ form.removeElementByName("daysBeforeOrderEnd");
+ form.removeElementByName("openingHour");
+ form.removeElementByName("closingHour");
+ }
+
+ if (form.isValid()) {
+
+ var createdDistribCycle = null;
+ var daysBeforeOrderStart = null;
+ var daysBeforeOrderEnd = null;
+ var openingHour = null;
+ var closingHour = null;
+
+ try{
+
+ if (contract.type == db.Contract.TYPE_VARORDER) {
+ daysBeforeOrderStart = form.getValueOf("daysBeforeOrderStart");
+ daysBeforeOrderEnd = form.getValueOf("daysBeforeOrderEnd");
+ openingHour = form.getValueOf("openingHour");
+ closingHour = form.getValueOf("closingHour");
+ }
+
+ createdDistribCycle = service.DistributionService.createCycle(
+ contract,
+ form.getElement("cycleType").getValue(),
+ form.getValueOf("startDate"),
+ form.getValueOf("endDate"),
+ form.getValueOf("startHour"),
+ form.getValueOf("endHour"),
+ daysBeforeOrderStart,
+ daysBeforeOrderEnd,
+ openingHour,
+ closingHour,
+ form.getValueOf("placeId"));
+ }
+ catch(e:tink.core.Error){
+ throw Error('/contractAdmin/distributions/' + contract.id,e.message);
+ }
+
+ if (createdDistribCycle != null) {
+ throw Ok('/contractAdmin/distributions/'+ contract.id, t._("The delivery has been saved"));
+ }
+
+ }
+ else{
+ dc.contract = contract;
+ app.event(PreNewDistribCycle(dc));
+ }
+
+ view.form = form;
+ view.title = t._("Schedule a recurrent delivery");
+ }
+
+ /**
+ * Delete a distribution cycle
+ */
+ public function doDeleteCycle(cycle:db.DistributionCycle){
+
+ if (!app.user.isContractManager(cycle.contract)) throw Error('/', t._("Forbidden action"));
+
+ var contractId = cycle.contract.id;
+ var messages = service.DistributionService.deleteCycleDistribs(cycle);
+ if (messages.length > 0){
+ App.current.session.addMessage( messages.join(" "),true);
+ }
+
+ throw Ok("/contractAdmin/distributions/" + contractId, t._("Recurrent deliveries deleted"));
+ }
+
+ /**
+ * Doodle-like participation planning
+ */
+ @tpl("distribution/planning.mtt")
+ public function doPlanning(contract:db.Contract) {
+
+ view.contract = contract;
+
+ var doodle = new Map}>();
+ var distribs = contract.getDistribs(true, 150);
+
+ for ( d in distribs ) {
+ for (u in [d.distributor1, d.distributor2, d.distributor3, d.distributor4]) {
+ if (u != null) {
+
+ var udoodle = doodle.get(u.id);
+
+ if (udoodle == null) udoodle = { user:u, planning:new Map() };
+ udoodle.planning.set(d.id, true);
+ doodle.set(u.id, udoodle);
+ }
+ }
+ }
+ view.distribs = distribs;
+ view.doodle = doodle;
+
+ }
+
+ /**
+ * Ajax service for doPlanning()
+ */
+ public function doRegister(args: { register:Bool, distrib:db.Distribution } ) {
+
+ if (args != null) {
+ var d = args.distrib;
+ d.lock();
+
+ if (args.register) {
+
+ if (d.distributor1 == null) d.distributor1 = app.user;
+ else if (d.distributor2 == null) d.distributor2 = app.user;
+ else if (d.distributor3 == null) d.distributor3 = app.user;
+ else if (d.distributor4 == null) d.distributor4 = app.user;
+
+ }else {
+ if (d.distributor1 == app.user) d.distributor1 = null;
+ else if (d.distributor2 == app.user) d.distributor2 = null;
+ else if (d.distributor3 == app.user) d.distributor3 = null;
+ else if (d.distributor4 == app.user) d.distributor4 = null;
+ }
+
+ d.update();
+ }
+
+ }
+
+ /**
+ * Validate a multi-distrib
+ * @param date
+ * @param place
+ */
+ @tpl('distribution/validate.mtt')
+ public function doValidate(date:Date, place:db.Place){
+
+ if (!app.user.isAmapManager()) throw t._("Forbidden access");
+
+ var md = MultiDistrib.get(date, place, db.Contract.TYPE_VARORDER);
+
+ view.confirmed = md.checkConfirmed();
+ view.users = md.getUsers();
+ view.date = date;
+ view.place = place;
+ }
+
+
+ /**
+ * Admin can autovalidate a multidistrib
+ */
+ @admin
+ public function doAutovalidate(date:Date,place:db.Place){
+
+ var md = MultiDistrib.get(date,place,db.Contract.TYPE_VARORDER);
+ for ( d in md.distributions){
+ if(d.validated) continue;
+ service.PaymentService.validateDistribution(d);
+ }
+ throw Ok("/contractAdmin",t._("This distribution have been validated"));
+ }
+
+ @admin
+ public function doUnvalidate(date:Date,place:db.Place){
+
+ var md = MultiDistrib.get(date,place,db.Contract.TYPE_VARORDER);
+ for ( d in md.distributions){
+ if(!d.validated) continue;
+ service.PaymentService.unvalidateDistribution(d);
+ }
+ throw Ok("/contractAdmin",t._("This distribution have been Unvalidated"));
+ }
+
+
+
+}
diff --git a/src/controller/Group.hx b/src/controller/Group.hx
new file mode 100644
index 0000000..bd750c3
--- /dev/null
+++ b/src/controller/Group.hx
@@ -0,0 +1,368 @@
+package controller;
+import sugoi.form.elements.StringInput;
+import service.OrderService;
+import service.WaitingListService;
+
+/**
+ * Groups
+ */
+class Group extends controller.Controller
+{
+
+ /**
+ * Public page of a group
+ */
+ @tpl('group/view.mtt')
+ function doDefault(group:db.Amap){
+
+ if (group.regOption == db.Amap.RegOption.Open) {
+ app.session.data.amapId = group.id;
+ throw Redirect("/");
+ }
+
+ view.group = group;
+ view.contracts = group.getActiveContracts();
+ view.pageTitle = group.name;
+ group.getMainPlace(); //just to update cache
+ if (app.user != null){
+
+ view.isMember = Lambda.has(app.user.getAmaps(), group);
+ }
+ }
+
+ /**
+ * Register to a waiting list.
+ * the user can be logged or not !
+ */
+ @tpl('form.mtt')
+ function doList(group:db.Amap){
+
+ //checks
+ if (group.regOption != db.Amap.RegOption.WaitingList) throw Redirect("/group/" + group.id);
+ if (app.user != null) {
+ try{
+ WaitingListService.canRegister(app.user,group);
+ }catch(e:tink.core.Error){
+ throw Error("/group/" + group.id,e.message);
+ }
+ }
+
+ //build form
+ var form = new sugoi.form.Form("reg");
+ if (app.user == null){
+ form.addElement(new StringInput("userFirstName", t._("Your firstname"),"",true));
+ form.addElement(new StringInput("userLastName", t._("Your lastname") ,"",true));
+ form.addElement(new StringInput("userEmail", t._("Your e-mail"), "", true));
+ }
+ form.addElement(new sugoi.form.elements.TextArea("msg", t._("Leave a message")));
+
+ if (form.isValid()){
+ try{
+ if (app.user == null){
+ var f = form;
+ var user = service.UserService.softRegistration(f.getValueOf("userFirstName"),f.getValueOf("userLastName"), f.getValueOf("userEmail") );
+ db.User.login(user, user.email);
+ }
+
+ WaitingListService.registerToWl(app.user,group,form.getValueOf("msg"));
+ throw Ok("/group/" + group.id,t._("Your subscription to the waiting list has been recorded. You will receive an e-mail as soon as your request is processed.") );
+ }catch(e:tink.core.Error){
+ throw Error("/group/list/" + group.id,e.message);
+ }
+
+ }
+
+ view.title = t._("Subscription to \"::groupeName::\" waiting list", {groupeName:group.name});
+ view.form = form;
+ }
+
+ /**
+ Cancel suscription request
+ **/
+ function doListCancel(group:db.Amap){
+ try{
+ WaitingListService.removeFromWl(app.user,group);
+ }catch(e:tink.core.Error){
+ throw Error("/group/" + group.id,e.message);
+ }
+ throw Ok("/group/" + group.id,t._("You've been removed from the waiting list"));
+ }
+
+
+ /**
+ * Register direclty in an open group
+ *
+ * the user can be logged or not !
+
+ @tpl('form.mtt')
+ function doRegister(group:db.Amap){
+
+ if (group.regOption != db.Amap.RegOption.Open) throw Redirect("/group/" + group.id);
+ if (app.user != null){
+ if ( db.UserAmap.manager.select($amapId == group.id && $user == app.user) != null) throw Error("/group/" + group.id, t._("You are already member of this group."));
+ }
+
+ var form = new sugoi.form.Form("reg");
+ form.submitButtonLabel = t._("Join the group");
+ form.addElement(new sugoi.form.elements.Html("html",t._("Confirm your subscription to \"::groupName::\"", {groupName:group.name})));
+ if (app.user == null){
+ form.addElement(new StringInput("userFirstName", t._("Your firstname"),"",true));
+ form.addElement(new StringInput("userLastName", t._("Your lastname"), "", true));
+ var em = new StringInput("userEmail", t._("Your e-mail"), "", true);
+ em.addValidator(new EmailValidator());
+ form.addElement(em);
+ form.addElement(new StringInput("address", t._("Address"), "", true));
+ form.addElement(new StringInput("zipCode", t._("Zip code"), "", true));
+ form.addElement(new StringInput("city", t._("City"), "", true));
+ form.addElement(new StringInput("phone", t._("Phone"), "", true));
+ }
+
+ if (form.isValid()){
+
+ if (app.user == null){
+ var f = form;
+ var user = new db.User();
+ user.email = f.getValueOf("userEmail");
+ user.firstName = f.getValueOf("userFirstName");
+ user.lastName = f.getValueOf("userLastName");
+ user.address1 = f.getValueOf("address");
+ user.zipCode = f.getValueOf("zipCode");
+ user.city = f.getValueOf("city");
+ user.phone = f.getValueOf("phone");
+
+ if ( db.User.getSameEmail(user.email).length > 0 ) {
+ throw Ok("/user/login",t._("You already subscribed to Cagette.net, please log in on this page"));
+ }
+
+ user.insert();
+ app.session.setUser(user);
+
+ }
+
+ var w = new db.UserAmap();
+ w.user = app.user;
+ w.amap = group;
+ w.insert();
+
+ throw Ok("/user/choose", t._("Your subscription has been taken into account"));
+ }
+
+ view.title = t._("Subscription to \"::groupName::\"", {groupName:group.name});
+ view.form = form;
+
+ }*/
+
+ /**
+ * create a new group
+ */
+ @tpl("form.mtt")
+ function doCreate() {
+
+ view.title = t._("Create a new Cagette Group");
+
+ var f = new sugoi.form.Form("c");
+ f.addElement(new StringInput("name", t._("Name of your group"), "", true));
+
+ //group type
+ var data = [
+ {label:t._("CSA"),value:"0"},
+ {label:t._("Grouped orders"),value:"1"},
+ {label:t._("Farmers collective"),value:"2"},
+ {label:t._("Farm shop"),value:"3"},
+ ];
+ var gt = new sugoi.form.elements.RadioGroup("type", t._("Group type"), data ,"1","1",true,true,true);
+ f.addElement(gt);
+
+ if (f.checkToken()) {
+
+ var user = app.user;
+
+ var g = new db.Amap();
+ g.name = f.getValueOf("name");
+ g.contact = user;
+
+ var type:db.Amap.GroupType = Type.createEnumIndex(db.Amap.GroupType, Std.parseInt(f.getValueOf("type")) );
+
+ switch(type){
+ case null :
+ throw "unknown group type";
+ case db.Amap.GroupType.Amap :
+ g.flags.set(db.Amap.AmapFlags.HasMembership);
+ g.regOption = db.Amap.RegOption.WaitingList;
+
+ case db.Amap.GroupType.GroupedOrders :
+ g.flags.set(db.Amap.AmapFlags.ShopMode);
+ g.flags.set(db.Amap.AmapFlags.HasMembership);
+ g.regOption = db.Amap.RegOption.WaitingList;
+
+ case db.Amap.GroupType.ProducerDrive :
+ g.flags.set(db.Amap.AmapFlags.ShopMode);
+ g.regOption = db.Amap.RegOption.Open;
+ g.flags.set(db.Amap.AmapFlags.PhoneRequired);
+
+ case db.Amap.GroupType.FarmShop :
+ g.flags.set(db.Amap.AmapFlags.ShopMode);
+ g.regOption = db.Amap.RegOption.Open;
+ g.flags.set(db.Amap.AmapFlags.PhoneRequired);
+ }
+
+ g.groupType = type;
+ g.insert();
+
+ var ua = new db.UserAmap();
+ ua.user = user;
+ ua.amap = g;
+ ua.rights = [db.UserAmap.Right.GroupAdmin,db.UserAmap.Right.Membership,db.UserAmap.Right.Messages,db.UserAmap.Right.ContractAdmin(null)];
+ ua.insert();
+
+ //example datas
+ var place = new db.Place();
+ place.name = t._("Market square");
+ place.zipCode = "000";
+ place.city = "St Martin de la Cagette";
+ place.amap = g;
+ place.insert();
+
+ //contrat AMAP
+ var vendor = new db.Vendor();
+ vendor.amap = g;
+ vendor.name = "Jean Martin EARL";
+ vendor.zipCode = "000";
+ vendor.city = "Langon";
+ vendor.email = "jean@cagette.net";
+ vendor.insert();
+
+ if (type == Amap){
+ var contract = new db.Contract();
+ contract.name = t._("CSA contract Vegetables - Example");
+ contract.description = t._("This contract is an example where the customer has to commit to buy the whole year as with AMAPs");
+ contract.amap = g;
+ contract.type = 0;
+ contract.vendor = vendor;
+ contract.startDate = Date.now();
+ contract.endDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 364);
+ contract.contact = user;
+ contract.distributorNum = 2;
+ contract.insert();
+
+ var p = new db.Product();
+ p.name = t._("Big basket of vegetables");
+ p.price = 15;
+ p.organic = true;
+ p.contract = contract;
+ p.insert();
+
+ var p = new db.Product();
+ p.name = t._("Small basket of vegetables");
+ p.price = 10;
+ p.organic = true;
+ p.contract = contract;
+ p.insert();
+
+ OrderService.make(user, 1, p, null, true);
+
+ var d = new db.Distribution();
+ d.contract = contract;
+ d.date = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 14);
+ d.end = DateTools.delta(d.date, 1000.0 * 60 * 90);
+ d.place = place;
+ d.insert();
+
+ }
+
+ //contrat variable
+ var vendor = new db.Vendor();
+ vendor.amap = g;
+ vendor.name = t._("Farm Galinette");
+ vendor.zipCode = "000";
+ vendor.city = t._("Bazas");
+ vendor.email = "galinette@cagette.net";
+ vendor.insert();
+
+ var contract = new db.Contract();
+ contract.name = t._("Chicken Contract - Example");
+ contract.description = t._("Example of contract with variable orders. It is allowed to order something else at every delivery.");
+ contract.amap = g;
+ contract.type = 1;
+ contract.vendor = vendor;
+ contract.startDate = Date.now();
+ contract.endDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 364);
+ contract.contact = user;
+ contract.distributorNum = 2;
+ contract.flags.set(db.Contract.ContractFlags.UsersCanOrder);
+ contract.insert();
+
+ var egg = new db.Product();
+ egg.name = t._("12 eggs");
+ egg.price = 5;
+ //egg.type = 6;
+ egg.organic = true;
+ egg.contract = contract;
+ egg.insert();
+
+ var p = new db.Product();
+ p.name = t._("Chicken");
+ //p.type = 2;
+ p.price = 9.50;
+ p.organic = true;
+ p.contract = contract;
+ p.insert();
+
+ var d = new db.Distribution();
+ d.contract = contract;
+ d.orderStartDate = Date.now();
+ d.orderEndDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 19);
+ d.date = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 21);
+ d.end = DateTools.delta(d.date, 1000.0 * 60 * 90);
+ d.place = place;
+ d.insert();
+
+ OrderService.make(user, 2, egg, d.id);
+ OrderService.make(user, 1, p, d.id);
+
+ App.current.session.data.amapId = g.id;
+ app.session.data.newGroup = true;
+ throw Redirect("/");
+ }
+
+ view.form= f;
+
+ }
+
+ /**
+ Displays a google map in a popup
+ **/
+ @tpl('group/place.mtt')
+ public function doPlace(place:db.Place){
+ view.place = place;
+
+ //build adress for google maps
+ var addr = "";
+ if (place.address1 != null) addr += place.address1;
+ if (place.address2 != null) addr += ", " + place.address2;
+ if (place.zipCode != null) addr += " " + place.zipCode;
+ if (place.city != null) addr += " " + place.city;
+
+ view.addr = view.escapeJS(addr);
+ }
+
+ /**
+ * Groups map
+ */
+ @tpl("group/map.mtt")
+ public function doMap(?args:{?lat:Float,?lng:Float,?address:String}){
+
+ view.container = "container-fluid";
+
+ //if no param is sent, focus on Paris
+ if (args == null || (args.address == null && args.lat == null && args.lng == null)){
+ args = {lat:48.855675, lng:2.3472365};
+ }
+
+ view.lat = args.lat;
+ view.lng = args.lng;
+ view.address = args.address;
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/controller/Install.hx b/src/controller/Install.hx
new file mode 100644
index 0000000..9b63450
--- /dev/null
+++ b/src/controller/Install.hx
@@ -0,0 +1,273 @@
+package controller;
+import sugoi.db.Variable;
+import sugoi.form.elements.StringInput;
+import thx.semver.Version;
+
+/**
+ * ...
+ * @author fbarbut
+ */
+class Install extends controller.Controller
+{
+ /**
+ * checks if its a first install or an update
+ */
+ @tpl("install/default.mtt")
+ public function doDefault() {
+ if (db.User.manager.get(1) == null) {
+
+ throw Redirect("/install/firstInstall");
+
+ }else {
+ //throw Error("/", "L'utilisateur admin a déjà été créé. Essayez de vous connecter avec admin@cagette.net, mot de passe : admin");
+
+ var status = new Array<{parameter:String,valid:Bool,message:String}>();
+
+ status.push(getVersionStatus());
+
+ view.status = status;
+ }
+ }
+
+ public function doDiagnostics(){
+
+ var webroot = sugoi.Web.getCwd();
+
+ if(!sys.FileSystem.exists(webroot+"file")){
+ Sys.println("no File directory : created");
+ sys.FileSystem.createDirectory(webroot+"file");
+ }
+ if(!sys.FileSystem.exists(webroot+"file/.htaccess")){
+ Sys.println("no .htaccess file in 'File' directory");
+ }
+ if(!sys.FileSystem.exists(webroot+"../tmp")) {
+ Sys.println("no tmp directory : created");
+ sys.FileSystem.createDirectory(webroot+"../tmp");
+ }
+
+ }
+
+ /**
+ * First install
+ */
+ @tpl("form.mtt")
+ public function doFirstInstall(){
+ view.title = "Installation de Cagette.net";
+
+ var f = new sugoi.form.Form("c");
+ f.addElement(new StringInput("amapName", t._("Name of your group"),"",true));
+ f.addElement(new StringInput("userFirstName", t._("Your firstname"),"",true));
+ f.addElement(new StringInput("userLastName", t._("Your lastname"),"",true));
+
+ if (f.checkToken()) {
+
+ var user = new db.User();
+ user.firstName = f.getValueOf("userFirstName");
+ user.lastName = f.getValueOf("userLastName");
+ user.email = "admin@cagette.net";
+ user.setPass("admin");
+ user.insert();
+
+ var amap = new db.Amap();
+ amap.name = f.getValueOf("amapName");
+ amap.contact = user;
+
+ amap.flags.set(db.Amap.AmapFlags.HasMembership);
+ //amap.flags.set(db.Amap.AmapFlags.IsAmap);
+ amap.insert();
+
+ var ua = new db.UserAmap();
+ ua.user = user;
+ ua.amap = amap;
+ ua.rights = [db.UserAmap.Right.GroupAdmin,db.UserAmap.Right.Membership,db.UserAmap.Right.Messages,db.UserAmap.Right.ContractAdmin(null)];
+ ua.insert();
+
+ //example datas
+ var place = new db.Place();
+ place.name = t._("Marketplace");
+ place.amap = amap;
+ place.address1 = t._("Place Jules Verne");
+ place.zipCode = "00000";
+ place.city = t._("St Martin de la Cagette");
+ place.insert();
+
+ var vendor = new db.Vendor();
+ vendor.amap = amap;
+
+ vendor.name = t._("Jean Martin EURL");
+ vendor.email = "jean.martin@cagette.net";
+ vendor.zipCode = "00000";
+ vendor.city = "Martignac";
+ vendor.insert();
+
+ var contract = new db.Contract();
+ contract.name = t._("Vegetables Contract Example");
+ contract.amap = amap;
+ contract.type = 0;
+ contract.vendor = vendor;
+ contract.startDate = Date.now();
+ contract.endDate = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 364);
+ contract.contact = user;
+ contract.distributorNum = 2;
+ contract.insert();
+
+ var p = new db.Product();
+ p.name = t._("Big basket of vegetables");
+ p.price = 15;
+ p.vat = 5;
+ p.contract = contract;
+ p.insert();
+
+ var p = new db.Product();
+ p.name = t._("Small basket of vegetables");
+ p.price = 10;
+ p.vat = 5;
+ p.contract = contract;
+ p.insert();
+
+ var uc = new db.UserContract();
+ uc.user = user;
+ uc.product = p;
+ uc.paid = true;
+ uc.quantity = 1;
+ uc.productPrice = 10;
+ uc.insert();
+
+ var d = new db.Distribution();
+ d.contract = contract;
+ d.date = DateTools.delta(Date.now(), 1000.0 * 60 * 60 * 24 * 14);
+ d.end = DateTools.delta(d.date, 1000.0 * 60 * 90);
+ d.place = place;
+ d.insert();
+
+ App.current.user = null;
+ App.current.session.setUser(user);
+ App.current.session.data.amapId = amap.id;
+
+ throw Ok("/", t._("Group and user 'admin' created. Your email is 'admin@cagette.net' and your password is 'admin'"));
+ }
+
+ view.form= f;
+ }
+
+ /**
+ * get version status
+ */
+ private function getVersionStatus(){
+
+
+ var out = {parameter:"version", valid:false, message:""};
+
+ var v = Variable.get("version");
+ if (v == null || v=="") {
+ Variable.set("version", App.VERSION.toString());
+ v = App.VERSION.toString();
+ }
+
+ var v :thx.semver.Version = thx.semver.Version.stringToVersion(v);
+
+ if (v.lessThan(App.VERSION)){
+
+ //need update !
+ out.valid = false;
+ out.message = t._("You must update your database to version ") +App.VERSION.toString()+"";
+
+ }else{
+ out.valid = true;
+ out.message = t._("Current version") +v.toString()+"";
+ }
+
+ return out;
+
+ }
+
+ /**
+ * perform migrations from a version to another
+ */
+ @admin
+ public function doUpdateversion(){
+
+ var log = [];
+
+ var currentVersion = thx.semver.Version.stringToVersion(Variable.get("version"));
+
+ //Migrations to 0.9.2
+ if (currentVersion.lessThan( thx.semver.Version.arrayToVersion([0,9,2]) )){
+
+ log.push(t._("Installation of the dictionnary of products (taxonomy)"));
+ _0_9_2_installTaxonomy();
+
+ log.push(t._("Improvement on saving orders"));
+ _0_9_2_dbMigration();
+
+ sugoi.db.Variable.set("version", "0.9.2");
+ }
+
+ //Migrations to 1.0.0
+ //...
+
+ throw Ok("/install", t._("Following update have been performed:
")+Lambda.map(log,function(x) return "
"+x+"
").join("")+"
");
+
+ }
+
+ @admin
+ function _0_9_2_installTaxonomy(){
+
+ db.TxpCategory.manager.delete(true);
+ db.TxpSubCategory.manager.delete(true);
+ db.TxpProduct.manager.delete(true);
+
+ var taxo = sys.io.File.getContent(sugoi.Web.getCwd() + "../data/productTaxonomy.json");
+ var taxo = haxe.Json.parse(taxo);
+
+ var categories : Array = taxo.categories;
+ var subcategories : Array = taxo.subCategories;
+ var products : Array = taxo.products;
+
+ for ( c in categories){
+ var cat = new db.TxpCategory();
+ cat.id = c.id;
+ cat.name = c.name;
+ cat.insert();
+ }
+
+ for ( sc in subcategories){
+ var scat = new db.TxpSubCategory();
+ scat.id = sc.id;
+ scat.name = sc.name;
+ scat.category = db.TxpCategory.manager.get(Std.parseInt(sc.category));
+ scat.insert();
+
+ }
+
+ for ( p in products){
+ var pro = new db.TxpProduct();
+ pro.name = p.name;
+ pro.id = p.id;
+ pro.category = db.TxpCategory.manager.get(Std.parseInt(p.category));
+ pro.subCategory = db.TxpSubCategory.manager.get(Std.parseInt(p.subCategory));
+ pro.insert();
+ }
+
+ }
+
+ @admin
+ function _0_9_2_dbMigration(){
+
+ //recompute prices on orders
+ for ( order in db.UserContract.manager.all(true)){
+ order.productPrice = order.product.price;
+ order.feesRate = order.product.contract.percentageValue;
+ order.update();
+ }
+
+
+ //activate payment orders
+ for ( a in db.Amap.manager.all(true)){
+ a.allowedPaymentsType = ["cash", "transfer", "check"];
+ a.update();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/controller/Main.hx b/src/controller/Main.hx
new file mode 100755
index 0000000..677a4cf
--- /dev/null
+++ b/src/controller/Main.hx
@@ -0,0 +1,261 @@
+package controller;
+import db.Distribution;
+import db.UserContract;
+import haxe.Json;
+import haxe.web.Dispatch;
+import sugoi.form.elements.StringInput;
+import sugoi.tools.ResultsBrowser;
+import Common;
+import tools.ArrayTool;
+
+class Main extends Controller {
+
+
+ /**
+ * public pages
+ */
+ function doGroup(d:haxe.web.Dispatch){
+ d.dispatch(new controller.Group());
+ }
+
+ /**
+ Group homepage
+ **/
+ @tpl("home.mtt")
+ function doDefault() {
+ view.category = 'home';
+
+ var group = app.getCurrentGroup();
+ if ( app.user!=null && group == null) {
+ throw Redirect("/user/choose");
+ }else if (app.user == null && (group==null || group.regOption!=db.Amap.RegOption.Open) ) {
+ throw Redirect("/user/login");
+ }
+
+ view.amap = group;
+
+ //contract with open orders
+ var openContracts = Lambda.filter(group.getActiveContracts(), function(c) return c.isUserOrderAvailable());
+ view.openContracts = openContracts;
+
+ //register to become "distributor"
+ view.contractsWithDistributors = app.user==null ? [] : Lambda.filter(app.user.amap.getActiveContracts(), function(c) return c.distributorNum > 0);
+
+ //freshly created group
+ view.newGroup = app.session.data.newGroup == true;
+
+ var n = Date.now();
+ var now = new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0);
+ var in3Month = DateTools.delta(now, 1000.0 * 60 * 60 * 24 * 30 * 3);
+
+ var distribs = MultiDistrib.getFromTimeRange(group,now,in3Month);
+ view.distribs = distribs;
+
+ //view functions
+ view.getWhosTurn = function(orderId:Int, distrib:Distribution) {
+ return db.UserContract.manager.get(orderId, false).getWhosTurn(distrib);
+ }
+
+ //event for additionnal blocks on home page
+ var e = Blocks([], "home");
+ app.event(e);
+ view.blocks = e.getParameters()[0];
+
+ //message if phone is required
+ if(app.user!=null && app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && app.user.phone==null){
+ app.session.addMessage(t._("Members of this group should provide a phone number. Please click here to update your account."),true);
+ }
+
+ }
+
+ //login and stuff
+ function doUser(d:Dispatch) {
+ d.dispatch(new controller.User());
+ }
+
+ function doCron(d:Dispatch) {
+ d.dispatch(new controller.Cron());
+ }
+
+ /**
+ * JSON REST API Entry point
+ */
+ function doApi(d:Dispatch) {
+
+ try {
+
+ d.dispatch(new controller.Api());
+
+ }catch (e:tink.core.Error){
+
+ //manage tink Errors (service errors)
+ sugoi.Web.setReturnCode(e.code);
+ Sys.print(Json.stringify( {error:{code:e.code,message:e.message,stack:e.exceptionStack}} ));
+
+ }catch (e:Dynamic){
+
+ //manage other errors
+ sugoi.Web.setReturnCode(500);
+ var stack = if ( App.config.DEBUG ) haxe.CallStack.toString(haxe.CallStack.exceptionStack()) else "";
+ App.current.logError(e, stack);
+ Sys.print(Json.stringify( {error:{code:500,message : Std.string(e), stack:stack }} ));
+ }
+
+ }
+
+ @tpl("cssDemo.mtt")
+ function doCssdemo() {
+ view.category = 'home';
+ }
+
+ @tpl("form.mtt")
+ function doInstall(d:Dispatch) {
+ d.dispatch(new controller.Install());
+ }
+
+
+ function doP(d:Dispatch) {
+
+ /*
+ * Invalid array access
+Stack (ADMIN|DEBUG)
+
+Called from C:\HaxeToolkit\haxe\std/haxe/web/Dispatch.hx line 463
+Called from controller/Main.hx line 117
+ *
+ var plugin = d.parts.shift();
+ for ( p in App.plugins) {
+ var n = Type.getClassName(Type.getClass(p)).toLowerCase();
+ n = n.split(".").pop();
+ if (plugin == n) {
+ d.dispatch( p.getController() );
+ return;
+ }
+ }
+
+ throw Error("/","Plugin '"+plugin+"' introuvable.");
+ */
+
+ d.dispatch(new controller.Plugin());
+ }
+
+
+ @logged
+ function doMember(d:Dispatch) {
+ view.category = 'members';
+ d.dispatch(new controller.Member());
+ }
+
+ @logged
+ function doStats(d:Dispatch) {
+ view.category = 'stats';
+ d.dispatch(new Stats());
+ }
+
+ @logged
+ function doAccount(d:Dispatch) {
+ view.category = 'account';
+ d.dispatch(new controller.Account());
+ }
+
+ @logged
+ function doVendor(d:Dispatch) {
+ view.category = 'contractadmin';
+ d.dispatch(new controller.Vendor());
+ }
+
+ @logged
+ function doPlace(d:Dispatch) {
+ view.category = 'contractadmin';
+ d.dispatch(new controller.Place());
+ }
+
+ @logged
+ function doTransaction(d:Dispatch) {
+ view.category = 'members';
+ d.dispatch(new controller.Transaction());
+ }
+
+ @logged
+ function doDistribution(d:Dispatch) {
+ view.category = 'contractadmin';
+ d.dispatch(new controller.Distribution());
+ }
+
+ @logged
+ function doMembership(d:Dispatch) {
+ view.category = 'members';
+ d.dispatch(new controller.Membership());
+ }
+
+ function doShop(d:Dispatch) {
+ view.category = 'shop';
+ d.dispatch(new controller.Shop());
+ }
+
+ @tpl('shop/default2.mtt')
+ function doShop2(place:db.Place, date:String) {
+ view.category = 'shop';
+ view.place = place;
+ view.date = date;
+ }
+
+ @logged
+ function doProduct(d:Dispatch) {
+ view.category = 'contractadmin';
+ d.dispatch(new controller.Product());
+ }
+
+ @logged
+ function doAmap(d:Dispatch) {
+ view.category = 'amap';
+ d.dispatch(new controller.Amap());
+ }
+
+ @logged
+ function doContract(d:Dispatch) {
+ view.category = 'contract';
+ d.dispatch(new Contract());
+ }
+
+ @logged
+ function doContractAdmin(d:Dispatch) {
+ view.category = 'contractadmin';
+ d.dispatch(new ContractAdmin());
+ }
+
+ @logged
+ function doMessages(d:Dispatch) {
+ view.category = 'messages';
+ d.dispatch(new Messages());
+ }
+
+ @logged
+ function doAmapadmin(d:Dispatch) {
+ view.category = 'amapadmin';
+ d.dispatch(new AmapAdmin());
+ }
+
+ @logged
+ function doValidate(date:Date, place:db.Place, user:db.User, d:haxe.web.Dispatch){
+
+ var v = new controller.Validate();
+ v.date = date;
+ v.place = place;
+ v.user = user;
+ d.dispatch(v);
+ }
+
+ @admin
+ function doAdmin(d:Dispatch) {
+ d.dispatch(new controller.admin.Admin());
+ }
+
+ @admin
+ function doDb(d:Dispatch) {
+ d.parts = []; //disable haxe.web.Dispatch
+ sys.db.Admin.handler();
+ }
+
+
+}
diff --git a/src/controller/Member.hx b/src/controller/Member.hx
new file mode 100755
index 0000000..138441a
--- /dev/null
+++ b/src/controller/Member.hx
@@ -0,0 +1,741 @@
+package controller;
+import Common;
+import haxe.Utf8;
+import sugoi.form.Form;
+import sugoi.form.elements.Selectbox;
+import sugoi.form.validators.EmailValidator;
+import sugoi.tools.Utils;
+
+
+class Member extends Controller
+{
+
+ public function new()
+ {
+ super();
+ if (!app.user.canAccessMembership()) throw Redirect("/");
+ }
+
+ @logged
+ @tpl('member/default.mtt')
+ function doDefault(?args: { ?search:String, ?select:String } ) {
+ checkToken();
+
+ var browse:Int->Int->List;
+ var uids = db.UserAmap.manager.search($amap == app.user.getAmap(), false);
+ var uids = Lambda.map(uids, function(ua) return ua.user.id);
+ if (args != null && args.search != null) {
+
+ //SEARCH
+ browse = function(index:Int, limit:Int) {
+ var search = "%"+StringTools.trim(args.search)+"%";
+ return db.User.manager.search(
+ ($lastName.like(search) ||
+ $lastName2.like(search) ||
+ $address1.like(search) ||
+ $address2.like(search) ||
+ $firstName.like(search) ||
+ $firstName2.like(search)
+ ) && $id in uids , { orderBy:-id }, false);
+ }
+ view.search = args.search;
+
+ }else if(args!=null && args.select!=null){
+
+ //SELECTION
+
+ switch(args.select) {
+ case "nocontract":
+ if (app.params.exists("csv")) {
+ sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_NoContracts()), ["firstName", "lastName", "email"], t._("Without contracts"));
+ return;
+ }else {
+ browse = function(index:Int, limit:Int) { return db.User.getUsers_NoContracts(index, limit); }
+ }
+ case "contract":
+
+ if (app.params.exists("csv")) {
+ sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_Contracts()), ["firstName", "lastName", "email"], t._("With orders"));
+ return;
+ }else {
+ browse = function(index:Int, limit:Int) { return db.User.getUsers_Contracts(index, limit); }
+ }
+
+ case "nomembership" :
+ if (app.params.exists("csv")) {
+ sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_NoMembership()), ["firstName", "lastName", "email"], t._("Memberships to be renewed"));
+ return;
+ }else {
+ browse = function(index:Int, limit:Int) { return db.User.getUsers_NoMembership(index, limit); }
+ }
+ case "newusers" :
+ if (app.params.exists("csv")) {
+ sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.getUsers_NewUsers()), ["firstName", "lastName", "email"], t._("Never connected"));
+ return;
+ }else {
+ browse = function(index:Int, limit:Int) { return db.User.getUsers_NewUsers(index, limit); }
+ }
+ default:
+ throw t._("Unknown selection");
+ }
+ view.select = args.select;
+
+ }else {
+ if (app.params.exists("csv")) {
+ var headers = ["firstName", "lastName", "email","phone", "firstName2", "lastName2","email2","phone2", "address1","address2","zipCode","city"];
+ sugoi.tools.Csv.printCsvDataFromObjects(Lambda.array(db.User.manager.search( $id in uids, {orderBy:lastName}, false)), headers, t._("Members"));
+ return;
+ }else {
+ //default display
+ browse = function(index:Int, limit:Int) {
+ return db.User.manager.search( $id in uids, { limit:[index,limit], orderBy:lastName }, false);
+ }
+ }
+ }
+
+ var count = uids.length;
+ var rb = new sugoi.tools.ResultsBrowser(count, (args.select!=null||args.search!=null)?1000:10, browse);
+ view.members = rb;
+
+ if (args.select == null || args.select != "newusers") {
+ //count new users
+ view.newUsers = db.User.getUsers_NewUsers().length;
+ }
+
+ view.waitingList = db.WaitingList.manager.count($group == app.user.amap);
+
+ }
+
+ /**
+ * Move to waiting list
+ */
+ function doMovetowl(u:db.User){
+
+ var ua = db.UserAmap.get(u, app.user.amap, true);
+ ua.delete();
+
+ var wl = new db.WaitingList();
+ wl.user = u;
+ wl.group = app.user.amap;
+ wl.insert();
+
+ throw Ok("/member", u.getName() +" "+ t._("is now on waiting list.") );
+
+
+ }
+
+ /**
+ * Display waiting list
+ */
+ @tpl('member/waiting.mtt')
+ function doWaiting(?args:{?add:db.User,?remove:db.User}){
+
+ if (args != null){
+ if (args.add != null){
+
+ service.WaitingListService.approveRequest(args.add,app.user.amap);
+ throw Ok("/member/waiting", t._("Membership request accepted") );
+
+ }else if (args.remove != null){
+
+ service.WaitingListService.cancelRequest(args.remove,app.user.amap);
+ throw Ok("/member/waiting", t._("Membership request refused") );
+
+ }
+ }
+
+ view.waitingList = db.WaitingList.manager.search($group == app.user.amap,{orderBy:-date});
+ }
+
+ /**
+ * Send an invitation to a new member
+ */
+ function doInviteMember(u:db.User){
+
+ if (checkToken() ) {
+ u.sendInvitation(app.user.amap);
+ throw Ok('/member/view/'+u.id, t._("Invitation sent.") );
+ }
+
+ }
+
+ /**
+ * Invite 'never logged' users
+ */
+ function doInvite() {
+
+ if (checkToken()) {
+
+ var users = db.User.getUsers_NewUsers();
+ try{
+ for ( u in users) {
+ u.sendInvitation(app.user.amap);
+ Sys.sleep(0.2);
+ }
+ }catch (e:String){
+ if (e.indexOf("curl") >-1) {
+ App.current.logError(e, haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
+ throw Error("/member", t._("An error occurred while sending emails, please retry"));
+ }
+ }
+
+ throw Ok('/member', t._("Congratulations, you just sent ::userLength:: invitations", {userLength:users.length}));
+ }
+
+ }
+
+
+ @tpl("member/view.mtt")
+ function doView(member:db.User) {
+
+ view.member = member;
+ var userAmap = db.UserAmap.get(member, app.user.amap);
+ if (userAmap == null) throw Error("/member", t._("This person does not belong to your group"));
+
+ view.userAmap = userAmap;
+ view.canLoginAs = (db.UserAmap.manager.count($userId == member.id) == 1 && app.user.isAmapManager()) || app.user.isAdmin();
+
+ //orders
+ var row = { constOrders:new Array(), varOrders:new Map>() };
+
+ //commandes fixes
+ var contracts = db.Contract.manager.search($type == db.Contract.TYPE_CONSTORDERS && $amap == app.user.amap && $endDate > DateTools.delta(Date.now(),-1000.0*60*60*24*30), false);
+ var orders = member.getOrdersFromContracts(contracts);
+ row.constOrders = service.OrderService.prepare(orders);
+
+ //commandes variables groupées par date de distrib
+ var contracts = db.Contract.manager.search($type == db.Contract.TYPE_VARORDER && $amap == app.user.amap && $endDate > DateTools.delta(Date.now(),-1000.0*60*60*24*30), false);
+ var distribs = new Map>();
+ for (c in contracts) {
+ var ds = c.getDistribs();
+ for (d in ds) {
+ var k = d.date.toString().substr(0, 10);
+ var orders = member.getOrdersFromDistrib(d);
+ if (orders.length > 0) {
+ if (!distribs.exists(k)) {
+ distribs.set(k, orders);
+ }else {
+
+ var v = distribs.get(k);
+ for ( o in orders ) v.add(o);
+ distribs.set(k, v);
+ }
+ }
+ }
+ }
+ for ( k in distribs.keys()){
+ var d = distribs.get(k);
+ var d2 = service.OrderService.prepare(d);
+ row.varOrders.set(k,d2);
+ }
+
+
+ view.userContracts = row;
+ checkToken(); //to insert a token in tpl
+
+ }
+
+ /**
+ * Admin : Log in as this user for debugging purpose
+ * @param user
+ * @param amap
+ */
+ function doLoginas(member:db.User, amap:db.Amap) {
+
+ if (!app.user.isAdmin()){
+ if (!app.user.isAmapManager()) return;
+ if (member.isAdmin()) return;
+ if ( db.UserAmap.manager.count($userId == member.id) > 1 ) return;
+
+ }
+
+ App.current.session.setUser(member);
+ App.current.session.data.amapId = amap.id;
+ throw Redirect("/member/view/" + member.id );
+ }
+
+ @tpl('member/lastMessages.mtt')
+ function doLastMessages(member:db.User){
+
+ var out = new Array<{date:Date,subject:String,success:String,failure:String}>();
+ var threeMonth = DateTools.delta(Date.now(), -1000.0 * 60 * 60 * 24 * 30.5 * 3);
+
+ for ( m in sugoi.db.BufferedMail.manager.search($remoteId == app.user.amap.id && $cdate > threeMonth, {limit:10, orderBy:-cdate})){
+
+ var status : sugoi.mail.IMailer.MailerResult = m.status;
+
+ if ( status!=null && status.get(member.email)!=null ){
+
+ var r = m.getMailerResultMessage(member.email);
+ out.push( {date:m.cdate,subject:m.title,success:r.success,failure:r.failure} );
+ }
+
+ }
+ view.emails = out;
+ }
+
+ /**
+ * Edit a Member
+ */
+ @tpl('form.mtt')
+ function doEdit(member:db.User) {
+
+ if (member.isAdmin() && !app.user.isAdmin()) throw Error("/", t._("You cannot modify the account of an administrator"));
+
+ var form = sugoi.form.Form.fromSpod(member);
+
+ //cleaning
+ form.removeElement( form.getElement("rights") );
+ form.removeElement( form.getElement("lang") );
+ form.removeElement( form.getElement("ldate") );
+ form.removeElement( form.getElement("apiKey") );
+
+
+ var isReg = member.isFullyRegistred();
+ var groupNum = db.UserAmap.manager.count($userId == member.id);
+
+ //an administrator can modify a user's email only if he's not member elsewhere
+ if (groupNum > 1){
+ form.removeElementByName("email");
+ form.removeElementByName("email2");
+ app.session.addMessage(t._("For security reasons, you cannot modify the e-mail of this person because this person is a member of more than 1 group."));
+ }
+
+ //an administrator can modify a user's pass only if he's a not registred user.
+ if (!isReg){
+ app.session.addMessage(t._("This person did not define yet a password. You are exceptionaly authorized to do it. Please don't forget to tell this person."));
+ form.getElement("pass").required = false;
+ }else{
+ form.removeElement( form.getElement("pass") );
+ }
+
+ if (form.checkToken()) {
+
+ if (app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && form.getValueOf("phone") == null ){
+ throw Error("/member/edit/"+member.id, t._("Phone number is required in this group."));
+ }
+
+ form.toSpod(member);
+
+ //check that the given emails are not already used elsewhere
+ var sim = db.User.getSameEmail(member.email,member.email2);
+ for ( s in sim) {
+ if (s.id == member.id) sim.remove(s);
+ }
+ if (sim.length > 0) {
+
+ //Let's merge the 2 users if it has no orders.
+ var id = sim.first().id;
+ if (db.UserContract.manager.search( $userId == id || $userId2 == id , false).length == 0) {
+ //merge
+ member.merge( sim.first() );
+ app.session.addMessage(t._("This e-mail was used by another user account. As this user account was not used, it has been merged into the current user account."));
+
+ } else {
+ var str = t._("Warning, this e-mail or this name already exists for another account : ");
+ str += Lambda.map(sim, function(u) return "" + u.getCoupleName() + "").join(",");
+ str += " "+t._("These accounts can't be merged because the second account has orders");
+ throw Error("/member/edit/" + member.id, str);
+ }
+ }
+
+ if (!isReg) member.setPass(form.getValueOf("pass"));
+
+ member.update();
+
+ if (!App.config.DEBUG && groupNum == 1) {
+
+ //warn the user that his email has been updated
+ if (form.getValueOf("email") != member.email) {
+ var m = new sugoi.mail.Mail();
+ m.setSender(App.config.get("default_email"), t._("Cagette.net"));
+ m.addRecipient(member.email);
+ m.setSubject(t._("Change your e-mail in your account Cagette.net"));
+ m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:app.user.getName() + t._(" just modified your e-mail in your account Cagette.net. Your e-mail is now:")+form.getValueOf("email") } ) );
+ App.sendMail(m);
+
+ }
+ if (form.getValueOf("email2") != member.email2 && member.email2!=null) {
+ var m = new sugoi.mail.Mail();
+ m.setSender(App.config.get("default_email"),"Cagette.net");
+ m.addRecipient(member.email2);
+ m.setSubject(t._("Change the e-mail of your account Cagette.net"));
+ m.setHtmlBody( app.processTemplate("mail/message.mtt", { text:app.user.getName() +t._(" just modified your e-mail in your account Cagette.net. Your e-mail is now:")+form.getValueOf("email2") } ) );
+ App.sendMail(m);
+ }
+ }
+
+ throw Ok('/member/view/'+member.id, t._("This member has beed updated"));
+ }
+
+ view.form = form;
+
+ }
+
+ /**
+ * Remove a user from this group
+ */
+ function doDelete(user:db.User,?args:{confirm:Bool,token:String}) {
+
+ if (checkToken()) {
+ if (!app.user.canAccessMembership()) throw t._("You cannot do that.");
+ if (user.id == app.user.id) throw Error("/member/view/" + user.id, t._("You cannot delete yourself."));
+ if ( Lambda.count(user.getOrders(app.user.amap),function(x) return x.quantity>0) > 0 && !args.confirm) {
+ throw Error("/member/view/"+user.id, t._("Warning, this account has orders. Remove anyway", {userid:user.id, argstoken:args.token}));
+ }
+
+ var ua = db.UserAmap.get(user, app.user.amap, true);
+ if (ua != null) {
+ ua.delete();
+ throw Ok("/member", t._("::user:: has been removed from your group",{user:user.getName()}));
+ }else {
+ throw Error("/member", t._("This person does not belong to \"::amapname::\"", {amapname:app.user.amap.name}));
+ }
+ }else {
+ throw Redirect("/member/view/"+user.id);
+ }
+ }
+
+ @tpl('form.mtt')
+ function doMerge(user:db.User) {
+
+ if (!app.user.canAccessMembership()) throw Error("/","Action interdite");
+
+ view.title = t._("Merge an account with another one");
+ view.text = t._("This action allows you to merge two accounts (when you have duplicates in the database for example). Contracts of account 2 will be moved to account 1, and account 2 will be deleted. Warning, it is not possible to cancel this action.");
+
+ var form = new Form("merge");
+
+ var members = app.user.amap.getMembers();
+ var members = Lambda.array(Lambda.map(members, function(x) return { key:Std.string(x.id), value:x.getName() } ));
+ var mlist = new Selectbox("member1", t._("Account 1"), members, Std.string(user.id));
+ form.addElement( mlist );
+ var mlist = new Selectbox("member2", t._("Account 2"), members);
+ form.addElement( mlist );
+
+ if (form.checkToken()) {
+
+ var m1 = Std.parseInt(form.getElement("member1").value);
+ var m2 = Std.parseInt(form.getElement("member2").value);
+ var m1 = db.User.manager.get(m1,true);
+ var m2 = db.User.manager.get(m2,true);
+
+ //if (m1.amapId != m2.amapId) throw "ils ne sont pas de la même amap !";
+
+ //on prend tout à m2 pour donner à m1
+ //change usercontracts
+ var contracts = db.UserContract.manager.search($user==m2 || $user2==m2,true);
+ for (c in contracts) {
+ if (c.user.id == m2.id) c.user = m1;
+ if (c.user2!=null && c.user2.id == m2.id) c.user2 = m1;
+ c.update();
+ }
+
+ //group memberships
+ var adh = db.UserAmap.manager.search($user == m2, true);
+ for ( a in adh) {
+ a.user = m1;
+ a.update();
+ }
+
+ //change contacts
+ var contacts = db.Contract.manager.search($contact==m2,true);
+ for (c in contacts) {
+ c.contact = m1;
+ c.update();
+ }
+ //if (m2.amap.contact == m2) {
+ //m1.amap.lock();
+ //m1.amap.contact = m1;
+ //m1.amap.update();
+ //}
+
+ m2.delete();
+
+ throw Ok("/member/view/" + m1.id, t._("Both accounts have been merged"));
+
+
+ }
+
+ view.form = form;
+
+ }
+
+
+ @tpl('member/import.mtt')
+ function doImport(?args: { confirm:Bool } ) {
+
+ var step = 1;
+ var request = Utils.getMultipart(1024 * 1024 * 4); //4mb
+
+ //on recupere le contenu de l'upload
+ var data = request.get("file");
+ if ( data != null) {
+
+ var csv = new sugoi.tools.Csv();
+ csv.setHeaders([t._("Firstname"), t._("Lastname"), t._("E-mail"), t._("Mobile phone"), t._("Partner's firstname"), t._("Partner's lastname"), t._("Partner's e-mail"), t._("Partner's Mobile phone"), t._("Address 1"), t._("Address 2"), t._("Post code"), t._("City")]);
+
+ //utf8 encode if needed
+ try{
+ if (!haxe.Utf8.validate(data)){
+ data = haxe.Utf8.encode(data);
+ }
+ }catch (e:Dynamic){ }
+ var unregistred = csv.importDatas(data);
+
+ /*var checkEmail = function(email){
+ if ( !sugoi.form.validators.EmailValidator.check(email) ) {
+ throw Error("/member", t._("The email ::email:: is invalid, please update your CSV file",{email:email}) );
+ }
+ }*/
+
+ //cleaning
+ for ( user in unregistred.copy() ) {
+
+ //check nom+prenom
+ if (user[0] == null || user[1] == null) {
+ throw Error("/member/import", t._("You must fill the name and the firstname of the person. This line is incomplete: ") + user);
+ }
+ if (user[2] == null) {
+ throw Error("/member/import", t._("Each person must have an e-mail to be able to log in. ::user0:: ::user1:: don't have one. ", {user0:user[0], user1:user[1]}) +user);
+ }
+ //uppercase du nom
+ if (user[1] != null) user[1] = user[1].toUpperCase();
+ if (user[5] != null) user[5] = user[5].toUpperCase();
+ //lowercase email
+ if (user[2] != null){
+ user[2] = user[2].toLowerCase();
+ //checkEmail(user[2]);
+ }
+ if (user[6] != null){
+ user[6] = user[6].toLowerCase();
+ //checkEmail(user[6]);
+ }
+ }
+
+ //utf-8 check
+ for ( row in unregistred.copy()) {
+
+ for ( i in 0...row.length) {
+ var t = row[i];
+ if (t != "" && t != null) {
+ try{
+ if (!Utf8.validate(t)) {
+ t = Utf8.encode(t);
+ }
+ }catch (e:Dynamic) {}
+ row[i] = t;
+ }
+ }
+ }
+
+ //put already registered people in another list
+ var registred = [];
+ for (r in unregistred.copy()) {
+ //var firstName = r[0];
+ //var lastName = r[1];
+ var email = r[2];
+
+ //var firstName2 = r[4];
+ //var lastName2 = r[5];
+ var email2 = r[6];
+
+ var us = db.User.getSameEmail(email, email2);
+
+ if (us.length > 0) {
+ unregistred.remove(r);
+ registred.push(r);
+ }
+ }
+
+
+ app.session.data.csvUnregistered = unregistred;
+ app.session.data.csvRegistered = registred;
+
+ view.data = unregistred;
+ view.data2 = registred;
+ step = 2;
+ }
+
+
+ if (args != null && args.confirm) {
+
+ //import unregistered members
+ var i : Iterable = cast app.session.data.csvUnregistered;
+ for (u in i) {
+ if (u[0] == null || u[0] == "null" || u[0] == "") continue;
+
+ var user = new db.User();
+ user.firstName = u[0];
+ user.lastName = u[1];
+ user.email = u[2];
+ if (user.email != null && user.email != "null" &&!EmailValidator.check(user.email)) {
+ throw t._("The E-mail ::useremail:: is invalid, please modify your file", {useremail:user.email});
+ }
+ user.phone = u[3];
+
+ user.firstName2 = u[4];
+ user.lastName2 = u[5];
+ user.email2 = u[6];
+ if (user.email2 != null && user.email2 != "null" && !EmailValidator.check(user.email2)) {
+ App.log(u);
+ throw t._("The E-mail of the partner of ::userFirstName:: ::userLastName:: '::userEmail::' is invalid, please check your file", {userFirstName:user.firstName, userLastName:user.lastName, userEmail:user.email2});
+ }
+ user.phone2 = u[7];
+ user.address1 = u[8];
+ user.address2 = u[9];
+ user.zipCode = u[10];
+ user.city = u[11];
+ user.insert();
+
+ var ua = new db.UserAmap();
+ ua.user = user;
+ ua.amap = app.user.amap;
+ ua.insert();
+ }
+
+ //import registered members
+ var i : Iterable> = cast app.session.data.csvRegistered;
+ for (u in i) {
+ var email = u[2];
+ var email2 = u[6];
+
+ var us = db.User.getSameEmail(email, email2);
+ var userAmaps = db.UserAmap.manager.search($amap == app.user.amap && $userId in Lambda.map(us, function(u) return u.id), false);
+
+ //member exists but is not member of this group.
+ if (userAmaps.length == 0) {
+ var ua = new db.UserAmap();
+ ua.user = us.first();
+ ua.amap = app.user.amap;
+ ua.insert();
+ }
+ }
+
+ view.numImported = app.session.data.csvUnregistered.length + app.session.data.csvRegistered.length;
+ app.session.data.csvUnregistered = null;
+ app.session.data.csvRegistered = null;
+
+ step = 3;
+ }
+
+ if (step == 1) {
+ //reset import when back to import page
+ app.session.data.csvUnregistered = null;
+ app.session.data.csvRegistered = null;
+ }
+
+ view.step = step;
+ }
+
+ @tpl("user/insert.mtt")
+ public function doInsert() {
+
+ if (!app.user.canAccessMembership()) throw Error("/", t._("Forbidden action"));
+
+ var m = new db.User();
+ var form = sugoi.form.Form.fromSpod(m);
+ form.removeElement(form.getElement("lang"));
+ form.removeElement(form.getElement("rights"));
+ form.removeElement(form.getElement("pass"));
+ form.removeElement(form.getElement("ldate") );
+ form.removeElement( form.getElement("apiKey") );
+ form.addElement(new sugoi.form.elements.Checkbox("warnAmapManager", t._("Send an E-mail to the person in charge of the group"), true));
+ form.getElement("email").addValidator(new EmailValidator());
+ form.getElement("email2").addValidator(new EmailValidator());
+
+ if (form.isValid()) {
+
+ //check doublon de User et de UserAmap
+ var userSims = db.User.getSameEmail(form.getValueOf("email"),form.getValueOf("email2"));
+ view.userSims = userSims;
+ var userAmaps = db.UserAmap.manager.search($amap == app.user.amap && $userId in Lambda.map(userSims, function(u) return u.id), false);
+ view.userAmaps = userAmaps;
+
+ if (userAmaps.length > 0) {
+ //user deja enregistré dans cette amap
+ throw Error('/member/view/' + userAmaps.first().user.id, t._("This person is already member of this group"));
+
+ }else if (userSims.length > 0) {
+ //des users existent avec ce nom ,
+ //if (userSims.length == 1) {
+ // si yen a qu'un on l'inserte
+ var ua = new db.UserAmap();
+ ua.user = userSims.first();
+ ua.amap = app.user.amap;
+ ua.insert();
+ throw Ok('/member/', t._("This person already had an account on Cagette.net, and is now member of your group."));
+ /*}else {
+ //demander validation avant d'inserer le userAmap
+ //TODO
+ throw Error('/member', t._("Not possible to add this person because there are already some people in the database having the same firstname and name. Please contact the administrator.")+userSims);
+ }*/
+ return;
+ }else {
+
+ if (app.user.amap.flags.has(db.Amap.AmapFlags.PhoneRequired) && form.getValueOf("phone") == null ){
+ throw Error("/member/insert", t._("Phone number is required in this group."));
+ }
+
+ //insert user
+ var u = new db.User();
+ form.toSpod(u);
+ u.lang = app.user.lang;
+ u.insert();
+
+ //insert userAmap
+ var ua = new db.UserAmap();
+ ua.user = u;
+ ua.amap = app.user.getAmap();
+ ua.insert();
+
+ if (form.getValueOf("warnAmapManager") == "1") {
+ var url = "http://" + App.config.HOST + "/member/view/" + u.id;
+ var text = t._("::admin:: just keyed-in contact details of a new member: ::newMember:: See contact details",{admin:app.user.getName(),newMember:u.getCoupleName(),url:url});
+ App.quickMail(
+ app.user.getAmap().contact.email,
+ app.user.amap.name +" - "+ t._("New member") + " : " + u.getCoupleName(),
+ app.processTemplate("mail/message.mtt", { text:text } )
+ );
+ }
+
+ throw Ok('/member/', t._("This person is now member of the group"));
+
+ }
+ }
+
+ view.form = form;
+ }
+
+
+ /**
+ * user payments history
+ */
+ @tpl('member/payments.mtt')
+ function doPayments(m:db.User){
+
+ service.PaymentService.updateUserBalance(m, app.user.amap);
+ var browse:Int->Int->List;
+
+ //default display
+ browse = function(index:Int, limit:Int) {
+ return db.Operation.getOperationsWithIndex(m,app.user.amap,index,limit,true);
+ }
+
+ var count = db.Operation.countOperations(m,app.user.amap);
+ var rb = new sugoi.tools.ResultsBrowser(count, 10, browse);
+ view.rb = rb;
+ view.member = m;
+ view.balance = db.UserAmap.get(m, app.user.amap).balance;
+
+ checkToken();
+ }
+
+ @tpl('member/balance.mtt')
+ function doBalance(){
+ view.balanced = db.UserAmap.manager.search($amap == app.user.amap && $balance == 0.0, false);
+ view.credit = db.UserAmap.manager.search($amap == app.user.amap && $balance > 0, false);
+ view.debt = db.UserAmap.manager.search($amap == app.user.amap && $balance < 0, false);
+ }
+
+}
\ No newline at end of file
diff --git a/src/controller/Membership.hx b/src/controller/Membership.hx
new file mode 100755
index 0000000..c7bb938
--- /dev/null
+++ b/src/controller/Membership.hx
@@ -0,0 +1,73 @@
+package controller;
+import sugoi.form.elements.IntSelect;
+using Std;
+/**
+ * Membership management
+ *
+ * @author fbarbut
+ */
+class Membership extends controller.Controller
+{
+
+ @tpl("membership/default.mtt")
+ function doDefault(member:db.User) {
+ var userAmap = db.UserAmap.get(member, app.user.amap,true);
+ if (userAmap == null) throw Error("/member", t._("This person is not a member of your group"));
+
+ //formulaire
+ var f = new sugoi.form.Form("membership");
+ var year = Date.now().getFullYear();
+ var data = [];
+ var now = Date.now();
+ for ( x in 0...5) {
+
+ var y = now.getFullYear() - x;
+ var yy = DateTools.delta(now, DateTools.days(365) * -x);
+ data.push({label:app.user.amap.getPeriodName(yy),value:app.user.amap.getMembershipYear(yy)});
+ }
+ f.addElement(new IntSelect("year", t._("Period"), data,app.user.amap.getMembershipYear(),true));
+ f.addElement(new sugoi.form.elements.DateDropdowns("date", t._("Date of payment of subscription"), null, true));
+ if (f.isValid()) {
+ var y : Int = f.getValueOf("year");
+
+ if (db.Membership.get(member, app.user.amap, y) != null) throw Error("/membership/"+member.id, t._("This subscription has been already keyed-in"));
+
+ var cotis = new db.Membership();
+ cotis.amap = app.user.amap;
+ cotis.user = member;
+ cotis.year = y;
+ cotis.date = f.getElement("date").value;
+ cotis.insert();
+ throw Ok("/membership/"+member.id, t._("Subscription saved"));
+ }
+
+ //années de cotisation
+ var memberships = db.Membership.manager.search($user == member && $amap == app.user.amap,{orderBy:-year}, false);
+ //for ( m in memberships) {
+ //Reflect.setField(m, 'yearDate', new Date(m.year, 1, 1, 1, 1, 1));
+ //}
+ view.memberships = memberships;
+
+ //view
+ view.form = f;
+ view.member = member;
+ checkToken();
+ }
+
+
+ public function doDelete(member:db.User, year:Int,?args:{token:String}) {
+
+ if (checkToken()) {
+ var cotis = db.Membership.get(member, app.user.amap, year, true);
+ if (cotis == null) throw Error("/", t._("This subscription does not exist"));
+
+ cotis.delete();
+ throw Ok("/membership/" + member.id, t._("Subscription deleted"));
+ }else {
+ throw Error("/", "Bad Token");
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/controller/Messages.hx b/src/controller/Messages.hx
new file mode 100755
index 0000000..cff5eae
--- /dev/null
+++ b/src/controller/Messages.hx
@@ -0,0 +1,211 @@
+package controller;
+import db.Message;
+import db.UserContract;
+import sugoi.form.ListData;
+import sugoi.form.elements.*;
+import sugoi.form.Form;
+
+class Messages extends Controller
+{
+
+ public function new()
+ {
+ super();
+ if (!app.user.canAccessMessages()) throw Redirect("/");
+ }
+
+ @tpl("messages/default.mtt")
+ function doDefault() {
+
+ var form = new Form("msg");
+
+ var senderName = "";
+ var senderMail = "";
+
+ if (App.current.session.data.whichUser == 1 && app.user.email2 != null) {
+ senderMail = app.user.email2;
+ senderName = app.user.firstName2 + " " + app.user.lastName2;
+
+ }else {
+ senderMail = app.user.email;
+ senderName = app.user.firstName + " " + app.user.lastName;
+ }
+
+ var lists = getLists();
+ form.addElement( new StringInput("senderName", t._("Sender name"),senderName,true));
+ form.addElement( new StringInput("senderMail", t._("Sender E-Mail"),senderMail,true));
+ form.addElement( new StringSelect("list", t._("Recipients"),lists,"1", true,null,"style='width:500px;'"));
+ form.addElement( new StringInput("subject", t._("Subject:"),"",false,null,"style='width:500px;'") );
+ form.addElement( new TextArea("text", t._("Message:"), "", false, null, "style='width:500px;height:350px;'") );
+
+ if (form.checkToken()) {
+
+ var listId = form.getElement("list").value;
+ var dest = getSelection(listId);
+ var mails = [];
+ for ( d in dest) {
+ if (d.email != null) mails.push(d.email);
+ if (d.email2 != null) mails.push(d.email2);
+ }
+
+ //send mail confirmation link
+ var e = new sugoi.mail.Mail();
+ e.setSubject(form.getValueOf("subject"));
+ for ( x in mails) e.addRecipient(x);
+
+ e.setSender(App.config.get("default_email"),form.getValueOf("senderName"));
+ e.setReplyTo(form.getValueOf("senderMail"),form.getValueOf("senderName"));
+ //sender : default email ( explicitly tells that the server send an email on behalf of the user )
+ //e.setHeader("Sender", App.config.get("default_email"));
+ var text :String = form.getValueOf("text");
+ var html = app.processTemplate("mail/message.mtt", { text:text,group:app.user.amap,list:getListName(listId) });
+ e.setHtmlBody(html);
+
+ App.sendMail(e,app.user.getAmap(),listId,app.user);
+
+ //store message
+ var lm = new db.Message();
+ lm.amap = app.user.amap;
+ lm.recipients = Lambda.array(Lambda.map(e.getRecipients(), function(x) return x.email));
+ lm.title = e.getSubject();
+ lm.date = Date.now();
+ lm.body = e.getHtmlBody();
+ if (listId != null) lm.recipientListId = listId;
+ lm.sender = app.user;
+ lm.insert();
+
+
+ throw Ok("/messages", t._("The message has been sent"));
+ }
+
+ view.form = form;
+
+ if (app.user.isAmapManager()) {
+ view.sentMessages = Message.manager.search($amap == app.user.amap && $recipientListId!=null, {orderBy:-date,limit:20}, false);
+ }else {
+ view.sentMessages = Message.manager.search($sender == app.user && $recipientListId!=null && $amap == app.user.amap , {orderBy:-date,limit:20}, false);
+ }
+
+ }
+
+ @tpl("messages/message.mtt")
+ public function doMessage(msg:Message) {
+
+ if (!app.user.isAmapManager() && msg.sender.id != app.user.id) throw Error("/", t._("Non authorized access"));
+
+ view.list = getListName(msg.recipientListId);
+ view.msg = msg;
+
+ //make status easier to display
+ var s = new Array<{email:String,success:String,failure:String}>();
+ /*if (msg.status != null){
+ for ( k in msg.status.keys()) {
+ var r = msg.getMailerResultMessage(k);
+ s.push({email:k,success:r.success,failure:r.failure});
+ }
+ }*/
+
+ view.status = s;
+
+ }
+
+ function getLists() :FormData{
+ var out = [
+ {value:'1', label: t._("Everyone")},
+ {value:'2', label: t._("The board: persons in charge + contracts + memberships")},
+ ];
+
+ out.push( { value:'3', label: t._("TEST: me + spouse") } );
+ out.push( { value:'4', label: t._("Members without contract/order") } );
+ if(app.user.amap.hasMembership()) out.push( { value:'5', label:t._("Memberships to be renewed")} );
+
+
+ var contracts = db.Contract.getActiveContracts(app.user.amap,true);
+ for ( c in contracts) {
+ var label = t._("Subscribers") + " " + c.toString();
+ out.push({value:'c'+c.id,label:label});
+ }
+ return out ;
+
+ }
+
+ /**
+ * get list name from id
+ * @param listId
+ */
+ function getListName(listId:String) {
+ var l = getLists();
+
+ for (ll in l) {
+ if (ll.value == listId) return ll.label;
+ }
+
+ return null;
+
+ }
+
+ function getSelection(listId:String) {
+ if (listId.substr(0, 1) == "c") {
+ //contrats
+ var contract = Std.parseInt(listId.substr(1));
+
+ var pids = db.Product.manager.search($contractId == contract, false);
+ var pids = Lambda.map(pids, function(x) return x.id);
+ var up = db.UserContract.manager.search($productId in pids, false);
+
+
+ var users = [];
+ for ( order in up) {
+ if (!Lambda.has(users, order.user)) {
+ users.push(order.user);
+ }
+ if (order.user2 != null && !Lambda.has(users, order.user2)) {
+ users.push(order.user2);
+ }
+ }
+ return users;
+
+ }else {
+ var out = [];
+ switch(listId) {
+ case "1":
+ //tout le monde
+ out = Lambda.array(app.user.amap.getMembers());
+
+ case "2":
+ var users = [];
+ users.push(app.user.amap.contact);
+ for ( c in db.Contract.manager.search($amap == app.user.amap)) {
+ if (!Lambda.has(users, c.contact)) {
+ users.push(c.contact);
+ }
+ }
+
+ //ajouter les autres personnes ayant les droits Admin ou Gestion Adhérents ou Gestion Contrats
+ for (ua in Lambda.array(db.UserAmap.manager.search($rights != null && $amap == app.user.amap, false))) {
+ if (ua.hasRight(GroupAdmin) || ua.hasRight(Membership) || ua.hasRight(ContractAdmin())) {
+ if (!Lambda.has(users, ua.user)) users.push(ua.user);
+ }
+ }
+
+ out = users;
+
+ case "3":
+ //moi
+ return [app.user];
+ case "4":
+ return Lambda.array(db.User.getUsers_NoContracts());
+ case "5":
+ return Lambda.array(db.User.getUsers_NoMembership());
+ }
+
+ return out;
+
+ }
+ }
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/controller/Place.hx b/src/controller/Place.hx
new file mode 100755
index 0000000..91b61ff
--- /dev/null
+++ b/src/controller/Place.hx
@@ -0,0 +1,87 @@
+package controller;
+import sugoi.form.Form;
+
+/**
+ * Place controller
+ */
+class Place extends Controller
+{
+
+ public function new()
+ {
+ super();
+ }
+
+ @tpl('place/view.mtt')
+ function doView(place:db.Place) {
+ view.place = place;
+
+ //build adress for google maps
+ var addr = "";
+ if (place.address1 != null) addr += place.address1;
+ if (place.address2 != null) addr += ", " + place.address2;
+ if (place.zipCode != null) addr += " " + place.zipCode;
+ if (place.city != null) addr += " " + place.city;
+
+ view.addr = view.escapeJS(addr);
+ }
+
+ @tpl('form.mtt')
+ function doEdit(p:db.Place) {
+
+ var currentAddress = p.getAddress();
+
+ var f = sugoi.form.Form.fromSpod(p);
+
+ if (f.isValid()) {
+
+ f.toSpod(p);
+
+ if(currentAddress!=p.getAddress() || p.lat==null){
+ try{
+ service.PlaceService.geocode(p);
+ }catch(e:Dynamic){
+ App.current.session.addMessage(t._("Oops, we're unable to find where is located this address. This place will not be shown on the map.")+' $e',true);
+ }
+ }
+
+ p.amap = app.user.amap;
+ p.update();
+ throw Ok('/contractAdmin',t._("this place has been updated"));
+ }
+
+ view.form = f;
+ view.title = t._("Edit a place");
+ }
+
+ @tpl("form.mtt")
+ public function doInsert() {
+
+ var d = new db.Place();
+ var f = sugoi.form.Form.fromSpod(d);
+
+ if (f.isValid()) {
+ f.toSpod(d);
+ d.amap = app.user.amap;
+ d.insert();
+ throw Ok('/contractAdmin',t._("The place has been registred") );
+ }
+
+ view.form = f;
+ view.title = t._("Register a new delivery place");
+ }
+
+ public function doDelete(p:db.Place) {
+ if (!app.user.isAmapManager()) throw "forbidden";
+ if (checkToken()) {
+
+ if (db.Distribution.manager.search($placeId == p.id).length > 0)
+ throw Error('/contractAdmin', t._("You can't delete this place because one or more distributions are linked to this place.") );
+
+ p.lock();
+ p.delete();
+ throw Ok("/contractAdmin", t._("Place deleted") );
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/controller/Plugin.hx b/src/controller/Plugin.hx
new file mode 100755
index 0000000..1cfc2f1
--- /dev/null
+++ b/src/controller/Plugin.hx
@@ -0,0 +1,39 @@
+package controller;
+
+/**
+ * ...
+ * @author fbarbut
+ */
+class Plugin extends sugoi.BaseController
+{
+
+ public function new()
+ {
+ super();
+ }
+
+ #if plugins
+
+ //cagette-hosted
+ public function doHosted(d:haxe.web.Dispatch) {
+ d.dispatch(new hosted.controller.Main());
+ }
+
+ //cagette-pro
+ public function doPro(d:haxe.web.Dispatch) {
+ d.dispatch(new pro.controller.Main());
+ }
+
+ //cagette-connector
+ public function doConnector(d:haxe.web.Dispatch) {
+ d.dispatch(new connector.controller.Main());
+ }
+
+ //cagette-wholesale-order
+ public function doWho(d:haxe.web.Dispatch) {
+ d.dispatch(new who.controller.Main());
+ }
+
+ #end
+
+}
\ No newline at end of file
diff --git a/src/controller/Product.hx b/src/controller/Product.hx
new file mode 100755
index 0000000..911e4d8
--- /dev/null
+++ b/src/controller/Product.hx
@@ -0,0 +1,339 @@
+package controller;
+import sugoi.form.Form;
+import Common;
+import sugoi.form.ListData.FormData;
+import sugoi.form.elements.FloatInput;
+import sugoi.form.elements.FloatSelect;
+import sugoi.form.elements.IntSelect;
+using Std;
+
+class Product extends Controller
+{
+ public function new()
+ {
+ super();
+ view.nav = ["contractadmin","products"];
+ }
+
+ @tpl('form.mtt')
+ function doEdit(d:db.Product) {
+
+ if (!app.user.canManageContract(d.contract)) throw t._("Forbidden access");
+
+ var f = sugoi.form.Form.fromSpod(d);
+
+ //stock mgmt ?
+ if (!d.contract.hasStockManagement()) f.removeElementByName('stock');
+
+ //VAT selector
+ f.removeElement( f.getElement('vat') );
+ var data :FormData = [];
+ for (k in app.user.amap.vatRates.keys()) {
+ data.push( { label:k, value:app.user.amap.vatRates[k] } );
+ }
+ f.addElement( new FloatSelect("vat", "TVA", data, d.vat ) );
+
+ f.removeElementByName("contractId");
+
+ //Product Taxonomy:
+ //view.taxo = db.TxpProduct.manager.all();
+ //f.addElement(new form.TxpProduct("txpProduct", "taxo",null,false) );
+ var txId = d.txpProduct == null ? "" : Std.string(d.txpProduct.id);
+ var html = '';
+ f.addElement(new sugoi.form.elements.Html("html",html, 'Nom'),1);
+
+ if (f.isValid()) {
+
+ f.toSpod(d);
+ app.event(EditProduct(d));
+ d.update();
+ throw Ok('/contractAdmin/products/'+d.contract.id, t._("The product has been updated"));
+ }else{
+ app.event(PreEditProduct(d));
+ }
+
+ view.form = f;
+ view.title = t._("Modify a product");
+ }
+
+ @tpl("form.mtt")
+ public function doInsert(contract:db.Contract ) {
+
+ if (!app.user.isContractManager(contract)) throw Error("/", t._("Forbidden action"));
+
+ var d = new db.Product();
+ var f = sugoi.form.Form.fromSpod(d);
+
+ f.removeElementByName("contractId");
+
+ //stock mgmt ?
+ if (!contract.hasStockManagement()) f.removeElementByName('stock');
+
+ //vat selector
+ f.removeElement( f.getElement('vat') );
+ var data = [];
+ for (k in app.user.amap.vatRates.keys()) {
+ data.push( { value:app.user.amap.vatRates[k], label:k } );
+ }
+ f.addElement( new FloatSelect("vat", "TVA", data, d.vat ) );
+
+ var formName = f.name;
+ var html = '';
+ f.addElement(new sugoi.form.elements.Html("html",html, 'Nom'),1);
+
+ if (f.isValid()) {
+ f.toSpod(d);
+ d.contract = contract;
+ app.event(NewProduct(d));
+ d.insert();
+ throw Ok('/contractAdmin/products/'+d.contract.id, t._("The product has been saved"));
+ }else{
+ app.event(PreNewProduct(contract));
+ }
+
+ view.form = f;
+ view.title = t._("Key-in a new product");
+ }
+
+ public function doDelete(p:db.Product) {
+
+ if (!app.user.canManageContract(p.contract)) throw t._("Forbidden access");
+
+ if (checkToken()) {
+
+ app.event(DeleteProduct(p));
+
+ var orders = db.UserContract.manager.search($productId == p.id, false);
+ if (orders.length > 0) {
+ throw Error("/contractAdmin", t._("Not possible to delete this product because some orders are referencing it"));
+ }
+ var cid = p.contract.id;
+ p.lock();
+ p.delete();
+
+ throw Ok("/contractAdmin/products/"+cid, t._("Product deleted"));
+ }
+ throw Error("/contractAdmin", t._("Token error"));
+ }
+
+
+ @tpl('product/import.mtt')
+ function doImport(c:db.Contract, ?args: { confirm:Bool } ) {
+
+ if (!app.user.canManageContract(c)) throw t._("Forbidden access");
+
+ var csv = new sugoi.tools.Csv();
+ csv.step = 1;
+ var request = sugoi.tools.Utils.getMultipart(1024 * 1024 * 4);
+ csv.setHeaders( ["productName","price","ref","desc","qt","unit","organic","floatQt","vat","stock"] );
+ view.contract = c;
+
+ // get the uploaded file content
+ if (request.get("file") != null) {
+ //convert to utf-8 if needed
+ var csvData = request.get("file");
+ try{
+ if (!haxe.Utf8.validate(csvData)){
+ csvData = haxe.Utf8.encode(csvData);
+ }
+ }catch (e:Dynamic){ }
+ var datas = csv.importDatasAsMap(csvData);
+
+ app.session.data.csvImportedData = datas;
+
+ csv.step = 2;
+ view.csv = csv;
+ }
+
+ if (args != null && args.confirm) {
+ var i : Iterable