Added a Sales Sheet page along with other changes.
This commit is contained in:
@@ -303,16 +303,8 @@
|
||||
<item url="file://C:/Tools/.meteor/packages/juliancwirko_s-alert/3.2.0/web.browser/client/s-alert-default.css" />
|
||||
<item url="file://C:/Tools/.meteor/packages/juliancwirko_s-alert/3.2.0/web.browser/client/s-alert.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/reactive-var/1.0.11/web.browser/reactive-var.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/findone.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/upsert.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/service-configuration/1.0.11/web.browser/service_configuration_common.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/users-compat.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/find.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/insert.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/collection-hooks.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/remove.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/allow-deny/1.0.5/web.browser/allow-deny.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/update.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/sha/1.0.9/web.browser/sha256.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/allow-deny/1.0.5/web.browser/allow-deny-tests.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/srp/1.0.10/web.browser/srp.js" />
|
||||
@@ -334,8 +326,6 @@
|
||||
<item url="file://C:/Tools/.meteor/packages/logging/1.1.16/web.browser/logging_test.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/logging/1.1.16/web.browser/logging.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/logging/1.1.16/web.browser/logging_cordova.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/zimme_collection-behaviours/1.0.4/web.browser/packages/zimme_collection-behaviours.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/zimme_collection-softremovable/1.0.5/web.browser/softremovable.coffee" />
|
||||
<item url="file://C:/Tools/.meteor/packages/ejson/1.0.13/web.browser/stringify.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/ejson/1.0.13/web.browser/ejson.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/es5-shim/4.6.15/web.browser/export_globals.js" />
|
||||
@@ -715,16 +705,8 @@
|
||||
<root url="file://C:/Tools/.meteor/packages/juliancwirko_s-alert/3.2.0/web.browser/client/s-alert-default.css" />
|
||||
<root url="file://C:/Tools/.meteor/packages/juliancwirko_s-alert/3.2.0/web.browser/client/s-alert.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/reactive-var/1.0.11/web.browser/reactive-var.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/findone.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/upsert.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/service-configuration/1.0.11/web.browser/service_configuration_common.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/users-compat.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/find.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/insert.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/collection-hooks.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/remove.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/allow-deny/1.0.5/web.browser/allow-deny.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/matb33_collection-hooks/0.8.4/web.browser/update.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/sha/1.0.9/web.browser/sha256.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/allow-deny/1.0.5/web.browser/allow-deny-tests.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/srp/1.0.10/web.browser/srp.js" />
|
||||
@@ -746,8 +728,6 @@
|
||||
<root url="file://C:/Tools/.meteor/packages/logging/1.1.16/web.browser/logging_test.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/logging/1.1.16/web.browser/logging.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/logging/1.1.16/web.browser/logging_cordova.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/zimme_collection-behaviours/1.0.4/web.browser/packages/zimme_collection-behaviours.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/zimme_collection-softremovable/1.0.5/web.browser/softremovable.coffee" />
|
||||
<root url="file://C:/Tools/.meteor/packages/ejson/1.0.13/web.browser/stringify.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/ejson/1.0.13/web.browser/ejson.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/es5-shim/4.6.15/web.browser/export_globals.js" />
|
||||
|
||||
4520
.idea/libraries/meteor_packages_auto_import_npm.xml
generated
4520
.idea/libraries/meteor_packages_auto_import_npm.xml
generated
File diff suppressed because it is too large
Load Diff
1107
.idea/workspace.xml
generated
1107
.idea/workspace.xml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,17 +13,17 @@ reactive-dict@1.1.8 # ???
|
||||
jquery@1.11.10 # Helpful client-side library
|
||||
tracker@1.1.1 # Meteor's client-side reactive programming library
|
||||
tomwasd:history-polyfill # Adds IE 8/9 support for HTML5 history.
|
||||
email # Adds the Meteor/Email package for sending lost password emails
|
||||
email@1.1.18 # Adds the Meteor/Email package for sending lost password emails
|
||||
|
||||
standard-minifier-css@1.3.2 # CSS minifier run for production mode
|
||||
standard-minifier-js@1.2.1 # JS minifier run for production mode
|
||||
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
|
||||
poorvavyas:es6-shim
|
||||
ecmascript # Enable ECMAScript2015+ syntax in app code
|
||||
ecmascript@0.6.1 # Enable ECMAScript2015+ syntax in app code
|
||||
|
||||
#accounts-ui
|
||||
#accounts-base
|
||||
accounts-password
|
||||
accounts-password@1.3.3
|
||||
useraccounts:core
|
||||
useraccounts:bootstrap
|
||||
useraccounts:flow-routing # Configures email flows. Used for AccountsTemplates class.
|
||||
@@ -34,18 +34,18 @@ arillo:flow-router-helpers # Provides various template helpers such as {{pathFo
|
||||
#tomwasd:flow-router-seo
|
||||
kadira:blaze-layout
|
||||
|
||||
shell-server # ???
|
||||
shell-server@0.2.1 # ???
|
||||
meteortoys:allthings
|
||||
stylus
|
||||
session
|
||||
stylus@2.513.8
|
||||
session@1.1.7
|
||||
##browser-policy # Adds support for specifying browser level security rules related to content and what's allowed to laod on the page.
|
||||
check # Allows for checking the structure and types of arguments passed to Meteor methods and publications.
|
||||
check@1.2.4 # Allows for checking the structure and types of arguments passed to Meteor methods and publications.
|
||||
#audit-argument-checks # Used in combination with the Check package for checking the structure and types of arguments passed to Meteor methods and publications. Automatically alerts when a method or publication does not use a check() call.
|
||||
|
||||
aldeed:simple-schema@1.5.3
|
||||
aldeed:collection2@2.10.0
|
||||
matb33:collection-hooks # Allows the collections to register handlers that run before or after database interactions.
|
||||
zimme:collection-softremovable
|
||||
#matb33:collection-hooks # Allows the collections to register handlers that run before or after database interactions.
|
||||
#zimme:collection-softremovable
|
||||
|
||||
#aldeed:autoform@5.8.1
|
||||
#aldeed:collection2-core@2.0.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@1.4.2.3
|
||||
METEOR@1.4.2.7
|
||||
|
||||
@@ -52,7 +52,6 @@ launch-screen@1.0.12
|
||||
livedata@1.0.18
|
||||
localstorage@1.0.12
|
||||
logging@1.1.16
|
||||
matb33:collection-hooks@0.8.4
|
||||
mdg:validation-error@0.2.0
|
||||
meteor@1.6.0
|
||||
meteor-base@1.0.4
|
||||
@@ -127,5 +126,3 @@ useraccounts:flow-routing@1.14.2
|
||||
webapp@1.3.12
|
||||
webapp-hashing@1.0.9
|
||||
zimme:active-route@2.3.2
|
||||
zimme:collection-behaviours@1.0.4
|
||||
zimme:collection-softremovable@1.0.5
|
||||
|
||||
100
README.md
100
README.md
@@ -9,18 +9,23 @@ At runtime (production environment), the application should be deployed on a lin
|
||||
Package this application by running 'npm run build' from the command line in the project base directory, or double click the npm "build" script in the development environment.
|
||||
|
||||
1. Install MongoDB on the linux machine.
|
||||
1. `sudo apt-get update`
|
||||
1. `sudo apt-get install -y mongodb` (installs 2.6.10 on Ubuntu LTS 16.04 - that is the minimum version of mongo required for Meteor 1.4 - you should install the latest mongo: 3.2 per the meteor install instructions)
|
||||
2. `sudo apt-get update`
|
||||
3. `sudo apt-get install -y mongodb` (installs 2.6.10 on Ubuntu LTS 16.04 - that is the minimum version of mongo required for Meteor 1.4 - you should install the latest mongo: 3.2 per the meteor install instructions)
|
||||
|
||||
**OR**
|
||||
|
||||
See [instructions](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) for installing newer versions of MongoDB (RECOMMENDED)
|
||||
1. Get the version of the mongo command line tool: `sudo mongo --version`
|
||||
1. Get the version of the mongo server: `sudo mongod --version`
|
||||
1. Start/stop/restart the mongo service: `sudo service mongod stop/start/restart`
|
||||
1. View logs: `/var/log/mongodb/mongod.log`
|
||||
1. Edit config: `/etc/mongod.conf` (see [docs](https://docs.mongodb.com/manual/reference/configuration-options/))
|
||||
1. Run tools: (see [descriptions](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/#packages))
|
||||
4. Get the version of the mongo command line tool: `sudo mongo --version`
|
||||
5. Get the version of the mongo server: `sudo mongod --version`
|
||||
6. Start/stop/restart the mongo service: `sudo service mongod stop/start/restart`
|
||||
7. View logs: `/var/log/mongodb/mongod.log`
|
||||
8. Edit config: `/etc/mongod.conf` (see [docs](https://docs.mongodb.com/manual/reference/configuration-options/))
|
||||
9. Make the mongo service run all the time. (SystemCtl)
|
||||
1. Edit the Mongo Service file: `sudo nano mongod.service` See docs for options.
|
||||
2. Reload the SystemCtl daemon: `sudo systemctl daemon-reload`
|
||||
3. Start the Mongod process: `sudo systemctl start mongod`
|
||||
4. Modify the system to always start Mongod: `sudo systemctl enable mongod`
|
||||
10. Run tools: (see [descriptions](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/#packages))
|
||||
- mongod (the mongo database server)
|
||||
- mongo (command line shell for interacting with a running db)
|
||||
- mongoimport (imports data from: Extended JSON, CSV, or TSV formats created by mongoexport or 3rd party tools)
|
||||
@@ -33,26 +38,26 @@ Package this application by running 'npm run build' from the command line in the
|
||||
- mongorestore
|
||||
- mongostat
|
||||
- mongotop (stats)
|
||||
1. Install nodejs & npm
|
||||
11. Install nodejs & npm
|
||||
1. `sudo apt-get install nodejs`
|
||||
1. `sudo apt-get install npm`
|
||||
1. Install n
|
||||
2. `sudo apt-get install npm`
|
||||
12. Install n
|
||||
1. `sudo npm install -g n`
|
||||
1. Update nodejs to 4.7.0 (for use with meteor 1.4 - may be another nodejs version for newer meteor versions): `n bin 4.7.0` (may need to first download 4.7.0 - see other docs on linux & nodejs)
|
||||
13. Update nodejs to 4.7.0 (for use with meteor 1.4 - may be another nodejs version for newer meteor versions): `n bin 4.7.0` (may need to first download 4.7.0 - see other docs on linux & nodejs)
|
||||
1. `sudo n 4.7.0`
|
||||
1. `sudo npm install -g npm` (updates npm?)
|
||||
1. Install nginx on the linux machine.
|
||||
1. Install samba on the linux machine. (`sudo nano /etc/samba/smb.conf` & `sudo service smbd restart`)
|
||||
2. `sudo npm install -g npm` (updates npm?)
|
||||
14. Install nginx on the linux machine.
|
||||
15. Install samba on the linux machine. (`sudo nano /etc/samba/smb.conf` & `sudo service smbd restart`)
|
||||
1. Share the /var/www directory using the www-data user.
|
||||
1. Share the /etc/nginx directory using the root user (optional & security risk - makes configuring /etc/nginx/sites-available easier).
|
||||
1. Share the nginx logs directory /var/logs/nginx using a standard user with read only access (maybe www-data?).
|
||||
1. Install passenger which will glue the nginx web server to one or more instances of nodejs/meteor running on the machine. (See passenger site for updated install instructions - these are specific to Ubuntu)
|
||||
2. Share the /etc/nginx directory using the root user (optional & security risk - makes configuring /etc/nginx/sites-available easier).
|
||||
3. Share the nginx logs directory /var/logs/nginx using a standard user with read only access (maybe www-data?).
|
||||
16. Install passenger which will glue the nginx web server to one or more instances of nodejs/meteor running on the machine. (See passenger site for updated install instructions - these are specific to Ubuntu)
|
||||
1. `sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7`
|
||||
1. `sudo apt-get install -y apt-transport-https ca-certificates`
|
||||
1. `sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger xenial main > /etc/apt/sources.list.d/passenger.list'`
|
||||
1. `sudo apt-get update`
|
||||
1. `sudo apt-get install -y nginx-extras passenger`
|
||||
1. Configure the nginx sites-available file ("PTapp") to link nginx to passenger, and provide passenger the settings to startup meteor. Example: (replace {{xxxx}} with your own content) (NOTE: setting the mail_url fails, I used a release.properties file in the app's private directory instead, with code to use the properties file first in the server.js file in meteor.)
|
||||
2. `sudo apt-get install -y apt-transport-https ca-certificates`
|
||||
3. `sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger xenial main > /etc/apt/sources.list.d/passenger.list'`
|
||||
4. `sudo apt-get update`
|
||||
5. `sudo apt-get install -y nginx-extras passenger`
|
||||
6. Configure the nginx sites-available file ("PTapp") to link nginx to passenger, and provide passenger the settings to startup meteor. Example: (replace {{xxxx}} with your own content) (NOTE: setting the mail_url fails, I used a release.properties file in the app's private directory instead, with code to use the properties file first in the server.js file in meteor.)
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
@@ -73,17 +78,46 @@ Package this application by running 'npm run build' from the command line in the
|
||||
}
|
||||
```
|
||||
1. Restart nginx: `sudo service nginx restart`
|
||||
1. Get status with `sudo passenger-status`
|
||||
1. Stop passenger & nginx: `sudo service nginx stop` (use start or restart or reload also)
|
||||
1. Create an app folder in /var/www (example: PTApp)
|
||||
1. Unpack the packaged application into this new folder (easily done via the samba share on the windows development machine by drag and dropping the contents of the archive created in step 1).
|
||||
2. Get status with `sudo passenger-status`
|
||||
3. Stop passenger & nginx: `sudo service nginx stop` (use start or restart or reload also)
|
||||
17. Create an app folder in /var/www (example: PTApp)
|
||||
18. Unpack the packaged application into this new folder (easily done via the samba share on the windows development machine by drag and dropping the contents of the archive created in step 1).
|
||||
1. Install the app with npm (navigate to /var/www/PTApp/bundle/programs/server): `npm install --production`
|
||||
1. **Restart the passenger app each time the application is updated**: `sudo passenger-config restart-app /var/www/PTApp`
|
||||
1. Use RDP to connect to the GUI for the linux server
|
||||
1. Install Mongo Chef (free version) to connect to the db on localhost:27017
|
||||
2. **Restart the passenger app each time the application is updated**: `sudo passenger-config restart-app /var/www/PTApp`
|
||||
19. Use RDP to connect to the GUI for the linux server
|
||||
20. Install Mongo Chef (free version) to connect to the db on localhost:27017
|
||||
1. Download the archive.
|
||||
1. Move the unpacked archive into /opt
|
||||
1. Modify the ~/.bashrc to include: `PATH=$PATH:/opt/mongochef-3.5.0-linux-x86-dist/bin` at the end (not sure if this is necessary).
|
||||
1. Run Mongo Chef from the GUI environment (how? - probably use the start menu -> Run, then pass the path like above + the executable name)
|
||||
2. Move the unpacked archive into /opt
|
||||
3. Modify the ~/.bashrc to include: `PATH=$PATH:/opt/mongochef-3.5.0-linux-x86-dist/bin` at the end (not sure if this is necessary).
|
||||
4. Run Mongo Chef from the GUI environment (how? - probably use the start menu -> Run, then pass the path like above + the executable name)
|
||||
|
||||
NOTE: Use MongoBooster on a windows development machine to connect to the dev database (localhost:3001) and to export.
|
||||
|
||||
#Updating a Meteor Deployment
|
||||
1. Run the NPM script for building the app. This can be done either from Webstorm by viewing the NPM display (shows a list of scripts in the package.json file), or typing `npm run build` from the command line. Alternatively you can simply type the build command in the command line: `meteor build --server-only ../` to build it. The command should exit with code zero for success.
|
||||
2. Find the archive file: it should be in the parent directory if you ran the above script exactly, otherwise it is where ever you specified (path at the end of the command). It should be called "PetitTetonMeteor.tar.gz" as of the writing of this documentation.
|
||||
3. Copy the archive to the server. Use what ever tools you want for this, samba and drag and drop works great. Otherwise sftp, or other method also works.
|
||||
4. Navigate to /var/www/PTApp (or what ever the folder is).
|
||||
5. Use `sudo tar -xvzf PetitTetonMeteor.tar.gz` to unpack it.
|
||||
6. Delete the archive (optional): `sudo rm PetitTetonMeteor.tar.gz`
|
||||
7. Modify the owner of the app: `sudo chown -R www-data bundle` Run this from inside the project directory /var/www/PTApp.
|
||||
8. Modify the permissions of the app: `sudo chmod -R 777 bundle` Run this from inside the project directory /var/www/PTApp.
|
||||
9. Optional: Run NPM's install to update the dependancies (if they changed): `cd /var/www/PTApp/bundle/programs/server && npm install && cd /var/www/PTApp`
|
||||
10. Restart the meteor app: `sudo passenger-config restart-app /var/www/PTApp`
|
||||
|
||||
# Updating a Meteor Deployment #2
|
||||
|
||||
1. Run the NPM script for building the app `npm run build` which will package the app for the selected platform (in the package.json definition for the build script). Can double click the build script in WebStorm's UI for NPM alternatively. Can also manually run the script: `npm install --product && meteor build --architecture os.linux.x86_64 --server-only ../`. This will generate an archive file for the project that is production ready.
|
||||
2. Copy the archive to the deployment server. Can use Samba for this. I place it in the web folder for the app.
|
||||
3. Run the deploy.sh script `sudo ./deploy.sh` which exists in the web directory for the app: `/var/www/PTApp`. This will unpack the archive, remove the archive, change file permissions, run NPM's install on the app, and restart the app in Phusion Passenger.
|
||||
4. Look at the debug output by viewing the html files stored in `/tmp`. Use Samba to view them remotely.
|
||||
5. Look at the Nginx logs (should be the same as the stuff in /tmp).
|
||||
|
||||
|
||||
|
||||
#Running Server Side Code
|
||||
This is useful for importing data or running scripts that might perform some one time task.
|
||||
1. Open a console and enter the server shell for meteor.
|
||||
|
||||
#Server Error Handling
|
||||
Errors are generated in meteor, but handled by passenger. Passenger will log the error on the client screen with a code, and will log the same message in the nginx error logs. The error will be findable in the tmp folder as an html file starting with passenger and ending with the code. This is the place to look for the real error output.
|
||||
@@ -6,6 +6,7 @@ import '/imports/ui/helpers.js';
|
||||
// import '/imports/util/normalize.css';
|
||||
import '/imports/util/validator.js';
|
||||
import '/imports/util/polyfills/blaze.js';
|
||||
import '/imports/util/polyfills/regex.js';
|
||||
import '/imports/util/polyfills/date.js';
|
||||
import '/imports/util/polyfills/array.js';
|
||||
import '/imports/util/de.combo.js';
|
||||
@@ -16,7 +17,10 @@ import '/imports/ui/layouts/Full.js';
|
||||
import '/imports/ui/accounts/accounts.js';
|
||||
import '/imports/util/select2/select2.css';
|
||||
import '/imports/util/select2/select2.full.js';
|
||||
//The SweetAlert2 NPM package is where this is being pulled from. The js file that actually wants to use it should import it (see Sales.js).
|
||||
import 'sweetalert2/dist/sweetalert2.min.css';
|
||||
import '/imports/util/simplegrid.css';
|
||||
import 'dragula/dist/dragula.css';
|
||||
|
||||
Blaze._allowJavascriptUrls();
|
||||
|
||||
|
||||
154
client/main.styl
154
client/main.styl
@@ -17,32 +17,14 @@ html
|
||||
scrollbar-arrow-color: #ffffff
|
||||
scrollbar-track-color: #505050
|
||||
height: 100%
|
||||
|
||||
html, body, #archives ul, #overall-footer, #content ul
|
||||
margin: 0 0 0 0
|
||||
padding: 0 0 0 0
|
||||
|
||||
min-height: 100%
|
||||
body
|
||||
font-family: verdana, arial, helvetica, sans-serif
|
||||
font-size: 12px
|
||||
height: 100%
|
||||
|
||||
min-height: 100%
|
||||
#__blaze-root
|
||||
//max-width: 950px //Use if the width should be limited.
|
||||
//min-width: 250px
|
||||
//margin: 0 auto //Use if the width should be limited.
|
||||
//min-height: 100% //Used with a full height layout.
|
||||
|
||||
//Flex container.
|
||||
//display: flex
|
||||
//flex-direction: row
|
||||
//flex-wrap: nowrap
|
||||
//align-items: stretch
|
||||
//align-content: stretch
|
||||
height: 100%;
|
||||
//width: 100%;
|
||||
//min-height: 100%;
|
||||
//min-width: 100%;
|
||||
height: 100%
|
||||
|
||||
//Standard Stylings
|
||||
.noselect
|
||||
@@ -69,7 +51,6 @@ body
|
||||
|
||||
//Table Styles
|
||||
.table
|
||||
border: 0
|
||||
padding: 0
|
||||
margin: 0
|
||||
border-collapse: collapse
|
||||
@@ -108,123 +89,6 @@ body
|
||||
.table-hover > tbody > tr:hover
|
||||
background-color: #ded
|
||||
|
||||
//Form Styles
|
||||
.select2-container
|
||||
font-size: 10px
|
||||
.select2-selection
|
||||
font-size: 13px //Make the font small enough the control can have a height similar to a standard input field.
|
||||
margin-bottom: 0px
|
||||
min-height: 10px !important //This is what really sets the height of the box containing the selection(s)
|
||||
padding-bottom: 2px //Add a little space below the selections to balance it all out.
|
||||
input
|
||||
padding: 6px
|
||||
border-radius: 4px
|
||||
border-width: 1px
|
||||
border-style: solid
|
||||
border-color: #ccc
|
||||
//input[type='button'].btn-success, input[type='submit'].btn-success
|
||||
// background-color: #5cb85c
|
||||
// :hover
|
||||
// background-color:
|
||||
//input[type='button'].btn-danger, input[type='submit'].btn-danger
|
||||
// background-color: #e55b46
|
||||
.form-control, .select2-selection //?
|
||||
font-size: 14px
|
||||
margin-bottom: 0px
|
||||
.form-group
|
||||
margin: 4px 0
|
||||
.has-error .form-control
|
||||
border-color: #a94442
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0)
|
||||
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control
|
||||
line-height: 34px
|
||||
|
||||
.form-control
|
||||
display: block
|
||||
width: 100%
|
||||
height: 34px
|
||||
padding: 6px 12px
|
||||
font-size: 14px
|
||||
line-height: 1.42857143
|
||||
color: #555
|
||||
background-color: #fff
|
||||
background-image: none
|
||||
border: 1px solid #ccc
|
||||
border-radius: 4px
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s
|
||||
input[type="date" i], input[type="datetime-local" i], input[type="month" i], input[type="time" i], input[type="week" i]
|
||||
align-items: center
|
||||
-webkit-padding-start: 1px
|
||||
overflow: hidden
|
||||
padding-left: 10px
|
||||
input
|
||||
-webkit-appearance: textfield
|
||||
background-color: white
|
||||
-webkit-rtl-ordering: logical
|
||||
user-select: text
|
||||
cursor: auto
|
||||
padding: 1px
|
||||
border-width: 2px
|
||||
border-style: inset
|
||||
border-color: initial
|
||||
border-image: initial
|
||||
.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control
|
||||
background-color: #eee
|
||||
opacity: 1
|
||||
input, textarea, keygen, select, button
|
||||
text-rendering: auto
|
||||
color: initial
|
||||
letter-spacing: normal
|
||||
word-spacing: normal
|
||||
text-transform: none
|
||||
text-indent: 0px
|
||||
text-shadow: none
|
||||
display: inline-block
|
||||
text-align: start
|
||||
margin: 0em 0em 0em 0em
|
||||
font: 13.3333px Arial
|
||||
input, textarea, keygen, select, button, meter, progress
|
||||
-webkit-writing-mode: horizontal-tb
|
||||
//.btn.disabled, .btn[disabled], fieldset[disabled] .btn
|
||||
// cursor: not-allowed
|
||||
// filter: unquote("alpha(opacity=65)")
|
||||
// -webkit-box-shadow: none
|
||||
// box-shadow: none
|
||||
// opacity: .65
|
||||
//button, html input[type="button"], input[type="reset"], input[type="submit"]
|
||||
// -webkit-appearance: button
|
||||
// cursor: pointer
|
||||
//button, html input[type="button"], input[type="reset"], input[type="submit"]
|
||||
// -webkit-appearance: button
|
||||
// cursor: pointer
|
||||
//.btn
|
||||
// display: inline-block;
|
||||
// padding: 6px 12px;
|
||||
// margin-bottom: 0;
|
||||
// font-size: 14px;
|
||||
// font-weight: normal;
|
||||
// line-height: 1.42857143;
|
||||
// text-align: center;
|
||||
// white-space: nowrap;
|
||||
// vertical-align: middle;
|
||||
// -ms-touch-action: manipulation;
|
||||
// touch-action: manipulation;
|
||||
// cursor: pointer;
|
||||
// -webkit-user-select: none;
|
||||
// -moz-user-select: none;
|
||||
// -ms-user-select: none;
|
||||
// user-select: none;
|
||||
// background-image: none;
|
||||
// border: 1px solid transparent;
|
||||
// border-radius: 4px;
|
||||
|
||||
.pagination
|
||||
text-align: right
|
||||
font-size: 15px
|
||||
@@ -232,7 +96,8 @@ input, textarea, keygen, select, button, meter, progress
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
margin: 0 0 10px 0
|
||||
overflow: visible
|
||||
whitespace: nowrap
|
||||
white-space: nowrap
|
||||
display: inline-block
|
||||
span
|
||||
padding: 2px 8px 3px 8px
|
||||
margin: 0 8px
|
||||
@@ -288,6 +153,12 @@ input, textarea, keygen, select, button, meter, progress
|
||||
overflow: visible !important
|
||||
max-width: none !important
|
||||
|
||||
@import "../imports/ui/styles/effects.import.styl"
|
||||
@import "../imports/ui/styles/buttons.import.styl"
|
||||
@import "../imports/ui/styles/maxHeightLayout.import.styl"
|
||||
@import "../imports/ui/styles/tabs.import.styl"
|
||||
@import "../imports/ui/styles/forms.import.styl"
|
||||
|
||||
//@import "../imports/util/selectize/selectize.default.import.styl"
|
||||
//@import "../imports/util/selectize/selectize.import.styl"
|
||||
@import "../imports/util/de.combo.import.styl"
|
||||
@@ -302,6 +173,9 @@ input, textarea, keygen, select, button, meter, progress
|
||||
@import "../imports/ui/Products.import.styl"
|
||||
@import "../imports/ui/ProductTags.import.styl"
|
||||
@import "../imports/ui/Sales.import.styl"
|
||||
@import "../imports/ui/SalesSheets.import.styl"
|
||||
@import "../imports/ui/SalesSheetForm.import.styl"
|
||||
@import "../imports/ui/SalesSheetEditor.import.styl"
|
||||
@import "../imports/ui/Pricing.import.styl"
|
||||
@import "../imports/ui/Production.import.styl"
|
||||
@import "../imports/ui/Graphs.import.styl"
|
||||
@@ -53,6 +53,11 @@ let SalesSchema = new SimpleSchema({
|
||||
// }
|
||||
// }
|
||||
},
|
||||
comment: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
label: "Created On",
|
||||
@@ -62,7 +67,7 @@ let SalesSchema = new SimpleSchema({
|
||||
Sales.attachSchema(SalesSchema);
|
||||
|
||||
if(Meteor.isServer) {
|
||||
Meteor.publish('sales', function(query, limit = 100, skipCount) {
|
||||
Meteor.publish('sales', function(query, sort, limit = 100, skipCount) {
|
||||
let dbQuery = [];
|
||||
|
||||
if(query) {
|
||||
@@ -90,7 +95,7 @@ if(Meteor.isServer) {
|
||||
if(!_.isNumber(skipCount) || skipCount < 0) skipCount = 0;
|
||||
|
||||
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
|
||||
return Meteor.collections.Sales.find(dbQuery, {limit: limit, sort: {date: -1, createdAt: -1}, skip: skipCount});
|
||||
return Meteor.collections.Sales.find(dbQuery, {limit: limit, sort, skip: skipCount});
|
||||
});
|
||||
// time: expects either undefined, 'weekly', or 'monthly'
|
||||
// options: expects either undefined, 'markets', or 'types'
|
||||
@@ -179,6 +184,21 @@ if(Meteor.isServer) {
|
||||
return Sales.find(query).count();
|
||||
},
|
||||
insertSale: function(sale) {
|
||||
check(sale, {
|
||||
date: Date,
|
||||
amount: Match.Where(function(x) {
|
||||
check(x, Number);
|
||||
return x > 0;
|
||||
}),
|
||||
price: Match.Where(function(x) {
|
||||
check(x, Number);
|
||||
return x > 0;
|
||||
}),
|
||||
measureId: String,
|
||||
productId: String,
|
||||
venueId: String,
|
||||
comment: Match.Optional(String)
|
||||
});
|
||||
//TODO: Check the structure of sale. Use: check(sale, {name: String, ...});
|
||||
sale.createdAt = new Date();
|
||||
|
||||
@@ -193,9 +213,45 @@ if(Meteor.isServer) {
|
||||
check(id, String);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Sales.remove(id, {bypassCollection2: true});
|
||||
Sales.remove(id);
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
editSaleComment: function(id, comment) {
|
||||
check(id, String);
|
||||
check(comment, String);
|
||||
//Trim and convert empty comment to undefined.
|
||||
comment = comment ? comment.trim() : undefined;
|
||||
comment = comment && comment.length > 0 ? comment : undefined;
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
console.log("Changed comment of " + id + " to: " + comment);
|
||||
|
||||
if(comment) {
|
||||
Sales.update(id, {$set: {comment}}, function(error, count) {
|
||||
if(error) throw new Meteor.Error(400, "Unexpected database error: " + error);
|
||||
});
|
||||
}
|
||||
else {
|
||||
Sales.update(id, {$unset: {comment: ""}}, function(error, count) {
|
||||
if(error) throw new Meteor.Error(400, "Unexpected database error: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
updateSale: function(id, date, venueId, price, amount) {
|
||||
check(id, String);
|
||||
check(date, Date);
|
||||
check(venueId, String);
|
||||
check(price, Number);
|
||||
check(amount, Number);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Sales.update(id, {$set: {date, venueId, price, amount}}, function(err, id) {
|
||||
if(err) console.log(err);
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
160
imports/api/SalesSheet.js
Normal file
160
imports/api/SalesSheet.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
import { check } from 'meteor/check';
|
||||
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
|
||||
|
||||
SalesSheets = new Mongo.Collection('SalesSheets');
|
||||
|
||||
const SalesSheetSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
label: "Name",
|
||||
optional: false,
|
||||
trim: true,
|
||||
index: 1,
|
||||
unique: false
|
||||
},
|
||||
products: { //An ordered array of product id's included on the sheet.
|
||||
type: Array,
|
||||
label: "products",
|
||||
optional: false,
|
||||
defaultValue: []
|
||||
},
|
||||
'products.$': {
|
||||
type: new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
label: "Name",
|
||||
optional: false,
|
||||
trim: true,
|
||||
unique: false
|
||||
},
|
||||
productId: { //Note: Will be non-existent for headings.
|
||||
type: String,
|
||||
label: "Product ID",
|
||||
trim: false,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true
|
||||
},
|
||||
measureIds: { //Note: Will be non-existent for headings.
|
||||
type: [String],
|
||||
label: "Measure IDs",
|
||||
optional: true
|
||||
}
|
||||
//measureIds: {
|
||||
// type: Array,
|
||||
// label: "Measure IDs",
|
||||
// optional: true
|
||||
//},
|
||||
//'measureIds.$': {
|
||||
// type: String,
|
||||
// label: "Measure ID",
|
||||
// trim: false,
|
||||
// regEx: SimpleSchema.RegEx.Id,
|
||||
// optional: false
|
||||
//}
|
||||
})
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
label: "Created On",
|
||||
optional: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
label: "Updated On",
|
||||
optional: true
|
||||
}
|
||||
});
|
||||
SalesSheets.attachSchema(SalesSheetSchema);
|
||||
|
||||
if(Meteor.isServer) {
|
||||
Meteor.publish('salesSheets', function() {
|
||||
return SalesSheets.find({});
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
createSalesSheet: function(name) {
|
||||
check(name, String);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
return SalesSheets.insert({name, products: [], createdAt: new Date()});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
// This gets ridiculous. What would be required, along with a ton of code to micro manage each change.
|
||||
//updateSalesSheet_addProduct: function(id, productId, productName, productMeasures) {
|
||||
//
|
||||
//},
|
||||
//updateSalesSheet_removeProduct: function(id, productId) {
|
||||
//
|
||||
//},
|
||||
//updateSalesSheet_updateProduct: function(id, productId, productName) {
|
||||
//
|
||||
//},
|
||||
//updateSalesSheet_updateProduct_addMeasure: function(id, productId, productName, productMeasures) {
|
||||
//
|
||||
//},
|
||||
//updateSalesSheet_updateProduct_removeMeasure: function(id, productId, productName, productMeasures) {
|
||||
//
|
||||
//},
|
||||
updateSalesSheet: function(id, name, products) {
|
||||
check(id, String);
|
||||
check(name, String);
|
||||
check(products, [{
|
||||
productId: Match.Maybe(String),
|
||||
name: String,
|
||||
measureIds: Match.Maybe([String])
|
||||
}]);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
try {
|
||||
// Generates some queries for testing.
|
||||
//console.log("db.SalesSheet.update({_id: " + id + "}, {{name: " + name + ", products: " + products + ", updatedAt: " + new Date() + "}})");
|
||||
//let productList = "";
|
||||
//let firstProduct = true;
|
||||
//for(next of products) {
|
||||
// if(firstProduct) firstProduct = false;
|
||||
// else productList += ',';
|
||||
// productList += '{id:"' + next.id + '",name:"' + next.name + '",measureIds:[';
|
||||
// let firstMeasure = true;
|
||||
// for(measureId of next.measureIds) {
|
||||
// if(firstMeasure) firstMeasure = false;
|
||||
// else productList += ',';
|
||||
// productList += '"' + measureId + '"';
|
||||
// }
|
||||
// productList += ']}';
|
||||
//}
|
||||
//console.log("db.SalesSheet.update({_id: '" + id + "'}, {$set: {name: '" + name + "', updatedAt: " + new Date() + "}, $pull: {$exists: true}, $pushAll: [" + productList + "]})");
|
||||
|
||||
// Forces the object to be re-written, versus piecemeal updated.
|
||||
SalesSheets.update({_id: id}, {$set: {name: name, products: products, updatedAt: new Date()}}, {validate: false}, function(err, count) {
|
||||
if(err) console.log(err);
|
||||
});
|
||||
|
||||
// Attempts to remove all products and re-add them. Note: Does not work!
|
||||
//SalesSheet.update({_id: id}, {$set: {name: name, updatedAt: new Date()}, $pull: {products: {$exists: true}}}, {bypassCollection2: true}, function(err, count) {
|
||||
// if(err) console.log(err);
|
||||
//});
|
||||
//SalesSheet.update({_id: id}, {$push: {products: {$each: [products]}}}, {bypassCollection2: true}, function(err, count) {
|
||||
// if(err) console.log(err);
|
||||
//});
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
removeSalesSheet: function(id) {
|
||||
check(id, String);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
SalesSheets.remove(id);
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default SalesSheets;
|
||||
@@ -46,16 +46,16 @@ if(Meteor.isServer) Meteor.publish('venues', function() {
|
||||
return Venues.find({});
|
||||
});
|
||||
|
||||
// //Requires: meteor add matb33:collection-hooks
|
||||
if(Meteor.isServer) {
|
||||
Venues.before.insert(function(userId, doc) {
|
||||
// check(userId, String);
|
||||
doc.createdAt = new Date();
|
||||
});
|
||||
Venues.before.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
modifier.$set = modifier.$set || {}; //Make sure there is an object.
|
||||
modifier.$set.updatedAt = new Date();
|
||||
});
|
||||
// //Requires: meteor add matb33:collection-hooks
|
||||
//Venues.before.insert(function(userId, doc) {
|
||||
// // check(userId, String);
|
||||
// doc.createdAt = new Date();
|
||||
//});
|
||||
//Venues.before.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
// modifier.$set = modifier.$set || {}; //Make sure there is an object.
|
||||
// modifier.$set.updatedAt = new Date();
|
||||
//});
|
||||
|
||||
Meteor.methods({
|
||||
createVenue: function(name, type) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//import Categories from "./Category.js";
|
||||
//import Subcategories from "./Subcategory.js";
|
||||
import Measures from "./Measure.js";
|
||||
import Venues from "./Venue.js";
|
||||
import Products from "./Product.js";
|
||||
import ProductTags from "./ProductTag.js";
|
||||
import Sales from "./Sale.js";
|
||||
import SalesSheets from "./SalesSheet.js";
|
||||
import Users from "./User.js";
|
||||
import UserRoles from "./Roles.js";
|
||||
|
||||
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, Users, UserRoles};
|
||||
//Save the collections in the Meteor.collections property for easy access without name conflicts.
|
||||
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, SalesSheets, Users, UserRoles};
|
||||
|
||||
//If this is the server then setup the default admin user if none exist.
|
||||
if(Meteor.isServer) {
|
||||
//Change this to find admin users, create a default admin user if none exists.
|
||||
if(Users.find({}).count() == 0) {
|
||||
|
||||
@@ -40,9 +40,18 @@ pri.route('/sales', {
|
||||
name: 'Sales',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/Sales.js");
|
||||
|
||||
BlazeLayout.render('Body', {content: 'Sales'});
|
||||
}
|
||||
});
|
||||
pri.route('/salesSheets', {
|
||||
name: 'SalesSheets',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/SalesSheets.js");
|
||||
|
||||
BlazeLayout.render('Body', {content: 'SalesSheets'});
|
||||
}
|
||||
});
|
||||
pri.route('/production', {
|
||||
name: 'Production',
|
||||
action: function(params, queryParams) {
|
||||
|
||||
@@ -267,6 +267,5 @@ Template.ProductEditor.events({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -1,44 +1,48 @@
|
||||
<template name="Sales">
|
||||
<div id="salesMain">
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="insertSale">
|
||||
{{>InsertSale}}
|
||||
<div class="paginationContainer">
|
||||
<div class="tableControls">
|
||||
<select name="sortSelect">
|
||||
<option value="date" selected>Sale Date</option>
|
||||
<option value="createdAt">Data Entry Date</option>
|
||||
</select>
|
||||
<div class="pagination">
|
||||
<span class="prevButton noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextButton noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="salesListRow">
|
||||
<div class="salesListCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr class="headers">
|
||||
<th class="amount noselect nonclickable">Amount</th>
|
||||
<th class="product noselect nonclickable">Product</th>
|
||||
<th class="price noselect nonclickable">Price</th>
|
||||
<th class="measure noselect nonclickable">Measure</th>
|
||||
<th class="date noselect nonclickable">Date (Week)</th>
|
||||
<th class="venue noselect nonclickable">Venue</th>
|
||||
<th class="actions noselect nonclickable">Actions</th>
|
||||
</tr>
|
||||
<tr class="footers">
|
||||
<th>{{>SaleSearch columnName='amount' width='90%'}}</th>
|
||||
<th>{{>SaleSearch columnName='productId' collectionQueryColumnName='name' collection='Products' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th>{{>SaleSearch columnName='price' width='90%'}}</th>
|
||||
<th>{{>SaleSearch columnName='measureId' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th></th>
|
||||
<th>{{>SaleSearch columnName='venueId' collectionQueryColumnName='name' collection='Venues' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th></th>
|
||||
<tr>
|
||||
<th class="amount noselect nonclickable">Amount {{>SaleSearch columnName='amount' width='90%'}}</th>
|
||||
<th class="product noselect nonclickable">Product <br/>{{>SaleSearch columnName='productId' collectionQueryColumnName='name' collection='Products' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th class="price noselect nonclickable">Price {{>SaleSearch columnName='price' width='90%'}}</th>
|
||||
<th class="measure noselect nonclickable">Measure {{>SaleSearch columnName='measureId' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th class="saleDate noselect nonclickable">Date (Week)</th>
|
||||
<th class="createdDate noselect nonclickable">Created On</th>
|
||||
<th class="venue noselect nonclickable">Venue {{>SaleSearch columnName='venueId' collectionQueryColumnName='name' collection='Venues' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th class="actions noselect nonclickable">Actions <span class="newSaleButton btn btn-success" title="Create Sale"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span> <i class="fa fa-commenting fa-lg showOnlyComments clickable" title="Show Commented Sales" aria-hidden="true"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewSale}}
|
||||
{{> InsertSale}}
|
||||
{{/if}}
|
||||
{{#each sales}}
|
||||
{{#if editing}}
|
||||
{{> SaleEditor}}
|
||||
{{else}}
|
||||
{{> Sale}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -50,9 +54,32 @@
|
||||
<td class="tdLarge noselect nonclickable left">{{productName productId}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatPrice price}}{{#if showTotalPrice amount}} ({{formatTotalPrice price amount}}){{/if}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{measureName measureId}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDate date}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDateAndWeek date}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDate createdAt}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{venueName venueId}}</td>
|
||||
<td class="tdLarge noselect left"><i class="fa fa-times-circle fa-lg saleRemove clickable" aria-hidden="true"></i></td>
|
||||
<td class="tdLarge noselect left"><i class="fa fa-pencil-square-o fa-lg actionEdit noselect clickable" title="Edit" aria-hidden="true"></i> <i class="fa fa-commenting fa-lg editComment noselect clickable {{commentClass}}" aria-hidden="true"></i> <i class="fa fa-times-circle fa-lg saleRemove noselect clickable" aria-hidden="true"></i></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="SaleEditor">
|
||||
<tr>
|
||||
<td colspan="7" class="saleEditor">
|
||||
<form name="editSaleForm">
|
||||
<div class="grid">
|
||||
<div class="col-6-12">
|
||||
<div class="editorDiv heading">{{productName}} - {{measureName measureId}}</div>
|
||||
<div class="editorDiv"><label>Date</label><input name="date" class="form-control" type="date" data-schema-key='date' required></div>
|
||||
<div class="editorDiv"><label>Venue</label><input name="venue" class="form-control" type="text" required/></div>
|
||||
</div>
|
||||
<div class="col-6-12">
|
||||
<div class="editorDiv"><label>Amount</label><input type="number" class="form-control amount" name="amount" min="0" step="0.01" data-schema-key='amount' value="{{amount}}" required></div>
|
||||
<div class="editorDiv"><label>Price</label><div class="priceContainer"><input type="number" class="form-control price" name="price" min="0" step="0.01" data-schema-key='currency' value="{{price}}" required><div class="priceButtons"><i class="fa fa-cogs setDefaultPrice noselect clickable" title="Calculate Default Price" aria-hidden="true"></i></div></div></div>
|
||||
<div class="editorDiv"><label>Total</label><input type="number" class="form-control total" name="total" data-schema-key='currency' value="{{total}}" tabindex="-1" readonly></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td class="center productEditorTd noselect"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" title="Save" aria-hidden="true"></i> / <i class="editorCancel fa fa-times-circle fa-lg noselect clickable" title="Cancel" aria-hidden="true"></i></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -61,13 +88,15 @@
|
||||
</template>
|
||||
|
||||
<template name="InsertSale">
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<form class="insertSaleForm" autocomplete="off">
|
||||
<div class="grid">
|
||||
<div class="col-4-12">
|
||||
<div class="formGroupHeading">New Sale</div>
|
||||
<div class="form-group">
|
||||
<label class='control-label'>Date</label>
|
||||
<input type="date" class="form-control" name="date" data-schema-key='date' required>
|
||||
<input name="date" class="form-control" type="date" data-schema-key='date' required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class='control-label'>Product</label>
|
||||
@@ -88,6 +117,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="InsertSaleMeasure">
|
||||
|
||||
121
imports/ui/Sales.import.styl
vendored
121
imports/ui/Sales.import.styl
vendored
@@ -1,39 +1,34 @@
|
||||
#salesMain
|
||||
margin: 10px 20px
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
text-align: left
|
||||
|
||||
.comboList .deactivated
|
||||
color: red
|
||||
background: #ffdbd9
|
||||
|
||||
.editor
|
||||
height: 100%
|
||||
overflow-y: auto
|
||||
|
||||
.insertSale
|
||||
width: 100%
|
||||
text-align: left
|
||||
.tableControls
|
||||
text-align: right
|
||||
margin-right: 20px
|
||||
.salesListRow
|
||||
display: table-row
|
||||
.salesListCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
.paginationContainer
|
||||
position: absolute
|
||||
right: 0
|
||||
bottom: -20px
|
||||
.pagination
|
||||
white-space: nowrap
|
||||
.form-group, label
|
||||
text-align: left
|
||||
|
||||
.formGroupHeading
|
||||
font-size: 1.6em
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-style: normal
|
||||
font-variant: normal
|
||||
font-weight: 500
|
||||
.grid
|
||||
height: 100%
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
//width: 100%
|
||||
//margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
//height: 100%
|
||||
label
|
||||
font-size: 10px
|
||||
font-weight: 800
|
||||
@@ -60,10 +55,74 @@
|
||||
> th.price
|
||||
width: 140px
|
||||
> th.measure
|
||||
width: 90px
|
||||
> th.date
|
||||
width: 100px
|
||||
> th.saleDate
|
||||
width: 140px
|
||||
> th.createdDate
|
||||
width: 100px
|
||||
> th.venue
|
||||
width: 160px
|
||||
> th.actions
|
||||
width: 90px
|
||||
.newSaleButton
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newSaleButton.active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
.showOnlyComments
|
||||
color: #bcb95f
|
||||
padding: 4px 8px
|
||||
.showOnlyComments:hover
|
||||
color: white
|
||||
text-shadow: 0px 0px 10px #ff6d1f
|
||||
.showOnlyComments.on
|
||||
color: white
|
||||
.editComment
|
||||
color: grey
|
||||
.hasComment
|
||||
color: black
|
||||
.actionEdit
|
||||
margin-right: 6px
|
||||
color: #44F
|
||||
.saleEditor
|
||||
.heading
|
||||
font-size: 2em
|
||||
font-family: verdana, arial, helvetica, sans-serif
|
||||
text-transform: uppercase
|
||||
font-weight: 800
|
||||
margin: 6px 0 14px 0
|
||||
.priceContainer
|
||||
display: table
|
||||
width: 100%
|
||||
.price
|
||||
display: table-cell
|
||||
padding-right: 10px
|
||||
.priceButtons
|
||||
display: table-cell
|
||||
width: 1.5em
|
||||
.setDefaultPrice
|
||||
font-size: 1.5em
|
||||
padding: 6px 8px
|
||||
margin-left: 8px
|
||||
border-radius: 8px
|
||||
.setDefaultPrice:hover
|
||||
text-shadow: 0px 0px 6px #00b900
|
||||
.setDefaultPrice:active
|
||||
text-shadow: 0px 0px 6px grey
|
||||
.insertSaleForm
|
||||
.form-group, label
|
||||
text-align: left
|
||||
.formGroupHeading
|
||||
font-size: 1.6em
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-style: normal
|
||||
font-variant: normal
|
||||
font-weight: 500
|
||||
@@ -1,29 +1,65 @@
|
||||
|
||||
import './Sales.html';
|
||||
import '/imports/util/selectize/selectize.js';
|
||||
import swal from 'sweetalert2';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Sales.";
|
||||
|
||||
Meteor.subscribe("products");
|
||||
Session.set(PREFIX + "sortOption", "date");
|
||||
Session.set(PREFIX + "showOnlyComments", false);
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("sales", Session.get(PREFIX + 'searchQuery'), QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
|
||||
let sortOption = Session.get(PREFIX + "sortOption");
|
||||
let sort = sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1};
|
||||
let showOnlyComments = Session.get(PREFIX + "showOnlyComments");
|
||||
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
|
||||
|
||||
if(showOnlyComments) {
|
||||
if(!query) query = {};
|
||||
query.comment = {$exists: true};
|
||||
}
|
||||
|
||||
Meteor.subscribe("sales", query, sort, QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
|
||||
Session.set(PREFIX + 'saleCount', Meteor.call('getSalesCount', Session.get(PREFIX + 'searchQuery')));
|
||||
});
|
||||
|
||||
Template.Sales.onCreated(function() {
|
||||
Session.set(PREFIX + "displayNewSale", false);
|
||||
});
|
||||
Template.Sales.helpers({
|
||||
displayNewSale: function() {
|
||||
return Session.get(PREFIX + "displayNewSale");
|
||||
},
|
||||
sales: function() {
|
||||
return Meteor.collections.Sales.find({}, {sort: {date: -1, createdAt: -1}});
|
||||
let sortOption = Session.get(PREFIX + "sortOption");
|
||||
|
||||
return Meteor.collections.Sales.find({}, {sort: (sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1})});
|
||||
},
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'saleCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
},
|
||||
editing: function() {
|
||||
let editedSale = Session.get(PREFIX + "editedSale");
|
||||
|
||||
return editedSale == this._id;
|
||||
}
|
||||
});
|
||||
Template.Sales.events({
|
||||
'click .newSaleButton': function(event, template) {
|
||||
if(template.$('.newSaleButton').hasClass('active')) {
|
||||
Session.set(PREFIX + 'displayNewSale', false);
|
||||
}
|
||||
else {
|
||||
Session.set(PREFIX + 'displayNewSale', true);
|
||||
Session.set(PREFIX + "editedSale", undefined); //Clear the edited sale so that only one editor is open at a time.
|
||||
}
|
||||
template.$('.newSaleButton').toggleClass('active');
|
||||
},
|
||||
'click .prevButton': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
@@ -31,6 +67,16 @@ Template.Sales.events({
|
||||
'click .nextButton': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
},
|
||||
'change select[name="sortSelect"]': function(event, template) {
|
||||
Session.get(PREFIX + 'skipCount', 0);
|
||||
Session.set(PREFIX + "sortOption", $(event.target).val());
|
||||
},
|
||||
'click .showOnlyComments': function(event, template) {
|
||||
let $button = $(event.target);
|
||||
|
||||
Session.set(PREFIX + "showOnlyComments", !$button.hasClass('on'));
|
||||
$button.toggleClass('on');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,9 +92,12 @@ Template.Sale.helpers({
|
||||
productName: function(id) {
|
||||
return Meteor.collections.Products.findOne({_id: id}, {fields: {name: 1}}).name;
|
||||
},
|
||||
formatDate: function(date) {
|
||||
formatDateAndWeek: function(date) {
|
||||
return moment(date).format("MM/DD/YYYY (w)");
|
||||
},
|
||||
formatDate: function(date) {
|
||||
return moment(date).format("MM/DD/YYYY");
|
||||
},
|
||||
formatPrice: function(price) {
|
||||
return price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
|
||||
},
|
||||
@@ -57,22 +106,176 @@ Template.Sale.helpers({
|
||||
},
|
||||
showTotalPrice: function(amount) {
|
||||
return amount > 1;
|
||||
},
|
||||
commentClass: function() {
|
||||
return this.comment ? "hasComment" : "";
|
||||
}
|
||||
});
|
||||
Template.Sale.events({
|
||||
"click .actionEdit": function(event, template) {
|
||||
Session.set(PREFIX + "editedSale", this._id);
|
||||
Session.set(PREFIX + 'displayNewSale', false); //Ensure the new sale editor is closed.
|
||||
template.$('.newSaleButton').removeClass('active');
|
||||
},
|
||||
"click .saleRemove": function(event, template) {
|
||||
let _this = this;
|
||||
bootbox.confirm({
|
||||
message: "Delete the sale?",
|
||||
buttons: {confirm: {label: "Yes", className: 'btn-success'}, cancel: {label: "No", className: "btn-danger"}},
|
||||
callback: function(result) {
|
||||
if(result) {
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "This will permanently remove the sale.",
|
||||
type: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "Yes"
|
||||
}).then(
|
||||
function(isConfirm) {
|
||||
if(isConfirm) {
|
||||
// Meteor.collections.Sales.remove(_this._id);
|
||||
Meteor.call('deleteSale', _this._id);
|
||||
}
|
||||
},
|
||||
function(dismiss) {
|
||||
|
||||
}
|
||||
);
|
||||
},
|
||||
"click .editComment": function(event, template) {
|
||||
let _this = this;
|
||||
|
||||
swal({
|
||||
title: "Sale Comment",
|
||||
text: "Change the comment, or clear it to remove the comment.",
|
||||
input: "textarea",
|
||||
showCancelButton: true,
|
||||
closeOnConfirm: true,
|
||||
closeOnCancel: true,
|
||||
animation: "slide-from-top",
|
||||
inputPlaceholder: "Write a comment...",
|
||||
allowEscapeKey: true,
|
||||
inputValue: _this.comment ? _this.comment : ""
|
||||
}).then(
|
||||
function(text) {
|
||||
Meteor.call('editSaleComment', _this._id, text);
|
||||
},
|
||||
function(dismiss) {}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Template.SaleEditor.onCreated(function() {
|
||||
let _this = this;
|
||||
|
||||
this.product = Meteor.collections.Products.findOne({_id: this.data.productId});
|
||||
this.selectedDate = new ReactiveVar(this.data.date);
|
||||
this.selectedVenue = new ReactiveVar(Meteor.collections.Venues.findOne({_id: this.data.venueId}));
|
||||
this.price = new ReactiveVar(this.data.price);
|
||||
this.amount = new ReactiveVar(this.data.amount);
|
||||
});
|
||||
Template.SaleEditor.onRendered(function() {
|
||||
this.$('form[name="editSaleForm"]').validator();
|
||||
this.$('[name="venue"]').buildCombo({cursor: Meteor.collections.Venues.find({}), selection: this.selectedVenue, comparator: function(a, b) {return a._id == b._id;}, textAttr: 'name', listClass: 'comboList'});
|
||||
this.$('input[name="date"]').val(moment(this.selectedDate.get()).format("YYYY-MM-DD"));
|
||||
});
|
||||
Template.SaleEditor.helpers({
|
||||
measureName: function(id) {
|
||||
let measure = Meteor.collections.Measures.findOne({_id: id}, {fields: {name: 1}});
|
||||
|
||||
return measure ? measure.name : "???";
|
||||
},
|
||||
productName: function() {
|
||||
let product = Template.instance().product;
|
||||
return product ? product.name : "???";
|
||||
},
|
||||
price: function() {
|
||||
return Template.instance().price.get();
|
||||
},
|
||||
amount: function() {
|
||||
return Template.instance().amount.get();
|
||||
},
|
||||
total: function() {
|
||||
let template = Template.instance();
|
||||
return (template.price.get() * template.amount.get()).toFixed(2);
|
||||
}
|
||||
});
|
||||
Template.SaleEditor.events({
|
||||
'click .setDefaultPrice': function(event, template) {
|
||||
let date = template.selectedDate.get();
|
||||
let prices = template.product.prices;
|
||||
let priceData;
|
||||
let price = 0;
|
||||
|
||||
if(prices) priceData = prices[template.data.measureId];
|
||||
|
||||
//If this product has pricing data for the given measure, then either use the price, or the previousPrice (if there is one and the effectiveDate is after the sale date).
|
||||
if(priceData) {
|
||||
if(priceData.effectiveDate && date && moment(priceData.effectiveDate).isAfter(date))
|
||||
price = priceData.previousPrice;
|
||||
else
|
||||
price = priceData.price
|
||||
}
|
||||
|
||||
template.price.set(price);
|
||||
},
|
||||
'change input[name="date"]': function(event, template) {
|
||||
template.selectedDate.set(moment(event.target.value, "YYYY-MM-DD").toDate());
|
||||
},
|
||||
'change .price': function(event, template) {
|
||||
template.price.set(parseFloat($(event.target).val()));
|
||||
},
|
||||
'change .amount': function(event, template) {
|
||||
template.amount.set(parseFloat($(event.target).val()));
|
||||
},
|
||||
"click .editorCancel": function(event, template) {
|
||||
Session.set(PREFIX + "editedSale", undefined);
|
||||
},
|
||||
"click .editorApply": function(event, template) {
|
||||
template.$('form[name="editSaleForm"]').data('bs.validator').validate(function(isValid) {
|
||||
if(isValid) {
|
||||
let id = template.data._id;
|
||||
let date = template.selectedDate.get();
|
||||
let venue = template.selectedVenue.get();
|
||||
let price = template.price.get();
|
||||
let amount = template.amount.get();
|
||||
|
||||
Meteor.call("updateSale", id, date, venue._id, price, amount, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else {
|
||||
sAlert.success("Sale updated.");
|
||||
Session.set(PREFIX + "editedSale", undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//let name = template.$("input[name='name']").val().trim();
|
||||
//let tags = template.$(".productTagsEditor").select2('data');
|
||||
//let aliases = template.$(".productAliasesEditor").select2('data');
|
||||
//let measures = template.$(".productMeasuresEditor").select2('data');
|
||||
//
|
||||
//tags = tags.map((n)=>n.id);
|
||||
//aliases = aliases.map((n)=>n.id);
|
||||
//measures = measures.map((n)=>n.id);
|
||||
//
|
||||
//if(Session.get(PREFIX + 'displayNewProduct')) {
|
||||
// Meteor.call("createProduct", name, tags, aliases, measures, function(error, result) {
|
||||
// if(error) sAlert.error(error);
|
||||
// else {
|
||||
// sAlert.success("Product created.");
|
||||
// Session.set(PREFIX + 'displayNewProduct', false);
|
||||
// template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
//else {
|
||||
// Meteor.call("updateProduct", this._id, name, tags, aliases, measures, function(error, result) {
|
||||
// if(error) sAlert.error(error);
|
||||
// else {
|
||||
// sAlert.success("Product updated.");
|
||||
// Session.set(PREFIX + "editedProduct", undefined);
|
||||
// template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
}
|
||||
});
|
||||
|
||||
Template.SaleSearch.helpers({
|
||||
@@ -116,37 +319,27 @@ Template.SaleSearch.events({
|
||||
});
|
||||
|
||||
Template.InsertSale.onCreated(function() {
|
||||
// $('#insertSale').validator();
|
||||
// $('#insertSale').data('bs.validator');
|
||||
// this.products = new ReactiveVar([]);
|
||||
this.selectedDate = new ReactiveVar();
|
||||
this.selectedProduct = new ReactiveVar();
|
||||
this.selectedVenue = new ReactiveVar();
|
||||
});
|
||||
Template.InsertSale.onRendered(function() {
|
||||
this.$('.insertSaleForm').validator();
|
||||
// this.$('[name="product"]').
|
||||
// this.autorun(function() {
|
||||
// this.$('[name="product"]').buildCombo(Meteor.collections.Products.find({}).fetch(), {textAttr: 'name', listClass: 'comboList'});
|
||||
// });
|
||||
|
||||
//TODO: Highlight deactivated products in combo
|
||||
//TODO: Default the price for each size product based on the date.
|
||||
//TODO: Make the query for products reactive, by putting it inside an autorun block.
|
||||
//TODO: Fix the combo's change event firing. It does not fire a change event when selecting an item for the first time. It's $(input).val() call also returns the name of the thing selected instead of the selected object.
|
||||
// Note: The combo will automatically update our selection reactive variable. No need to capture change events.
|
||||
this.$('[name="product"]').buildCombo({cursor: Meteor.collections.Products.find({$or: [{hidden: false}, {hidden: {$exists:false}}]}), selection: this.selectedProduct, textAttr: 'name', listClass: 'comboList', getClasses: function(data) {
|
||||
return (data && data.deactivated) ? "deactivated" : "";
|
||||
}});
|
||||
this.$('[name="venue"]').buildCombo({cursor: Meteor.collections.Venues.find({}), selection: this.selectedVenue, textAttr: 'name', listClass: 'comboList'});
|
||||
|
||||
// this.autorun(function(){
|
||||
// this.products.set(Meteor.collections.Products.find({}));
|
||||
// }.bind(this));
|
||||
});
|
||||
Template.InsertSale.events({
|
||||
'change input[name="product"]': function(event, template) {
|
||||
let selectedId = template.$('input[name="product"]').val();
|
||||
let selected = Meteor.collections.Products.findOne(selectedId);
|
||||
template.selectedProduct.set(selected);
|
||||
},
|
||||
//'change input[name="product"]': function(event, template) {
|
||||
// let selectedId = template.$('input[name="product"]').val();
|
||||
// let selected = Meteor.collections.Products.findOne(selectedId);
|
||||
// template.selectedProduct.set(selected);
|
||||
//},
|
||||
'change input[name="date"]': function(event, template) {
|
||||
template.selectedDate.set(moment(event.target.value, "YYYY-MM-DD").toDate());
|
||||
},
|
||||
@@ -155,17 +348,20 @@ Template.InsertSale.events({
|
||||
template.$('.insertSaleForm').data('bs.validator').validate(function(isValid) {
|
||||
if(isValid) {
|
||||
let sales = [];
|
||||
let insertSaleMeasures = template.$(".insertSaleMeasure");
|
||||
|
||||
let sale = {
|
||||
date: moment(template.find("[name='date']").value, "YYYY-MM-DD").toDate(),
|
||||
productId: template.selectedProduct.get()._id,
|
||||
venueId: template.selectedVenue.get()._id
|
||||
};
|
||||
let insertSaleMeasures = template.$(".insertSaleMeasure");
|
||||
|
||||
//Iterate over the measures for the sale (based on the product chosen) and collection amounts and prices.
|
||||
for(let next = 0; next < insertSaleMeasures.length; next++) {
|
||||
let nextMeasure = $(insertSaleMeasures[next]);
|
||||
let measureId = nextMeasure.find(".measureId").val();
|
||||
let price = parseFloat(nextMeasure.find(".price").val()).toFixed(2);
|
||||
let amount = parseFloat(nextMeasure.find(".amount").val()).toFixed(2);
|
||||
let price = parseFloat(nextMeasure.find(".price").val());
|
||||
let amount = parseFloat(nextMeasure.find(".amount").val());
|
||||
|
||||
if(amount > 0) {
|
||||
let nextSale = _.clone(sale);
|
||||
@@ -177,12 +373,22 @@ Template.InsertSale.events({
|
||||
}
|
||||
}
|
||||
|
||||
//Iterate over the product measures that have a quantity greater than zero and add them as a sale.
|
||||
for(let index = 0; index < sales.length; index++) {
|
||||
let next = sales[index];
|
||||
//console.log("Inserting: " + JSON.stringify(next));
|
||||
Meteor.call('insertSale', next, function(error) {
|
||||
if(error) sAlert.error("Failed to insert the sale!\n" + error);
|
||||
else sAlert.success("Sale Created");
|
||||
else {
|
||||
sAlert.success("Sale Created");
|
||||
|
||||
//Clear the measure quantity fields so the user can enter another sale without the quantities already set.
|
||||
for(let next = 0; next < insertSaleMeasures.length; next++) {
|
||||
let nextMeasure = $(insertSaleMeasures[next]);
|
||||
|
||||
nextMeasure.find(".amount").val(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -190,9 +396,6 @@ Template.InsertSale.events({
|
||||
}
|
||||
});
|
||||
Template.InsertSale.helpers({
|
||||
products: function() {
|
||||
return [{label: "Hermies", value: 1}, {label: "Ralfe", value: 2}, {label: "Bob", value: 3}];
|
||||
},
|
||||
productMeasures: function() {
|
||||
let product = Template.instance().selectedProduct.get();
|
||||
let result = product ? product.measures : [];
|
||||
@@ -242,6 +445,20 @@ Template.InsertSaleMeasure.events({
|
||||
},
|
||||
'change .amount': function(event, template) {
|
||||
template.amount.set(parseFloat($(event.target).val()));
|
||||
},
|
||||
'focus input[name="amount"],input[name="price"]': function(event, template) {
|
||||
//See http://stackoverflow.com/questions/3150275/jquery-input-select-all-on-focus
|
||||
//Handle selecting the text in the field on receipt of focus.
|
||||
let $this = $(this)
|
||||
.one('mouseup.mouseupSelect', function() {
|
||||
$this.select();
|
||||
return false;
|
||||
})
|
||||
.one('mousedown', function() {
|
||||
// compensate for untriggered 'mouseup' caused by focus via tab
|
||||
$this.off('mouseup.mouseupSelect');
|
||||
})
|
||||
.select();
|
||||
}
|
||||
});
|
||||
Template.InsertSaleMeasure.helpers({
|
||||
|
||||
76
imports/ui/SalesSheetEditor.html
Normal file
76
imports/ui/SalesSheetEditor.html
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
<!-- ******** Sheet Editor - Edit a sales sheet structure (has two sub-parts: product selector, and configuration) ********* -->
|
||||
|
||||
<template name="SalesSheetEditor">
|
||||
<div class="salesSheetEditorControls vscFixed">
|
||||
<ul class="tabRow"><li class="productSelection {{productSelectionSelected}}">Selection</li><li class="sheetConfiguration {{sheetConfigurationSelected}}">Configuration</li></ul>
|
||||
</div>
|
||||
{{> Template.dynamic template=salesSheetEditorForm data=salesSheetEditorData}}
|
||||
</template>
|
||||
|
||||
<!-- ******** The Sheet Editor's Product Selector ********* -->
|
||||
|
||||
<template name="SalesSheetEditorProductSelection">
|
||||
<div class="salesSheetEditorProductSelectionControls vscFixed">
|
||||
<span class="button showAlternateNames">Alt. Names</span>
|
||||
<i class="fa fa-question-circle clickable noselect" aria-hidden="true"></i>
|
||||
<label>Filter </label><input class="form-control" type="text" name="productFilter" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="selectionProductsListing columnContainer">
|
||||
{{#each products}}
|
||||
{{>SalesSheetEditorProductSelectionRow}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="SalesSheetEditorProductSelectionRow">
|
||||
<div class="selectionProduct {{#if sheetProduct}}selected{{/if}} columnContent">
|
||||
<span class="include clickable">
|
||||
{{#if sheetProduct}}
|
||||
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-circle-o" aria-hidden="true"></i>
|
||||
{{/if}}
|
||||
<span class="productName noselect">{{name}}</span>
|
||||
</span>
|
||||
{{#if sheetProduct}}
|
||||
<div class="includeAs"> as "{{sheetProductName}}"</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ******** The Sheet Editor's Configuration ********* -->
|
||||
<!-- The overall Sheet Configuration Editor. Contains multiple rows in columns, one row for each PRODUCT or HEADER. -->
|
||||
<template name="SalesSheetEditorConfiguration">
|
||||
<div class="vscFixed configurationControls">
|
||||
<div class="heading columnContent noselect">
|
||||
<div class="name clickable">New Heading</div>
|
||||
<div class="nameEditor"><input name="name" tabindex="1" value="New Heading"/> <i class="fa fa-check-circle accept" aria-hidden="true"></i> <i class="fa fa-times-circle reject" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configurationProductsListing columnContainer">
|
||||
{{#each products}}
|
||||
{{>SalesSheetEditorConfigurationRow}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- A single row of the Sheet Configuration Editor (allows reordering, addition of headers, sorting, and renaming of sheet products). -->
|
||||
<template name="SalesSheetEditorConfigurationRow">
|
||||
{{#if isProduct}} {{! PRODUCT }}
|
||||
<div class="product columnContent noselect" data-model="{{productId}}">
|
||||
<div class="name clickable">{{name}}</div>
|
||||
<div class="nameEditor"><input class="form-control" name="name" type="text" tabindex="1" value="{{name}}"/> <i class="fa fa-check-circle accept" aria-hidden="true"></i> <i class="fa fa-times-circle reject" aria-hidden="true"></i></div>
|
||||
<div class="measures">
|
||||
{{#each measureId in measures}}
|
||||
<span class="measureButton button {{#if isSelected measureId}}selected{{/if}}" data-model="{{measureId}}">{{measureName measureId}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}} {{! HEADING }}
|
||||
<div class="heading columnContent noselect">
|
||||
<div class="headingNameRow"><span class="name clickable">{{name}}</span><span class="sort clickable noselect"><i class="fa fa-arrow-down" aria-hidden="true"></i> sort <i class="fa fa-arrow-up" aria-hidden="true"></i></span></div>
|
||||
<div class="nameEditor"><input class="form-control" name="name" type="text" tabindex="1" value="{{name}}"/> <i class="fa fa-check-circle accept" aria-hidden="true"></i> <i class="fa fa-times-circle reject" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
149
imports/ui/SalesSheetEditor.import.styl
vendored
Normal file
149
imports/ui/SalesSheetEditor.import.styl
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
#salesSheetsMain
|
||||
.salesSheetEditorControls
|
||||
margin-bottom: 8px
|
||||
.salesSheetEditorProductSelectionControls
|
||||
margin-bottom: 8px
|
||||
width: 100%
|
||||
text-align: right
|
||||
input[name='productFilter']
|
||||
font-size: 1.2em
|
||||
display: inline
|
||||
width: auto
|
||||
.showAlternateNames
|
||||
margin-right: 20px
|
||||
.selectionProductsListing
|
||||
width: 100%
|
||||
.selectionProduct
|
||||
color: #9f9f9f
|
||||
font-size: 1.5em
|
||||
width: 400px
|
||||
.include, .includeAs
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
.includedRemove, .includedAdd
|
||||
cursor: pointer
|
||||
.includedRemove:hover, .includedAdd:hover
|
||||
color: blue
|
||||
.selectionProduct.selected
|
||||
color: black
|
||||
.configurationControls
|
||||
width: 100%
|
||||
background: #c1c2ff
|
||||
border-bottom: 2px solid #a7a8ff
|
||||
.heading
|
||||
.name
|
||||
font-size: 1.5em
|
||||
text-transform: uppercase
|
||||
font-weight: 800
|
||||
.nameEditor
|
||||
display: none
|
||||
.configurationProductsListing
|
||||
width: 100%
|
||||
.product
|
||||
width: 300px
|
||||
.name
|
||||
color: #9f9f9f
|
||||
font-size: 1.5em
|
||||
margin-bottom: 6px
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
.name.edit
|
||||
display: none
|
||||
.nameEditor
|
||||
display: none
|
||||
margin-bottom: 6px
|
||||
input[name='name']
|
||||
flex: 1 1 auto
|
||||
display: inline-block
|
||||
.accept, .reject
|
||||
flex: 0 0 auto
|
||||
font-size: 1.7em
|
||||
margin-left: 8px
|
||||
.accept:hover
|
||||
color: green
|
||||
.reject:hover
|
||||
color: red
|
||||
.accept:active
|
||||
text-shadow: 0px 0px 10px #fda1ff
|
||||
.reject:active
|
||||
text-shadow: 0px 0px 10px #fda1ff
|
||||
.nameEditor.edit
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-content: stretch;
|
||||
.measures
|
||||
margin: 1px 0 8px 0
|
||||
.heading
|
||||
width: 300px
|
||||
.headingNameRow
|
||||
display: flex
|
||||
flex-flow: row nowrap
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
margin-top: 4px
|
||||
margin-bottom: 2px
|
||||
border-bottom: 1px solid grey
|
||||
.name
|
||||
flex: 1 0 auto
|
||||
color: black
|
||||
font-size: 1.5em
|
||||
text-transform: uppercase
|
||||
font-weight: 800
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
.sort
|
||||
flex: 0 0 auto
|
||||
.headingNameRow.edit
|
||||
display: none
|
||||
.nameEditor
|
||||
display: none
|
||||
input[name='name']
|
||||
flex: 1 1 auto
|
||||
display: inline-block
|
||||
.accept, .reject
|
||||
flex: 0 0 auto
|
||||
font-size: 1.7em
|
||||
margin-left: 8px
|
||||
.accept:hover
|
||||
color: green
|
||||
.reject:hover
|
||||
color: red
|
||||
.accept:active
|
||||
text-shadow: 0px 0px 10px #fda1ff
|
||||
.reject:active
|
||||
text-shadow: 0px 0px 10px #fda1ff
|
||||
.nameEditor.edit
|
||||
display: flex
|
||||
flex-flow: row nowrap
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
/*** These styles are for the drag and drop. The D&D element is a child of the body tag while it exists, and as such will not use the styles above. ***/
|
||||
.heading.gu-mirror
|
||||
width: 300px
|
||||
.name
|
||||
color: black
|
||||
font-size: 1.5em
|
||||
text-transform: uppercase
|
||||
font-weight: 800
|
||||
margin-top: 4px
|
||||
margin-bottom: 2px
|
||||
border-bottom: 1px solid grey
|
||||
.nameEditor
|
||||
display: none
|
||||
.product.gu-mirror
|
||||
color: #9f9f9f
|
||||
font-size: 1.5em
|
||||
width: 300px
|
||||
.name
|
||||
font-size: 1em
|
||||
.nameEditor
|
||||
display: none
|
||||
.measures
|
||||
display: none
|
||||
419
imports/ui/SalesSheetEditor.js
Normal file
419
imports/ui/SalesSheetEditor.js
Normal file
@@ -0,0 +1,419 @@
|
||||
|
||||
import './SalesSheetEditor.html';
|
||||
import swal from 'sweetalert2';
|
||||
import dragula from 'dragula';
|
||||
|
||||
let PREFIX = "SalesSheetEditor.";
|
||||
|
||||
//******************************************************************
|
||||
//** The parent template for editing a sheet. Has two children which allow picking products, and organizing products.
|
||||
//******************************************************************
|
||||
Template.SalesSheetEditor.onCreated(function() {
|
||||
// Default the currently displayed form to the product selection form.
|
||||
let currentFormName = Session.get(PREFIX + "currentFormName");
|
||||
if(currentFormName != "SalesSheetEditorProductSelection" && currentFormName != "SalesSheetEditorConfiguration") Session.set(PREFIX + "currentFormName", "SalesSheetEditorProductSelection");
|
||||
|
||||
//this.currentFormName = new ReactiveVar("SalesSheetEditorProductSelection");
|
||||
// Save the data as the sheet. This is easier to read in the code, and it avoids the problem in onDestroyed() where our data is being changed out from under us.
|
||||
this.sheet = Meteor.collections.SalesSheets.findOne(this.data);
|
||||
});
|
||||
Template.SalesSheetEditor.onRendered(function() {
|
||||
});
|
||||
Template.SalesSheetEditor.onDestroyed(function() {
|
||||
let sheet = this.sheet; //Note: this.data does not refer to the SAME sheet instance, but rather another copy of the same sheet. So any changes would be lost if we referenced `this.data`.
|
||||
|
||||
swal({
|
||||
title: "Save Changes",
|
||||
text: "Would you like to save any changes you have made to this sheet?",
|
||||
type: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#7cdd7f",
|
||||
confirmButtonText: "Yes"
|
||||
}).then(
|
||||
function(isConfirm) {
|
||||
if(isConfirm) {
|
||||
Meteor.call("updateSalesSheet", sheet._id, sheet.name, sheet.products, function(error) {
|
||||
if(error) sAlert.error("Failed to update the sheet!\n" + error);
|
||||
else {
|
||||
sAlert.success("Updated the Sales Sheet.");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
function(dismiss) {}
|
||||
);
|
||||
});
|
||||
Template.SalesSheetEditor.events({
|
||||
'click .productSelection': function(event, template) {
|
||||
// Toggle which form template is active.
|
||||
if(Session.get(PREFIX + "currentFormName") != "SalesSheetEditorProductSelection") {
|
||||
$(event.target).addClass('selected').siblings().removeClass('selected');
|
||||
Session.set(PREFIX + "currentFormName", "SalesSheetEditorProductSelection");
|
||||
}
|
||||
},
|
||||
'click .sheetConfiguration': function(event, template) {
|
||||
// Toggle which form template is active.
|
||||
if(Session.get(PREFIX + "currentFormName") != "SalesSheetEditorConfiguration") {
|
||||
$(event.target).addClass('selected').siblings().removeClass('selected');
|
||||
Session.set(PREFIX + "currentFormName", "SalesSheetEditorConfiguration");
|
||||
}
|
||||
}
|
||||
});
|
||||
Template.SalesSheetEditor.helpers({
|
||||
salesSheetEditorForm: function() {
|
||||
return Session.get(PREFIX + "currentFormName");
|
||||
},
|
||||
salesSheetEditorData: function() {
|
||||
return {parentTemplate: Template.instance(), salesSheet: Template.instance().sheet};
|
||||
},
|
||||
productSelectionSelected: function() {
|
||||
return Session.get(PREFIX + "currentFormName") == 'SalesSheetEditorProductSelection' ? "selected" : "";
|
||||
},
|
||||
sheetConfigurationSelected: function() {
|
||||
return Session.get(PREFIX + "currentFormName") == 'SalesSheetEditorConfiguration' ? "selected" : "";
|
||||
}
|
||||
});
|
||||
|
||||
//******************************************************************
|
||||
//** Lets the user pick the products on the sheet.
|
||||
//******************************************************************
|
||||
Template.SalesSheetEditorProductSelection.onCreated(function() {
|
||||
//Here, this is the template, and this.data is an object containing the 'parentTemplate' and 'salesSheet' properties.
|
||||
//Save the sales sheet as a property of this template to make the code later easier to read.
|
||||
//Note: This is not reactive because we don't expect the sales sheet to change without closing the editor (re-editing would open a new template instance). Also, the sales sheet is a clone of the real one, so any changes will be lost if not saved.
|
||||
this.salesSheet = this.data.salesSheet;
|
||||
this.productNameFilter = new ReactiveVar("");
|
||||
});
|
||||
Template.SalesSheetEditorProductSelection.events({
|
||||
'keyup input[name="productFilter"]': _.throttle(function(event, template) {
|
||||
template.productNameFilter.set($(event.target).val());
|
||||
})
|
||||
});
|
||||
Template.SalesSheetEditorProductSelection.helpers({
|
||||
products: function() {
|
||||
let salesSheet = this.salesSheet;
|
||||
let filter = Template.instance().productNameFilter.get();
|
||||
let products = salesSheet.products ? salesSheet.products : [];
|
||||
let productMap = {};
|
||||
let dbQuery;
|
||||
|
||||
//Map the products in the sales sheet by their id so we can later only add products to the list that are not on the sheet.
|
||||
for(next of products) {
|
||||
if(next.productId) //Ignore any elements that don't have an id since they may be headers that have no associated product.
|
||||
productMap[next.productId] = next;
|
||||
}
|
||||
|
||||
//If we have a filter, split the filter (space delimited) and use each piece to match against the name, and the alternate names for the product.
|
||||
//Only match where the word in the name starts with the characters in the filter. So "ra j" would match "raspberry jam".
|
||||
if(filter && filter.trim().length > 0) {
|
||||
let searches = filter.trim().split(/\s+/);
|
||||
let regex = "";
|
||||
|
||||
for(let search of searches) {
|
||||
search = RegExp.escape(search);
|
||||
regex += '(?=.*\\b' + search + ')'
|
||||
}
|
||||
|
||||
regex += '.*';
|
||||
dbQuery = {name: {$regex: regex, $options: 'i'}};
|
||||
}
|
||||
else dbQuery = {};
|
||||
let allProducts = Meteor.collections.Products.find(dbQuery).fetch();
|
||||
|
||||
//Mark all the products that are currently included in the sheet and note the name they are included as.
|
||||
for(next of allProducts) {
|
||||
//Attach the sheet data for the product to the actual product model if it is in the sheet.
|
||||
if(productMap[next._id]) {
|
||||
//Add the sheet product data to the product for those products that are on the sheet. Use this to determine if a product is on the sheet, and to remove it from the sheet.
|
||||
next.sheetProduct = productMap[next._id];
|
||||
}
|
||||
else next.sheetProduct = undefined;
|
||||
}
|
||||
|
||||
return allProducts;
|
||||
}
|
||||
});
|
||||
|
||||
Template.SalesSheetEditorProductSelectionRow.onCreated(function() {
|
||||
//Here, this refers to the template, and this.data is the Product object which has been modified to reference a Sheet object via the 'sheetProduct' property if the product is on the sheet.
|
||||
//We are creating a reactive variable to hold the sheet's product data, if the product is on the sheet, otherwise it is empty.
|
||||
//Note: The Product's sheetProduct references the product data for the sheet, but it must also be referenced in the sheet object held by the parent.
|
||||
this.sheetProduct = new ReactiveVar(this.data.sheetProduct);
|
||||
});
|
||||
Template.SalesSheetEditorProductSelectionRow.events({
|
||||
'click .include': function(event, template) {
|
||||
let sheet = template.parentTemplate(1).salesSheet;
|
||||
|
||||
if(template.sheetProduct.get()) { //Remove the product from the sheet, or rename it (if the names don't match and the user clicked on the name instead of the checkbox).
|
||||
//If the click was on the product's name, and the product name is different from the name used for it in the sheet, then use the product name as the name in the sheet instead of removing it from the sheet.
|
||||
if($(event.target).closest('.productName') && this.name != template.sheetProduct.get().name) {
|
||||
template.sheetProduct.get().name = this.name;
|
||||
}
|
||||
else {
|
||||
let index = sheet.products.indexOf(this.sheetProduct);
|
||||
|
||||
//Remove the product data from the sheet first. Template.parentData(1) is the sheet.
|
||||
if(index >= 0) sheet.products.splice(index, 1);
|
||||
//Clear the sheet product data from the actual product.
|
||||
template.sheetProduct.set(undefined);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let sheetProduct = {name: this.name, productId: this._id, measureIds: this.measures.length > 2 ? this.measures.slice(0,2) : this.measures};
|
||||
|
||||
//Save the sheet product in the sheet. Template.parentData(1) is the sheet.
|
||||
sheet.products.push(sheetProduct);
|
||||
//Attach the sheet product data to the actual product.
|
||||
template.sheetProduct.set(sheetProduct);
|
||||
}
|
||||
}
|
||||
});
|
||||
Template.SalesSheetEditorProductSelectionRow.helpers({
|
||||
sheetProduct: function() {
|
||||
return Template.instance().sheetProduct.get();
|
||||
},
|
||||
sheetProductName: function() {
|
||||
return Template.instance().sheetProduct.get().name;
|
||||
}
|
||||
});
|
||||
|
||||
//******************************************************************
|
||||
//** Lets the user configure the products on the sheet.
|
||||
//******************************************************************
|
||||
Template.SalesSheetEditorConfiguration.onCreated(function() {
|
||||
let template = this;
|
||||
//Here, this is the template, and this.data is an object containing the 'parentTemplate' and 'salesSheet' properties.
|
||||
//Save the sales sheet as a property of this template to make the code later easier to read.
|
||||
//Note: This is not reactive because we don't expect the sales sheet to change without closing the editor (re-editing would open a new template instance). Also, the sales sheet is a clone of the real one, so any changes will be lost if not saved.
|
||||
this.salesSheet = this.data.salesSheet;
|
||||
this.measures = new ReactiveDict();
|
||||
this.productsDependancy = new Tracker.Dependency;
|
||||
|
||||
Tracker.autorun(function() {
|
||||
let measures = Meteor.collections.Measures.find({}).fetch();
|
||||
|
||||
template.measures.clear();
|
||||
for(let measure of measures) {
|
||||
template.measures.set(measure._id, measure);
|
||||
}
|
||||
});
|
||||
});
|
||||
Template.SalesSheetEditorConfiguration.onRendered(function() {
|
||||
let template = this;
|
||||
|
||||
//Setup the drag and drop for the view.
|
||||
this.drake = dragula([this.$('.configurationProductsListing')[0], this.$('.configurationControls')[0]], {
|
||||
moves: function(el, container, handle, sibling) {
|
||||
//Don't allow drag and drop of buttons - we want them to be clickable.
|
||||
return !$(handle).hasClass("button");
|
||||
},
|
||||
//Checks whether the element `el` can be moved from the container `target`, to the container `source`, above the `sibling` element.
|
||||
accepts: function(el, target, source, sibling) {
|
||||
return (!sibling || !$(sibling).hasClass('newHeading'));
|
||||
},
|
||||
copy: function(el, source) {
|
||||
return $(el).hasClass('heading') && $(source).hasClass('configurationControls');
|
||||
},
|
||||
ignoreInputTextSelection: true
|
||||
}).on('drop', function(el, target, source, sibling) {
|
||||
if($(el).hasClass('heading')) {
|
||||
if(el.parentNode) {
|
||||
let array = template.salesSheet.products;
|
||||
|
||||
//Add the heading to the product array.
|
||||
array.add({name: "New Heading"}, $(el).index());
|
||||
//Remove the element that was just added by the D&D. The element will be re-added by the template in just a moment. We need the template to add the element so that events will be properly handled for it by meteor.
|
||||
el.parentNode.removeChild(el);
|
||||
//Notify the template engine that the products list has changed so it can be re-rendered.
|
||||
template.productsDependancy.changed();
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Get the item from the DOM using the blaze data structure. We could make this more blaze agnostic by attaching the object as data to the DOM in the view, but we really can't escape blaze, so why bother.
|
||||
//let item = el.$blaze_range.view._templateInstance.data;
|
||||
let productId = $(el).data('model');
|
||||
let array = template.salesSheet.products;
|
||||
let item = undefined;
|
||||
|
||||
for(let product of array) {
|
||||
if(productId == product.productId) {
|
||||
item = product;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(item) {
|
||||
//Rearrange the array of products on the sheet.
|
||||
array.move(array.indexOf(item), $(el).index());
|
||||
}
|
||||
else {
|
||||
console.log("ERROR: Unable to locate the moved item.");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Template.SalesSheetEditorConfiguration.onDestroyed(function() {
|
||||
//Clean up after the drag and drop.
|
||||
this.drake.destroy();
|
||||
});
|
||||
Template.SalesSheetEditorConfiguration.events({
|
||||
});
|
||||
Template.SalesSheetEditorConfiguration.helpers({
|
||||
products: function() {
|
||||
let template = Template.instance();
|
||||
let products = template.salesSheet.products;
|
||||
|
||||
//Mark this call as depending on the products array. When we change the array later, we will call changed() on the dependency and it will trigger this function (and the calling template setup) to be re-run.
|
||||
template.productsDependancy.depend();
|
||||
|
||||
return products;
|
||||
}
|
||||
});
|
||||
|
||||
//Note: The data to this template is a product metadata object that is part of a sheet and wrappers (by ID association) a product in the system. See the schema in SalesSheet.js, look for 'products.$' to see the type definition for this data.
|
||||
Template.SalesSheetEditorConfigurationRow.onCreated(function() {
|
||||
let template = this;
|
||||
|
||||
this.handleHeaderEditorCancelAndClose = function() {
|
||||
let $inputField = template.$("input[name='name']");
|
||||
let index = template.$('.heading').index();
|
||||
|
||||
//Reset the text field.
|
||||
$inputField.val(template.parentTemplate(1).salesSheet.products[index].name);
|
||||
template.$('.heading .nameEditor, .heading .headingNameRow').removeClass('edit');
|
||||
};
|
||||
this.handleHeaderEditorApplyAndClose = function() {
|
||||
let $inputField = template.$("input[name='name']");
|
||||
let name = $inputField.val();
|
||||
let index = template.$('.heading').index();
|
||||
|
||||
if(name) name = name.trim();
|
||||
|
||||
if(name && name.length > 0) {
|
||||
template.parentTemplate(1).salesSheet.products[index].name = name;
|
||||
template.$('.heading .name').text(name);
|
||||
}
|
||||
else {
|
||||
template.parentTemplate(1).salesSheet.products.splice(index, 1);
|
||||
template.parentTemplate(1).productsDependancy.changed();
|
||||
}
|
||||
|
||||
template.$('.heading .nameEditor, .heading .headingNameRow').removeClass('edit');
|
||||
};
|
||||
this.handleProductEditorCancelAndClose = function() {
|
||||
let $inputField = template.$("input[name='name']");
|
||||
let index = template.$('.product').index();
|
||||
|
||||
//Reset the text field.
|
||||
$inputField.val(template.parentTemplate(1).salesSheet.products[index].name);
|
||||
template.$('.product .nameEditor, .product .name').removeClass('edit');
|
||||
};
|
||||
this.handleProductEditorApplyAndClose = function() {
|
||||
let $inputField = template.$("input[name='name']");
|
||||
let name = $inputField.val();
|
||||
let index = template.$('.product').index();
|
||||
|
||||
template.parentTemplate(1).salesSheet.products[index].name = name;
|
||||
template.$('.product .name').text(name);
|
||||
template.$('.product .nameEditor, .product .name').removeClass('edit');
|
||||
};
|
||||
});
|
||||
Template.SalesSheetEditorConfigurationRow.helpers({
|
||||
measureName: function(measureId) {
|
||||
return Template.instance().parentTemplate(1).measures.get(measureId).name;
|
||||
},
|
||||
measures: function() {
|
||||
let product = Meteor.collections.Products.findOne(this.productId);
|
||||
|
||||
return product.measures;
|
||||
},
|
||||
isSelected: function(measureId) {
|
||||
return this.measureIds.includes(measureId);
|
||||
},
|
||||
isProduct: function() {
|
||||
return !!this.productId;
|
||||
}
|
||||
});
|
||||
Template.SalesSheetEditorConfigurationRow.events({
|
||||
'click .heading .name': function(event, template) {
|
||||
template.$('.nameEditor, .headingNameRow').addClass('edit');
|
||||
template.$('input[name="name"]').select();
|
||||
},
|
||||
'blur .heading input[name="name"]': function(event, template) {
|
||||
template.handleHeaderEditorApplyAndClose();
|
||||
},
|
||||
'keyup .heading input[name="name"]': function(event, template) {
|
||||
if(event.which === 13 || event.which === 9) { //Enter or Tab
|
||||
template.handleHeaderEditorApplyAndClose();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
else if(event.which === 27) { //Escape
|
||||
template.handleHeaderEditorCancelAndClose();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
'click .heading .accept': function(event, template) {
|
||||
template.handleHeaderEditorApplyAndClose();
|
||||
},
|
||||
'click .heading .reject': function(event, template) {
|
||||
template.handleHeaderEditorCancelAndClose();
|
||||
},
|
||||
'click .product .name': function(event, template) {
|
||||
template.$('.nameEditor, .name').addClass('edit');
|
||||
template.$('input[name="name"]').select();
|
||||
},
|
||||
'blur .product input[name="name"]': function(event, template) {
|
||||
template.handleProductEditorApplyAndClose();
|
||||
},
|
||||
'keyup .product input[name="name"]': function(event, template) {
|
||||
if(event.which === 13 || event.which === 9) { //Enter or Tab
|
||||
template.handleProductEditorApplyAndClose();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
else if(event.which === 27) { //Escape
|
||||
template.handleProductEditorCancelAndClose();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
'click .product .accept': function(event, template) {
|
||||
template.handleProductEditorApplyAndClose();
|
||||
},
|
||||
'click .product .reject': function(event, template) {
|
||||
template.handleProductEditorCancelAndClose();
|
||||
},
|
||||
'click .measureButton': function(event, template) {
|
||||
let measureId = $(event.target).data("model");
|
||||
|
||||
$(event.target).toggleClass("selected");
|
||||
|
||||
if(this.measureIds.includes(measureId))
|
||||
this.measureIds.remove(measureId);
|
||||
else
|
||||
this.measureIds.add(measureId);
|
||||
},
|
||||
'click .heading .sort': function(event, template) {
|
||||
let width = event.currentTarget.offsetWidth;
|
||||
let x = event.pageX - event.currentTarget.offsetLeft;
|
||||
let sortAlphabetical = x <= (width / 2);
|
||||
let headingIndex = template.$(event.target).closest(".heading").index();
|
||||
let firstIndex = headingIndex + 1;
|
||||
let products = template.parentTemplate(1).salesSheet.products;
|
||||
let length = 0;
|
||||
|
||||
while(firstIndex + length < products.length && products[firstIndex + length].productId) {
|
||||
length++;
|
||||
}
|
||||
|
||||
//Sort the part of the array that contains products under the sorted heading.
|
||||
products.partialSort(firstIndex, length, function(a, b) {
|
||||
return sortAlphabetical ? (a.name < b.name ? -1 : 1) : (a.name > b.name ? -1 : 1);
|
||||
});
|
||||
|
||||
//Notify anything depending on the products list that they have been modified.
|
||||
template.parentTemplate(1).productsDependancy.changed();
|
||||
}
|
||||
});
|
||||
70
imports/ui/SalesSheetForm.html
Normal file
70
imports/ui/SalesSheetForm.html
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
<!-- ******** Sales Sheet - Allows user to fill out the selected sales sheet (entering sales data). ********* -->
|
||||
|
||||
<template name="SalesSheetForm">
|
||||
{{#if this}}
|
||||
<div class="vscFixed">
|
||||
<div class="sheetHeader grid">
|
||||
<div class="form-group col-6-12">
|
||||
<label class='control-label'>Date</label>
|
||||
<input type="date" class="form-control" name="date" data-schema-key='date' required>
|
||||
</div>
|
||||
<div class="form-group col-6-12">
|
||||
<label class='control-label'>Venue</label>
|
||||
<input name="venue" class="form-control" type="text" required/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer vscExpand salesSheetProducts">
|
||||
{{#each product in products}}
|
||||
{{#if isHeading product}}
|
||||
{{>SalesSheetFormHeader product}}
|
||||
{{else}}
|
||||
{{>SalesSheetFormProduct product}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
<div class="sheetControls columnContent">
|
||||
<div class="saveSheet clickable noselect">Save Sales</div>
|
||||
<div class="resetSheet clickable noselect">Reset Sheet</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<!-- ******** Sales Sheet Header - The headers in the sales sheet form's list of products. ********* -->
|
||||
|
||||
<template name="SalesSheetFormHeader">
|
||||
<div class="header columnContent">
|
||||
<div class="name">{{name}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ******** Sales Sheet Product - The individual products in the sales sheet form. ********* -->
|
||||
|
||||
<template name="SalesSheetFormProduct">
|
||||
{{#if isHeading}}
|
||||
<div class="header columnContent">
|
||||
<div class="name">{{name}}</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="product columnContent {{#if odd}}odd{{/if}}" data-model="{{productId}}">
|
||||
<div class="nameAndTotal">
|
||||
<div class="name">{{name}}</div>
|
||||
<div class="total">${{total}}</div>
|
||||
</div>
|
||||
<div class="measures">
|
||||
{{#each measures}}
|
||||
{{>SalesSheetFormProductMeasure}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="SalesSheetFormProductMeasure">
|
||||
<div class="measure">
|
||||
<span class="label">{{measureName}}</span>
|
||||
#<input name="amount" type="number" step="1" min="0" value="{{measureAmount}}" data-model="{{this}}"/>
|
||||
$<input name="price" type="number" step="1" min="0" value="{{measurePrice}}" data-model="{{this}}" data-default-price="{{measurePrice}}"/>
|
||||
</div>
|
||||
</template>
|
||||
109
imports/ui/SalesSheetForm.import.styl
vendored
Normal file
109
imports/ui/SalesSheetForm.import.styl
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
#salesSheetsMain
|
||||
.salesSheetProducts
|
||||
> div
|
||||
width: 400px
|
||||
margin: 0 4px 0 4px
|
||||
.header
|
||||
padding-top: 4px
|
||||
.name
|
||||
font-size: 1.5em
|
||||
text-transform: uppercase
|
||||
color: #0a6f10
|
||||
text-shadow: 0px 0px 12px #8fa4d1
|
||||
background: white
|
||||
font-weight: 800
|
||||
width: 100%
|
||||
padding: 4px 6px 6px 6px
|
||||
border-bottom: 2px solid #335a4a
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
.product
|
||||
background-color: #ffecde
|
||||
-webkit-box-shadow: inset 0px 0px 40px 14px white
|
||||
-moz-box-shadow: inset 0px 0px 40px 14px white
|
||||
box-shadow: inset 0px 0px 40px 14px white
|
||||
.nameAndTotal
|
||||
display: flex
|
||||
flex-flow: row wrap
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
margin-bottom: 4px
|
||||
.name
|
||||
flex: 1 1 auto
|
||||
font-size: 1.5em
|
||||
color: #575757
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
.total
|
||||
flex: 0 0 auto
|
||||
width: 80px
|
||||
font-size: 1.2em
|
||||
color: #00378b
|
||||
text-shadow: 0px 0px 8px #7690d1
|
||||
overflow: hidden
|
||||
.measures
|
||||
display: flex
|
||||
flex-flow: row wrap
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
margin-bottom: 6px
|
||||
.measure
|
||||
flex: 1 0 auto
|
||||
display: flex
|
||||
flex-flow: row wrap
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
.label
|
||||
font-size: 1em
|
||||
font-weight: 600
|
||||
margin-right: 4px
|
||||
input[name="price"]
|
||||
width: 63px
|
||||
input[name="amount"]
|
||||
width: 47px
|
||||
.product.odd
|
||||
background-color: #ede0f1
|
||||
-webkit-box-shadow: inset 0px 0px 40px 14px white
|
||||
-moz-box-shadow: inset 0px 0px 40px 14px white
|
||||
box-shadow: inset 0px 0px 40px 14px white
|
||||
.sheetControls
|
||||
padding-top: 4px
|
||||
display: flex
|
||||
flex-flow: row wrap
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
.saveSheet, .resetSheet
|
||||
flex: 1 1 auto
|
||||
font-size: 1em
|
||||
color: #e9e9e9
|
||||
font-weight: 800
|
||||
text-transform: uppercase
|
||||
text-align: center
|
||||
padding: 10px
|
||||
margin: 4px
|
||||
.saveSheet
|
||||
background: #007200
|
||||
-webkit-box-shadow: inset 0px 0px 20px -2px white
|
||||
-moz-box-shadow: inset 0px 0px 20px -2px white
|
||||
box-shadow: inset 0px 0px 20px -2px white
|
||||
.resetSheet
|
||||
background: #960000
|
||||
-webkit-box-shadow: inset 0px 0px 20px -2px white
|
||||
-moz-box-shadow: inset 0px 0px 20px -2px white
|
||||
box-shadow: inset 0px 0px 20px -2px white
|
||||
.saveSheet:hover, .resetSheet:hover
|
||||
text-shadow: 0px 0px 16px white
|
||||
.saveSheet:hover
|
||||
background: #005600
|
||||
.resetSheet:hover
|
||||
background: #7f0000
|
||||
.saveSheet:active
|
||||
background: #009000
|
||||
.resetSheet:active
|
||||
background: #b90000
|
||||
372
imports/ui/SalesSheetForm.js
Normal file
372
imports/ui/SalesSheetForm.js
Normal file
@@ -0,0 +1,372 @@
|
||||
|
||||
import './SalesSheetForm.html';
|
||||
import swal from 'sweetalert2';
|
||||
|
||||
let PREFIX = "SalesSheetForm.";
|
||||
|
||||
//******************************************************************
|
||||
//** The form for filling out a sheet.
|
||||
//******************************************************************
|
||||
Template.SalesSheetForm.onCreated(function() {
|
||||
let template = this;
|
||||
|
||||
this.selectedDate = new ReactiveVar();
|
||||
this.selectedVenue = new ReactiveVar();
|
||||
this.salesSheet = new ReactiveVar();
|
||||
//this.measures = new ReactiveDict();
|
||||
this.oddProductIds = new ReactiveDict(false);
|
||||
this.productTemplates = [];
|
||||
|
||||
//Tracker.autorun(function() {
|
||||
// let measures = Meteor.collections.Measures.find({}).fetch();
|
||||
//
|
||||
// template.measures.clear();
|
||||
// for(let measure of measures) {
|
||||
// template.measures.set(measure._id, measure);
|
||||
// }
|
||||
//});
|
||||
|
||||
// Place the sales sheet in a reactive var and put the setting of the reactive var in an autorun.
|
||||
// The autorun is needed apparently to ensure changes to the data force a change in the reactive var.
|
||||
Tracker.autorun(function() {
|
||||
//Force this to be reactive on the current data.
|
||||
try {
|
||||
Template.currentData();
|
||||
} catch(err) {
|
||||
// Ignore it. This always has an error accessing the currentData as the template is destroyed.
|
||||
}
|
||||
|
||||
//For some reason the current data is not always set, and does not always equal the template.data. We will use the template.data to get the actual ID of the sales sheet for the query.
|
||||
template.salesSheet.set(Meteor.collections.SalesSheets.findOne(template.data));
|
||||
});
|
||||
|
||||
Tracker.autorun(function() {
|
||||
let products = template.salesSheet.get().products;
|
||||
let index = 1;
|
||||
|
||||
// Note: We will ignore orphaned data in the dictionary so we don't have to clear the dictionary, or identify the orphans. The orphans are just a few extra product id's mapped to booleans, and should be fairly rare anyway.
|
||||
|
||||
// While ignoring headers (resetting index upon header), collect all odd product id's into a set.
|
||||
for(let next of products) {
|
||||
if(next.productId) {
|
||||
if(index % 2 != 0) {
|
||||
template.oddProductIds.set(next.productId, true);
|
||||
}
|
||||
else {
|
||||
template.oddProductIds.delete(next.productId);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
else index = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
Template.SalesSheetForm.onRendered(function() {
|
||||
this.$('.sheetHeader').validator();
|
||||
this.$('[name="venue"]').buildCombo({cursor: Meteor.collections.Venues.find({}), selection: this.selectedVenue, textAttr: 'name', listClass: 'comboList'});
|
||||
});
|
||||
Template.SalesSheetForm.helpers({
|
||||
isHeading: function(product) {
|
||||
return !product.productId;
|
||||
},
|
||||
products: function() {
|
||||
if(Template.instance().salesSheet.get())
|
||||
return Template.instance().salesSheet.get().products;
|
||||
else
|
||||
return [];
|
||||
},
|
||||
productMeasures: function() {
|
||||
let product = Template.instance().selectedProduct.get();
|
||||
let result = product ? product.measures : [];
|
||||
|
||||
for(let i = 0; i < result.length; i++) {
|
||||
result[i] = Meteor.collections.Measures.findOne(result[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
venues: function() {
|
||||
return Meteor.collections.Venues.find({});
|
||||
}
|
||||
});
|
||||
Template.SalesSheetForm.events({
|
||||
'change input[name="date"]': function(event, template) {
|
||||
template.selectedDate.set(moment(event.target.value, "YYYY-MM-DD").toDate());
|
||||
},
|
||||
'click .sheetControls .resetSheet': function(event, template) {
|
||||
for(let next of template.productTemplates) {
|
||||
next.reset();
|
||||
}
|
||||
},
|
||||
'click .sheetControls .saveSheet': function(event, template) {
|
||||
event.preventDefault();
|
||||
template.$('.sheetHeader').data('bs.validator').validate(function(isValid) {
|
||||
if(isValid) {
|
||||
let date = template.selectedDate.get();
|
||||
let venueId = template.selectedVenue.get()._id;
|
||||
// Track the inserts and errors, display output to the user and log when everything is done.
|
||||
let insertMetadata = {
|
||||
serverErrors: [],
|
||||
insertCount: 0,
|
||||
finishedCount: 0,
|
||||
isDoneInserting: false,
|
||||
incrementInsertCount: function() {
|
||||
this.insertCount++;
|
||||
},
|
||||
incrementFinishedCount: function() {
|
||||
this.finishedCount++;
|
||||
this.finished();
|
||||
},
|
||||
doneInserting: function() {
|
||||
this.isDoneInserting = true;
|
||||
this.finished();
|
||||
},
|
||||
finished: function() {
|
||||
if(this.isDoneInserting && this.finishedCount == this.insertCount) {
|
||||
if(this.serverErrors.length > 0) {
|
||||
let log = "__________________________________________\n";
|
||||
|
||||
log += "Server Errors:\n\n";
|
||||
|
||||
for(let e of this.serverErrors) {
|
||||
log += e + "\n";
|
||||
}
|
||||
|
||||
log += "\n__________________________________________";
|
||||
console.log(log);
|
||||
sAlert.error("Failed to insert some or all of the sales! See the browser logs for details. Successful sales had their amounts set to zero.");
|
||||
}
|
||||
else {
|
||||
sAlert.success("All " + this.insertCount + " sales were saved.");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over the product templates.
|
||||
for(let productTemplate of template.productTemplates) {
|
||||
let productId = productTemplate.product.get()._id;
|
||||
|
||||
// Iterate over each measure template in each product template.
|
||||
for(let measureTemplate of productTemplate.measureTemplates) {
|
||||
let measureId = measureTemplate.measure.get()._id;
|
||||
let amount = measureTemplate.amount.get();
|
||||
let price = measureTemplate.price.get();
|
||||
|
||||
// If the amount and price are above zero then we should record a sale.
|
||||
if(amount > 0 && price > 0) {
|
||||
let sale = {date, venueId, productId, measureId, amount, price};
|
||||
|
||||
insertMetadata.incrementInsertCount();
|
||||
|
||||
// Record the sale.
|
||||
Meteor.call("insertSale", sale, function(error) {
|
||||
if(error) {
|
||||
insertMetadata.serverErrors.push(error);
|
||||
}
|
||||
else {
|
||||
measureTemplate.amount.set(0);
|
||||
measureTemplate.price.set(measureTemplate.autoSetPrice);
|
||||
}
|
||||
|
||||
insertMetadata.incrementFinishedCount();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertMetadata.doneInserting();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ***** A header in the sales sheet. *****
|
||||
Template.SalesSheetFormHeader.onCreated(function() {
|
||||
//this.parentTemplate(1).productTemplates.push(this);
|
||||
});
|
||||
Template.SalesSheetFormHeader.onDestroyed(function() {
|
||||
//this.parentTemplate(1).productTemplates.remove(this);
|
||||
});
|
||||
Template.SalesSheetFormHeader.events({
|
||||
});
|
||||
Template.SalesSheetFormHeader.helpers({
|
||||
});
|
||||
|
||||
// ***** A product in the sales sheet. *****
|
||||
// The data is the SalesSheet's Product metadata (not a database Product object). It has a name and productId among other things.
|
||||
Template.SalesSheetFormProduct.onCreated(function() {
|
||||
let parent = this.parentTemplate(1);
|
||||
let template = this;
|
||||
|
||||
parent.productTemplates.push(this);
|
||||
|
||||
this.parent = parent;
|
||||
this.measureTemplates = [];
|
||||
this.measureTemplatesDependancy = new Tracker.Dependency;
|
||||
this.product = new ReactiveVar(); //The actual product for this sheet product.
|
||||
this.total = new ReactiveVar(0);
|
||||
|
||||
|
||||
this.reset = function() {
|
||||
for(let measureTemplate of template.measureTemplates) {
|
||||
measureTemplate.reset();
|
||||
}
|
||||
};
|
||||
|
||||
//Set the product data in a reactive variable so that changes to the product (such as pricing) are immediately reflected in this sheet. This ensures that the view updates when the reactive variable value changes.
|
||||
//Set the product reactive variable value in an autorun block so that if the product is updated in the local database, we are notified. This ensures the reactive variable value updates when the database changes.
|
||||
template.autorun(function() {
|
||||
// Save a reference to the product.
|
||||
template.product.set(Meteor.collections.Products.findOne(Template.currentData().productId));
|
||||
|
||||
// Depend on the array of measure templates.
|
||||
template.measureTemplatesDependancy.depend();
|
||||
|
||||
for(let measureTemplate of template.measureTemplates) {
|
||||
measureTemplate.resetPrice();
|
||||
}
|
||||
});
|
||||
|
||||
//Auto calculate a total from the amounts and prices. This is in a separate autorun so that it only runs when prices or amounts change.
|
||||
template.autorun(function() {
|
||||
let total = 0;
|
||||
|
||||
// Depend on the array of measure templates.
|
||||
template.measureTemplatesDependancy.depend();
|
||||
|
||||
for(let measureTemplate of template.measureTemplates) { //Iterate over the child templates that display measure data (price & amount).
|
||||
total += measureTemplate.amount.get() * measureTemplate.price.get();
|
||||
}
|
||||
|
||||
template.total.set(total);
|
||||
});
|
||||
});
|
||||
Template.SalesSheetFormProduct.onDestroyed(function() {
|
||||
this.parentTemplate(1).productTemplates.remove(this);
|
||||
});
|
||||
Template.SalesSheetFormProduct.events({
|
||||
});
|
||||
Template.SalesSheetFormProduct.helpers({
|
||||
measures: function() {
|
||||
return this.measureIds;
|
||||
},
|
||||
total: function() {
|
||||
let total = Template.instance().total.get();
|
||||
|
||||
return (total ? total : 0).toFixed(2);
|
||||
},
|
||||
odd: function() {
|
||||
return Template.instance().parent.oddProductIds.get(Template.currentData().productId);
|
||||
}
|
||||
});
|
||||
|
||||
// ***** A measure in the product in the sales sheet. *****
|
||||
// Passed a measureId as the data.
|
||||
Template.SalesSheetFormProductMeasure.onCreated(function() {
|
||||
let parent = this.parentTemplate(1);
|
||||
let template = this;
|
||||
|
||||
//Save this template as a child of the parent in a way the parent can used to keep us up to date.
|
||||
parent.measureTemplates.push(this);
|
||||
parent.measureTemplatesDependancy.changed();
|
||||
this.measure = new ReactiveVar();
|
||||
this.amount = new ReactiveVar(0);
|
||||
this.price = new ReactiveVar(0);
|
||||
this.autoSetPrice = 0;
|
||||
|
||||
this.autorun(function() {
|
||||
template.measure.set(Meteor.collections.Measures.findOne(Template.currentData()));
|
||||
});
|
||||
|
||||
this.resetPrice = function(overrideUserData) { // overrideUserData: Optional - used to crush any user defined pricing.
|
||||
let date = parent.parent.selectedDate.get();
|
||||
let prices = parent.product.get().prices;
|
||||
|
||||
// Change the prices based on the price data.
|
||||
// Ensure there is price data for the product.
|
||||
if(prices) {
|
||||
let measureId = template.measure.get()._id;
|
||||
|
||||
if(prices[measureId] && prices[measureId].price) { //Ensure the product has a default price for this measureId.
|
||||
let price;
|
||||
|
||||
//Determine whether we should use the current or previous price.
|
||||
if(prices[measureId].effectiveDate && date && moment(prices[measureId].effectiveDate).isAfter(date))
|
||||
price = prices[measureId].previousPrice;
|
||||
else
|
||||
price = prices[measureId].price;
|
||||
|
||||
//Change the auto set price for the product. This will be changed if either the prices for the product change, or if the date of the sale changes the price (current vs previous pricing in the product).
|
||||
if(overrideUserData || template.autoSetPrice != price) {
|
||||
// Change the displayed price in the measure if the previous auto set price is the same as the currently displayed price.
|
||||
// This prevents the auto pricing from over writing a user defined price.
|
||||
if(overrideUserData || template.price.get() == template.autoSetPrice) {
|
||||
template.price.set(price);
|
||||
}
|
||||
|
||||
// Save the auto set price so we know in the future if it has really changed. For example changing the date won't necessarily change this value.
|
||||
template.autoSetPrice = price;
|
||||
}
|
||||
}
|
||||
else if(overrideUserData) {
|
||||
template.price.set(0);
|
||||
}
|
||||
}
|
||||
else if(overrideUserData) {
|
||||
template.price.set(0);
|
||||
}
|
||||
};
|
||||
this.reset = function() {
|
||||
template.resetPrice(true);
|
||||
template.amount.set(0);
|
||||
};
|
||||
});
|
||||
Template.SalesSheetFormProductMeasure.onDestroyed(function() {
|
||||
let parent = this.parentTemplate(1);
|
||||
|
||||
parent.measureTemplates.remove(this);
|
||||
parent.measureTemplatesDependancy.changed();
|
||||
});
|
||||
Template.SalesSheetFormProductMeasure.events({
|
||||
'change input[name="price"]': function(event, template) {
|
||||
let $input = $(event.target);
|
||||
let value = parseFloat($input.val());
|
||||
|
||||
if(isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
//Save the price in the price map by measureId.
|
||||
template.price.set(value);
|
||||
},
|
||||
'change input[name="amount"]': function(event, template) {
|
||||
let $input = $(event.target);
|
||||
let value = parseFloat($input.val());
|
||||
|
||||
if(isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
//Save the amount in the amount map by measureId.
|
||||
template.amount.set(value);
|
||||
},
|
||||
'focus input[name="price"]': function(event, template) {
|
||||
$(event.target).select();
|
||||
},
|
||||
'focus input[name="amount"]': function(event, template) {
|
||||
$(event.target).select();
|
||||
}
|
||||
});
|
||||
Template.SalesSheetFormProductMeasure.helpers({
|
||||
measureName: function() {
|
||||
return Template.instance().measure.get().name;
|
||||
},
|
||||
measurePrice: function() {
|
||||
return Template.instance().price.get().toFixed(2);
|
||||
},
|
||||
measureAmount: function() {
|
||||
return Template.instance().amount.get().toFixed(2);
|
||||
}
|
||||
});
|
||||
34
imports/ui/SalesSheets.html
Normal file
34
imports/ui/SalesSheets.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!-- A simple container that ensures that we set some session variables before setting up the child components, and after the subscriptions are ready. -->
|
||||
<!-- I was having trouble with the tab & tabData helpers being called before the session variables that use them were setup. -->
|
||||
<template name="SalesSheets">
|
||||
<div id="salesSheetsMain" class="verticalStack">
|
||||
{{#if Template.subscriptionsReady}}
|
||||
{{> SalesSheetsMain}}
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="SalesSheetsMain">
|
||||
<section class="optionsSection vscFixed">
|
||||
<div class="options">
|
||||
<label style="margin-right: 10px">Selected Sheet</label>
|
||||
<select name="sheetSelection" class="form-control">
|
||||
{{#each sheets}}
|
||||
<option value="{{_id}}" {{sheetsSelectIsSelected this isFirst}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<i class="fa fa-wrench editSheet noselect clickable {{#if disableButtons}}disabled{{/if}} {{#if isEditingSheet}}selected{{/if}}" aria-hidden="true">
|
||||
</i><i class="fa fa-trash-o deleteSheet noselect clickable {{#if disableButtons}}disabled{{/if}}" aria-hidden="true">
|
||||
</i><input type="text" name="newSheetName" class="newSheetName form-control"/><i class="fa fa-plus-circle createSheet noselect clickable {{#if disableNext}}disabled{{/if}}" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="separator" style="width: 70%"></div>
|
||||
<div class="separator" style="width: 60%; opacity: .5"></div>
|
||||
<div class="separator" style="width: 50%; opacity: .25"></div>
|
||||
</section>
|
||||
<section class="tabSection verticalStack vscExpand">
|
||||
{{#if isSheetSelected}}
|
||||
{{>Template.dynamic template=tab data=tabData}}
|
||||
{{/if}}
|
||||
</section>
|
||||
</template>
|
||||
98
imports/ui/SalesSheets.import.styl
vendored
Normal file
98
imports/ui/SalesSheets.import.styl
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
#salesSheetsMain
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
label
|
||||
font-family: "Segoe UI", Candara, "Bitstream Vera Sans", "DejaVu Sans", "Bitstream Vera Sans", "Trebuchet MS", Verdana, "Verdana Ref", sans-serif
|
||||
font-weight: 800
|
||||
text-transform: uppercase
|
||||
|
||||
.optionsSection
|
||||
display: flex
|
||||
flex-flow: column
|
||||
justify-content: flex-start
|
||||
align-items: flex-start
|
||||
align-content: stretch
|
||||
width: 100%
|
||||
.options
|
||||
flex: 0 0 auto
|
||||
display: flex
|
||||
flex-flow: row
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
align-content: stretch
|
||||
padding: 6px 20px 10px 20px
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
height: 50px
|
||||
.form-control
|
||||
display: inline
|
||||
label
|
||||
flex: 0 0 auto
|
||||
vertical-align: text-bottom
|
||||
font-size: 1.2em
|
||||
select[name="sheetSelection"]
|
||||
flex: 0 0 auto
|
||||
font-size: 1.2em
|
||||
padding: 2px
|
||||
margin-right: 4px
|
||||
min-width: 160px
|
||||
width: auto
|
||||
input[name="newSheetName"]
|
||||
flex: 0 0 auto
|
||||
transition: all .75s ease
|
||||
width: 0
|
||||
border: 0
|
||||
opacity: 0
|
||||
font-size: 1.2em
|
||||
input[name="newSheetName"].show
|
||||
opacity: 1
|
||||
border: 1px solid #ccc
|
||||
border-radius: 4px
|
||||
width: 200px
|
||||
transform: translateX(4px)
|
||||
.createSheet, .editSheet, .deleteSheet
|
||||
flex: 0 0 auto
|
||||
padding: 6px
|
||||
margin: 0 4px
|
||||
width: 33px
|
||||
text-align: center
|
||||
font-size: 1.5em
|
||||
border-radius: 8px
|
||||
border: 1px solid rgba(0, 0, 0, 0)
|
||||
box-sizing: border-box
|
||||
.createSheet:hover, .editSheet:hover, .deleteSheet:hover
|
||||
border: 1px inset #b100d1
|
||||
-webkit-box-shadow: inset 0px 0px 20px 0px #de7cff
|
||||
-moz-box-shadow: inset 0px 0px 20px 0px #de7cff
|
||||
box-shadow: inset 0px 0px 20px 0px #de7cff
|
||||
.editSheet.selected
|
||||
color: white
|
||||
border: 1px inset #b100d1
|
||||
-webkit-box-shadow: inset 0px 0px 36px 0px #57006c
|
||||
-moz-box-shadow: inset 0px 0px 36px 0px #57006c
|
||||
box-shadow: inset 0px 0px 36px 0px #57006c
|
||||
.editSheet
|
||||
vertical-align: top
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
.disabled
|
||||
color: grey
|
||||
cursor: default
|
||||
.createSheet
|
||||
transform: translateX(-25px) rotate(0deg)
|
||||
transition: all .75s ease
|
||||
.createSheet.move
|
||||
transform: translateX(6px) rotate(720deg)
|
||||
.separator
|
||||
flex: 0 0 auto
|
||||
width: 100%
|
||||
margin: 0 auto
|
||||
padding-top: 6px
|
||||
height: 1px
|
||||
border-bottom: 1px solid #333
|
||||
.separator:last-child
|
||||
margin-bottom: 10px
|
||||
160
imports/ui/SalesSheets.js
Normal file
160
imports/ui/SalesSheets.js
Normal file
@@ -0,0 +1,160 @@
|
||||
|
||||
import './SalesSheets.html';
|
||||
import './SalesSheetForm.js';
|
||||
import './SalesSheetEditor.js';
|
||||
import swal from 'sweetalert2';
|
||||
|
||||
let PREFIX = "SalesSheets.";
|
||||
|
||||
Template.SalesSheets.onCreated(function() {
|
||||
this.subscribe("products");
|
||||
this.subscribe("venues");
|
||||
this.subscribe("measures");
|
||||
this.subscribe("salesSheets");
|
||||
});
|
||||
Template.SalesSheets.onDestroyed(function() {
|
||||
// Reset the view's session variables used for navigation.
|
||||
Session.set(PREFIX + "currentFormName", undefined);
|
||||
Session.set(PREFIX + "tab", undefined);
|
||||
});
|
||||
|
||||
//******************************************************************
|
||||
//** Container template that allows a user to pick a sheet and either fill it out OR edit it.
|
||||
//******************************************************************
|
||||
Template.SalesSheetsMain.onCreated(function() {
|
||||
//Save the previous session state - whether we are editing the selected sheet.
|
||||
//The name of the currently active page tab. This will either be the SalesSheetForm or the SalesSheetEditor.
|
||||
if(!Session.get(PREFIX + "tab")) Session.set(PREFIX + "tab", "SalesSheetForm");
|
||||
if(!Session.get(PREFIX + 'selectedSheet')) {
|
||||
Session.set(PREFIX + 'selectedSheet', Meteor.collections.SalesSheets.findOne({}, {sort: {name: 1}}));
|
||||
}
|
||||
});
|
||||
Template.SalesSheetsMain.helpers({
|
||||
sheets: function() {
|
||||
//let sheets = Meteor.collections.SalesSheets.find({}, {sort: {name: 1}}).fetch();
|
||||
//
|
||||
//if(sheets && sheets.length > 0) sheets[0].isFirst = true;
|
||||
//
|
||||
//return sheets;
|
||||
return Meteor.collections.SalesSheets.find({}, {sort: {name: 1}});
|
||||
},
|
||||
sheetsSelectIsSelected: function(sheet, isFirst) {
|
||||
let selectedSheet = Session.get(PREFIX + "selectedSheet");
|
||||
|
||||
if(!selectedSheet && isFirst) Session.set(PREFIX + "selectedSheet", selectedSheet = sheet);
|
||||
|
||||
return selectedSheet == sheet ? "selected" : "";
|
||||
},
|
||||
disableButtons: function() {
|
||||
//Disable the edit & delete functionality if nothing is selected.
|
||||
return !Session.get(PREFIX + "selectedSheet");
|
||||
},
|
||||
selected: function() {
|
||||
//Get whether the current sheet is selected and return the string for use in the option tag.
|
||||
//return this.isSelected ? "selected" : "";
|
||||
return this._id == Session.get(PREFIX + 'selectedSheet')._id;
|
||||
},
|
||||
tab: function() {
|
||||
return Session.get(PREFIX + "tab");
|
||||
},
|
||||
tabData: function() {
|
||||
return Session.get(PREFIX + "selectedSheet")._id;
|
||||
},
|
||||
isSheetSelected: function() {
|
||||
return Session.get(PREFIX + "selectedSheet");
|
||||
},
|
||||
isEditingSheet: function() {
|
||||
return Session.get(PREFIX + "tab") == "SalesSheetEditor";
|
||||
}
|
||||
});
|
||||
Template.SalesSheetsMain.events({
|
||||
'change select[name="sheetSelection"]': function(event, template) {
|
||||
let id = $(event.target).val();
|
||||
let selected = Meteor.collections.SalesSheets.findOne(id);
|
||||
|
||||
Session.set(PREFIX + "selectedSheet", selected);
|
||||
// Reset the editor button & the displayed tab.
|
||||
Session.set(PREFIX + "tab", "SalesSheetForm");
|
||||
},
|
||||
'click .editSheet': function(event, template) {
|
||||
if(!$(event.target).hasClass("selected")) {
|
||||
// Display the editor for the sheet.
|
||||
Session.set(PREFIX + "tab", "SalesSheetEditor");
|
||||
}
|
||||
else {
|
||||
// Remove the sheet editor and show the form to fill out the sheet.
|
||||
Session.set(PREFIX + "tab", "SalesSheetForm");
|
||||
// Reset the editor session variables.
|
||||
Session.set(PREFIX + "currentFormName", undefined);
|
||||
}
|
||||
},
|
||||
'click .deleteSheet': function(event, template) {
|
||||
let selectedSheet = Session.get(PREFIX + "selectedSheet");
|
||||
|
||||
if(selectedSheet) {
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "This will permanently remove the sale sheet named " + selectedSheet.name + ".",
|
||||
type: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "Yes"
|
||||
}).then(
|
||||
function(isConfirm) {
|
||||
if(isConfirm) {
|
||||
Meteor.call('removeSalesSheet', selectedSheet._id, function(error) {
|
||||
if(error) sAlert.error("Failed to delete the sheet!\n" + error);
|
||||
else {
|
||||
Session.set(PREFIX + "selectedSheet", Meteor.collections.SalesSheets.findOne({}, {sort: {name: 1}}));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
function(dismiss) {}
|
||||
);
|
||||
}
|
||||
},
|
||||
'click .createSheet': function(event, template) {
|
||||
let $input = template.$('input[name="newSheetName"]');
|
||||
|
||||
if($input.hasClass('show')) {
|
||||
let name = $input.val();
|
||||
|
||||
name = name ? name.trim() : undefined;
|
||||
name = name && name.length > 0 ? name : undefined;
|
||||
|
||||
if(name) {
|
||||
Meteor.call('createSalesSheet', name, function(error, id) {
|
||||
if(error) sAlert.error("Failed to create the sheet!\n" + error);
|
||||
else {
|
||||
//Quick hack to attempt to allow the sheet we created to be added to the select box before we try to select it and edit it.
|
||||
let count = 0;
|
||||
let interval = setInterval(function() {
|
||||
let selected = Meteor.collections.SalesSheets.findOne(id);
|
||||
|
||||
if(selected) {
|
||||
//Select the sheet in the drop down.
|
||||
template.$('select[name="sheetSelection"]').val(id);
|
||||
Session.set(PREFIX + "selectedSheet", selected);
|
||||
//Display the editor tab.
|
||||
Session.set(PREFIX + "tab", "SalesSheetEditor");
|
||||
clearInterval(interval);
|
||||
}
|
||||
else count++;
|
||||
|
||||
//Avoid infinite loop that should never happen.
|
||||
if(count > 100) clearInterval(interval);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$input.removeClass('show');
|
||||
$(event.target).toggleClass('move');
|
||||
}
|
||||
else {
|
||||
$input.addClass('show');
|
||||
$(event.target).toggleClass('move');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
<template name="Body">
|
||||
{{> sAlert}}
|
||||
<div id="layoutBody">
|
||||
<div class="mainBody">
|
||||
<div class="leftSidebar">
|
||||
<!--<div id="layoutBody" class="verticalStack">-->
|
||||
<div id="mainBody" class="mainBody verticalStack vscExpand">
|
||||
<div class="leftSidebar vscFixed">
|
||||
<div class="logoArea">
|
||||
<i class="fa fa-sign-out fa-2x signOut" aria-hidden="true"></i>
|
||||
<div class="logo">
|
||||
<img src="/images/PetitTetonLogo_v2.png"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menuArea">
|
||||
<ul>
|
||||
{{#if isInRole 'manage'}}
|
||||
<li class="{{isActiveRoute 'UserManagement'}}">
|
||||
@@ -20,6 +23,11 @@
|
||||
Sales <span class="tag">Test Tag</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{isActiveRoute 'SalesSheets'}}">
|
||||
<a href="{{pathFor 'SalesSheets'}}">
|
||||
Sales Sheets
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{isActiveRoute 'Production'}}">
|
||||
<a href="{{pathFor 'Production'}}">
|
||||
Production <span class="tag">sample</span>
|
||||
@@ -57,21 +65,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contentBody">
|
||||
<div class="contentContainer">
|
||||
<div class="header">
|
||||
|
||||
</div>
|
||||
<div class="content">
|
||||
{{> Template.dynamic template=content}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
© Petit Teton LLC 2017
|
||||
</div>
|
||||
</div>
|
||||
<div class="contentBody verticalStack">
|
||||
{{> Template.dynamic template=content}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--</div>-->
|
||||
</template>
|
||||
|
||||
<!--<template name="Body">-->
|
||||
|
||||
136
imports/ui/layouts/Body.import.styl
vendored
136
imports/ui/layouts/Body.import.styl
vendored
@@ -1,37 +1,52 @@
|
||||
#layoutBody
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: table
|
||||
text-align: center
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 0
|
||||
//#layoutBody
|
||||
// width: 100%
|
||||
// height: 100%
|
||||
// //display: table //Firefox does not like this - no idea why. Not required apparently.
|
||||
// margin: 0
|
||||
// padding: 0
|
||||
// border: 0
|
||||
// overflow: hidden
|
||||
|
||||
.mainBody
|
||||
display: table
|
||||
height: 100%
|
||||
width: 100%
|
||||
#mainBody
|
||||
//position: relative
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
//display: inline-block // Requried by Firefox for absolute positioning.
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 0
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
.leftSidebar
|
||||
display: table-cell
|
||||
position: relative
|
||||
flex: 0 0 auto
|
||||
display: flex
|
||||
flex-flow: column
|
||||
justify-content: flex-start
|
||||
align-items: flex-start
|
||||
align-content: stretch
|
||||
height: 100%
|
||||
//position: absolute
|
||||
border: 0
|
||||
vertical-align: top
|
||||
padding: 0
|
||||
text-align: left
|
||||
//top: 0px
|
||||
//left: 0px
|
||||
//bottom: 0px
|
||||
width: 220px
|
||||
height: 100%
|
||||
//Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D
|
||||
background: #90b272 //Old browsers
|
||||
background-color: #90b272 //Old browsers
|
||||
background: -moz-linear-gradient(-180deg, #90b272 0%, #4d7727 100%) //FF3.6-15
|
||||
background: -webkit-linear-gradient(-180deg, #90b272 0%,#4d7727 100%) //Chrome10-25,Safari5.1-6
|
||||
background: linear-gradient(180deg, #90b272 0%,#4d7727 100%) //W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+
|
||||
font-size: 14px
|
||||
font-weight: 700
|
||||
overflow: hidden
|
||||
|
||||
.logoArea
|
||||
flex: 0 0 auto
|
||||
width: 100%
|
||||
.signOut
|
||||
position: absolute
|
||||
left: 10px
|
||||
@@ -42,16 +57,20 @@
|
||||
color: #BBB
|
||||
.signOut:active
|
||||
color: black
|
||||
|
||||
.logo
|
||||
text-align: center
|
||||
margin-top: 20px
|
||||
|
||||
img:hover
|
||||
//-webkit-animation: neon6_drop 1.5s ease-in-out infinite alternate;
|
||||
//-moz-animation: neon6_drop 1.5s ease-in-out infinite alternate;
|
||||
animation: neon6_drop 1.5s ease-in-out infinite alternate;
|
||||
.menuArea
|
||||
flex: 1 1 auto
|
||||
width: 100%
|
||||
ul
|
||||
padding: 20px 0 0 0
|
||||
margin: 0
|
||||
list-style: none
|
||||
|
||||
li:first-child
|
||||
border-top: 1px solid #e4e5e7
|
||||
li
|
||||
@@ -59,14 +78,12 @@
|
||||
color: #96a2ae
|
||||
text-transform: uppercase
|
||||
display: block
|
||||
|
||||
a
|
||||
color: black
|
||||
padding: 15px 20px
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
display: block
|
||||
|
||||
.tag
|
||||
padding: .2em .5em
|
||||
font-size: .7em
|
||||
@@ -78,40 +95,59 @@
|
||||
float: right
|
||||
li:hover
|
||||
background-color: #333
|
||||
-webkit-animation: neon6 1.5s ease-in-out infinite alternate;
|
||||
-moz-animation: neon6 1.5s ease-in-out infinite alternate;
|
||||
animation: neon6 1.5s ease-in-out infinite alternate;
|
||||
li.active
|
||||
background-color: #333
|
||||
|
||||
a
|
||||
color: #96a2ae
|
||||
.contentBody
|
||||
display: table-cell
|
||||
//background: #4d7727
|
||||
|
||||
.contentContainer
|
||||
display: table
|
||||
width: 100%
|
||||
height: 100%
|
||||
//border-radius 20px
|
||||
//border: 0;
|
||||
background: white
|
||||
|
||||
.header
|
||||
display: table-row
|
||||
background: #90b272
|
||||
width: 100%
|
||||
height: 1px
|
||||
.content
|
||||
display: table-row
|
||||
width: 100%
|
||||
-webkit-box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
|
||||
-moz-box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
|
||||
box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
|
||||
.footer
|
||||
display: table-row
|
||||
height: 1px
|
||||
text-align: center
|
||||
background: #4d7727
|
||||
li.active:hover
|
||||
background-color: #333
|
||||
a
|
||||
color: white
|
||||
.footer
|
||||
flex: 0 0 auto
|
||||
width: 100%
|
||||
font-size: 9px
|
||||
text-align: center
|
||||
|
||||
.contentBody
|
||||
flex: 1 1 1px
|
||||
padding: 10px 20px
|
||||
-webkit-box-shadow: inset 4px 2px 10px -3px rgba(168,165,168,1)
|
||||
-moz-box-shadow: inset 4px 2px 10px -3px rgba(168,165,168,1)
|
||||
box-shadow: inset 8px 0px 10px -3px rgba(168,165,168,1)
|
||||
//position: absolute
|
||||
//top: 0
|
||||
//bottom: 0
|
||||
//left: 220px
|
||||
//right: 0
|
||||
overflow: hidden
|
||||
|
||||
//.contentBody
|
||||
// //display: table-cell
|
||||
// position: absolute
|
||||
// top: 0
|
||||
// bottom: 0
|
||||
// left: 220px
|
||||
// right: 0
|
||||
// //background: #4d7727
|
||||
//
|
||||
// .contentContainer
|
||||
// display: table
|
||||
// width: 100%
|
||||
// height: 100%
|
||||
// //border-radius 20px
|
||||
// //border: 0;
|
||||
// background: white
|
||||
//
|
||||
// .content
|
||||
// display: table-row
|
||||
// width: 100%
|
||||
// -webkit-box-shadow: inset 4px 2px 10px -3px rgba(168,165,168,1)
|
||||
// -moz-box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
|
||||
// box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
|
||||
|
||||
|
||||
//#layoutBody
|
||||
|
||||
52
imports/ui/styles/buttons.import.styl
vendored
Normal file
52
imports/ui/styles/buttons.import.styl
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
span.button
|
||||
margin: 0 0 0 1px
|
||||
padding: 0.5em 1em
|
||||
border: 1px solid #d4d4d4
|
||||
border-radius: 50em
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
cursor: pointer
|
||||
outline: none
|
||||
background-color: #ececec
|
||||
color: #333
|
||||
font: 11px/normal sans-serif
|
||||
text-shadow: 1px 1px 0 #fff
|
||||
text-align: center
|
||||
text-decoration: none
|
||||
//Prevent selection
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||
-khtml-user-select: none; /* Konqueror */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently not supported by any browser */
|
||||
span.button:hover
|
||||
color: blue
|
||||
span.button:active
|
||||
color: #fff
|
||||
background-color: #141414
|
||||
text-shadow: 1px 1px 0 #000
|
||||
border: 1px solid #292929
|
||||
span.button.primary
|
||||
font-weight: 800
|
||||
span.button.selected //Use this if they are toggle buttons
|
||||
color: #fff
|
||||
background-color: #141414
|
||||
text-shadow: 1px 1px 0 #000
|
||||
cursor: default
|
||||
span.buttonGroup
|
||||
:not(:first-child):not(:last-child)
|
||||
border-radius: 0
|
||||
:first-child
|
||||
border-top-left-radius: 50em
|
||||
border-bottom-left-radius: 50em
|
||||
border-top-right-radius: 0
|
||||
border-bottom-right-radius: 0
|
||||
margin-left: 0
|
||||
:last-child
|
||||
border-top-left-radius: 0
|
||||
border-bottom-left-radius: 0
|
||||
border-top-right-radius: 50em
|
||||
border-bottom-right-radius: 50em
|
||||
34
imports/ui/styles/effects.import.styl
vendored
Normal file
34
imports/ui/styles/effects.import.styl
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
@-webkit-keyframes neon6
|
||||
from
|
||||
text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #ff00de, 0 0 70px #ff00de, 0 0 80px #ff00de, 0 0 100px #ff00de, 0 0 150px #ff00de
|
||||
to
|
||||
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #ff00de, 0 0 35px #ff00de, 0 0 40px #ff00de, 0 0 50px #ff00de, 0 0 75px #ff00de
|
||||
@-moz-keyframes neon6
|
||||
from
|
||||
text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #ff00de, 0 0 70px #ff00de, 0 0 80px #ff00de, 0 0 100px #ff00de, 0 0 150px #ff00de
|
||||
to
|
||||
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #ff00de, 0 0 35px #ff00de, 0 0 40px #ff00de, 0 0 50px #ff00de, 0 0 75px #ff00de
|
||||
@keyframes neon6
|
||||
from
|
||||
text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #ff00de, 0 0 70px #ff00de, 0 0 80px #ff00de, 0 0 100px #ff00de, 0 0 150px #ff00de
|
||||
to
|
||||
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #ff00de, 0 0 35px #ff00de, 0 0 40px #ff00de, 0 0 50px #ff00de, 0 0 75px #ff00de
|
||||
|
||||
//@-webkit-keyframes neon6_drop
|
||||
// from
|
||||
// -webkit-filter: drop-shadow(0px 0px 20px rgba(194,0,0,0.7))
|
||||
// filter: url(shadow.svg#drop-shadow)
|
||||
// 50%
|
||||
// -webkit-filter: drop-shadow(0px 0px 20px rgba(255,0,0,1))
|
||||
// filter: url(shadow.svg#drop-shadow)
|
||||
// to
|
||||
// -webkit-filter: drop-shadow(0px 0px 20px rgba(194,0,0,0.7))
|
||||
// filter: url(shadow.svg#drop-shadow)
|
||||
|
||||
@keyframes neon6_drop
|
||||
from
|
||||
filter: drop-shadow(0px 0px 20px rgba(194,0,0,0.7)) brightness(120%)
|
||||
50%
|
||||
filter: drop-shadow(0px 0px 20px rgba(255,0,0,1)) brightness(80%)
|
||||
to
|
||||
filter: drop-shadow(0px 0px 20px rgba(194,0,0,0.7)) brightness(100%)
|
||||
117
imports/ui/styles/forms.import.styl
vendored
Normal file
117
imports/ui/styles/forms.import.styl
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
//Form Styles
|
||||
.select2-container
|
||||
font-size: 10px
|
||||
.select2-selection
|
||||
font-size: 13px //Make the font small enough the control can have a height similar to a standard input field.
|
||||
margin-bottom: 0px
|
||||
min-height: 10px !important //This is what really sets the height of the box containing the selection(s)
|
||||
padding-bottom: 2px //Add a little space below the selections to balance it all out.
|
||||
input
|
||||
padding: 6px
|
||||
border-radius: 4px
|
||||
border-width: 1px
|
||||
border-style: solid
|
||||
border-color: #ccc
|
||||
//input[type='button'].btn-success, input[type='submit'].btn-success
|
||||
// background-color: #5cb85c
|
||||
// :hover
|
||||
// background-color:
|
||||
//input[type='button'].btn-danger, input[type='submit'].btn-danger
|
||||
// background-color: #e55b46
|
||||
.form-control, .select2-selection //?
|
||||
font-size: 14px
|
||||
margin-bottom: 0px
|
||||
.form-group
|
||||
margin: 4px 0
|
||||
.has-error .form-control
|
||||
border-color: #a94442
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0)
|
||||
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control
|
||||
line-height: 34px
|
||||
|
||||
.form-control
|
||||
display: block
|
||||
width: 100%
|
||||
height: 34px
|
||||
padding: 6px 12px
|
||||
font-size: 14px
|
||||
line-height: 1.42857143
|
||||
color: #555
|
||||
background-color: #fff
|
||||
background-image: none
|
||||
border: 1px solid #ccc
|
||||
border-radius: 4px
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075)
|
||||
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s
|
||||
input[type="date"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"]
|
||||
align-items: center
|
||||
-webkit-padding-start: 1px
|
||||
overflow: hidden
|
||||
padding-left: 10px
|
||||
input
|
||||
-webkit-appearance: textfield
|
||||
background-color: white
|
||||
-webkit-rtl-ordering: logical
|
||||
user-select: text
|
||||
cursor: auto
|
||||
padding: 1px
|
||||
border-width: 2px
|
||||
border-style: inset
|
||||
border-color: initial
|
||||
border-image: initial
|
||||
.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control
|
||||
background-color: #eee
|
||||
opacity: 1
|
||||
input, textarea, keygen, select, button
|
||||
text-rendering: auto
|
||||
color: initial
|
||||
letter-spacing: normal
|
||||
word-spacing: normal
|
||||
text-transform: none
|
||||
text-indent: 0px
|
||||
text-shadow: none
|
||||
display: inline-block
|
||||
text-align: start
|
||||
margin: 0em 0em 0em 0em
|
||||
font: 13.3333px Arial
|
||||
input, textarea, keygen, select, button, meter, progress
|
||||
-webkit-writing-mode: horizontal-tb
|
||||
//.btn.disabled, .btn[disabled], fieldset[disabled] .btn
|
||||
// cursor: not-allowed
|
||||
// filter: unquote("alpha(opacity=65)")
|
||||
// -webkit-box-shadow: none
|
||||
// box-shadow: none
|
||||
// opacity: .65
|
||||
//button, html input[type="button"], input[type="reset"], input[type="submit"]
|
||||
// -webkit-appearance: button
|
||||
// cursor: pointer
|
||||
//button, html input[type="button"], input[type="reset"], input[type="submit"]
|
||||
// -webkit-appearance: button
|
||||
// cursor: pointer
|
||||
//.btn
|
||||
// display: inline-block;
|
||||
// padding: 6px 12px;
|
||||
// margin-bottom: 0;
|
||||
// font-size: 14px;
|
||||
// font-weight: normal;
|
||||
// line-height: 1.42857143;
|
||||
// text-align: center;
|
||||
// white-space: nowrap;
|
||||
// vertical-align: middle;
|
||||
// -ms-touch-action: manipulation;
|
||||
// touch-action: manipulation;
|
||||
// cursor: pointer;
|
||||
// -webkit-user-select: none;
|
||||
// -moz-user-select: none;
|
||||
// -ms-user-select: none;
|
||||
// user-select: none;
|
||||
// background-image: none;
|
||||
// border: 1px solid transparent;
|
||||
// border-radius: 4px;
|
||||
133
imports/ui/styles/maxHeightLayout.import.styl
vendored
Normal file
133
imports/ui/styles/maxHeightLayout.import.styl
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
section.maxHeightContainer, div.maxHeightContainer
|
||||
display: table
|
||||
height: 100%
|
||||
width: 100%
|
||||
section.maxHeightRow, div.maxHeightRow
|
||||
display: table-row
|
||||
section.maxHeightContent, div.maxHeightContent //Use this for a row of content that should shrink to fit the content size.
|
||||
display: table-cell
|
||||
height: 1px
|
||||
section.maxHeightContentExpandAndScroll, div.maxHeightContentExpandAndScroll //Use this for a row of content that should take up all remaining space and will contain potentially scrolled content.
|
||||
display: table-cell
|
||||
height: 100%
|
||||
position: relative
|
||||
section.maxHeightContentScrolled, div.maxHeightContentScrolled //Use this to create the scrolled content. Can use any display within it.
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
overflow-y: auto
|
||||
|
||||
// *****************
|
||||
// ** Vertical Stack
|
||||
// ** Use .verticalStack on containers, and .vscFixed or .vscExpand on children (they can also be containers).
|
||||
// ** Designed to use Flexbox to allow full screen (vertical) layouts. Fixed children will fit the content, and expand children will consume all available vertical space, but will not exceed the vertical space.
|
||||
// ** Use .columnContainer to setup a horizontally scrolling, full height container where children are tiled down first, then wrap to the next column.
|
||||
/*
|
||||
Test Code:
|
||||
-------CSS------
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
body {
|
||||
background: purple;
|
||||
color: black;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.vscFixed {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.vscExpand {
|
||||
flex: 1 1 1px;
|
||||
width: 100%;
|
||||
}
|
||||
.verticalStack {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: stretch;
|
||||
}
|
||||
.columnContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
align-content: flex-start;
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
}
|
||||
.columnContent {
|
||||
flex: none;
|
||||
width: 300px;
|
||||
}
|
||||
-------Javascript------
|
||||
var container = document.querySelector('.columnContainer');
|
||||
for(var i = 0; i < 400; i++) {
|
||||
var child = document.createElement("div");
|
||||
child.innerHTML = "Element " + i;
|
||||
child.className = "columnContent";
|
||||
container.appendChild(child);
|
||||
}
|
||||
-------HTML------
|
||||
<div class="verticalStack">
|
||||
<div class='vscFixed' style="width: 100%; background:green">
|
||||
<p>
|
||||
Some content.
|
||||
</p>
|
||||
<p>
|
||||
More content...
|
||||
</p>
|
||||
</div>
|
||||
<div class="verticalStack vscExpand">
|
||||
<div class='vscFixed' style="background: yellow;">
|
||||
Test bar.
|
||||
</div>
|
||||
<div class="columnContainer vscExpand">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
*/
|
||||
.vscFixed {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.vscExpand {
|
||||
flex: 1 1 1px;
|
||||
width: 100%;
|
||||
}
|
||||
.verticalStack {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: stretch;
|
||||
}
|
||||
.columnContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
align-content: flex-start;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.columnContent {
|
||||
flex: none;
|
||||
}
|
||||
61
imports/ui/styles/tabs.import.styl
vendored
Normal file
61
imports/ui/styles/tabs.import.styl
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
ul.tabRow
|
||||
position: relative
|
||||
text-align: left
|
||||
list-style: none
|
||||
margin: 0
|
||||
padding: 0 0 0 10px
|
||||
line-height: 24px
|
||||
height: 26px
|
||||
font-size: 12px
|
||||
overflow: hidden
|
||||
li
|
||||
position: relative
|
||||
z-index: 0
|
||||
border: 1px solid #AAA
|
||||
background: #D1D1D1
|
||||
display: inline-block
|
||||
border-top-left-radius: 6px
|
||||
border-top-right-radius: 6px
|
||||
background: -o-linear-gradient(top, #ECECEC 50%, #D1D1D1 100%)
|
||||
background: -ms-linear-gradient(top, #ECECEC 50%, #D1D1D1 100%)
|
||||
background: -moz-linear-gradient(top, #ECECEC 50%, #D1D1D1 100%)
|
||||
background: -webkit-linear-gradient(top, #ECECEC 50%, #D1D1D1 100%)
|
||||
background: linear-gradient(top, #ECECEC 50%, #D1D1D1 100%)
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.4), inset 0 1px 0 #FFF
|
||||
text-shadow: 0 1px #FFF
|
||||
margin: 0 -5px
|
||||
padding: 0 20px
|
||||
li:before, li:after
|
||||
position: absolute
|
||||
bottom: -1px
|
||||
width: 5px
|
||||
height: 5px
|
||||
content: " "
|
||||
border: 1px solid #AAA
|
||||
li:before
|
||||
left: -6px
|
||||
border-bottom-right-radius: 6px
|
||||
border-width: 0 1px 1px 0
|
||||
box-shadow: 2px 2px 0 #D1D1D1
|
||||
li:after
|
||||
right: -6px
|
||||
border-bottom-left-radius: 6px
|
||||
border-width: 0 0 1px 1px
|
||||
box-shadow: -2px 2px 0 #D1D1D1
|
||||
li.selected
|
||||
z-index: 2
|
||||
background: white
|
||||
color: #333
|
||||
border-bottom-color: #FFF
|
||||
li.selected:before
|
||||
box-shadow: 2px 2px 0 #FFF
|
||||
li.selected:after
|
||||
box-shadow: -2px 2px 0 #FFF
|
||||
ul.tabRow:before
|
||||
position: absolute
|
||||
width: 100%
|
||||
bottom: 0
|
||||
left: 0
|
||||
border-bottom: 1px solid #AAA
|
||||
z-index: 1
|
||||
content: " "
|
||||
@@ -199,11 +199,31 @@
|
||||
if(this.$hidden && _this.$hidden.val()) {
|
||||
hiddenInputChanged();
|
||||
}
|
||||
|
||||
//TODO: Should probably check to ensure comparator is a function? Not that it will help much if the function is not written correctly. Stupid Javascript!
|
||||
if(this.options.selection && this.options.comparator) {
|
||||
Tracker.autorun(function() {
|
||||
let selectedData = this.options.selection.get();
|
||||
|
||||
if(selectedData) {
|
||||
let listItems = this.$list.children();
|
||||
let found = false;
|
||||
|
||||
for(let i = 0; !found && i < listItems.length; i++) {
|
||||
if(this.options.comparator($(listItems[i]).data('model'), selectedData)) {
|
||||
found = true;
|
||||
this.select($(listItems[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
Combo.DEFAULTS = {
|
||||
cursor: undefined, //A meteor Cursor.
|
||||
selection: undefined, //A meteor ReactiveVar whose value will be set to the current selection.
|
||||
comparator: undefined, //A function that takes two collection objects and compares them for equality. If the combo shows users for example, this comparator would compare one user id to another. Required for the combo to set the selection if the view changes it externally relative to this combo.
|
||||
textAttr: 'text', //The attribute of the data elements to use for the name. This can also be a function that takes the data object and returns the text.
|
||||
idAttr: 'id', //The attribute of the data elements to use for the ID. This can also be a function that takes the data obejct and returns the ID.
|
||||
// groupFunctions: The object containing three functions: 'groupParent', 'parentText', 'children'.
|
||||
@@ -222,7 +242,7 @@
|
||||
Combo.prototype.select = function($li) {
|
||||
if($li.length == 0) {
|
||||
if(this.$input.val() != '') {
|
||||
this.$input.val("")
|
||||
this.$input.val("");
|
||||
if(this.$hidden) this.$hidden.val(undefined).change();
|
||||
this.filter();
|
||||
//Note: Don't trigger the select event - for some reason it causes the dropdown to reopen and the control to retain focus when clicking out of the widget.
|
||||
@@ -242,14 +262,18 @@
|
||||
this.filter();
|
||||
//this.trigger('select', $li);
|
||||
|
||||
//Set the reactive var for the selection if one is provided.
|
||||
if(this.options.selection) {
|
||||
//Set the reactive var for the selection if one is provided and the selection has changed relative to the model.
|
||||
if(this.options.selection && this.options.selection.get() != $li.data('model')) {
|
||||
this.options.selection.set($li.data('model'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Combo.prototype.escapeRegex = function(s) {
|
||||
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
};
|
||||
|
||||
//Filters the list items by marking those that match the text in the text field as having the class 'visible'.
|
||||
Combo.prototype.filter = function() {
|
||||
try {
|
||||
@@ -273,7 +297,7 @@
|
||||
|
||||
if(searches) {
|
||||
for(let i = 0; i < searches.length; i++) {
|
||||
regexs.push(new RegExp("\\b" + searches[i]));
|
||||
regexs.push(new RegExp("\\b" + this.escapeRegex(searches[i])));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
|
||||
if (!Array.prototype.includes) {
|
||||
if(!Array.prototype.includes) {
|
||||
Object.defineProperty(Array.prototype, 'includes', {
|
||||
value: function(searchElement, fromIndex) {
|
||||
|
||||
@@ -8,10 +8,10 @@ if (!Array.prototype.includes) {
|
||||
throw new TypeError('"this" is null or not defined');
|
||||
}
|
||||
|
||||
var o = Object(this);
|
||||
let o = Object(this);
|
||||
|
||||
// 2. Let len be ? ToLength(? Get(O, "length")).
|
||||
var len = o.length >>> 0;
|
||||
let len = o.length >>> 0;
|
||||
|
||||
// 3. If len is 0, return false.
|
||||
if (len === 0) {
|
||||
@@ -20,14 +20,14 @@ if (!Array.prototype.includes) {
|
||||
|
||||
// 4. Let n be ? ToInteger(fromIndex).
|
||||
// (If fromIndex is undefined, this step produces the value 0.)
|
||||
var n = fromIndex | 0;
|
||||
let n = fromIndex | 0;
|
||||
|
||||
// 5. If n ≥ 0, then
|
||||
// a. Let k be n.
|
||||
// 6. Else n < 0,
|
||||
// a. Let k be len + n.
|
||||
// b. If k < 0, let k be 0.
|
||||
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
|
||||
let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
@@ -46,3 +46,63 @@ if (!Array.prototype.includes) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//http://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another
|
||||
if(!Array.prototype.move) {
|
||||
Array.prototype.move = function (old_index, new_index) {
|
||||
if (new_index >= this.length) {
|
||||
let k = new_index - this.length;
|
||||
while ((k--) + 1) {
|
||||
this.push(undefined);
|
||||
}
|
||||
}
|
||||
this.splice(new_index, 0, this.splice(old_index, 1)[0]);
|
||||
return this; // for testing purposes
|
||||
};
|
||||
}
|
||||
|
||||
//My own implementation to work around Javascript's shitty naming and support for collection operations.
|
||||
if(!Array.prototype.remove) {
|
||||
Array.prototype.remove = function(item) {
|
||||
let index = this.indexOf(item);
|
||||
|
||||
if(index != -1) {
|
||||
this.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//My own implementation to work around Javascript's shitty naming and support for collection operations.
|
||||
// index is optional
|
||||
if(!Array.prototype.add) {
|
||||
Array.prototype.add = function(item, index) {
|
||||
if(index == undefined || isNaN(index) || index >= this.length) return this.push(item);
|
||||
else return this.splice(index, 0, item);
|
||||
}
|
||||
}
|
||||
|
||||
//My own implementation to work around Javascript's shitty naming and support for collection operations.
|
||||
// Sorts a contiguous section of the array.
|
||||
// Index is the index of the first element to be sorted (inclusive).
|
||||
// Length is the number of elements of the array to be sorted (must be >= 2). If the length + index is greater than the array length then it will be adjusted to the end of the array.
|
||||
// All other invalid inputs will result in no sorting action taken and no error.
|
||||
if(!Array.prototype.partialSort) {
|
||||
Array.prototype.partialSort = function(index, length, compareFunction) {
|
||||
if(index >= 0 && length >= 2 && index <= (this.length - 2)) {
|
||||
//Adjust the length so it doesn't over-run the array. This is the only error correction we will perform.
|
||||
if(index + length > this.length) length = this.length - index;
|
||||
|
||||
//Shallow copy of the data in the segment to be sorted.
|
||||
let sorted = this.slice(index, length + index);
|
||||
|
||||
sorted.sort(compareFunction);
|
||||
|
||||
//Put the sorted array elements back into the array.
|
||||
for(let i = index, j = 0; i <= length; i++, j++) {
|
||||
this[i] = sorted[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @param {Number} [levels] How many levels to go up. Default is 1
|
||||
* @returns {Blaze.TemplateInstance}
|
||||
*/
|
||||
Blaze.TemplateInstance.prototype.parentTemplate = function(levels) {
|
||||
Blaze.TemplateInstance.prototype.parentTemplate = Blaze.TemplateInstance.prototype.parentInstance = function(levels) {
|
||||
let view = this.view;
|
||||
|
||||
levels = (typeof levels === "undefined") ? 1 : levels;
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
* validate(fn) - Forces validation to occur and takes an optional callback which will be passed a flag (boolean) indicating the success of the validation (isValid).
|
||||
* reset() - Resets the form's validation status. Clears all error information, without turning validation off.
|
||||
* update() - Updates the collection of fields that require validation. Call this after making changes to the form, including initializing any form elements that may generate HTML (such as Select2).
|
||||
*
|
||||
* Notes:
|
||||
* To handle decimal values "0.05", you need to add `step='0.01'` or similar to the input field.
|
||||
* To get the validator to validate a field, you must add `required` to the property list: `<input type='text' required/>`
|
||||
* I have modified this to not require a form-group or form-input classed container. If one is found, then the container will be used to mark for errors and success, otherwise the field element will be marked instead. Example: `<div class='form-group'><input type='text' required/></div>`
|
||||
*/
|
||||
|
||||
+function ($) {
|
||||
@@ -372,8 +377,11 @@
|
||||
|
||||
$block.empty().append(errors);
|
||||
|
||||
if($group.length > 0)
|
||||
//Add the 'has-error' and 'has-danger' classes to the grouping.
|
||||
$group.addClass('has-error has-danger');
|
||||
else
|
||||
$el.addClass('has-error has-danger');
|
||||
|
||||
//If this is a select2 control then look for the child of a sibling that has the .select2-selection class and use it instead.
|
||||
if($el.hasClass('select2-hidden-accessible')) {
|
||||
@@ -395,7 +403,11 @@
|
||||
var $feedback = $group.find('.form-control-feedback');
|
||||
|
||||
$block.html($block.data('bs.validator.originalContent'));
|
||||
|
||||
if($group.length > 0)
|
||||
$group.removeClass('has-error has-danger has-success');
|
||||
else
|
||||
$el.removeClass('has-error has-danger has-success');
|
||||
|
||||
//Clean the sibling controls for select2.
|
||||
$el.parent().find('.select2 .select2-selection').removeClass('has-error has-danger has-success');
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "meteor run",
|
||||
"build": "meteor build --server-only ../"
|
||||
"build": "npm install --product && meteor build --architecture os.linux.x86_64 --server-only ../"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.18.0",
|
||||
"csv-parse": "latest",
|
||||
"d3": "^4.4.2",
|
||||
"dragula": "^3.7.2",
|
||||
"jquery": "^3.1.1",
|
||||
"meteor-node-stubs": "^0.2.4",
|
||||
"properties-reader": "0.0.15",
|
||||
"simpl-schema": "0.0.3"
|
||||
"simpl-schema": "0.0.3",
|
||||
"sweetalert2": "^6.3.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user