Prototyped the barcode idea; Added a basic production system.

This commit is contained in:
Wynne Crisman
2019-10-07 15:51:50 -07:00
parent 8211da6b39
commit 2e57558ef4
50 changed files with 8949 additions and 782 deletions

View File

@@ -33,8 +33,8 @@ arillo:flow-router-helpers # Provides various template helpers such as {{pathFo
#tomwasd:flow-router-seo
kadira:blaze-layout
shell-server@0.4.0 # ???
meteortoys:allthings
#shell-server@0.4.0 # ???
#meteortoys:allthings
stylus@2.513.13
session@1.2.0
##browser-policy # Adds support for specifying browser level security rules related to content and what's allowed to laod on the page.
@@ -55,7 +55,7 @@ aldeed:collection2
#twbs:bootstrap # Requires jquery 1.9-2.x, not 3+
fortawesome:fontawesome
momentjs:moment
mizzao:bootboxjs # ???
#mizzao:bootboxjs # ???
aldeed:template-extension
juliancwirko:s-alert # Client error/alert handling
jcbernack:reactive-aggregate # Allows us to create a new client collection (from the server) with the contents being an aggregate of server data. Note that aggregation can only be done on the server currently as mini-mongo does not support it.
@@ -66,3 +66,4 @@ markdown@1.0.12
wcrisman:jquery-custom-scrollbar
underscore@1.0.10
meteorhacks:aggregate # Allows databaseName.aggragate(pipeline) calls the exact same way you would on the command line in the mongo tool.
babrahams:constellation

View File

@@ -12,6 +12,9 @@ arillo:flow-router-helpers@0.5.2
autoupdate@1.6.0
babel-compiler@7.3.4
babel-runtime@1.3.0
babrahams:constellation@0.4.10
babrahams:editable-json@0.6.5
babrahams:temple@0.4.7
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.3
@@ -23,6 +26,14 @@ caching-html-compiler@1.1.3
callback-hook@1.1.0
check@1.3.1
coffeescript@1.0.17
constellation:autopublish@0.4.7
constellation:console@1.4.7
constellation:plugins@0.4.9
constellation:position@0.4.7
constellation:session@0.4.7
constellation:subscriptions@0.4.7
constellation:tiny@0.4.7
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
@@ -41,6 +52,8 @@ es5-shim@4.8.0
fetch@0.1.1
fortawesome:fontawesome@4.7.0
geojson-utils@1.0.10
gwendall:body-events@0.1.6
gwendall:session-json@0.1.7
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
@@ -52,6 +65,7 @@ jquery@1.11.11
juliancwirko:s-alert@3.2.0
kadira:blaze-layout@2.3.0
kadira:flow-router@2.12.1
lai:collection-extensions@0.2.1_1
launch-screen@1.1.1
livedata@1.0.18
localstorage@1.2.0
@@ -62,27 +76,9 @@ meteor@1.9.3
meteor-base@1.4.0
meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.0
meteortoys:allthings@4.0.0
meteortoys:authenticate@4.0.0
meteortoys:autopub@4.0.0
meteortoys:blueprint@4.0.0
meteortoys:email@4.0.0
meteortoys:hotreload@4.0.0
meteortoys:listen@4.0.0
meteortoys:method@4.0.0
meteortoys:mobile@4.0.0
meteortoys:pub@4.0.0
meteortoys:result@4.0.0
meteortoys:shell@4.0.0
meteortoys:status@4.0.0
meteortoys:sub@4.0.0
meteortoys:throttle@4.0.0
meteortoys:toggle@4.0.0
meteortoys:toykit@4.0.2
minifier-css@1.4.2
minifier-js@2.4.1
minimongo@1.4.5
mizzao:bootboxjs@4.4.0
mobile-experience@1.0.5
mobile-status-bar@1.0.14
modern-browsers@0.1.4
@@ -94,8 +90,6 @@ mongo-decimal@0.1.1
mongo-dev-server@1.1.0
mongo-id@1.0.7
mongo-livedata@1.0.12
msavin:jetsetter@4.0.0
msavin:mongol@4.0.1
npm-bcrypt@0.9.3
npm-mongo@3.1.2
observe-sequence@1.0.16
@@ -115,7 +109,6 @@ routepolicy@1.1.0
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
shell-server@0.4.0
socket-stream-client@0.2.2
softwarerero:accounts-t9n@1.3.11
spacebars@1.0.15

View File

@@ -1,4 +1,22 @@
# Petit Teton Data Management Application (PTApp)
# Current Configuration
See below for initial setup and updating instructions.
Currently we have three servers in two locations. Media and FS2 are computers residing on the 18501 property (big barn back top room, and Wynne/Sarah house), and FS1 is a computer residing in SF. All three are accessible via Putty (a windows SSH client - any SSH client will work) via the IP's 192.168.3.101 (Media), 192.168.2.239 (FS1), and 192.168.3.164 (FS2). Note that FS1 is in SF, but is accessible via the LAN because we have a VPN setup between our routers (192.168.3.1, and 192.168.2.1). The VPN makes them look like they are on the same network.
If you SSH (Putty) into each server you can update the server using APT (`sudo apt update`, followed by `update apt upgrade`), reboot `sudo shutdown -r now`, and perform other maintenance routines.
**Currently** Media is the primary server for the PTApp (the Meteor Petit Teton Webapp - versus the Petit Teton web site which is a customer focused informational web site). FS2 is the primary server for all other web sites and web apps and it clones its SSL certifications and www folders to the other two machines. Ultimately the PTApp will need to also be on FS2 primarily along with SSL certificates and an Nginx configuration that only allows certain access outside our LAN (for sales interaction at markets and Cam's home access). The database (MongoDB) is installed on all three machines, forming a cluster such that data written to any of the three is propagated to the other two automatically.
Database backups are not trivial due to the cluster system. Essentially we are doing a simple backup/restore and ignoring the possibility that the database we take from might not have all the latest updates. Doing this on our primary server (FS2) helps ensure that we don't have any problems. MongoDB being a very flexible database and our app not caring too much about data integrity helps as well.
TODO: Linux update procedure.
TODO: Backup procedure.
TODO: Mongo update procedure.
# Setup Petit Teton Data Management Application (PTApp)
This application is designed to track sales and production data.
@@ -27,21 +45,22 @@ Package this application by running 'npm run build' from the command line in the
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)
- bsondump (converts bson file formats [binary] into human readable formats [JSON] - use with mongodump files)
- mongodump (produces binary output for backups - not for use with shared clusters and replica sets)
- mongoexport (produces JSON or CSV data)
- mongofiles (command line gridfs integration - for manipulating files)
- mongooplog (replication & polling - ?)
- mongoperf
- mongorestore
- mongostat
- mongotop (stats)
- 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)
- bsondump (converts bson file formats [binary] into human readable formats [JSON] - use with mongodump files)
- mongodump (produces binary output for backups - not for use with shared clusters and replica sets)
- mongoexport (produces JSON or CSV data)
- mongofiles (command line gridfs integration - for manipulating files)
- mongooplog (replication & polling - ?)
- mongoperf
- mongorestore
- mongostat
- mongotop (stats)
11. Install nodejs & npm
1. `sudo apt-get install nodejs`
2. `sudo apt-get install npm`
12. Install n
1. `sudo npm install -g n`
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`
@@ -93,11 +112,12 @@ Package this application by running 'npm run build' from the command line in the
NOTE: Use MongoBooster on a windows development machine to connect to the dev database (localhost:3001) and to export.
#Updating a Meteor Deployment
# Updating a Meteor Deployment (OLD)
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).
3. Copy the archive to the server (**currently** Media/www/PTApp). 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). `cd /var/www/PTApp`
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.
@@ -105,23 +125,27 @@ NOTE: Use MongoBooster on a windows development machine to connect to the dev da
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
# Updating a Meteor Deployment (NEW)
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.
2. Copy the archive to the deployment server (**currently** Media/www/PTApp). 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).
# Updating a Meteor Deployment *with* a NodeJS and Meteor version change.
Check which version of meteor you have on the development machine (meteor is not installed ever on the production machine). If the version has changed, it may require a newer version of NodeJS. You have to read the Meteor notes for your version (or older versions) to figure out which NodeJS is required.
1. Update the meteor app as normal (copy it to the /var/www/xxx directory as a build bundle, then run the script to unpack it).
2. Run `sudo n` to get the current version of NodeJS being used. Use `sudo n x.x.x` to download and change to the new version of NodeJS.
3. Edit the app's nginx file in /etc/nginx/sites-available/ to reference the new NodeJS install location. For example my current install location is specified as `server {... passenger_nodejs /usr/local/n/versions/node/8.9.3/bin/node ...}`}
## Running Server Side Code
#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
## 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.

View File

@@ -14,6 +14,7 @@ import '/imports/util/resize/ResizeSensor.js';
import '/imports/util/resize/ElementQueries.js';
import '/imports/ui/layouts/Body.js';
import '/imports/ui/layouts/Login.js';
import '/imports/ui/layouts/Empty.js';
import '/imports/ui/accounts/accounts.js';
import '/imports/util/select2/select2.css';
import '/imports/util/select2/select2.full.js';

View File

@@ -1,5 +1,6 @@
<head>
<title>PT App</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/camanjs/4.0.0/caman.full.min.js"></script>
<!--<meta http-equiv="content-type" content="text/html; charset=UTF8">-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF8">

4195
client/main.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -188,4 +188,6 @@ body
@import "../imports/ui/Graphs.import.styl"
@import "../imports/ui/Reports.import.styl"
@import "../imports/ui/Label.import.styl"
@import "../imports/ui/TestList.import.styl"
@import "../imports/ui/TestList.import.styl"
@import "../imports/ui/PrintLabel.import.styl"

81
imports/api/Barcode.js Normal file
View File

@@ -0,0 +1,81 @@
import {Mongo} from "meteor/mongo";
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
Barcodes = new Mongo.Collection('Barcodes');
// A simple mapping between a concatenation of the product & measure ID and a unique sequential number for the barcode. This allows us to have a small number to keep our barcodes simple, while maintaining the more traditional MongoDB ID's for the Product and Measure.
const BarcodesSchema = new SimpleSchema({
barcodeId: {
type: Number,
label: "Barcode ID",
optional: false,
index: 1,
unique: true
},
productAndMeasureId: { //Just the two ids jammed together with a single space between them.
type: String,
label: "Product And Measure ID",
optional: false,
index: 1,
unique: true
}
});
if(Meteor.isServer) {
//Meteor.publish('barcodes', function() {
// return Barcodes.find({});
//});
Meteor.methods({
getBarcodeId: function(productId, measureId) {
check(productId, String);
check(measureId, String);
let hasProduct = Meteor.collections.Products.findOne({_id: productId}, {fields: {}});
let hasMeasure = Meteor.collections.Measures.findOne({_id: measureId}, {fields: {}});
if(hasProduct && hasMeasure) {
let existing = Barcodes.findOne({productAndMeasureId: productId + ' ' + measureId});
if(existing) {
return existing.barcodeId;
}
else {
let c = 0;
//Try a thousand times before failing. Should never fail, should also not ever need to try a thousand times (unless we somehow automate label generation to the point where a 1000 processes at once are requesting labels that have never been generated before - highly unlikely).
while(c++ < 1000) {
//Lookup the most likely next barcode id from the db, then attempt to insert with it. If it fails due to duplication, then increment and repeat.
let cursor = Products.find({}, {barcodeId: 1}).sort({barcodeId: -1}).limit(1); //Since currently products are never removed, we shouldn't need to detect sequence gaps and fill them in (odds are we will never use more than 10k numbers anyway).
let barcodeId = cursor.hasNext() ? cursor.next().barcodeId + 1 : 1;
Barcodes.insert({productAndMeasureId: productId + ' ' + measureId, barcodeId}, function(err, id) {
if(err) console.log(err);
else return barcodeId;
});
}
//If we are still here, then there was a massive failure (c exceeded 1000).
console.log("We failed to generate a new barcode ID 1000 times, so we are giving up. This should never happen.");
throw new Meteor.Error(403, "Unable to generate a barcode ID.");
}
}
else {
//Cannot find either the product or the measure in the db. Cannot give an id.
console.log("Unable to generate a barcode ID because we could not find the product " + productId + " OR we could not find the measure " + measureId);
throw new Meteor.Error(403, "Unable to find the product or the measure. Both must exist in order to generate a barcode ID.");
}
},
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
Barcodes.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default Barcodes;

265
imports/api/Batch.js Normal file
View File

@@ -0,0 +1,265 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
/**
* Notes:
* The Batch object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(batch.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
* A Batch in this system refers to one or more instances of cooking or preparing a product on a given date. It does NOT refer to each instance of cooking the product on that date (what might be called a batch in a kitchen). This might be more effectively called a Run, but that is a confusing word to use in a software system, so I chose to reuse the word Batch since we will not be tracking kitchen batches, just kitchen runs.
*/
let Batches = new Mongo.Collection('Batches');
let BatchesSchema = new SimpleSchema({
date: {
type: Number, // A number in the format of YYYYMMDD to allow for searching using greater and less than, and to prevent timezones from messing everything up.
label: "Date",
optional: false,
index: 1
},
timestamp: { //This is based off the date with zero for the time and set to GMT (Zulu time).
type: Date,
label: "Timestamp",
optional: true
},
weekOfYear: {
type: Number,
label: "Week Of Year",
optional: true
},
amount: {
type: Number,
label: "Amount",
optional: false,
decimal: true
},
measureId: {
type: String,
label: "Measure Id",
trim: false,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
productId: {
type: String,
label: "Product Id",
trim: false,
regEx: SimpleSchema.RegEx.Id,
index: 1,
optional: false
},
cookId: {
type: String,
label: "Cook Worker Id",
trim: false,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
cannerId: {
type: String,
label: "Canner Worker Id",
trim: false,
regEx: SimpleSchema.RegEx.Id,
index: 1,
optional: false
},
hasLabels: {
type: Boolean,
label: "Has Labels",
optional: false,
defaultValue: false
},
comment: {
type: String,
trim: false,
optional: true
},
createdAt: {
type: Date,
label: "Created On",
optional: false
},
deletedAt: {
type: Date,
label: "Deleted On",
optional: true
}
});
Batches.attachSchema(BatchesSchema);
//Ensure that the product ID, measure ID, and date combination are unique.
// Note: I took this out because while it provides for cleaner views, it is overly complicated and could be easily done with a cleanup routine after the fact, or by aggregating the data in the queries.
// What makes this complicated is the notes, cook, and canner references which may not be the same.
//Batches.createIndex({productId: 1, measureId: 1, date: 1}, {unique: true, name: "ProductMeasureDateIndex"});
if(Meteor.isServer) {
Meteor.publish('batches', function(query, sort, limit = 100, skipCount) {
let dbQuery = [];
if(query) {
_.each(_.keys(query), function(key) {
//if(_.isObject(query[key])) dbQuery.push({[key]: query[key]});
if(_.isObject(query[key])) {
if(query[key].type === 'dateRange') {
if(query[key].start && query[key].end)
dbQuery.push({[key]: {$gte: query[key].start, $lte: query[key].end}});
else if(query[key].start)
dbQuery.push({[key]: {$gte: query[key].start}});
else if(query[key].end)
dbQuery.push({[key]: {$lte: query[key].end}});
// Do nothing if a start and/or end are not provided.
}
else {
dbQuery.push({[key]: query[key]});
}
}
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]});
else {
let searchValue = query[key];
let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined;
for(let search of searches) {
dbQuery.push({[key]: {$regex: '\\b' + search, $options: 'i'}});
}
}
});
}
if(!_.isNumber(limit)) limit = 100;
if(!_.isNumber(skipCount) || skipCount < 0) skipCount = 0;
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
return Meteor.collections.Batches.find(dbQuery, {limit: limit, sort, skip: skipCount});
});
Meteor.methods({
getBatchCount: function(query) {
//TODO: Validate the query?
return Sales.find(query).count();
},
insertBatches: function(batches) { //Insert one or more batches (if one, you can pass just the batch).
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
//Force it to be an array if it isn't.
if(!Array.isArray(batches)) batches = [batches];
//Validate them all.
for(let batch of batches) {
check(batch, {
date: Number, // TODO: Check that the format is YYYYMMDD
amount: Match.Where(function(x) {
check(x, Number);
return x > 0;
}),
measureId: String,
productId: String,
cookId: String,
cannerId: String,
comment: Match.Optional(String)
});
}
for(let batch of batches) {
let dateString = batch.date.toString();
batch.createdAt = new Date();
batch.timestamp = new Date(dateString.substring(0, 4) + "-" + dateString.substring(4, 6) + "-" + dateString.substring(6, 8) + "T00:00:00Z");
batch.weekOfYear = batch.timestamp.getWeek().toString();
if(batch.hasLabels === undefined) batch.hasLabels = false;
}
for(let batch of batches) {
Batches.insert(batch, function(err, id) {
if(err) console.log(err);
}, {bypassCollection2: true});
}
}
else throw new Meteor.Error(403, "Not authorized.");
},
deleteBatch: function(id) { //Does not actually delete the batch, but rather just marks it for deleting by applying a deletion date.
check(id, String);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
let deletedAt = new Date();
//Batches.remove(id);
Batches.update(id, {$set: {deletedAt}}, function(err, id) {
if(err) console.log(err);
});
}
else throw new Meteor.Error(403, "Not authorized.");
},
undeleteBatch: function(id) { //Revokes the previous deletion.
check(id, String);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Batches.update(id, {$unset: {deletedAt:""}}, function(err, id) {
if(err) console.log(err);
});
}
else throw new Meteor.Error(403, "Not authorized.");
},
editBatchComment: 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) {
Batches.update(id, {$set: {comment}}, function(error, count) {
if(error) throw new Meteor.Error(400, "Unexpected database error: " + error);
});
}
else {
Batches.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.");
},
updateBatch: function(id, date, amount) {
check(id, String);
check(date, Number); // TODO: Check that the format is YYYYMMDD
check(amount, Number);
let dateString = date.toString();
let timestamp = new Date(dateString.substring(0, 4) + "-" + dateString.substring(4, 6) + "-" + dateString.substring(6, 8) + "T00:00:00Z");
let weekOfYear = timestamp.getWeek().toString();
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Batches.update(id, {$set: {date, amount, timestamp, weekOfYear}}, function(err, id) {
if(err) console.log(err);
}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
setBatchHasLabels: function(id, hasLabels) {
//console.log(id);
//console.log(hasLabels);
//check(id, Meteor.validators.ObjectID);
check(id, String);
check(hasLabels, Boolean);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Batches.update(id, {$set: {hasLabels}}, function(err, id) {
if(err) console.log(err);
}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
}
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
Batches.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default Batches;

130
imports/api/Label.js Normal file
View File

@@ -0,0 +1,130 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
if(Meteor.isServer) {
const puppeteer = require('puppeteer');
//let Future = Npm.require('fibers/future');
//
//async function printLabels(data, callback) {
// let params = "";
// let url = Meteor.absoluteUrl("/LabelPrint");
//
// params = Object.keys(data).map(function(k) {
// return encodeURIComponent(k) + "=" + encodeURIComponent(data[k]);
// }).join('&');
//
// url += "?" + params;
//
// const browser = await puppeteer.launch();
// const page = await browser.newPage();
// console.log("Going to: " + url);
// await page.goto(url, {waitUntil: 'networkidle0'});
// // By removing the `path` option, we will receive a `Buffer` from `page.pdf`.
// const pdf = await page.pdf({ width: "6in", height: "4in"}); // format: "A4" //path: 'C:\\Users\\Grumpy\\label.pdf'
//
// await browser.close();
// callback(null, pdf);
//}
Meteor.methods({
//printLabels: function(width, height, layout, title1, title2, ingredients, date) {
// console.log("Loaded Label");
// let future = new Future();
//
// let boundCallback = Meteor.bindEnvironment(function(err, res) {
// if(err) {
// future.throw(err);
// }
// else {
// future.return(res);
// }
// });
//
// printLabels({width, height, layout, title1, title2, ingredients, date}, boundCallback);
//
// return future.wait();
//}
async printLabels(width, height, layout, title1, title2, ingredients, date) {
let data = {width, height, layout, title1, title2, ingredients, date};
let params = "";
let url = Meteor.absoluteUrl("/PrintLabel");
//Switch to the static page - for some reason the Meteor page is not loading correctly (it just appears blank).
//url = Meteor.absoluteUrl("/LabelPrint.html");
params = Object.keys(data).map(function(k) {
return encodeURIComponent(k) + "=" + encodeURIComponent(data[k]);
}).join('&');
url += "?" + params;
const browser = await puppeteer.launch();
const page = await browser.newPage();
//url = Meteor.absoluteUrl("/StaticTest.html");
//url = Meteor.absoluteUrl("/StaticTest.html");
console.log("Going to: " + url);
await page.goto(url, {waitUntil: 'networkidle0'}); //, {waitUntil: 'networkidle0'}
const pdf = await page.pdf({width: '3in', height: '2in'}); //path: 'C:\\Users\\Grumpy\\label.pdf',
//const pdf = await page.pdf({format: 'A4'});
//await page.pdf({path: 'C:\\Users\\Grumpy\\label.pdf', width: '6in', height: '4in'});
await browser.close();
return new Uint8Array(pdf, 0, pdf.length);
}
});
//Returns a JSON containing a denormalized list of products {product_id, measure_id, product_name, measure_name, price, }
WebApp.connectHandlers.use("/labels/GetBarCodeData", (req, res, next) => {
try {
let barcodes = Meteor.collections.Barcodes.find({}, {fields: {_id: 0, barcodeId: 1, productAndMeasureId: 1}});
let measures = Meteor.collections.Measures.find({}, {fields: {_id: 1, name: 1}, sort: {order: 1}}).fetch();
//Note: Price data looks like this: {XZ5Z3CM49NDrJNADA /* MeasureID */: {price: 10.5, effectiveDate: ISODate("2017-01-12T13:14:18.876-08:00"), previousPrice: 9}, ...}
//Measures is an array of MeasureIDs valid for this product.
let products = Meteor.collections.Products.find({}, {fields: {_id: 1, name: 1, measures: 1, prices: 1}, sort: {order: 1}}).fetch();
//let measuresById = measures.reduce((map, measure) => (map[measure._id] = measure), {});
let measuresById = {};
let barcodesByProductAndMeasureIds = {};
let result = {};
let today = new Date();
for(measure of measures) measuresById[measure._id] = measure;
for(barcode of barcodes) barcodesByProductAndMeasureIds[barcode.productAndMeasureId] = barcode.barcodeId;
//console.log(measuresById);
//for(let measureId of Object.keys(measuresById)) {
// console.log(measureId + ":" + measuresById[measureId].name);
//}
for(let product of products) {
for(let measureId of product.measures) {
let measureName = measuresById[measureId] ? measuresById[measureId].name : undefined;
let priceData = product.prices ? product.prices[measureId] : undefined;
let price = (priceData ? (priceData.effectiveDate && moment(priceData.effectiveDate).isAfter(today) ? priceData.previousPrice : priceData.price) : 0); //Get the price based on the effective date - whether we should use the new price or the old.
let barcodeId = barcodesByProductAndMeasureIds[productId + " " + measureId];
//Ignore any product/measure combinations that don't have barcodes.
if(barcodeId) {
result[barcodeId] = {productId: product._id, measureId: measureId, productName: product.name, measureName, price};
}
//TODO: Pass the product & measure data separately from the barcodes also to handle the missing barcode scenario. When a user types in a product name and picks a measure, we can record in the sale the product ID and measure ID, and we can lookup any pricing data.
//Log any errors so we can figure out what is going on.
if(measureName === undefined) {
//Note: We will pass a price of zero if the price is unknown. This should be fine for now.
console.log(product._id + " " + product.name + " references a measure (" + measureId + ") which is not in the measures array.");
}
}
}
res.end(JSON.stringify(result), "JSON");
} catch(err) {
console.log(err);
res.end();
}
});
}

View File

@@ -120,4 +120,11 @@ if(Meteor.isServer) {
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
Measures.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default Measures;

View File

@@ -132,12 +132,12 @@ const ProductsSchema = new SimpleSchema({
label: "Updated On",
optional: true
},
deactivated: {
deactivated: { //This is turned on first, if true it will hide the product in production views, but keep the product available in the sale views. It is intended to be turned on for products that are no longer produced, but for which we have remaining inventory.
type: Boolean,
label: "Deactivated",
optional: true
},
hidden: {
hidden: { //Deactivated must first be true. Hides the product everywhere in the system except in historical pages. The inventory should be all sold prior to hiding a product.
type: Boolean,
label: "Hidden",
optional: true
@@ -188,6 +188,10 @@ if(Meteor.isServer) {
if(measures) check(measures, [String]);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
//Lookup the most likely next barcode id from the db, then attempt to insert with it. If it fails due to duplication, then increment and repeat.
//let cursor = Products.find({}, {barCodeId: 1}).sort({barCodeId: -1}).limit(1); //Since currently products are never removed, we shouldn't need to detect sequence gaps and fill them in (odds are we will never use more than 10k numbers anyway).
//let barCodeId = cursor.hasNext() ? cursor.next().barCodeId : 1;
//
Products.insert({name, tags, aliases, measures, createdAt: new Date()}, {bypassCollection2: true}, function(err, id) {
if(err) console.log(err);
});

View File

@@ -90,4 +90,11 @@ if(Meteor.isServer) {
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
ProductTags.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default ProductTags;

View File

@@ -157,4 +157,11 @@ if(Meteor.isServer) {
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
SalesSheets.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default SalesSheets;

View File

@@ -126,4 +126,11 @@ if(Meteor.isServer) {
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
Venues.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default Venues;

View File

@@ -3,7 +3,7 @@ import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
Workers = new Mongo.Collection('Workers');
let Workers = new Mongo.Collection('Workers');
let WORKER_ACTIVITIES = ['sales', 'prep', 'canning', 'farming'];
let workersSchema = new SimpleSchema({
name: {
@@ -60,7 +60,7 @@ let workersSchema = new SimpleSchema({
workersSchema.constants = {activities: WORKER_ACTIVITIES};
Workers.attachSchema(workersSchema);
if(Meteor.isServer) Meteor.publish('Workers', function() {
if(Meteor.isServer) Meteor.publish('workers', function() {
return Workers.find({});
});
@@ -122,4 +122,11 @@ if(Meteor.isServer) {
});
}
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
Workers.allow({
insert: function() {return false;},
update: function() {return false;},
remove: function() {return false;}
});
export default Workers;

View File

@@ -1,3 +1,4 @@
import {Mongo} from 'meteor/mongo';
import Measures from "./Measure.js";
import Venues from "./Venue.js";
import Products from "./Product.js";
@@ -8,15 +9,18 @@ import Logs from "./Logs.js";
import Users from "./User.js";
import UserRoles from "./Roles.js";
import Workers from "./Worker.js";
import Barcodes from "./Barcode.js";
import Batches from "./Batch.js";
import './Reports.js';
import './Label.js';
//Save the collections in the Meteor.collections property for easy access without name conflicts.
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, SalesSheets, Logs, Users, UserRoles, Workers};
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, SalesSheets, Logs, Users, UserRoles, Workers, Barcodes, Batches};
//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) {
if(Users.find({}).count() === 0) {
try {
console.log("Creating a default admin user: admin/admin");
@@ -28,4 +32,14 @@ if(Meteor.isServer) {
console.log(err);
}
}
Meteor.validators = {};
Meteor.validators.ObjectID = Match.Where(function(id) {
if(id instanceof Mongo.ObjectID) {
id = id._str;
}
check(id, String);
return /[0-9a-fA-F]{24}/.test(id);
});
}

View File

@@ -123,6 +123,13 @@ pri.route('/labels', {
BlazeLayout.render('Body', {content: 'LabelMaker'});
}
});
FlowRouter.route('/PrintLabel', {
name: 'PrintLabel',
action: function(params, queryParams) {
require("/imports/ui/PrintLabel.js");
BlazeLayout.render('Empty', {content: 'PrintLabel'});
}
});
pri.route('/testList', {
name: 'TestList',
action: function(params, queryParams) {

View File

@@ -1,31 +1,46 @@
<template name="LabelMaker">
<div id="labelMaker">
<div class="labelOptions"></div>
<div class="labelContents">
<div class="labelOptions">
<div>Label Size</div>
<div><label for="labelWidth">Width (mm):</label> <input type="number" name="labelWidth" class="labelWidth input" step="0.25" value="{{labelWidth}}"/></div>
<div><label for="labelHeight">Height (mm):</label> <input type="number" name="labelHeight" class="labelHeight input" step="0.25" value="{{labelHeight}}"/></div>
<div><label for="labelSpacing">Spacing (in):</label> <input type="number" name="labelSpacing" class="labelSpacing input" step="0.05" value="{{labelSpacing}}"/></div>
<div>Label Contents</div>
<!-- <label for="title1YOffset">Vertical Offset:</label> <input type="number" name="title1YOffset" class="title1YOffset input" value="{{title1YOffset}}"/>-->
<div><label for="title1">Title:</label> <input type="text" name="title1" class="title1 input" value="{{title1}}"/></div>
<div><label for="title2">Title:</label> <input type="text" name="title2" class="title2 input" value="{{title2}}"/></div>
<div><label for="ingredients" style="vertical-align: top">Ingredients:</label> <textarea name="ingredients" class="ingredients">{{ingredients}}</textarea></div>
<div><label for="date">Date:</label> <input type="number" name="date" class="date input" value="{{date}}"/></div>
<div><label for="date">Starting Number:</label> <input type="number" name="startNumber" class="startNumber input" value="{{startNumber}}"/></div>
<div><label for="date">Count:</label> <input type="number" name="count" class="count input" value="{{count}}"/></div>
<div><button name="generate" class="generate">Generate</button></div>
<div><button name="print" class="print">Print</button></div>
<div><button name="preview" class="preview">Preview</button></div>
</div>
<div class="labelContainer">
<div class="labelSample label">
<img class="labelLogo" alt="logo" src="/images/PetitTetonLabelLogo_2in Width_v1.png"/>
<div class="labelTagline">We grow it. We can it.</div>
{{{labelText}}}
<div class='label'>
<!-- /images/Logo_0.8x0.73_300ppi.png-->
<!-- <div class='barcodeContainer'><svg class='barcode' jsbarcode-format='upc' jsbarcode-value='123456789012' jsbarcode-textmargin='0' jsbarcode-fontoptions='bold' jsbarcode-margin='0' jsbarcode-width='1.5em' jsbarcode-height='10em'></svg></div>-->
<!-- <img class="qrcode" src="">-->
<div id="qrcode" class="qrcode"></div>
<img class='labelLogo' alt='logo' src='/images/3x2 Label Logo BW.svg'/>
<div class='title1'>{{title1}}</div>
<div class='title2'>{{title2}}</div>
<div class='ingredients'><span class='ingredientsPrefix'>Ingredients</span>: {{ingredients}}</div>
<div class='ingredientsEnding'>*<span style='font-style: oblique'>grown by us</span> <span class='size'></span> FD1951 (<span class="date">{{date}}</span>)</div>
<div class='instructions'>Refrigerate after opening; return jar when done</div>
<div class='address'>18601 Hwy 128, Yorkville, CA 95494</div>
<div class='website'>www.PetitTeton.com</div>
</div>
</div>
</div>
</template>
<!-- <div class="canvasContainer">-->
<!-- &lt;!&ndash; 3x2" == 76x50mm; 300ppi == 11.81ppmm; So 3x2" == 897 x 590x. &ndash;&gt;-->
<!--&lt;!&ndash; <canvas class="labelCanvas" width="{{labelPxWidthActual}}" height="{{labelPxHeightActual}}">&ndash;&gt;-->
<!--&lt;!&ndash; </canvas>&ndash;&gt;-->
<!-- <canvas class="labelCanvas" width="{{labelPxWidth}}" height="{{labelPxHeight}}">-->
<!-- </canvas>-->
<!-- </div>-->
<div class="testImage">
<template name="Labels">
{{each label}}
<div class="label">
<img class="labelLogo" alt="logo" src="/images/PetitTetonLabelLogo_2in Width_v1.png"/>
<div class="labelTagline">We grow it. We can it.</div>
{{{labelText}}}
</div>
<div class="printableLabel"></div>
</div>
{{/each}}
</template>

View File

@@ -1,93 +1,163 @@
#labelMaker
margin: 10px 20px
height: 100%
width: 100%
margin 10px 20px
height 100%
width 100%
overflow-y auto
.labelContents
label
font-family: TimesNewRoman, Times New Roman, Times
font-weight: 200
font-size: 14px
.title1
width: 500px
font-family: TimesNewRoman, Times New Roman, Times
font-size: .142in
font-weight: 800
line-height: .142in
.title2
width: 500px
font-family: TimesNewRoman, Times New Roman, Times
font-size: 14px
font-weight: 800
line-height: 16px
.ingredients
width: 500px
height: 100px
font-family: TimesNewRoman, Times New Roman, Times
font-size: 12px
font-weight: 100
line-height: 14px
.date
width: 500px
font-family: TimesNewRoman, Times New Roman, Times
font-size: 12px
font-weight: 100
line-height: 14px
.labelContainer
text-align: center
width: 100%
width-min: 3in
height-min: 2in
background-color: grey
padding: 20px
.labelSample
display: inline-block
width: 3in
height: 2in
background-color: white
color: black
text-align: center
.labelLogo
width: .8in
.labelTagline
font-family: TimesNewRoman, Times New Roman, Times
font-size: .1in
font-weight: 100
line-height: .2in
@media not print
.labelOptions
label
font-family TimesNewRoman, Times New Roman, Times
font-weight 200
font-size 14px
.title1
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .2in
font-weight: 800
text-transform: uppercase
width 500px
font-family TimesNewRoman, Times New Roman, Times
font-size .142in
font-weight 800
line-height .142in
.title2
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .15in
font-weight: 800
width 500px
font-family TimesNewRoman, Times New Roman, Times
font-size 14px
font-weight 800
line-height 16px
.ingredients
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .1in
font-weight: 100
width 500px
height 100px
font-family TimesNewRoman, Times New Roman, Times
font-size 12px
font-weight 100
line-height 14px
.date
width 500px
font-family TimesNewRoman, Times New Roman, Times
font-size 12px
font-weight 100
line-height 14px
.labelContainer
text-align center
width 100%
width-min 3in
height-min 2in
background-color grey
padding 20px
.labels
display none
.printableLabel
display none
.label
display inline-block
width 3in
height 2in
.canvasContainer
padding 10px
background-color gray
@media all
.label
position relative
background-color white
color black
text-align center
font-family TimesNewRoman, Times New Roman, Times
//font-family Arial, Helvetica, sans-serif
font-size .1in
width 3in
height 2in
.barcodeContainer
position absolute
transform rotate(270deg) scale(0.7)
right -10em
top 7em
.qrcode
position absolute
left 3px
top 3px
//padding: 2px
//border: 2px solid black
.labelLogo
width 8em
padding 0
margin 0
padding-top .15em
margin-bottom .2em
.labelLogo3
width 14em
padding 0
margin 0
padding-top .15em
margin-bottom .8em
.labelTagline
font-size 1em
font-weight 100
line-height 1em
.title1
width 100%
font-size 2.5em
line-height .9em
font-weight 800
text-transform uppercase
.title2
width 100%
font-size 1.5em
line-height .9em
font-weight 800
padding-bottom .2em
.ingredients
width 100%
font-size 1.2em
font-weight 100
line-height 1em
min-height 2em
.ingredientsEnding
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .1in
font-weight: 100
width 100%
font-size 1.2em
font-weight 100
.instructions
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .1in
font-weight: 800
width 100%
font-size 1.2em
font-weight 800
.address
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .1in
font-weight: 100
width 100%
font-size 1.2em
font-weight 100
.website
width: 100%
font-family: TimesNewRoman, Times New Roman, Times
font-size: .1in
font-weight: 100
width 100%
font-size 1.2em
font-weight 100
@media print
@page
size 3in 2in
margin 0
padding 0
.labelOptions, .labelContainer, .canvasContainer, .labelCanvas
display none
.printableLabel
display block
margin 0
padding 0
//.printableLabels
// display inline-block
// margin 0
// padding 0
//#labelMaker
// display none
//.labels
// display block
//.label
// display block
// width 3in
// height 2in
// //width 100%
// //height 100%
// page-break-after always
// page-break-inside avoid
.canvasContainer
display none
@media print
.labelMaker
margin 0
padding 0
overflow visible

View File

@@ -1,75 +1,416 @@
import dti from 'dom-to-image';
import './Label.html';
import PDF from 'jspdf';
import { saveAs } from 'file-saver';
import swal from 'sweetalert2';
import dragula from 'dragula';
//import JsBarcode from 'JsBarcode';
import QRCode from '/imports/util/qrcode/qrcode';
//let QRCode = require('/imports/util/qrcode/qrcode.js');
console.log(QRCode);
//let {qrcode, svg2url} = require('pure-svg-code');
//let QRCode = require('qrcode-svg');
//******************************************************************
//** Creates printable labels for a roll style printer.
//******************************************************************
let PREFIX = "LabelMaker_";
let PX_PER_MM = 300 / 25.4;
let SCREEN_PX_PER_MM = 96 / 25.4;
let imagePath = "/images/3x2 Label Logo BW.svg";//"/images/Logo_0.8x0.73_300ppi.png";
function generateLabels(title1, title2, ingredients, date, count, topSpacing, bottomSpacing) {
//TODO: Allow logo to be removed or altered with an alternative logo for sizing fixes
let label = "<div class='label'>" +
"<div class='barcodeContainer'><svg class='barcode' jsbarcode-format='UPC' jsbarcode-value='123456789012' jsbarcode-textmargin='0' jsbarcode-fontoptions='bold' jsbarcode-margin='0' jsbarcode-width='2in' jsbarcode-height='10em'></svg></div>" +
//"<div style='height: " + (topSpacing) + "in'></div>" +
//"<img class='labelLogo' alt='logo' src='/images/PetitTetonLabelLogo_2in Width_v1.png'/>" +
"<img class='labelLogo' alt='logo' src='" + imagePath + "'/>" +
//"<div class='labelTagline'>We grow it. We can it.</div>" +
"<div class='title1'>" + title1 + "</div>" +
"<div class='title2'>" + (title2 === undefined ? "" : title2) + "</div>" +
"<div class='ingredients'><span class='ingredientsPrefix'>Ingredients</span>:" + ingredients + "</div>" +
"<div class='ingredientsEnding'>*<span style='font-style: oblique'>grown by us</span> <span class='size'>8oz</span> FD1951 (" + date + ")</div>" +
"<div class='instructions'>Refrigerate after opening; return jar when done</div>" +
"<div class='address'>18601 Hwy 128, Yorkville, CA 95494</div>" +
"<div class='website'>www.PetitTeton.com</div>" +
//"<div style='height: " + bottomSpacing + "in'></div>" +
"</div>";
let labels = "";
//TODO: Include bar code and identifying numbers.
for(let i = 0; i < count; i++) {
labels += label;
}
return labels;
}
function printImageOld(template, dataUrl) {
let img = new Image();
img.onload = function() {
let imageCanvas = $('.labelCanvas')[0];
let ctx = imageCanvas.getContext('2d');
//let threshold = 255 + 200 + 0;
let desiredContrast = 100;
let contrastCorrectionFactor = (259 * (desiredContrast + 255)) / (255 * (259 - desiredContrast));
imageCanvas.width = img.width;
imageCanvas.height = img.height;
//console.log("Generated image: " + img.width + ", " + img.height);
//ctx.scale(0.25, 0.25);
ctx.drawImage(img, 0, 0);
let imageData = ctx.getImageData(0, 0, imageCanvas.width, imageCanvas.height);
let data = imageData.data;
//Run three times
for(let c = 0; c < 3; c++) {
//Set each pixel to either black or white with the given threshold.
for(let i = 0; i < data.length; i += 4) {
data[i] = contrastCorrectionFactor * (data[i] - 128) + 128;
data[i + 1] = contrastCorrectionFactor * (data[i + 1] - 128) + 128;
data[i + 2] = contrastCorrectionFactor * (data[i + 2] - 128) + 128;
//if(data[i] + data[i + 1] + data[i + 2] < threshold) {
// data[i] = 0;
// data[i + 1] = 0;
// data[i + 2] = 0;
//}
//else {
// data[i] = 255;
// data[i + 1] = 255;
// data[i + 2] = 255;
//}
data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
//Convert the canvas content to an image for printing. (cannot print a canvas?)
//sendToPrinter(template, imageCanvas.toDataURL("image/png"));
//Save to disk.
//imageCanvas.toBlob(function(blob) {
// saveAs(blob, "label.png");
//}, "image/png", 1);
//imageCanvas.toBlob(function(blob) {
// let url = URL.createObjectURL(blob);
// window.open(url, "_blank");
//}, "image/png", 1);
//let imgData = imageCanvas.toDataURL('image/jpeg', 1.0);
imageCanvas.toBlob(function(blob) {
let pdf = new PDF();
let url = URL.createObjectURL(blob);
pdf.addImage(url, "image/png", 0, 0);
pdf.save("label.pdf");
}, "image/png", 1);
};
img.src = dataUrl;
}
function printImage(template, raw, width, height) {
let data = raw;
//Run three times
for(let c = 0; c < 3; c++) {
//Set each pixel to either black or white with the given threshold.
for(let i = 0; i < data.length; i += 4) {
data[i] = contrastCorrectionFactor * (data[i] - 128) + 128;
data[i + 1] = contrastCorrectionFactor * (data[i + 1] - 128) + 128;
data[i + 2] = contrastCorrectionFactor * (data[i + 2] - 128) + 128;
//if(data[i] + data[i + 1] + data[i + 2] < threshold) {
// data[i] = 0;
// data[i + 1] = 0;
// data[i + 2] = 0;
//}
//else {
// data[i] = 255;
// data[i + 1] = 255;
// data[i + 2] = 255;
//}
data[i + 3] = 255;
}
}
let url = URL.createObjectURL(new Blob(raw, {type: "image/png"}));
let test = new Image();
test.src = url;
$('.testImage').html(test);
}
// Note: Not working correctly. Output seems to be at 96dpi instead of 300+dpi. Using a large image scaled down in in an Image tag seems to not print well.
function sendToPrinter(template, dataUrl) {
let canvasImg = new Image();
canvasImg.onload = function() {
//window.print();
};
canvasImg.src = dataUrl;
canvasImg.style.height = template.labelHeight.get() + "mm";
canvasImg.style.width = template.labelWidth.get() + "mm";
$(".printableLabel").html(canvasImg);
//$('.printableLabel').html(img);
window.print();
}
function loadImage(url) {
return new Promise(r => { let i = new Image(); i.onload = (() => r(i)); i.src = url; });
}
async function refreshLabelCanvas(template) {
let mmWidth = template.labelWidth.get();
let mmHeight = template.labelHeight.get();
let title1 = template.title1.get();
let title1Font = template.title1Font.get();
let title1YOffset = template.title1YOffset.get();
let title2 = template.title2.get();
let title2Font = template.title2Font.get();
let title2YOffset = template.title2YOffset.get();
let ingredients = template.ingredients.get();
let ingredientsFont = template.ingredientsFont.get();
let ingredientsYOffset = template.ingredientsYOffset.get();
let date = template.date.get();
let $labelCanvas = $('.labelCanvas');
let ctx = $labelCanvas[0].getContext('2d');
let width = Math.floor(mmWidth * PX_PER_MM);
let height = Math.floor(mmHeight * PX_PER_MM);
let center = width / 2;
let img = await loadImage(imagePath);
let imageWidth = 241;
let imageHeight = 219;
let ingredientsY = 180;
let ingredientsEndingY = 240;
let instructionsY = 280;
let addressY = 320;
let websiteY = 380;
ctx.fillStyle = 'white';
ctx.fillRect(0,0, width, height);
ctx.fillStyle = 'black';
ctx.drawImage(img, center - (imageWidth / 2), 0);
ctx.font = title1Font;
ctx.textAlign = 'center';
ctx.fillText(title1, center, title1YOffset);
////TODO: Allow logo to be removed or altered with an alternative logo for sizing fixes
//let label = "<div class='label'>" +
// "<div style='height: " + topSpacing + "in'></div>" +
// //"<img class='labelLogo' alt='logo' src='/images/PetitTetonLabelLogo_2in Width_v1.png'/>" +
// "<img class='labelLogo' alt='logo' src='/images/Logo_0.8x0.73_300ppi.png'/>" +
// "<div class='labelTagline'>We grow it. We can it.</div>" +
// "<div class='title1'>" + title1 + "</div>" +
// "<div class='title2'>" + (title2 === undefined ? "" : title2) + "</div>" +
// "<div class='ingredients'><span class='ingredientsPrefix'>Ingredients</span>:" + ingredients + "</div>" +
// "<div class='ingredientsEnding'>*<span style='font-style: oblique'>grown by us</span> <span class='size'>8oz</span> FD1951 (" + date + ")</div>" +
// "<div class='instructions'>Refrigerate after opening; return jar when done</div>" +
// "<div class='address'>18601 Hwy 128, Yorkville, CA 95494</div>" +
// "<div class='website'>www.PetitTeton.com</div>" +
// "<div style='height: " + bottomSpacing + "in'></div>" +
// "</div>";
//let labels = "";
//
////TODO: Include bar code and identifying numbers.
//for(let i = 0; i < count; i++) {
// labels += label;
//}
//
//return labels;
}
function bufferToBase64(buf) {
var binstr = Array.prototype.map.call(buf, function (ch) {
return String.fromCharCode(ch);
}).join('');
return btoa(binstr);
}
Template.LabelMaker.onCreated(function() {
this.labelWidth = new ReactiveVar(76);
this.labelHeight = new ReactiveVar(50);
this.title1 = new ReactiveVar("Strawberry");
this.title2 = new ReactiveVar("w/ Espelette");
this.ingredients = new ReactiveVar("*strawberry, sugar, *espelette");
this.date = new ReactiveVar(19001);
//Session.set(PREFIX + "title1", "Strawberry");
//Session.set(PREFIX + "title1Font", "50px Times New Roman");
//Session.set(PREFIX + "title1YOffset", 260);
//Session.set(PREFIX + "title2", "w/ Espelette");
//Session.set(PREFIX + "title2Font", "40px Times New Roman");
//Session.set(PREFIX + "title2YOffset", 340);
//Session.set(PREFIX + "ingredients", "*strawberry, sugar, *espelette");
//Session.set(PREFIX + "date", 19001);
//Session.set(PREFIX + "count", 3);
});
Template.LabelMaker.onRendered(function() {
let template = this;
//Re-run this routine when ever the session variables change.
//Tracker.autorun(function() {
// //refreshLabelCanvas(Session.get(PREFIX + "title1"), Session.get(PREFIX + "title2"), Session.get(PREFIX + "ingredients"), Session.get(PREFIX + "date"), 1, Session.get(PREFIX + "labelSpacing"), 0);
// refreshLabelCanvas(template);
//});
//
//template.$('.labelContainer').on('DOMSubtreeModified', function() {
// console.log("Initialized barcode");
// //
//});
//JsBarcode(".barcode").init();
//const svgString = qrcode({content: "1234567890", padding: 0, width: 60, height: 60, color: "#000000", background: "#FFFFFF", ecl: "L"});
//this.$('.qrcode').attr('src', svg2url(svgString));
//this.$('.qrcode').attr('src', svg2url(new QRCode({content: '1234567890', width: 80, height: 80, color: "#000000", background: "#FFFFFF"}).svg()));
new QRCode(document.getElementById("qrcode"), {text: "1234567890", width: 60, height: 60});
});
Template.LabelMaker.onDestroyed(function() {
});
Template.LabelMaker.events({
'change .title1': function(event, template) {
Session.set(PREFIX + "title1", $(event.target).val());
'change .labelWidth': function(e, t) {
let x = $(e.target).val();
t.labelWidth.set(!Number.isNaN(x) && x > 0 ? parseFloat(x) : 76);
},
'change .title2': function(event, template) {
Session.set(PREFIX + "title2", $(event.target).val());
'change .labelHeight': function(event, template) {
let x = $(event.target).val();
t.labelHeight.set(!Number.isNaN(x) && x > 0 ? parseFloat(x) : 50);
},
'change .ingredients': function(event, template) {
Session.set(PREFIX + "ingredients", $(event.target).val());
},
'change .date': function(event, template) {
Session.set(PREFIX + "date", parseInt($(event.target).val()));
},
'click .generate': function(event, template) {
'change .title1': function(e, t) {t.title1.set($(e.target).val());},
'change .title1Font': function(e, t) {t.title1Font.set($(e.target).val());},
'change .title1YOffset': function(e, t) {t.title1YOffset.set(parseInt($(e.target).val()));},
'change .title2': function(e, t) {t.title2.set($(e.target).val());},
'change .title2Font': function(e, t) {t.title2Font.set($(e.target).val());},
'change .title2YOffset': function(e, t) {t.title2YOffset.set(parseInt($(e.target).val()));},
'change .ingredients': function(e, t) {t.ingredients.set($(e.target).val());},
'change .ingredientsFont': function(e, t) {t.ingredientsFont.set($(e.target).val());},
'change .ingredientsYOffset': function(e, t) {t.ingredientsYOffset.set(parseInt($(e.target).val()));},
'change .date': function(e, t) {t.date.set(parseInt($(e.target).val()));},
'click .preview': function(event, template) {
let params = {};
params['title1'] = "Strawberry";
params['title2'] = "";
params['ingredients'] = "Fairies";
params['date'] = "1234";
window.open('/PrintLabel?' + $.param(params));
},
'click .print': function(event, template) {
let _this = template;
let node = $('.label')[0];
let width = _this.labelWidth.get();
let height =_this.labelHeight.get();
let title1 =_this.title1.get();
let title2 =_this.title2.get();
let ingredients =_this.ingredients.get();
let date =_this.date.get();
Meteor.call('printLabels', width, height, "3x2Standard", title1, title2, ingredients, date, (error, result) => {
if(error) {
console.log(error);
}
else {
const blob = new Blob([result], {type: 'application/pdf'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = "labels.pdf";
link.click();
}
});
//dti.toBlob(node, {bgcolor: 'white'}).then(function(blob) {
// printImage(template, blob);
//}).catch(function(err) {
// console.log(err);
//});
//dti.toPng(node, {bgcolor: 'white'}).then(function(dataUrl) {
// printImageOld(template, dataUrl);
//}).catch(function(err) {
// console.log(err);
//});
//let count = template.$('input[name="count"]').val();
//let spacing = Session.get(PREFIX + "labelSpacing");
//
////count = (count === undefined || Number.isNaN(count)) ? 1 : parseInt(count);
////
////if(count < 1) {
//// count = 1;
////}
////
////Session.set(PREFIX + "generatedLabels", generateLabels(Session.get(PREFIX + "title1"), Session.get(PREFIX + "title2"), Session.get(PREFIX + "ingredients"), Session.get(PREFIX + "date"), count, 0, spacing));
//
//let $label = $('.printableLabel');
//
//console.log($label);
//console.log($label.length);
//domtoimage.toPng($label).then(function(dataUrl) {
// console.log("A");
// let img = new Image();
// img.src = dataUrl;
// $('.printableLabel').html(img);
// //caman('.printableLabel img', function() {
// // this.contrast(100);
// // this.render();
// // window.print();
// //});
//}).catch(function(error) {
// console.error("failed to convert dom to image");
// console.error(error);
//});
}
});
Template.LabelMaker.helpers({
title1: function() {return Session.get(PREFIX + "title1")},
title2: function() {return Session.get(PREFIX + "title2")},
ingredients: function() {return Session.get(PREFIX + "ingredients")},
date: function() {return Session.get(PREFIX + "date")},
labelText: function() {
return "<div class='title1'>" + Session.get(PREFIX + "title1") + "</div>" +
"<div class='title2'>" + Session.get(PREFIX + "title2") + "</div>" +
"<div class='ingredients'><span class='ingredientsPrefix'>Ingredients</span>:" + Session.get(PREFIX + "ingredients") + "</div>" +
"<div class='ingredientsEnding'>*<span style='font-style: oblique'>grown by us</span> <span class='size'>8oz</span> FD1951 (" + Session.get(PREFIX + "date") + ")</div>" +
"<div class='instructions'>Refrigerate after opening; return jar when done</div>" +
"<div class='address'>18601 Hwy 128, Yorkville, CA 95494</div>" +
"<div class='website'>www.PetitTeton.com</div>";
}
});
Template.Labels.onCreated(function() {
});
Template.Labels.onRendered(function() {
let template = this;
});
Template.Labels.onDestroyed(function() {
});
Template.Labels.events({
});
Template.Labels.helpers({
labels: function() {return Session.get(PREFIX + "labels")},
title2: function() {return Session.get(PREFIX + "title2")},
ingredients: function() {return Session.get(PREFIX + "ingredients")},
date: function() {return Session.get(PREFIX + "date")},
labelText: function() {
return "<div class='title1'>" + Session.get(PREFIX + "title1") + "</div>" +
"<div class='title2'>" + Session.get(PREFIX + "title2") + "</div>" +
"<div class='ingredients'><span class='ingredientsPrefix'>Ingredients</span>:" + Session.get(PREFIX + "ingredients") + "</div>" +
"<div class='ingredientsEnding'>*<span style='font-style: oblique'>grown by us</span> <span class='size'>8oz</span> FD1951 (" + Session.get(PREFIX + "date") + ")</div>" +
"<div class='instructions'>Refrigerate after opening; return jar when done</div>" +
"<div class='address'>18601 Hwy 128, Yorkville, CA 95494</div>" +
"<div class='website'>www.PetitTeton.com</div>";
title1: function() {return Template.instance().title1.get();},
title2: function() {return Template.instance().title2.get();},
ingredients: function() {return Template.instance().ingredients.get();},
date: function() {return Template.instance().date.get();},
labelWidth: function() {return Template.instance().labelWidth.get();},
labelHeight: function() {return Template.instance().labelHeight.get();},
sampleLabel: function() {
let t = Template.instance();
setTimeout(function() {JsBarcode(".barcode").init();}, 500);
return generateLabels(t.title1.get(), t.title2.get(), t.ingredients.get(), t.date.get(), 1, 0);
},
//canvasLabel: function() {
// return generateLabelCanvas(Session.get(PREFIX + "title1"), Session.get(PREFIX + "title2"), Session.get(PREFIX + "ingredients"), Session.get(PREFIX + "date"), 1, Session.get(PREFIX + "labelSpacing"), 0);
//},
labels: function() {
return Session.get(PREFIX + "generatedLabels");
},
labelPxWidth: function() {
//console.log("label width: " + Template.instance().labelWidth.get());
//console.log("px_per_mm: " + PX_PER_MM);
//console.log("labelPxWidth: " + (Template.instance().labelWidth.get() * PX_PER_MM));
return Math.floor(Template.instance().labelWidth.get() * PX_PER_MM);
},
labelPxHeight: function() {
return Math.floor(Template.instance().labelHeight.get() * PX_PER_MM);
},
labelPxWidthActual: function() {
//console.log("label width: " + Template.instance().labelWidth.get());
//console.log("px_per_mm: " + PX_PER_MM);
//console.log("labelPxWidth: " + (Template.instance().labelWidth.get() * PX_PER_MM));
return Math.floor(Template.instance().labelWidth.get() * SCREEN_PX_PER_MM);
},
labelPxHeightActual: function() {
return Math.floor(Template.instance().labelHeight.get() * SCREEN_PX_PER_MM);
}
});

View File

@@ -0,0 +1,19 @@
<template name="PrintLabel">
<div id="PrintLabel">
<div class="labelContainer">
<div class='label'>
<!-- /images/Logo_0.8x0.73_300ppi.png-->
<!-- <div class='barcodeContainer'><svg class='barcode' jsbarcode-format='upc' jsbarcode-value='123456789012' jsbarcode-textmargin='0' jsbarcode-fontoptions='bold' jsbarcode-margin='0' jsbarcode-width='1.5em' jsbarcode-height='10em'></svg></div>-->
<div id="qrcode" class="qrcode" src=""></div>
<img class='labelLogo' alt='logo' src='/images/3x2 Label Logo BW.svg'/>
<div class='title1'></div>
<div class='title2'></div>
<div class='ingredients'><span class='ingredientsPrefix'>Ingredients</span>: </div>
<div class='ingredientsEnding'>*<span style='font-style: oblique'>grown by us</span> <span class='size'></span> FD1951 (<span class="date"></span>)</div>
<div class='instructions'>Refrigerate after opening; return jar when done</div>
<div class='address'>18601 Hwy 128, Yorkville, CA 95494</div>
<div class='website'>www.PetitTeton.com</div>
</div>
</div>
</div>
</template>

85
imports/ui/PrintLabel.import.css vendored Normal file
View File

@@ -0,0 +1,85 @@
#PrintLabel .labelContainer {
text-align: center;
width: 100%;
width-min: 3in;
height-min: 2in;
background-color: #808080;
padding: 20px;
}
#PrintLabel .labels {
display: none;
}
#PrintLabel .printableLabel {
display: none;
}
#PrintLabel .label {
display: inline-block;
width: 3in;
height: 2in;
}
#PrintLabel .canvasContainer {
padding: 10px;
background-color: #808080;
}
#PrintLabel .label {
background-color: #fff;
color: #000;
text-align: center;
font-family: TimesNewRoman, Times New Roman, Times;
font-size: 0.2in;
width: 6in;
height: 4in;
}
#PrintLabel .label .labelLogo {
width: 8em;
padding: 0;
margin: 0;
padding-top: 0.15em;
margin-bottom: -0.25em;
}
#PrintLabel .label .labelTagline {
font-size: 1em;
font-weight: 100;
line-height: 1em;
}
#PrintLabel .label .title1 {
width: 100%;
font-size: 2.5em;
line-height: 0.9em;
font-weight: 800;
text-transform: uppercase;
}
#PrintLabel .label .title2 {
width: 100%;
font-size: 1.5em;
line-height: 0.9em;
font-weight: 800;
padding-bottom: 0.2em;
}
#PrintLabel .label .ingredients {
width: 100%;
font-size: 1.2em;
font-weight: 100;
line-height: 1em;
min-height: 2em;
}
#PrintLabel .label .ingredientsEnding {
width: 100%;
font-size: 1.2em;
font-weight: 100;
}
#PrintLabel .label .instructions {
width: 100%;
font-size: 1.2em;
font-weight: 800;
}
#PrintLabel .label .address {
width: 100%;
font-size: 1.2em;
font-weight: 100;
}
#PrintLabel .label .website {
width: 100%;
font-size: 1.2em;
font-weight: 100;
}

92
imports/ui/PrintLabel.import.styl vendored Normal file
View File

@@ -0,0 +1,92 @@
#PrintLabel
.labelContainer
text-align center
//width 100%
width-min 3in
width 3in
height-min 2in
height 2in
//background-color grey
//padding 20px
.labels
display none
.printableLabel
display none
.label
display inline-block
width 3in
height 2in
.canvasContainer
padding 10px
background-color gray
.label
position relative
background-color white
color black
text-align center
font-family TimesNewRoman, Times New Roman, Times
//font-family Arial, Helvetica, sans-serif
font-size .1in
width 3in
height 2in
.barcodeContainer
position absolute
transform rotate(270deg)
right -4.5em
top 5em
.qrcode
position absolute
left 10px
top 10px
.labelLogo
width 8em
padding 0
margin 0
padding-top .15em
margin-bottom .2em
.labelLogo3
width 14em
padding 0
margin 0
padding-top .15em
margin-bottom .8em
.labelTagline
font-size 1em
font-weight 100
line-height 1em
.title1
width 100%
font-size 2.5em
line-height .9em
font-weight 800
text-transform uppercase
.title2
width 100%
font-size 1.5em
line-height .9em
font-weight 800
padding-bottom .2em
.ingredients
width 100%
font-size 1.2em
font-weight 100
line-height 1em
min-height 2em
.ingredientsEnding
width 100%
font-size 1.2em
font-weight 100
.instructions
width 100%
font-size 1.2em
font-weight 800
.address
width 100%
font-size 1.2em
font-weight 100
.website
width 100%
font-size 1.2em
font-weight 100

32
imports/ui/PrintLabel.js Normal file
View File

@@ -0,0 +1,32 @@
import './PrintLabel.html';
import JsBarcode from 'JsBarcode';
//import QRCode from "../util/qrcode/qrcode";
import QRCode from '/imports/util/qrcode/qrcode';
//let {qrcode, svg2url} = require('pure-svg-code');
Template.PrintLabel.onCreated(function() {
function getUrlVars() {
let vars = {};
let parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = decodeURIComponent(value);
});
return vars;
}
this.vars = getUrlVars();
});
Template.PrintLabel.onRendered(function() {
let vars = this.vars;
$('.title1').html(vars['title1']);
$('.title2').html(vars['title2'] === undefined ? "" : vars['title2']);
$('.ingredients').append(vars['ingredients']);
$('.date').html(vars['date']);
$('.size').html(vars['size']);
//JsBarcode(".barcode").init();
//const svgString = qrcode({content: "1234567890", padding: 0, width: 50, height: 50, color: "#000000", background: "#FFFFFF", ecl: "L"});
//this.$('.qrcode').attr('src', svg2url(svgString));
new QRCode(document.getElementById("qrcode"), {text: "1234567890", width: 60, height: 60});
});

View File

@@ -1,5 +1,136 @@
<template name="Production">
<div id="production">
todo
{{#if Template.subscriptionsReady}}
<div class="tableControls">
<div class="contentControls">
<a class="loadMoreLink {{#if disableLoadMore}}disabled{{/if}}" href="javascript:">Load More...</a>
</div>
</div>
<div class="separatedTableHeader">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="hasLabels"></th>
<th class="name">Name {{>BatchSearch columnName='name'}}</th>
<th class="date">Date {{>BatchDateRangeSearch columnName='date' width='90%'}}</th>
<th class="amount">Amount</th>
<th class="cook">Cook {{>BatchSearch columnName='cook' collectionQueryColumnName='name' collection='Workers' collectionResultColumnName='_id'}}</th>
<th class="canner">Canner {{>BatchSearch columnName='canner' collectionQueryColumnName='name' collection='Workers' collectionResultColumnName='_id'}}</th>
<th class="comment">Comment</th>
<th class="actions">Actions <span class="newButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span> <span class="showDeletedButton btn btn-success {{showDeletedSelected}}"><i class="fa fa-trash" aria-hidden="true"></i></span></th>
</tr>
</thead>
</table>
</div>
<div class="listRow">
<div class="listCell">
<div class="tableContainer mCustomScrollbar" data-mcs-theme="dark">
<table class="table table-striped table-hover">
<tbody>
{{#if displayNew}}
{{> BatchNew}}
{{/if}}
{{#each batches}}
{{> Batch}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
{{/if}}
</div>
</template>
<template name="Batch">
<tr class="{{getRowClass}}">
{{#if editing}}
{{> BatchEditor}}
{{else}}
<td class="hasLabels noselect left"><i class="fa fa-print clickable {{hasLabelsClass}}" aria-hidden="true"></i></td>
<td class="name noselect nonclickable left">{{name}}</td>
<td class="date noselect nonclickable left">{{date}}</td>
<td class="amount noselect nonclickable left">{{amount}}</td>
<td class="cook noselect nonclickable left">{{cook}}</td>
<td class="canner noselect nonclickable left">{{canner}}</td>
<td class="comment noselect nonclickable left">{{comment}}</td>
<td class="actions center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i>&nbsp;/&nbsp;{{#if isDeleted}}<i class="actionUndelete fa fa-check-circle fa-lg noselect clickable" title="Delete" aria-hidden="true"></i>{{else}}<i class="actionDelete fa fa-times-circle fa-lg noselect clickable" title="Delete" aria-hidden="true"></i>{{/if}}</td>
{{/if}}
</tr>
</template>
<template name="BatchEditor">
<td colspan="7" class="editorTd">
<form name="editorForm" class="insertForm" autocomplete="off">
<div class="grid">
<div class="col-4-12">
<div class="editorDiv heading">{{name}}</div>
<div class="editorDiv heading">{{date}}</div>
<div class="editorDiv heading">Cook: {{cook}} / Canner: {{canner}}</div>
</div>
<div class="col-2-12">
<div class="editorDiv"><label>Amount:</label><input type="number" class="form-control amount" name="amount" min="0" step="1" data-schema-key='amount' value="{{amount}}" required></div>
</div>
<div class="col-6-12">
<div class="editorDiv"><label for="batchEditorComment">Comment:</label><textarea id="batchEditorComment" class="comment" rows="4" cols="50" style="width: 100%; height: 80px">{{comment}}</textarea></div>
</div>
</div>
</form>
</td>
<td class="center editorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
</template>
<template name="BatchNew">
<td colspan="7" class="editorTd">
<form name="insertForm" class="insertForm" autocomplete="off">
<div class="grid">
<div class="col-4-12">
<div class="form-group">
<label class='control-label'>Product</label>
<input name="product" class="form-control" type="text" required/>
</div>
<div class="form-group">
<label class='control-label'>Date</label>
<input name="date" class="form-control" type="date" data-schema-key='date' value="{{today}}" required>
</div>
</div>
<div class="col-4-12">
<div class="form-group">
<label class='control-label'>Cook</label>
<input name="cook" class="form-control" type="text" required/>
</div>
<div class="form-group">
<label class='control-label'>Canner</label>
<input name="canner" class="form-control" type="text" required/>
</div>
</div>
{{#each productMeasures}}
{{>InsertBatchMeasure this}}
{{/each}}
</div>
<div class="editorDiv" style="width: 100%; height: 150px"><label for="batchNewComment">Comment:</label><textarea id="batchNewComment" class="comment" rows="4" cols="50" style="width: 100%; height: 120px">{{comment}}</textarea></div>
</form>
</td>
<td class="center editorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
</template>
<template name="InsertBatchMeasure">
<div class="col-2-12 insertMeasure">
<input type="hidden" class="measureId" value="{{this._id}}">
<div class="form-group">
<label class='control-label'>{{name}} Count</label>
<input type="number" class="form-control amount" name="amount" min="0" data-schema-key='amount' value="{{amount}}" required>
</div>
</div>
</template>
<template name="BatchSearch">
<div class="search">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
</div>
</template>
<template name="BatchDateRangeSearch">
<div style="padding-right: 10px; width: {{width}};"><input type="date" class="searchDateStartInput" value="{{startDate}}" data-schema-key='date' required> - <input type="date" class="searchDateEndInput" value="{{endDate}}" data-schema-key='date' required></div>
</template>

View File

@@ -1,60 +1,142 @@
#production
margin: 10px 20px
display: table
content-box: border-box
padding: 10px 20px
height: 100%
//Flex container options.
flex-flow: column nowrap
justify-content: space-around //Spacing between sales along the primary axis. (vertical spacing for a column layout)
align-items: flex-start //Align the sales within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box
display: -moz-box
display: -ms-flexbox
display: -moz-flex
display: -webkit-flex
display: flex
width: 100%
text-align: left
.editor
height: 100%
overflow-y: auto
.insertSale
flex: none
.tableControls
text-align: right
margin-right: 20px
margin-bottom: 4px
display: table
width: 100%
.contentControls
vertical-align: bottom
display: table-cell
text-align: right
min-width: 100px
a
font-size: 12px
font-family: "Arial", san-serif
font-weight: 800
color: #2d1b8c
text-decoration: none
a:hover
text-decoration: underline
a.disabled
visibility: hidden
.formGroupHeading
font-size: 1.6em
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-style: normal
font-variant: normal
font-weight: 500
.grid
flex: auto
align-self: stretch
overflow-y: auto
overflow-x: auto
margin-bottom: 20px
border: 0
padding-top: 20px
.table > thead > tr > th
border: 0
padding-top: 0
padding-bottom: 6px
.left
text-align: left
.center
text-align: center
.dataTable
table-layout: fixed
.tdLarge
font-size: 1.3em
.saleRemove
color: red
margin-left: 8px
.saleEdit
color: darkblue
margin-right: 8px
.table
table-layout: fixed
min-width: 100%
thead, tbody
> tr
> .hasLabels
width: 30px
.hasLabels
color green
.noLabels
color red
> .name
//width: auto
width: 100%
> .date
//width: auto
min-width: 150px
max-width: 180px
> .amount
//width: auto
min-width: 100px
max-width: 100px
> .cook
//width: auto
min-width: 150px
max-width: 180px
> .canner
//width: auto
min-width: 150px
max-width: 180px
> .comment
width: 220px
min-width: 220px
max-width: 220px
> .actions
width: 90px
min-width: 90px
max-width: 90px
> tr.deleted
background-color: gray
.separatedTableHeader
table
thead
> tr
> th.actions
text-align: center
.newButton
margin-top: 4px
padding: 0 12px
.fa-plus-circle
display: inline-block
.fa-times-circle
display: none
.newButton:active
background-color: #fb557b
color: black
.fa-times-circle
display: inline-block
.fa-plus-circle
display: none
.showDeletedButton
margin-top: 4px
padding: 0 12px
color gray
.showDeletedButton.selected
color black
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
width: 100%
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
table
thead
visibility: hidden
display: none
.search
margin: 3px 0 2px 1px
.editorTd
background: #deeac0
input, select
width: 100%
.editorDiv
margin: 4px 0
label
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-size: .9em
padding-bottom: 4px
select2
font-size: .4em
> tbody
> tr
.actionRemove
color: #F77
.actionEdit
color: #44F
.editorApply
color: green
.editorCancel
color: red

View File

@@ -1,2 +1,446 @@
import './Production.html';
let QUERY_LIMIT = 100;
let QUERY_LIMIT_INCREMENT = 100;
let PREFIX = "Production.";
Tracker.autorun(function() {
//Meteor.subscribe("batches");
Meteor.subscribe("workers");
Meteor.subscribe("products");
Meteor.subscribe("measures");
});
Template.Production.onCreated(function() {
Session.set(PREFIX + "displayNew", false); //Whether the new dialog is inlined in the table and visible.
Session.set(PREFIX + "sortOption", 'date'); //Allows us to sort the results of the batch query by batch attribute.
Session.set(PREFIX + 'skipCount', 0); //Allows us to page through the results of the batch query. Currently not used.
Session.set(PREFIX + 'batchCount', 0); //A count of all batches in the system (that fit our current query). Useful for paging and dynamic loading.
Session.set(PREFIX + "queryLimit", QUERY_LIMIT);
Session.set(PREFIX + "showDeleted", false);
Session.set(PREFIX + "editedId", undefined);
Tracker.autorun(function() {
let sortOption = Session.get(PREFIX + "sortOption");
let sort = sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1};
//let showOnlyComments = Session.get(PREFIX + "showOnlyComments"); //Not needed here. Shows how to limit the query to only records with certain features.
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
//if(showOnlyComments) {
// if(!query) query = {};
// query.comment = {$exists: true};
//}
if(!Session.get(PREFIX + "showDeleted")) {
if(!query) query = {};
query.deletedAt = {$exists: false};
}
Template.Production.batchesSubscription = Meteor.subscribe("batches", query, sort, QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
Session.set(PREFIX + 'batchCount', Meteor.call('getBatchCount', Session.get(PREFIX + 'searchQuery')));
});
});
Template.Production.onDestroyed(function() {
if(Template.Production.batchSubscription) {
Template.Production.batchSubscription.stop();
}
});
Template.Production.onRendered(function() {
$(".tableContainer").mCustomScrollbar({
scrollButtons: {enable:true},
theme: "light-thick",
scrollbarPosition: "outside",
scrollEasing: "linear"
});
});
Template.Production.helpers({
displayNew: function() {
return Session.get(PREFIX + "displayNew");
},
batches: function() {
let sortOption = Session.get(PREFIX + "sortOption");
return Meteor.collections.Batches.find({}, {sort: (sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1})});
},
disableLoadMore: function() {
return Session.get(PREFIX + 'batchCount') - (Session.get(PREFIX + 'skipCount') || 0) - Session.get(PREFIX + "queryLimit") <= 0;
},
showDeletedSelected: function() {
return (Session.get(PREFIX + "showDeleted")) ? "selected" : "";
}
});
Template.Production.events({
'click .loadMoreLink': function(event, template) {
event.preventDefault();
Session.set(PREFIX + 'queryLimit', Session.get(PREFIX + "queryLimit") + QUERY_LIMIT_INCREMENT);
},
'click .newButton': function(event, template) {
if(template.$('.newButton').hasClass('active')) {
Session.set(PREFIX + 'displayNew', false);
}
else {
Session.set(PREFIX + 'displayNew', true);
Session.set(PREFIX + "editedId", undefined); //Clear the edited product so that only one editor is open at a time.
}
template.$('.newButton').toggleClass('active');
},
'click .showDeletedButton': function(event, template) {
Session.set(PREFIX + "showDeleted", !Session.get(PREFIX + "showDeleted")); //Toggle the display of deleted production.
}
});
Template.Batch.onCreated(function() {
});
Template.Batch.onRendered(function() {
});
Template.Batch.helpers({
hasLabelsClass: function() {
return this.hasLabels === true ? "hasLabels" : "noLabels";
},
name: function() {
let product = Meteor.collections.Products.findOne({_id: this.productId});
let measure = Meteor.collections.Measures.findOne({_id: this.measureId});
return product.name + " (" + measure.name + ")";
},
date: function() {
return moment(this.date, "YYYYMMDD").format("MM/DD/YYYY");
},
cook: function() {
let worker = Meteor.collections.Workers.findOne({_id: this.cookId});
return worker.name;
},
canner: function() {
let worker = Meteor.collections.Workers.findOne({_id: this.cannerId});
return worker.name;
},
editing: function() {
let editedId = Session.get(PREFIX + "editedId");
return editedId && editedId.toString() === this._id.toString();
},
getRowClass: function() {
return this.deletedAt ? "deleted" : "";
},
isDeleted: function() {
return this.deletedAt;
}
});
Template.Batch.events({
"click .hasLabels": function(event, template) {
Meteor.call('setBatchHasLabels', this._id, !(this.hasLabels === true), function(error, result) {
if(error) sAlert.error(error);
});
},
"click .actionEdit": function(event, template) {
Session.set(PREFIX + "editedId", this._id);
Session.set(PREFIX + 'displayNew', false); //Ensure the new editor is closed.
template.parentTemplate().$('.newButton').removeClass('active');
},
"click .actionDelete": function(event, template) {
Meteor.call('deleteBatch', this._id, function(error, result) {
if(error) sAlert.error(error);
else sAlert.success("Production Batch Deleted");
});
},
'click .actionUndelete': function(event, template) {
Meteor.call('undeleteBatch', this._id, function(error, result) {
if(error) sAlert.error(error);
else sAlert.success("Production Batch No Longer Deleted");
});
}
});
Template.BatchEditor.onCreated(function() {
});
Template.BatchEditor.onRendered(function() {
});
Template.BatchEditor.helpers({
name: function() {
let product = Meteor.collections.Products.findOne({_id: this.productId});
let measure = Meteor.collections.Measures.findOne({_id: this.measureId});
return (product ? product.name : "") + " (" + (measure ? measure.name : "") + ")";
},
date: function() {
return moment(this.date, "YYYYMMDD").format("MM/DD/YYYY");
},
cook: function() {
let worker = Meteor.collections.Workers.findOne({_id: this.cookId});
return worker.name;
},
canner: function() {
let worker = Meteor.collections.Workers.findOne({_id: this.cannerId});
return worker.name;
}
});
Template.BatchEditor.events({
'click .editorCancel': function(event, template) {
Session.set(PREFIX + "editedId", undefined);
},
'click input[type="submit"]': function(event, template) {
event.preventDefault();
template.$('.insertForm').data('bs.validator').validate(function(isValid) {
if(isValid) {
//Allow the user to edit the comment and the amount produced. Sometimes jars pop after the product cools and these things need to be recorded.
//TODO
}
});
}
});
Template.BatchNew.onCreated(function() {
this.selectedProduct = new ReactiveVar();
this.selectedCook = new ReactiveVar();
this.selectedCanner = new ReactiveVar();
});
Template.BatchNew.onRendered(function() {
this.$('.insertForm').validator();
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="cook"]').buildCombo({cursor: Meteor.collections.Workers.find({}), selection: this.selectedCook, textAttr: 'name', listClass: 'comboList'});
this.$('[name="canner"]').buildCombo({cursor: Meteor.collections.Workers.find({}), selection: this.selectedCanner, textAttr: 'name', listClass: 'comboList'});
this.$('input[name="product"]').focus();
});
Template.BatchNew.helpers({
//products: function() {
// return Meteor.collections.Products.find({});
//},
//productSelected: function() {
// let product = this;
// let batch = Template.parentData();
//
// return batch.productId === product._id ? "selected" : "";
//},
productMeasures: function() {
//Show only the list allowed by the product
let product = Template.instance().selectedProduct.get();
if(product) {
let measures = Meteor.collections.Measures.find({}).fetch();
let measuresById = {};
let allowedMeasureIds = product.measures;
let allowedMeasures = [];
//Create a hashmap of measures by their id.
for(let measure of measures) measuresById[measure._id.toString()] = measure;
//Create a list of measures allowed for the product.
for(let measureId of allowedMeasureIds) {
allowedMeasures.push(measuresById[measureId.toString()]);
}
return allowedMeasures;
}
else return [];
},
today: () => {
return moment().format("YYYY-MM-DD");
}
});
Template.BatchNew.events({
'change input[name="date"]': function(event, template) {
template.selectedDate.set(moment(event.target.value, "YYYY-MM-DD").toDate());
},
'click .editorCancel': function(event, template) {
Session.set(PREFIX + "editedId", undefined);
},
'click .editorApply': function(event, template) {
event.preventDefault();
template.$('.insertForm').data('bs.validator').validate(function(isValid) {
if(isValid) {
let batches = [];
let insertMeasure = template.$(".insertMeasure");
let batch = {
date: ~~(moment(template.find("[name='date']").value, "YYYY-MM-DD").format("YYYYMMDD")), // Note: ~~ performs a bitwise not which is a fast method of converting a string to a number.
productId: template.selectedProduct.get()._id,
cookId: template.selectedCook.get()._id,
cannerId: template.selectedCanner.get()._id,
comment: template.$('.comment').val()
};
//Iterate over the measures for the batch (based on the product chosen) and amounts.
for(let next = 0; next < insertMeasure.length; next++) {
let nextMeasure = $(insertMeasure[next]);
let measureId = nextMeasure.find(".measureId").val();
let amount = parseFloat(nextMeasure.find(".amount").val());
if(amount > 0) {
let next = _.clone(batch);
next.measureId = measureId;
next.amount = amount;
batches.push(next);
}
}
Meteor.call('insertBatches', batches, function(error) {
if(error) sAlert.error("Failed to insert the batch!\n" + error);
else {
let productCombo = template.$("input[name='product']");
let measureFields = template.$(".amount");
sAlert.success("Production batches created.");
//Clear the measure quantity fields so the user can enter another sale without the quantities already set.
measureFields.val(0);
//Set the focus to the product field of the form.
//productCombo.focus();
//Clear the product since it is highly unlikely the same product will be added twice for the same date.
productCombo.val("");
Session.set(PREFIX + "displayNew", false);
}
});
}
});
}
});
Template.InsertBatchMeasure.onCreated(function() {
let _this = this;
this.amount = new ReactiveVar(0);
});
Template.InsertBatchMeasure.onRendered(function() {
});
Template.InsertBatchMeasure.helpers({
amount: function() {
return Template.instance().amount.get();
}
});
Template.InsertBatchMeasure.events({
'change .amount': function(event, template) {
template.amount.set(parseFloat($(event.target).val()));
},
'focus input[name="amount"]': 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.BatchSearch.onCreated(function() {
});
Template.BatchSearch.onRendered(function() {
});
Template.BatchSearch.helpers({
searchValue: function() {
let searchFields = Session.get(PREFIX + 'searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});
Template.BatchSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
let searchFields = Session.get(PREFIX + 'searchFields') || {};
let searchValue = template.$(event.target).val();
if(searchValue) {
if(this.number) searchValue = parseFloat(searchValue);
// A collection name will be provided if there is a related table of data that will contain the text provided and will map to an ID that is then searched for in the current table of data.
// For example we are displaying a table of Sales which has the ID of a Product. The Product table has a Name field and the search box searches for Product Names. The ID's of the Products found should be used to filter the Sales by Product ID.
if(this.collection) {
let ids = Meteor.collections[this.collection].find({[this.collectionQueryColumnName]: {$regex: searchValue, $options: 'i'}}, {fields: {[this.collectionResultColumnName]: 1}}).fetch();
//Convert the ids to an array of ids instead of an array of objects containing an id.
for(let i = 0; i < ids.length; i++) {ids[i] = ids[i]._id;}
searchQuery[this.columnName] = {$in: ids};
searchFields[this.columnName] = searchValue;
}
else {
searchFields[this.columnName] = searchQuery[this.columnName] = searchValue;
}
}
else {
//Remove columns from the search query whose values are empty so we don't bother the database with them.
delete searchQuery[this.columnName];
delete searchFields[this.columnName];
}
Session.set(PREFIX + 'searchQuery', searchQuery);
Session.set(PREFIX + 'searchFields', searchFields);
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
Session.set(PREFIX + "queryLimit", QUERY_LIMIT); //Reset the query limit in case we loaded more
}, 500)
});
Template.BatchDateRangeSearch.helpers({
startDate: function() {
let searchFields = Session.get(PREFIX + 'searchFields');
let searchValue = (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : {};
return searchValue.start ? moment(searchValue.start.toString(), "YYYYMMDD").format("MM/DD/YYYY") : "";
},
endDate: function() {
let searchFields = Session.get(PREFIX + 'searchFields');
let searchValue = (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : {};
return searchValue.end ? moment(searchValue.end.toString(), "YYYYMMDD").format("MM/DD/YYYY") : "";
}
});
Template.BatchDateRangeSearch.events({
"change .searchDateStartInput": function(event, template) {Template.DateRangeSearch.dateChanged(true, event, template)},
"keyup .searchDateStartInput": _.throttle(function(event, template) {Template.DateRangeSearch.dateChanged(true, event, template)}, 500),
"change .searchDateEndInput": function(event, template) {Template.DateRangeSearch.dateChanged(false, event, template)},
"keyup .searchDateEndInput": _.throttle(function(event, template) {Template.DateRangeSearch.dateChanged(false, event, template)}, 500)
});
Template.BatchDateRangeSearch.dateChanged = function(isStart, event, template) {
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
let searchFields = Session.get(PREFIX + 'searchFields') || {};
let searchValue = template.$(event.target).val();
let columnName = template.data.columnName;
if(searchValue) {
let search = searchQuery[columnName];
// Create a search object and attach it to the searchFields and searchQuery objects if needed.
if(!search) {
search = {type: 'dateRange'};
searchFields[columnName] = searchQuery[columnName] = search;
}
// Use moment to parse date and convert it to YYYYMMDD for searching the database.
searchValue = ~~(moment(searchValue, searchValue.includes("-") ? "YYYY-MM-DD" : "MM/DD/YYYY").format("YYYYMMDD")); // Note: ~~ performs a bitwise not which is a fast method of converting a string to a number.
// Save the search ending date.
isStart ? search.start = searchValue : search.end = searchValue;
}
else {
if(searchQuery[columnName]) {
// Remove columns from the search query whose values are empty so we don't bother the database with them.
if(isStart) {
delete searchQuery[columnName].start;
delete searchFields[columnName].start;
}
else {
delete searchQuery[columnName].end;
delete searchFields[columnName].end;
}
}
}
Session.set(PREFIX + 'searchQuery', searchQuery);
Session.set(PREFIX + 'searchFields', searchFields);
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
Session.set(PREFIX + "queryLimit", QUERY_LIMIT); //Reset the query limit in case we loaded more
};

View File

@@ -22,7 +22,7 @@
<th class="tags">Tags {{>ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
<th class="aliases">Aliases {{>ProductSearch columnName='aliases'}}</th>
<th class="measures">Measures {{>ProductSearch columnName='measures' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id'}}</th>
<th class="actions">Actions <span class="newProductButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
<th class="actions">Actions <span class="newButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
</thead>
</table>
@@ -32,7 +32,7 @@
<div class="tableContainer mCustomScrollbar" data-mcs-theme="dark">
<table class="table table-striped table-hover">
<tbody>
{{#if displayNewProduct}}
{{#if displayNew}}
{{> ProductEditor isNew=true}}
{{/if}}
{{#each products}}
@@ -75,7 +75,7 @@
</template>
<template name="ProductEditor">
<td colspan="4" class="productEditorTd">
<td colspan="4" class="editorTd">
<div class="editorDiv"><label>Name:</label><input name="name" class="form-control" type="text" value="{{name}}" autocomplete="off" required></div>
<div class="editorDiv"><label>Tags:</label>
<select class="productTagsEditor" multiple="multiple">
@@ -99,7 +99,7 @@
</select>
</div>
</td>
<td class="center productEditorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
<td class="center editorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
</template>
<template name="ConvertProduct">
@@ -109,11 +109,11 @@
<input name="product" class="form-control" type="text" required/>
<label><em>Convert sales from this product to an alternate.</em></label>
</td>
<td class="center productEditorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
<td class="center editorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
</template>
<template name="ProductSearch">
<div class="productSearch">
<div class="search">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
</div>
</template>

View File

@@ -69,14 +69,14 @@
> tr
> th.actions
text-align: center
.newProductButton
.newButton
margin-top: 4px
padding: 0 12px
.fa-plus-circle
display: inline-block
.fa-times-circle
display: none
.newProductButton:active
.newButton:active
background-color: #fb557b
color: black
.fa-times-circle
@@ -105,9 +105,9 @@
thead
visibility: hidden
display: none
.productSearch
.search
margin: 3px 0 2px 1px
.productEditorTd
.editorTd
background: #deeac0
input[name="name"], .productTagsEditor, .productAliasesEditor, .productMeasuresEditor
width: 100%

View File

@@ -12,7 +12,7 @@ Tracker.autorun(function() {
});
Template.Products.onCreated(function() {
Session.set(PREFIX + "displayNewProduct", false);
Session.set(PREFIX + "displayNew", false);
Session.set(PREFIX + "showHidden", false);
Session.set(PREFIX + "queryLimit", QUERY_LIMIT);
});
@@ -25,8 +25,8 @@ Template.Products.onRendered(function() {
});
});
Template.Products.helpers({
displayNewProduct: function() {
return Session.get(PREFIX + "displayNewProduct");
displayNew: function() {
return Session.get(PREFIX + "displayNew");
},
products: function() {
let skipCount = Session.get(PREFIX + 'skipCount') || 0;
@@ -68,16 +68,16 @@ Template.Products.events({
event.preventDefault();
Session.set(PREFIX + 'queryLimit', Session.get(PREFIX + "queryLimit") + QUERY_LIMIT_INCREMENT);
},
'click .newProductButton': function(event, template) {
if(template.$('.newProductButton').hasClass('active')) {
Session.set(PREFIX + 'displayNewProduct', false);
'click .newButton': function(event, template) {
if(template.$('.newButton').hasClass('active')) {
Session.set(PREFIX + 'displayNew', false);
}
else {
Session.set(PREFIX + 'displayNewProduct', true);
Session.set(PREFIX + "editedProduct", undefined); //Clear the edited product so that only one editor is open at a time.
Session.set(PREFIX + 'displayNew', true);
Session.set(PREFIX + "editedId", undefined); //Clear the edited product so that only one editor is open at a time.
Session.set(PREFIX + "convertedProduct", undefined); //Clear the converted product so that only one editor is open at a time.
}
template.$('.newProductButton').toggleClass('active');
template.$('.newButton').toggleClass('active');
},
'change input[name="showHidden"]': function(event, template) {
//console.log("changed " + $(event.target).prop('checked'));
@@ -166,9 +166,9 @@ Template.Product.helpers({
return result;
},
editing: function() {
let editedProduct = Session.get(PREFIX + "editedProduct");
let editedId = Session.get(PREFIX + "editedId");
return editedProduct == this._id;
return editedId == this._id;
},
converting: function() {
let convertedProduct = Session.get(PREFIX + "convertedProduct");
@@ -181,10 +181,10 @@ Template.Product.helpers({
});
Template.Product.events({
"click .actionEdit": function(event, template) {
Session.set(PREFIX + "editedProduct", this._id);
Session.set(PREFIX + 'displayNewProduct', false); //Ensure the new product editor is closed.
Session.set(PREFIX + "editedId", this._id);
Session.set(PREFIX + 'displayNew', false); //Ensure the new editor is closed.
Session.set(PREFIX + "convertedProduct", undefined); //Clear the converted product so that only one editor is open at a time.
template.parentTemplate().$('.newProductButton').removeClass('active');
template.parentTemplate().$('.newButton').removeClass('active');
},
"click .actionDeactivate": function(event, template) {
Meteor.call('deactivateProduct', this._id, function(error, result) {
@@ -206,9 +206,9 @@ Template.Product.events({
},
"click .actionConvert": function(event, template) {
Session.set(PREFIX + "convertedProduct", this._id);
Session.set(PREFIX + 'displayNewProduct', false); //Ensure the new product editor is closed.
Session.set(PREFIX + "editedProduct", undefined); //Clear the edited product so that only one editor is open at a time.
template.$('.newProductButton').removeClass('active');
Session.set(PREFIX + 'displayNew', false); //Ensure the new editor is closed.
Session.set(PREFIX + "editedId", undefined); //Clear the edited product so that only one editor is open at a time.
template.$('.newButton').removeClass('active');
},
'click .actionHide': function(event, template) {
Meteor.call('hideProduct', this._id, function(error, result) {
@@ -249,9 +249,9 @@ Template.ProductEditor.helpers({
});
Template.ProductEditor.events({
"click .editorCancel": function(event, template) {
Session.set(PREFIX + "editedProduct", undefined);
Session.set(PREFIX + 'displayNewProduct', false);
template.parentTemplate().$('.newProductButton').removeClass('active');
Session.set(PREFIX + "editedId", undefined);
Session.set(PREFIX + 'displayNew', false);
template.parentTemplate().$('.newButton').removeClass('active');
},
"click .editorApply": function(event, template) {
let name = template.$("input[name='name']").val().trim();
@@ -263,13 +263,13 @@ Template.ProductEditor.events({
aliases = aliases.map((n)=>n.id);
measures = measures.map((n)=>n.id);
if(Session.get(PREFIX + 'displayNewProduct')) {
if(Session.get(PREFIX + 'displayNew')) {
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');
Session.set(PREFIX + 'displayNew', false);
template.parentTemplate().$('.newButton').removeClass('active');
}
});
}
@@ -278,8 +278,8 @@ Template.ProductEditor.events({
if(error) sAlert.error(error);
else {
sAlert.success("Product updated.");
Session.set(PREFIX + "editedProduct", undefined);
template.parentTemplate().$('.newProductButton').removeClass('active');
Session.set(PREFIX + "editedId", undefined);
template.parentTemplate().$('.newButton').removeClass('active');
}
});
}
@@ -296,10 +296,10 @@ Template.ConvertProduct.onRendered(function() {
});
Template.ConvertProduct.events({
"click .editorCancel": function(event, template) {
Session.set(PREFIX + "editedProduct", undefined);
Session.set(PREFIX + "editedId", undefined);
Session.set(PREFIX + "convertedProduct", undefined);
Session.set(PREFIX + 'displayNewProduct', false);
template.parentTemplate().$('.newProductButton').removeClass('active');
Session.set(PREFIX + 'displayNew', false);
template.parentTemplate().$('.newButton').removeClass('active');
},
"click .editorApply": function(event, template) {
let productId = template.selectedProduct.get()._id;
@@ -308,10 +308,10 @@ Template.ConvertProduct.events({
if(error) sAlert.error(error);
else {
sAlert.success("Sales of this product were converted.");
Session.set(PREFIX + "editedProduct", undefined);
Session.set(PREFIX + "editedId", undefined);
Session.set(PREFIX + "convertedProduct", undefined);
Session.set(PREFIX + 'displayNewProduct', false);
template.parentTemplate().$('.newProductButton').removeClass('active');
Session.set(PREFIX + 'displayNew', false);
template.parentTemplate().$('.newButton').removeClass('active');
}
});
}

View File

@@ -128,45 +128,4 @@
{{> Template.dynamic template=content}}
</div>
</div>
</template>
<!--<template name="Body">-->
<!--<div id="layoutBody">-->
<!--<div class="bodyTableRow">-->
<!--<div class="left bodyTableCell">-->
<!--<ul>-->
<!--{{#if isInRole 'manage'}}-->
<!--<li class="{{isActiveRoute 'UserManagement'}}">-->
<!--<a href="{{pathFor 'UserManagement'}}">-->
<!--User Management-->
<!--</a>-->
<!--</li>-->
<!--{{/if}}-->
<!--<li class="{{isActiveRoute 'Sales'}}">-->
<!--<a href="{{pathFor 'Sales'}}">-->
<!--Sales <span class="tag">Test Tag</span>-->
<!--</a>-->
<!--</li>-->
<!--<li class="{{isActiveRoute 'Production'}}">-->
<!--<a href="{{pathFor 'Production'}}">-->
<!--Production <span class="tag">sample</span>-->
<!--</a>-->
<!--</li>-->
<!--</ul>-->
<!--</div>-->
<!--<div class="bodyTableCell">-->
<!--<div class="bodyTable">-->
<!--<div class="header bodyTableRow">-->
<!--&nbsp;-->
<!--</div>-->
<!--<div class="content bodyTableRow">-->
<!--{{> Template.dynamic template=content}}-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--<div class="footer bodyTableRow">-->
<!--&copy; Petit Teton LLC 2017-->
<!--</div>-->
<!--</div>-->
<!--</template>-->
</template>

View File

@@ -18,202 +18,211 @@
height: 100%
width: 100%
nav.leftSidebarContainer
z-index:999
position: fixed
top: 0
width: 220px
padding: 0
height: 100%
border: 0
vertical-align: top
text-align: left
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: visible
margin: 0 0 0 -220px
-webkit-transition: .5s ease-in
-moz-transition: .5s ease-in
-o-transition: .5s ease-in
-ms-transition: .5s ease-in
transition: .5s ease-in
.leftSidebarMenuButton
position: absolute
right: -30px
@media not print
nav.leftSidebarContainer
z-index:999
position: fixed
top: 0
width: 220px
padding: 0
height: 100%
border: 0
vertical-align: top
text-align: left
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: visible
margin: 0 0 0 -220px
-webkit-transition: .5s ease-in
-moz-transition: .5s ease-in
-o-transition: .5s ease-in
-ms-transition: .5s ease-in
transition: .5s ease-in
-webkit-border-top-right-radius: 5px
-webkit-border-bottom-right-radius: 5px
-moz-border-radius-topright: 5px
-moz-border-radius-bottomright: 5px
border-top-right-radius: 5px
border-bottom-right-radius: 5px
color: black
font-size: 20px
line-height: 20px
font-weight: 900
text-align: center
text-decoration: none
width: 30px
height: 30px
padding: 5px 0
background-color: #90b272
display: block
border-top: 1px solid #494
border-right: 1px solid #494
border-bottom: 1px solid #494
.leftSidebarMenuButton:hover
color: rgba(150,0,0,.5)
nav.generalSidebar
.leftSidebarMenuButton
top: 10px
nav.graphsSidebar
.leftSidebarMenuButton
top: 50px
nav.settingsSidebar
.leftSidebarMenuButton
top: 90px
nav.menuHide .leftSidebarMenuButton
right: 60px
nav.menuShow
margin: 0
nav.menuShow .leftSidebarMenuButton
right: -15px
-webkit-transform: rotate(45deg) !important
-moz-transform: rotate(45deg) !important
-o-transform: rotate(45deg) !important
-ms-transform: rotate(45deg) !important
transform: rotate(45deg) !important
-moz-border-radius-bottomright: 0
//border-top-right-radius: 0
border-bottom-right-radius: 0
border-bottom: 0
.leftSidebar
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
//Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D
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
.leftSidebarMenuButton
position: absolute
left: 10px
top: 10px
color: white
cursor: pointer
.signOut:hover
color: #BBB
.signOut:active
right: -30px
-webkit-transition: .5s ease-in
-moz-transition: .5s ease-in
-o-transition: .5s ease-in
-ms-transition: .5s ease-in
transition: .5s ease-in
-webkit-border-top-right-radius: 5px
-webkit-border-bottom-right-radius: 5px
-moz-border-radius-topright: 5px
-moz-border-radius-bottomright: 5px
border-top-right-radius: 5px
border-bottom-right-radius: 5px
color: black
.logo
font-size: 20px
line-height: 20px
font-weight: 900
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
border-bottom: 1px solid #e4e5e7
color: #96a2ae
text-transform: uppercase
display: block
a
color: black
padding: 10px 20px
cursor: pointer
text-decoration: none
display: block
.tag
padding: .3em .6em
margin-top: -.2em
font-size: .8em
color: #ddd
white-space: nowrap
vertical-align: baseline
border-radius: .5em
border: 1px solid #000000
float: right
.subMenu
background-color: #999
padding: .3em .6em
margin-top: -.2em
font-size: .8em
color: #fff
white-space: nowrap
vertical-align: baseline
border-radius: .5em
border: 1px solid #000000
float: right
.subMenu.active
background-color: #333
li:hover
// Note: neon6 is defined in effects.import.styl
background-color: #666
-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
.subMenu
// Note: neon6 is defined in effects.import.styl
background-color: #999
-webkit-animation: neon7 1.5s ease-in-out infinite alternate
-moz-animation: neon7 1.5s ease-in-out infinite alternate
animation: neon7 1.5s ease-in-out infinite alternate
li.active
background-color: #333
> a
color: #96a2ae
li.active:hover
background-color: #333
> a
color: white
.footer
text-decoration: none
width: 30px
height: 30px
padding: 5px 0
background-color: #90b272
display: block
border-top: 1px solid #494
border-right: 1px solid #494
border-bottom: 1px solid #494
.leftSidebarMenuButton:hover
color: rgba(150,0,0,.5)
nav.generalSidebar
.leftSidebarMenuButton
top: 10px
nav.graphsSidebar
.leftSidebarMenuButton
top: 50px
nav.settingsSidebar
.leftSidebarMenuButton
top: 90px
nav.menuHide .leftSidebarMenuButton
right: 60px
nav.menuShow
margin: 0
nav.menuShow .leftSidebarMenuButton
right: -15px
-webkit-transform: rotate(45deg) !important
-moz-transform: rotate(45deg) !important
-o-transform: rotate(45deg) !important
-ms-transform: rotate(45deg) !important
transform: rotate(45deg) !important
-moz-border-radius-bottomright: 0
//border-top-right-radius: 0
border-bottom-right-radius: 0
border-bottom: 0
.leftSidebar
flex: 0 0 auto
width: 100%
font-size: 9px
text-align: center
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
//Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D
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
.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)
overflow: hidden
.logoArea
flex: 0 0 auto
width: 100%
.signOut
position: absolute
left: 10px
top: 10px
color: white
cursor: pointer
.signOut:hover
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
border-bottom: 1px solid #e4e5e7
color: #96a2ae
text-transform: uppercase
display: block
a
color: black
padding: 10px 20px
cursor: pointer
text-decoration: none
display: block
.tag
padding: .3em .6em
margin-top: -.2em
font-size: .8em
color: #ddd
white-space: nowrap
vertical-align: baseline
border-radius: .5em
border: 1px solid #000000
float: right
.subMenu
background-color: #999
padding: .3em .6em
margin-top: -.2em
font-size: .8em
color: #fff
white-space: nowrap
vertical-align: baseline
border-radius: .5em
border: 1px solid #000000
float: right
.subMenu.active
background-color: #333
li:hover
// Note: neon6 is defined in effects.import.styl
background-color: #666
-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
.subMenu
// Note: neon6 is defined in effects.import.styl
background-color: #999
-webkit-animation: neon7 1.5s ease-in-out infinite alternate
-moz-animation: neon7 1.5s ease-in-out infinite alternate
animation: neon7 1.5s ease-in-out infinite alternate
li.active
background-color: #333
> a
color: #96a2ae
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 0 10px -3px rgba(168,165,168,1)
overflow: hidden
@media print
nav.leftSidebarContainer
display: none
.contentBody
flex: 1 1 1px
padding: 0
margin: 0
overflow: hidden

View File

@@ -0,0 +1,3 @@
<template name="Empty">
{{> Template.dynamic template=content}}
</template>

View File

@@ -0,0 +1,2 @@
import { Template } from 'meteor/templating';
import './Empty.html';

View File

@@ -0,0 +1,46 @@
# QRCode.js
QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM.
QRCode.js has no dependencies.
## Basic Usages
```
<div id="qrcode"></div>
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"), "http://jindo.dev.naver.com/collie");
</script>
```
or with some options
```
<div id="qrcode"></div>
<script type="text/javascript">
var qrcode = new QRCode(document.getElementById("qrcode"), {
text: "http://jindo.dev.naver.com/collie",
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
</script>
```
and you can use some methods
```
qrcode.clear(); // clear the code.
qrcode.makeCode("http://naver.com"); // make another code.
```
## Browser Compatibility
IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC.
## License
MIT License
## Contact
twitter @davidshimjs
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

View File

@@ -0,0 +1,616 @@
/**
* @fileoverview
* - Using the 'QRCode for Javascript library'
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
* - this library has no dependencies.
*
* @author davidshimjs
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
var QRCode;
(function () {
//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
// http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE;
this.data = data;
this.parsedData = [];
// Added to support UTF-8 Characters
for (var i = 0, l = this.data.length; i < l; i++) {
var byteArray = [];
var code = this.data.charCodeAt(i);
if (code > 0x10000) {
byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[3] = 0x80 | (code & 0x3F);
} else if (code > 0x800) {
byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[2] = 0x80 | (code & 0x3F);
} else if (code > 0x80) {
byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
byteArray[1] = 0x80 | (code & 0x3F);
} else {
byteArray[0] = code;
}
this.parsedData.push(byteArray);
}
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
if (this.parsedData.length != this.data.length) {
this.parsedData.unshift(191);
this.parsedData.unshift(187);
this.parsedData.unshift(239);
}
}
QR8bitByte.prototype = {
getLength: function (buffer) {
return this.parsedData.length;
},
write: function (buffer) {
for (var i = 0, l = this.parsedData.length; i < l; i++) {
buffer.put(this.parsedData[i], 8);
}
}
};
function QRCodeModel(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
+buffer.getLengthInBits()
+">"
+totalDataCount*8
+")");}
if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD1,8);}
return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=256){n-=255;}
return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
function _isSupportCanvas() {
return typeof CanvasRenderingContext2D != "undefined";
}
// android 2.x doesn't support Data-URI spec
function _getAndroid() {
var android = false;
var sAgent = navigator.userAgent;
if (/android/i.test(sAgent)) { // android
android = true;
var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
if (aMat && aMat[1]) {
android = parseFloat(aMat[1]);
}
}
return android;
}
var svgDrawer = (function() {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
this.clear();
function makeSVG(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
return el;
}
var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
_el.appendChild(svg);
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
if (oQRCode.isDark(row, col)) {
var child = makeSVG("use", {"x": String(col), "y": String(row)});
child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
svg.appendChild(child);
}
}
}
};
Drawing.prototype.clear = function () {
while (this._el.hasChildNodes())
this._el.removeChild(this._el.lastChild);
};
return Drawing;
})();
var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
// Drawing in DOM by using Table tag
var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_el.innerHTML = aHTML.join('');
// Fix the margin values as real size.
var elTable = _el.childNodes[0];
var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";
}
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._el.innerHTML = '';
};
return Drawing;
})() : (function () { // Drawing in Canvas
function _onMakeImage() {
this._elImage.src = this._elCanvas.toDataURL("image/png");
this._elImage.style.display = "block";
this._elCanvas.style.display = "none";
}
// Android 2.1 bug workaround
// http://code.google.com/p/android/issues/detail?id=5141
if (this._android && this._android <= 2.1) {
var factor = 1 / window.devicePixelRatio;
var drawImage = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (("nodeName" in image) && /img/i.test(image.nodeName)) {
for (var i = arguments.length - 1; i >= 1; i--) {
arguments[i] = arguments[i] * factor;
}
} else if (typeof dw == "undefined") {
arguments[1] *= factor;
arguments[2] *= factor;
arguments[3] *= factor;
arguments[4] *= factor;
}
drawImage.apply(this, arguments);
};
}
/**
* Check whether the user's browser supports Data URI or not
*
* @private
* @param {Function} fSuccess Occurs if it supports Data URI
* @param {Function} fFail Occurs if it doesn't support Data URI
*/
function _safeSetDataURI(fSuccess, fFail) {
var self = this;
self._fFail = fFail;
self._fSuccess = fSuccess;
// Check it just once
if (self._bSupportDataURI === null) {
var el = document.createElement("img");
var fOnError = function() {
self._bSupportDataURI = false;
if (self._fFail) {
self._fFail.call(self);
}
};
var fOnSuccess = function() {
self._bSupportDataURI = true;
if (self._fSuccess) {
self._fSuccess.call(self);
}
};
el.onabort = fOnError;
el.onerror = fOnError;
el.onload = fOnSuccess;
el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
return;
} else if (self._bSupportDataURI === true && self._fSuccess) {
self._fSuccess.call(self);
} else if (self._bSupportDataURI === false && self._fFail) {
self._fFail.call(self);
}
};
/**
* Drawing QRCode by using canvas
*
* @constructor
* @param {HTMLElement} el
* @param {Object} htOption QRCode Options
*/
var Drawing = function (el, htOption) {
this._bIsPainted = false;
this._android = _getAndroid();
this._htOption = htOption;
this._elCanvas = document.createElement("canvas");
this._elCanvas.width = htOption.width;
this._elCanvas.height = htOption.height;
el.appendChild(this._elCanvas);
this._el = el;
this._oContext = this._elCanvas.getContext("2d");
this._bIsPainted = false;
this._elImage = document.createElement("img");
this._elImage.alt = "Scan me!";
this._elImage.style.display = "none";
this._el.appendChild(this._elImage);
this._bSupportDataURI = null;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _elImage = this._elImage;
var _oContext = this._oContext;
var _htOption = this._htOption;
var nCount = oQRCode.getModuleCount();
var nWidth = _htOption.width / nCount;
var nHeight = _htOption.height / nCount;
var nRoundedWidth = Math.round(nWidth);
var nRoundedHeight = Math.round(nHeight);
_elImage.style.display = "none";
this.clear();
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
var bIsDark = oQRCode.isDark(row, col);
var nLeft = col * nWidth;
var nTop = row * nHeight;
_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.lineWidth = 1;
_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
// 안티 앨리어싱 방지 처리
_oContext.strokeRect(
Math.floor(nLeft) + 0.5,
Math.floor(nTop) + 0.5,
nRoundedWidth,
nRoundedHeight
);
_oContext.strokeRect(
Math.ceil(nLeft) - 0.5,
Math.ceil(nTop) - 0.5,
nRoundedWidth,
nRoundedHeight
);
}
}
this._bIsPainted = true;
};
/**
* Make the image from Canvas if the browser supports Data URI.
*/
Drawing.prototype.makeImage = function () {
if (this._bIsPainted) {
_safeSetDataURI.call(this, _onMakeImage);
}
};
/**
* Return whether the QRCode is painted or not
*
* @return {Boolean}
*/
Drawing.prototype.isPainted = function () {
return this._bIsPainted;
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
this._bIsPainted = false;
};
/**
* @private
* @param {Number} nNumber
*/
Drawing.prototype.round = function (nNumber) {
if (!nNumber) {
return nNumber;
}
return Math.floor(nNumber * 1000) / 1000;
};
return Drawing;
})();
/**
* Get the type by string length
*
* @private
* @param {String} sText
* @param {Number} nCorrectLevel
* @return {Number} type
*/
function _getTypeNumber(sText, nCorrectLevel) {
var nType = 1;
var length = _getUTF8Length(sText);
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
var nLimit = 0;
switch (nCorrectLevel) {
case QRErrorCorrectLevel.L :
nLimit = QRCodeLimitLength[i][0];
break;
case QRErrorCorrectLevel.M :
nLimit = QRCodeLimitLength[i][1];
break;
case QRErrorCorrectLevel.Q :
nLimit = QRCodeLimitLength[i][2];
break;
case QRErrorCorrectLevel.H :
nLimit = QRCodeLimitLength[i][3];
break;
}
if (length <= nLimit) {
break;
} else {
nType++;
}
}
if (nType > QRCodeLimitLength.length) {
throw new Error("Too long data");
}
return nType;
}
function _getUTF8Length(sText) {
var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
return replacedText.length + (replacedText.length != sText ? 3 : 0);
}
/**
* @class QRCode
* @constructor
* @example
* new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
*
* @example
* var oQRCode = new QRCode("test", {
* text : "http://naver.com",
* width : 128,
* height : 128
* });
*
* oQRCode.clear(); // Clear the QRCode.
* oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
*
* @param {HTMLElement|String} el target element or 'id' attribute of element.
* @param {Object|String} vOption
* @param {String} vOption.text QRCode link data
* @param {Number} [vOption.width=256]
* @param {Number} [vOption.height=256]
* @param {String} [vOption.colorDark="#000000"]
* @param {String} [vOption.colorLight="#ffffff"]
* @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
*/
QRCode = function (el, vOption) {
this._htOption = {
width : 256,
height : 256,
typeNumber : 4,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRErrorCorrectLevel.H
};
if (typeof vOption === 'string') {
vOption = {
text : vOption
};
}
// Overwrites options
if (vOption) {
for (var i in vOption) {
this._htOption[i] = vOption[i];
}
}
if (typeof el == "string") {
el = document.getElementById(el);
}
if (this._htOption.useSVG) {
Drawing = svgDrawer;
}
this._android = _getAndroid();
this._el = el;
this._oQRCode = null;
this._oDrawing = new Drawing(this._el, this._htOption);
if (this._htOption.text) {
this.makeCode(this._htOption.text);
}
};
/**
* Make the QRCode
*
* @param {String} sText link data
*/
QRCode.prototype.makeCode = function (sText) {
this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
this._oQRCode.addData(sText);
this._oQRCode.make();
this._el.title = sText;
this._oDrawing.draw(this._oQRCode);
this.makeImage();
};
/**
* Make the Image from Canvas element
* - It occurs automatically
* - Android below 3 doesn't support Data-URI spec.
*
* @private
*/
QRCode.prototype.makeImage = function () {
if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
this._oDrawing.makeImage();
}
};
/**
* Clear the QRCode
*/
QRCode.prototype.clear = function () {
this._oDrawing.clear();
};
/**
* @name QRCode.CorrectLevel
*/
QRCode.CorrectLevel = QRErrorCorrectLevel;
})();
export default QRCode;

1
imports/util/qrcode/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1619
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,24 @@
"build": "npm install --product && meteor build --architecture os.linux.x86_64 --server-only ../"
},
"dependencies": {
"@babel/runtime": "^7.2.0",
"@babel/runtime": "^7.5.5",
"babel-runtime": "^6.18.0",
"csv-parse": "latest",
"csv-parse": "^4.4.6",
"d3": "^4.4.2",
"dom-to-image": "latest",
"dragula": "^3.7.2",
"jquery": "^3.1.1",
"file-saver": "^2.0.2",
"jquery": "^3.4.1",
"jquery-mousewheel": "^3.1.13",
"jsbarcode": "^3.11.0",
"jspdf": "^1.5.3",
"malihu-custom-scrollbar-plugin": "latest",
"meteor-node-stubs": "^0.2.4",
"meteor-node-stubs": "^0.4.1",
"nocache": "^2.1.0",
"properties-reader": "0.0.15",
"puppeteer": "^1.20.0",
"pure-svg-code": "^1.0.6",
"qrcode-svg": "^1.0.0",
"simpl-schema": "latest",
"sweetalert2": "^6.3.8"
}

3
public/JsBarcode.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

106
public/LabelPrint.css Normal file
View File

@@ -0,0 +1,106 @@
body {
padding: 0;
margin: 0;
}
#PrintLabel {
padding: 0;
margin: 0;
}
.labelContainer {
text-align: center;
width-min: 3in;
width: 3in;
height-min: 2in;
height: 2in;
}
.labels {
display: none;
}
.printableLabel {
display: none;
}
.label {
display: inline-block;
width: 3in;
height: 2in;
}
.canvasContainer {
padding: 10px;
background-color: #808080;
}
.label {
position: relative;
background-color: #fff;
color: #000;
text-align: center;
font-family: TimesNewRoman, Times New Roman, Times;
font-size: 0.1in;
width: 3in;
height: 2in;
}
.label .barcodeContainer {
position: absolute;
transform: rotate(270deg) scale(0.7);
right: -10em;
top: 7em;
}
.label .labelLogo {
width: 8em;
padding: 0;
margin: 0;
padding-top: 0.15em;
margin-bottom: 0.2em;
}
.label .labelLogo3 {
width: 14em;
padding: 0;
margin: 0;
padding-top: 0.15em;
margin-bottom: 0.8em;
}
.label .labelTagline {
font-size: 1em;
font-weight: 100;
line-height: 1em;
}
.label .title1 {
width: 100%;
font-size: 2.5em;
line-height: 0.9em;
font-weight: 800;
text-transform: uppercase;
}
.label .title2 {
width: 100%;
font-size: 1.5em;
line-height: 0.9em;
font-weight: 800;
padding-bottom: 0.2em;
}
.label .ingredients {
width: 100%;
font-size: 1.2em;
font-weight: 100;
line-height: 1em;
min-height: 2em;
}
.label .ingredientsEnding {
width: 100%;
font-size: 1.2em;
font-weight: 100;
}
.label .instructions {
width: 100%;
font-size: 1.2em;
font-weight: 800;
}
.label .address {
width: 100%;
font-size: 1.2em;
font-weight: 100;
}
.label .website {
width: 100%;
font-size: 1.2em;
font-weight: 100;
}

1
public/StaticTest.html Normal file
View File

@@ -0,0 +1 @@
Hello World!

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

2
public/jquery-3.4.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,9 @@ import '/imports/startup/both';
import '/imports/api';
import '/imports/startup/server/postStartup/version.js'; //Run this right after the api - relies on the API to upgrade the app database & data to the current version.
//const nocache = require('nocache');
//app.use(nocache());
let PropertiesReader = require('properties-reader');
let props = PropertiesReader('./assets/app/release.properties');
@@ -19,3 +22,8 @@ if (!process.env.MAIL_URL) {
// console.log("Mail settings: " + process.env.MAIL_URL);
if(Meteor.log) Meteor.log.info("Server Started");
WebApp.rawConnectHandlers.use('/', function(req, res, next) {
res.setHeader('cache-control', 'no-cache');
next();
});