summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSebastian Reichel <sre@ring0.de>2012-10-02 01:05:51 +0200
committerSebastian Reichel <sre@ring0.de>2012-10-02 01:05:51 +0200
commit186049b3ed33f025eeb87eb34c19a28e1d5ba70a (patch)
tree5d892564001404fe979e18eac0e65dfcad65ed5e /src
parent9713c98dbceb54d8d00c186ba8f41f3a5befcfd1 (diff)
downloadserial-barcode-scanner-186049b3ed33f025eeb87eb34c19a28e1d5ba70a.tar.bz2
restructure code, switch from GTK to Web based UI
- move barcode generation scripts into generation/ - move code to src/ - remove database analysis from invoice/graph - put database creation sql files into sql/ - remove glade builder file - add new templates/ directory, which contains files used by the Web-UI
Diffstat (limited to 'src')
-rw-r--r--src/db.vala740
-rw-r--r--src/device.vala280
-rw-r--r--src/graph-data.vala185
-rw-r--r--src/main.vala113
-rw-r--r--src/price.vapi9
-rw-r--r--src/session.vala141
-rw-r--r--src/template.vala103
-rw-r--r--src/web.vala592
8 files changed, 2163 insertions, 0 deletions
diff --git a/src/db.vala b/src/db.vala
new file mode 100644
index 0000000..43db62b
--- /dev/null
+++ b/src/db.vala
@@ -0,0 +1,740 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public const int day_in_seconds = 24*60*60;
+
+public struct StockEntry {
+ public string id;
+ public string name;
+ public int amount;
+ public string memberprice;
+ public string guestprice;
+}
+
+public struct PriceEntry {
+ public int64 valid_from;
+ public Price memberprice;
+ public Price guestprice;
+}
+
+public struct RestockEntry {
+ public int64 timestamp;
+ public int amount;
+ public string price;
+}
+
+public struct UserInfo {
+ public int id;
+ public string firstname;
+ public string lastname;
+ public string email;
+ public string gender;
+ public string street;
+ public int plz;
+ public string city;
+}
+
+public struct UserAuth {
+ public int id;
+ public bool disabled;
+ public bool superuser;
+}
+
+public struct Product {
+ public uint64 ean;
+ public string name;
+}
+
+public struct InvoiceEntry {
+ public int64 timestamp;
+ Product product;
+ Price price;
+}
+
+public struct StatsInfo {
+ public int count_articles;
+ public int count_users;
+ public Price stock_value;
+ public Price sales_total;
+ public Price profit_total;
+ public Price sales_today;
+ public Price profit_today;
+ public Price sales_this_month;
+ public Price profit_this_month;
+ public Price sales_per_day;
+ public Price profit_per_day;
+ public Price sales_per_month;
+ public Price profit_per_month;
+}
+
+public class Database {
+ private class Statement {
+ private Sqlite.Statement stmt;
+
+ public Statement(Sqlite.Database db, string query) {
+ int rc = db.prepare_v2(query, -1, out stmt);
+
+ if(rc != Sqlite.OK) {
+ error("could not prepare statement: %s", query);
+ }
+ }
+
+ public void reset() {
+ stmt.reset();
+ }
+
+ public int step() {
+ return stmt.step();
+ }
+
+ public int bind_int(int index, int value) {
+ return stmt.bind_int(index, value);
+ }
+
+ public int bind_text(int index, string value) {
+ return stmt.bind_text(index, value);
+ }
+
+ public int bind_int64(int index, int64 value) {
+ return stmt.bind_int64(index, value);
+ }
+
+ public int column_int(int index) {
+ return stmt.column_int(index);
+ }
+
+ public string column_text(int index) {
+ return stmt.column_text(index);
+ }
+
+ public int64 column_int64(int index) {
+ return stmt.column_int64(index);
+ }
+ }
+
+ private Sqlite.Database db;
+ private static Gee.HashMap<string,string> queries = new Gee.HashMap<string,string>();
+ private static Gee.HashMap<string,Statement> statements = new Gee.HashMap<string,Statement>();
+
+ int32 user = 0;
+ bool logged_in = false;
+
+ public Database(string file) {
+ int rc;
+
+ rc = Sqlite.Database.open(file, out db);
+ if(rc != Sqlite.OK) {
+ error("could not open database!");
+ }
+
+ /* setup queries */
+ queries["product_name"] = "SELECT name FROM products WHERE id = ?";
+ queries["product_amount"] = "SELECT amount FROM products WHERE id = ?";
+ queries["products"] = "SELECT id, name, amount FROM products ORDER BY name";
+ queries["purchase"] = "INSERT INTO sells ('user', 'product', 'timestamp') VALUES (?, ?, ?)";
+ queries["last_purchase"] = "SELECT product FROM sells WHERE user = ? ORDER BY timestamp DESC LIMIT 1";
+ queries["undo"] = "DELETE FROM sells WHERE user = ? ORDER BY timestamp DESC LIMIT 1";
+ queries["product_create"] = "INSERT INTO products ('id', 'name', 'amount') VALUES (?, ?, ?)";
+ queries["price_create"] = "INSERT INTO prices ('product', 'valid_from', 'memberprice', 'guestprice') VALUES (?, ?, ?, ?)";
+ queries["stock"] = "INSERT INTO restock ('user', 'product', 'amount', 'price', 'timestamp') VALUES (?, ?, ?, ?, ?)";
+ queries["price"] = "SELECT memberprice, guestprice FROM prices WHERE product = ? AND valid_from <= ? ORDER BY valid_from DESC LIMIT 1";
+ queries["prices"] = "SELECT valid_from, memberprice, guestprice FROM prices WHERE product = ? ORDER BY valid_from ASC;";
+ queries["restocks"] = "SELECT timestamp, amount, price FROM restock WHERE product = ? ORDER BY timestamp ASC;";
+ queries["profit_complex"] = "SELECT SUM(memberprice - (SELECT price FROM purchaseprices WHERE product = purch.product)) FROM sells purch, prices WHERE purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1);";
+ queries["sales_complex"] = "SELECT SUM(memberprice) FROM sells purch, prices WHERE purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1);";
+ queries["stock_status"] = "SELECT id, name, amount, memberprice, guestprice FROM products, prices WHERE products.id = prices.product AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = products.id ORDER BY valid_from DESC LIMIT 1) ORDER BY name";
+ queries["stock_amount"] = "SELECT timestamp, amount FROM restock WHERE product = ? UNION ALL SELECT timestamp, -1 AS amount FROM sells WHERE product = ? ORDER BY timestamp DESC";
+ queries["session_set"] = "UPDATE authentication SET session=? WHERE user = ?";
+ queries["session_get"] = "SELECT user FROM authentication WHERE session = ?";
+ queries["username"] = "SELECT firstname, lastname FROM users WHERE id = ?";
+ queries["password_get"] = "SELECT password FROM authentication WHERE user = ?";
+ queries["userinfo"] = "SELECT firstname, lastname, email, gender, street, plz, city FROM users WHERE id = ?";
+ queries["userauth"] = "SELECT disabled, superuser FROM authentication WHERE user = ?";
+ queries["profit_by_product"] = "SELECT name, SUM(memberprice - (SELECT price FROM purchaseprices WHERE product = purch.product)) AS price FROM sells purch, prices, products WHERE purch.product = products.id AND purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1) GROUP BY name ORDER BY price;";
+ queries["invoice"] = "SELECT timestamp, productid, productname, price FROM invoice WHERE user = ? AND timestamp >= ? AND timestamp < ?;";
+ queries["purchase_first"] = "SELECT timestamp FROM sells WHERE user = ? ORDER BY timestamp ASC LIMIT 1";
+ queries["purchase_last"] = "SELECT timestamp FROM sells WHERE user = ? ORDER BY timestamp DESC LIMIT 1";
+ queries["count_articles"] = "SELECT COUNT(*) FROM products";
+ queries["count_users"] = "SELECT COUNT(*) FROM users";
+ queries["stock_value"] = "SELECT SUM(amount * price) FROM products INNER JOIN purchaseprices ON products.id = purchaseprices.product";
+ queries["total_sales"] = "SELECT SUM(price) FROM invoice WHERE user >= 0 AND timestamp >= ?";
+ queries["total_profit"] = "SELECT SUM(price - (SELECT price FROM purchaseprices WHERE product = productid)) FROM invoice WHERE user >= 0 AND timestamp >= ?";
+
+ /* compile queries into statements */
+ foreach(var entry in queries.entries) {
+ statements[entry.key] = new Statement(db, entry.value);
+ }
+ }
+
+ public bool login(int32 id) {
+ this.user = id;
+ this.logged_in = true;
+ return true;
+ }
+
+ public bool logout() {
+ this.user = 0;
+ this.logged_in = false;
+ return true;
+ }
+
+ public Gee.HashMap<string,string> get_products() {
+ var result = new Gee.HashMap<string,string>(null, null);
+ statements["products"].reset();
+
+ while(statements["products"].step() == Sqlite.ROW)
+ result[statements["products"].column_text(0)] = statements["products"].column_text(1);
+
+ return result;
+ }
+
+ public stock get_stats_stock() {
+ var result = new stock();
+ var now = time_t();
+
+ /* init products */
+ statements["products"].reset();
+ while(statements["products"].step() == Sqlite.ROW) {
+ var id = uint64.parse(statements["products"].column_text(0));
+ var name = statements["products"].column_text(1);
+ int amount = int.parse(statements["products"].column_text(2));
+ var product = new stock.product(id, name);
+ result.add(product);
+ product.add(now, amount);
+
+ statements["stock_amount"].reset();
+ statements["stock_amount"].bind_text(1, "%llu".printf(id));
+ statements["stock_amount"].bind_text(2, "%llu".printf(id));
+
+ while(statements["stock_amount"].step() == Sqlite.ROW) {
+ var timestamp = uint64.parse(statements["stock_amount"].column_text(0));
+ var diff = statements["stock_amount"].column_int(1);
+ product.add(timestamp+1, amount);
+ amount -= diff;
+ product.add(timestamp, amount);
+ }
+ }
+
+ return result;
+ }
+
+ public profit_per_product get_stats_profit_per_products() {
+ var result = new profit_per_product();
+
+ statements["profit_by_product"].reset();
+ statements["profit_by_product"].bind_int(1, 0);
+ statements["profit_by_product"].bind_text(2, "99999999999999");
+
+ while(statements["profit_by_product"].step() == Sqlite.ROW) {
+ var name = statements["profit_by_product"].column_text(0);
+ var profit = statements["profit_by_product"].column_int(1);
+ result.add(name, profit);
+ }
+
+ return result;
+ }
+
+ public profit_per_weekday get_stats_profit_per_weekday() {
+ var result = new profit_per_weekday();
+
+ var now = new DateTime.now_utc();
+ var today = new DateTime.utc(now.get_year(), now.get_month(), now.get_day_of_month(), 8, 0, 0);
+ var tomorrow = today.add_days(1);
+ var weekday = tomorrow.get_day_of_week()-1;
+
+ var to = tomorrow.to_unix();
+ var from = to - day_in_seconds;
+
+ var weeks = 8;
+
+ for(int i=0; i<weeks*7; i++) {
+ statements["profit_complex"].reset();
+ statements["profit_complex"].bind_text(1, "%llu".printf(from));
+ statements["profit_complex"].bind_text(2, "%llu".printf(to));
+
+ if(statements["profit_complex"].step() == Sqlite.ROW)
+ result.day[weekday] += statements["profit_complex"].column_int(0);
+
+ from-=day_in_seconds;
+ to-=day_in_seconds;
+ weekday = (weekday + 1) % 7;
+ }
+
+ for(int i=0; i<7; i++)
+ result.day[i] /= weeks;
+
+ return result;
+ }
+
+ public profit_per_day get_stats_profit_per_day() {
+ var result = new profit_per_day();
+ var to = time_t();
+ var from = to - day_in_seconds;
+
+ /* 8 weeks */
+ for(int i=0; i<8*7; i++) {
+ statements["profit_complex"].reset();
+ statements["profit_complex"].bind_text(1, "%llu".printf(from));
+ statements["profit_complex"].bind_text(2, "%llu".printf(to));
+ statements["sales_complex"].reset();
+ statements["sales_complex"].bind_text(1, "%llu".printf(from));
+ statements["sales_complex"].bind_text(2, "%llu".printf(to));
+
+
+ if(statements["profit_complex"].step() == Sqlite.ROW)
+ result.add_profit(from, statements["profit_complex"].column_int(0));
+ if(statements["sales_complex"].step() == Sqlite.ROW)
+ result.add_sales(from, statements["sales_complex"].column_int(0));
+
+ from-=day_in_seconds;
+ to-=day_in_seconds;
+ }
+
+ return result;
+ }
+
+ public Gee.List<StockEntry?> get_stock() {
+ var result = new Gee.ArrayList<StockEntry?>();
+ statements["stock_status"].reset();
+
+ while(statements["stock_status"].step() == Sqlite.ROW) {
+ StockEntry entry = {
+ statements["stock_status"].column_text(0),
+ statements["stock_status"].column_text(1),
+ statements["stock_status"].column_int(2),
+ null,
+ null
+ };
+
+ Price mprice = statements["stock_status"].column_int(3);
+ Price gprice = statements["stock_status"].column_int(4);
+
+ entry.memberprice = @"$mprice";
+ entry.guestprice = @"$gprice";
+
+ result.add(entry);
+ }
+
+ return result;
+ }
+
+ public Gee.List<PriceEntry?> get_prices(uint64 product) {
+ var result = new Gee.ArrayList<PriceEntry?>();
+ statements["prices"].reset();
+ statements["prices"].bind_text(1, "%llu".printf(product));
+
+ while(statements["prices"].step() == Sqlite.ROW) {
+ PriceEntry entry = {
+ statements["prices"].column_int64(0),
+ statements["prices"].column_int(1),
+ statements["prices"].column_int(2)
+ };
+
+ result.add(entry);
+ }
+
+ return result;
+ }
+
+ public Gee.List<RestockEntry?> get_restocks(uint64 product) {
+ var result = new Gee.ArrayList<RestockEntry?>();
+ statements["restocks"].reset();
+ statements["restocks"].bind_text(1, "%llu".printf(product));
+
+ while(statements["restocks"].step() == Sqlite.ROW) {
+ RestockEntry entry = {
+ statements["restocks"].column_int64(0),
+ statements["restocks"].column_int(1)
+ };
+
+ Price p = statements["restocks"].column_int(2);
+ entry.price = @"$p";
+
+ result.add(entry);
+ }
+
+ return result;
+ }
+
+ public bool buy(uint64 article) {
+ if(is_logged_in()) {
+ int rc = 0;
+ int64 timestamp = (new DateTime.now_utc()).to_unix();
+
+ statements["purchase"].reset();
+ statements["purchase"].bind_text(1, "%d".printf(user));
+ statements["purchase"].bind_text(2, "%llu".printf(article));
+ statements["purchase"].bind_text(3, "%llu".printf(timestamp));
+
+ rc = statements["purchase"].step();
+ if(rc != Sqlite.DONE)
+ error("[interner Fehler: %d]".printf(rc));
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public string get_product_name(uint64 article) {
+ statements["product_name"].reset();
+ statements["product_name"].bind_text(1, "%llu".printf(article));
+
+ int rc = statements["product_name"].step();
+
+ switch(rc) {
+ case Sqlite.ROW:
+ return statements["product_name"].column_text(0);
+ case Sqlite.DONE:
+ return "unbekanntes Produkt: %llu".printf(article);
+ default:
+ return "[interner Fehler: %d]".printf(rc);
+ }
+ }
+
+ public int get_product_amount(uint64 article) {
+ statements["product_amount"].reset();
+ statements["product_amount"].bind_text(1, "%llu".printf(article));
+
+ int rc = statements["product_amount"].step();
+
+ switch(rc) {
+ case Sqlite.ROW:
+ return statements["product_amount"].column_int(0);
+ case Sqlite.DONE:
+ warning("unbekanntes Produkt: %llu".printf(article));
+ return -1;
+ default:
+ warning("[interner Fehler: %d]".printf(rc));
+ return -1;
+ }
+ }
+
+ public int get_product_price(uint64 article) {
+ int64 timestamp = (new DateTime.now_utc()).to_unix();
+ bool member = user != 0;
+
+ statements["price"].reset();
+ statements["price"].bind_text(1, "%llu".printf(article));
+ statements["price"].bind_text(2, "%lld".printf(timestamp));
+
+ int rc = statements["price"].step();
+
+ switch(rc) {
+ case Sqlite.ROW:
+ if(member)
+ return statements["price"].column_int(0);
+ else
+ return statements["price"].column_int(1);
+ case Sqlite.DONE:
+ write_to_log("unbekanntes Produkt: %llu\n", article);
+ return 0;
+ default:
+ write_to_log("[interner Fehler: %d]\n", rc);
+ return 0;
+ }
+ }
+
+ public bool undo() {
+ if(is_logged_in()) {
+ uint64 pid = 0;
+ int rc = 0;
+
+ statements["last_purchase"].reset();
+ statements["last_purchase"].bind_text(1, "%d".printf(user));
+
+ rc = statements["last_purchase"].step();
+ switch(rc) {
+ case Sqlite.ROW:
+ pid = uint64.parse(statements["last_purchase"].column_text(0));
+ write_to_log("remove purchase of %llu", pid);
+ break;
+ case Sqlite.DONE:
+ write_to_log("undo not possible without purchases");
+ return false;
+ default:
+ error("[interner Fehler: %d]".printf(rc));
+ }
+
+ statements["undo"].reset();
+ statements["undo"].bind_text(1, "%d".printf(user));
+
+ rc = statements["undo"].step();
+ if(rc != Sqlite.DONE)
+ error("[interner Fehler: %d]".printf(rc));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool restock(int user, uint64 product, uint amount, uint price) {
+ if(user > 0) {
+ int rc = 0;
+ int64 timestamp = (new DateTime.now_utc()).to_unix();
+
+ statements["stock"].reset();
+ statements["stock"].bind_int(1, user);
+ statements["stock"].bind_text(2, @"$product");
+ statements["stock"].bind_text(3, @"$amount");
+ statements["stock"].bind_text(4, @"$price");
+ statements["stock"].bind_int64(5, timestamp);
+
+ rc = statements["stock"].step();
+ if(rc != Sqlite.DONE)
+ error("[interner Fehler: %d]".printf(rc));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool new_product(uint64 id, string name, int memberprice, int guestprice) {
+ statements["product_create"].reset();
+ statements["product_create"].bind_text(1, @"$id");
+ statements["product_create"].bind_text(2, name);
+ statements["product_create"].bind_int(3, 0);
+ int rc = statements["product_create"].step();
+
+ if(rc != Sqlite.DONE) {
+ warning("[interner Fehler: %d]".printf(rc));
+ return false;
+ }
+
+ return new_price(id, 0, memberprice, guestprice);
+ }
+
+ public bool new_price(uint64 product, int64 timestamp, int memberprice, int guestprice) {
+ statements["price_create"].reset();
+ statements["price_create"].bind_text(1, @"$product");
+ statements["price_create"].bind_int64(2, timestamp);
+ statements["price_create"].bind_int(3, memberprice);
+ statements["price_create"].bind_int(4, guestprice);
+ int rc = statements["price_create"].step();
+
+ if(rc != Sqlite.DONE) {
+ warning("[interner Fehler: %d]".printf(rc));
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool is_logged_in() {
+ return this.logged_in;
+ }
+
+ public bool check_user_password(int32 user, string password) {
+ statements["password_get"].reset();
+ statements["password_get"].bind_int(1, user);
+
+ if(statements["password_get"].step() == Sqlite.ROW) {
+ var pwhash_db = statements["password_get"].column_text(0);
+ var pwhash_user = Checksum.compute_for_string(ChecksumType.SHA256, password);
+
+ stdout.printf("tried login: %s\n", pwhash_user);
+
+ return pwhash_db == pwhash_user;
+ } else {
+ return false;
+ }
+ }
+
+ public void set_sessionid(int user, string sessionid) {
+ statements["session_set"].reset();
+ statements["session_set"].bind_text(1, sessionid);
+ statements["session_set"].bind_int(2, user);
+
+ int rc = statements["session_set"].step();
+ if(rc != Sqlite.DONE)
+ error("[interner Fehler: %d]".printf(rc));
+ }
+
+ public int get_user_by_sessionid(string sessionid) throws WebSessionError {
+ statements["session_get"].reset();
+ statements["session_get"].bind_text(1, sessionid);
+
+ if(statements["session_get"].step() == Sqlite.ROW) {
+ return statements["session_get"].column_int(0);
+ } else {
+ throw new WebSessionError.SESSION_NOT_FOUND("No such session available in database!");
+ }
+ }
+
+ public UserInfo get_user_info(int user) {
+ var result = UserInfo();
+ statements["userinfo"].reset();
+ statements["userinfo"].bind_int(1, user);
+
+ if(statements["userinfo"].step() == Sqlite.ROW) {
+ result.id = user;
+ result.firstname = statements["userinfo"].column_text(0);
+ result.lastname = statements["userinfo"].column_text(1);
+ result.email = statements["userinfo"].column_text(2);
+ result.gender = statements["userinfo"].column_text(3);
+ result.street = statements["userinfo"].column_text(4);
+ result.plz = statements["userinfo"].column_int(5);
+ result.city = statements["userinfo"].column_text(6);
+ }
+
+ return result;
+ }
+
+ public UserAuth get_user_auth(int user) {
+ var result = UserAuth();
+ result.id = user;
+
+ statements["userauth"].reset();
+ statements["userauth"].bind_int(1, user);
+ if(statements["userauth"].step() == Sqlite.ROW) {
+ result.disabled = statements["userauth"].column_int(0) == 1;
+ result.superuser = statements["userauth"].column_int(1) == 1;
+ }
+
+ return result;
+ }
+
+ public string get_username(int user) throws WebSessionError {
+ statements["username"].reset();
+ statements["username"].bind_int(1, user);
+
+ if(statements["username"].step() == Sqlite.ROW) {
+ return statements["username"].column_text(0)+" "+statements["username"].column_text(1);
+ } else {
+ throw new WebSessionError.USER_NOT_FOUND("No such user available in database!");
+ }
+ }
+
+ public Gee.List<InvoiceEntry?> get_invoice(int user, int64 from=0, int64 to=-1) {
+ var result = new Gee.ArrayList<InvoiceEntry?>();
+
+ if(to == -1) {
+ to = time_t();
+ }
+
+ statements["invoice"].reset();
+ statements["invoice"].bind_int(1, user);
+ statements["invoice"].bind_int64(2, from);
+ statements["invoice"].bind_int64(3, to);
+
+ while(statements["invoice"].step() == Sqlite.ROW) {
+ InvoiceEntry entry = {};
+ entry.timestamp = statements["invoice"].column_int64(0);
+ entry.product.ean = uint64.parse(statements["invoice"].column_text(1));
+ entry.product.name = statements["invoice"].column_text(2);
+ entry.price = statements["invoice"].column_int(3);
+ result.add(entry);
+ }
+
+ return result;
+ }
+
+ public DateTime get_first_purchase(int user) {
+ statements["purchase_first"].reset();
+ statements["purchase_first"].bind_int(1, user);
+
+ if(statements["purchase_first"].step() == Sqlite.ROW)
+ return new DateTime.from_unix_utc(statements["purchase_first"].column_int64(0));
+ else
+ return new DateTime.from_unix_utc(0);
+ }
+
+ public DateTime get_last_purchase(int user) {
+ statements["purchase_last"].reset();
+ statements["purchase_last"].bind_int(1, user);
+
+ if(statements["purchase_last"].step() == Sqlite.ROW)
+ return new DateTime.from_unix_utc(statements["purchase_last"].column_int64(0));
+ else
+ return new DateTime.from_unix_utc(0);
+ }
+
+ public StatsInfo get_stats_info() {
+ var result = StatsInfo();
+
+ DateTime now = new DateTime.now_local();
+ DateTime today = new DateTime.local(now.get_year(), now.get_month(), now.get_hour() < 8 ? now.get_day_of_month()-1 : now.get_day_of_month(), 8, 0, 0);
+ DateTime month = new DateTime.local(now.get_year(), now.get_day_of_month() < 16 ? now.get_month()-1 : now.get_month(), 16, 8, 0, 0);
+
+ DateTime last4weeks = now.add_days(-28);
+ DateTime last4months = now.add_months(-4);
+
+ statements["count_articles"].reset();
+ if(statements["count_articles"].step() == Sqlite.ROW)
+ result.count_articles = statements["count_articles"].column_int(0);
+
+ statements["count_users"].reset();
+ if(statements["count_users"].step() == Sqlite.ROW)
+ result.count_users = statements["count_users"].column_int(0);
+
+ statements["stock_value"].reset();
+ if(statements["stock_value"].step() == Sqlite.ROW)
+ result.stock_value = statements["stock_value"].column_int(0);
+
+ statements["total_sales"].reset();
+ statements["total_sales"].bind_int64(1, 0);
+ if(statements["total_sales"].step() == Sqlite.ROW)
+ result.sales_total = statements["total_sales"].column_int(0);
+
+ statements["total_profit"].reset();
+ statements["total_profit"].bind_int64(1, 0);
+ if(statements["total_profit"].step() == Sqlite.ROW)
+ result.profit_total = statements["total_profit"].column_int(0);
+
+ statements["total_sales"].reset();
+ statements["total_sales"].bind_int64(1, today.to_unix());
+ if(statements["total_sales"].step() == Sqlite.ROW)
+ result.sales_today = statements["total_sales"].column_int(0);
+
+ statements["total_profit"].reset();
+ statements["total_profit"].bind_int64(1, today.to_unix());
+ if(statements["total_profit"].step() == Sqlite.ROW)
+ result.profit_today = statements["total_profit"].column_int(0);
+
+ statements["total_sales"].reset();
+ statements["total_sales"].bind_int64(1, month.to_unix());
+ if(statements["total_sales"].step() == Sqlite.ROW)
+ result.sales_this_month = statements["total_sales"].column_int(0);
+
+ statements["total_profit"].reset();
+ statements["total_profit"].bind_int64(1, month.to_unix());
+ if(statements["total_profit"].step() == Sqlite.ROW)
+ result.profit_this_month = statements["total_profit"].column_int(0);
+
+ statements["total_sales"].reset();
+ statements["total_sales"].bind_int64(1, last4weeks.to_unix());
+ if(statements["total_sales"].step() == Sqlite.ROW)
+ result.sales_per_day = statements["total_sales"].column_int(0) / 28;
+
+ statements["total_profit"].reset();
+ statements["total_profit"].bind_int64(1, last4weeks.to_unix());
+ if(statements["total_profit"].step() == Sqlite.ROW)
+ result.profit_per_day = statements["total_profit"].column_int(0) / 28;
+
+ statements["total_sales"].reset();
+ statements["total_sales"].bind_int64(1, last4months.to_unix());
+ if(statements["total_sales"].step() == Sqlite.ROW)
+ result.sales_per_month = statements["total_sales"].column_int(0) / 4;
+
+ statements["total_profit"].reset();
+ statements["total_profit"].bind_int64(1, last4months.to_unix());
+ if(statements["total_profit"].step() == Sqlite.ROW)
+ result.profit_per_month = statements["total_profit"].column_int(0) / 4;
+
+ return result;
+ }
+}
diff --git a/src/device.vala b/src/device.vala
new file mode 100644
index 0000000..5d7fc09
--- /dev/null
+++ b/src/device.vala
@@ -0,0 +1,280 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public class Device {
+ private Posix.termios newtio;
+ private Posix.termios restoretio;
+ public int fd=-1;
+ private IOChannel io_read;
+ public int byterate;
+ private File lockfile;
+
+ public signal void received_barcode(string barcode);
+
+ public Device(string device, int rate, int bits, int stopbits) {
+ Posix.speed_t baudrate = Posix.B9600;
+
+ try {
+ /* check lock file */
+ lockfile = File.new_for_path("/var/lock/LCK.." + device.replace("/dev/", ""));
+ if(lockfile.query_exists()) {
+ int pid = -1;
+ uint8[] data;
+ if(lockfile.load_contents(null, out data, null)) {
+ pid = int.parse((string) data);
+ } else {
+ error("Can't read lock file!\n");
+ }
+
+ if(Posix.kill(pid, 0) == 0) {
+ error("serial device is locked!\n");
+ }
+ }
+
+ var pid = "%d\n".printf(Posix.getpid());
+ lockfile.replace_contents(pid.data, null, false, FileCreateFlags.NONE, null);
+
+ fd = Posix.open(device, Posix.O_RDWR /*| Posix.O_NONBLOCK*/);
+
+ if(fd < 0) {
+ fd = -1;
+ lockfile.delete();
+ error("Could not open device!\n");
+ }
+
+ } catch(Error e) {
+ error("Could not create lock file: %s!\n", e.message);
+ }
+
+
+ Posix.tcflush(fd, Posix.TCIOFLUSH);
+
+ Posix.tcgetattr(fd, out restoretio);
+
+ /* apply settings */
+ switch(rate) {
+ case 300:
+ baudrate = Posix.B300;
+ break;
+ case 600:
+ baudrate = Posix.B600;
+ break;
+ case 1200:
+ baudrate = Posix.B1200;
+ break;
+ case 2400:
+ baudrate = Posix.B2400;
+ break;
+ case 4800:
+ baudrate = Posix.B4800;
+ break;
+ case 9600:
+ baudrate = Posix.B9600;
+ break;
+ case 19200:
+ baudrate = Posix.B19200;
+ break;
+ case 38400:
+ baudrate = Posix.B38400;
+ break;
+ case 57600:
+ baudrate = Posix.B57600;
+ break;
+ case 115200:
+ baudrate = Posix.B115200;
+ break;
+ case 230400:
+ baudrate = Posix.B230400;
+ break;
+ default:
+ /* not supported */
+ rate = 9600;
+ break;
+ }
+
+ Posix.cfsetospeed(ref newtio, baudrate);
+ Posix.cfsetispeed(ref newtio, baudrate);
+
+ switch(bits) {
+ case 5:
+ newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS5;
+ break;
+ case 6:
+ newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS6;
+ break;
+ case 7:
+ newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS7;
+ break;
+ case 8:
+ default:
+ newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS8;
+ break;
+ }
+
+ newtio.c_cflag |= Posix.CLOCAL | Posix.CREAD;
+
+ newtio.c_cflag &= ~(Posix.PARENB | Posix.PARODD);
+
+ /* TODO: parity */
+
+ newtio.c_cflag &= ~Linux.Termios.CRTSCTS;
+
+ if(stopbits == 2)
+ newtio.c_cflag |= Posix.CSTOPB;
+ else
+ newtio.c_cflag &= ~Posix.CSTOPB;
+
+ newtio.c_iflag = Posix.IGNBRK;
+
+ newtio.c_lflag = 0;
+ newtio.c_oflag = 0;
+
+ newtio.c_cc[Posix.VTIME]=1;
+ newtio.c_cc[Posix.VMIN]=1;
+
+ newtio.c_lflag &= ~(Posix.ECHONL|Posix.NOFLSH);
+
+ int mcs=0;
+ Posix.ioctl(fd, Linux.Termios.TIOCMGET, out mcs);
+ mcs |= Linux.Termios.TIOCM_RTS;
+ Posix.ioctl(fd, Linux.Termios.TIOCMSET, out mcs);
+
+ Posix.tcsetattr(fd, Posix.TCSANOW, newtio);
+
+ this.byterate = rate/bits;
+
+ try {
+ io_read = new IOChannel.unix_new(fd);
+ io_read.set_line_term("\r\n", 2);
+ if(io_read.set_encoding(null) != IOStatus.NORMAL)
+ error("Failed to set encoding");
+ if(!(io_read.add_watch(IOCondition.IN | IOCondition.HUP, device_read) != 0)) {
+ error("Could not bind IOChannel");
+ }
+ } catch(IOChannelError e) {
+ error("IOChannel: %s", e.message);
+ }
+ }
+
+ ~Device() {
+ /* restore old tty config */
+ Posix.tcsetattr(fd, Posix.TCSANOW, restoretio);
+
+ /* close file */
+ Posix.close(fd);
+
+ /* remove lock */
+ lockfile.delete();
+ }
+
+ private bool device_read(IOChannel gio, IOCondition cond) {
+ IOStatus ret;
+ string msg;
+ size_t len, term_char;
+
+ if((cond & IOCondition.HUP) == IOCondition.HUP)
+ error("Lost device");
+
+ try {
+ ret = gio.read_line(out msg, out len, out term_char);
+ msg = msg[0:(long)term_char];
+
+ if(msg.has_prefix("USER ") || msg.has_prefix("STOCK") || msg.has_prefix("AMOUNT ")) {
+ if(!check_code39_checksum(msg))
+ received_barcode("SCANNER RETURNED INCORRECT DATA");
+ else {/* remove checksum */
+ msg = msg[0:-1];
+ received_barcode(msg);
+ }
+ }
+ else
+ received_barcode(msg);
+ }
+ catch(IOChannelError e) {
+ stderr.printf("IOChannel Error: %s", e.message);
+ return false;
+ }
+ catch(ConvertError e) {
+ stderr.printf("Convert Error: %s", e.message);
+ return false;
+ }
+ return true;
+ }
+
+ private ssize_t write(void *buf, size_t count) {
+ ssize_t size = Posix.write(fd, buf, count);
+ return size;
+ }
+
+ private bool check_code39_checksum(string data) {
+ int result = 0;
+
+ for(int i = 0; i<data.length-1; i++) {
+ if(data[i] >= '0' && data[i] <= '9')
+ result += data[i] - '0';
+ else if(data[i] >= 'A' && data[i] <= 'Z')
+ result += data[i] - 'A' + 10;
+ else
+ switch(data[i]) {
+ case '-':
+ result += 36; break;
+ case '.':
+ result += 37; break;
+ case ' ':
+ result += 38; break;
+ case '$':
+ result += 39; break;
+ case '/':
+ result += 40; break;
+ case '+':
+ result += 41; break;
+ case '%':
+ result += 42; break;
+ default:
+ /* invalid character */
+ return false;
+ }
+
+ result %= 43;
+ }
+
+ if(result < 10)
+ result = result + '0';
+ else if(result < 36)
+ result = result - 10 + 'A';
+ else
+ switch(result) {
+ case 36: result = '-'; break;
+ case 37: result = '.'; break;
+ case 38: result = ' '; break;
+ case 39: result = '$'; break;
+ case 40: result = '/'; break;
+ case 41: result = '+'; break;
+ case 42: result = '%'; break;
+ }
+
+ return (data[data.length-1] == result);
+ }
+
+ /**
+ * @param duration duration of the blink in 0.1 seconds
+ */
+ public void blink(uint duration) {
+ uint size = byterate/10 * duration;
+ var msg = new uint8[size];
+ Posix.memset(msg, 0xFF, msg.length);
+ this.write(msg, msg.length);
+ }
+}
diff --git a/src/graph-data.vala b/src/graph-data.vala
new file mode 100644
index 0000000..045c6ad
--- /dev/null
+++ b/src/graph-data.vala
@@ -0,0 +1,185 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public class stock {
+ public class product {
+ public struct subitem {
+ public uint64 timestamp;
+ public int amount;
+
+ public string json {
+ owned get {
+ return "[%llu, %d]".printf(timestamp*1000, amount);
+ }
+ }
+ }
+
+ public uint64 id;
+ public string name;
+ subitem[] data;
+
+ public product(uint64 id, string name) {
+ this.id = id;
+ this.name = name;
+ this.data = null;
+ }
+
+ public void add(uint64 timestamp, int amount) {
+ subitem newitem = {timestamp, amount};
+ this.data += newitem;
+ }
+
+ public string json {
+ owned get {
+ var data_array = "[";
+ if(data != null) {
+ for(int i=0; i < data.length-1; i++)
+ data_array += data[i].json + ", ";
+ data_array += data[data.length-1].json;
+ }
+ data_array += "]";
+
+ return "{label: \"%s\", data: %s}".printf(name, data_array);
+ }
+ }
+ }
+
+ Gee.HashMap<uint64?,product> data;
+
+ public stock() {
+ data = new Gee.HashMap<uint64?,product>();
+ }
+
+ public void add(product i) {
+ data[i.id] = i;
+ }
+
+ public product get_product(uint64 id) {
+ return data[id];
+ }
+
+ public string json {
+ owned get {
+ var result = "{";
+ foreach(var entry in data.entries) {
+ uint64? id = entry.key;
+ string pdata = entry.value.json;
+ result += @"\"product_$id\": $pdata, ";
+ }
+ result = result.substring(0, result.length-2);
+ result += "}";
+ return result;
+ }
+ }
+}
+
+public class profit_per_day {
+ public struct subitem {
+ public uint64 timestamp;
+ public Price amount;
+
+ public string json {
+ owned get {
+ return @"[$(timestamp*1000), $amount]";
+ }
+ }
+ }
+
+ private subitem[] profit_data;
+ private subitem[] sales_data;
+
+ public void add_profit(uint64 timestamp, int amount) {
+ subitem newitem = {timestamp, amount};
+ this.profit_data += newitem;
+ }
+
+ public void add_sales(uint64 timestamp, int amount) {
+ subitem newitem = {timestamp, amount};
+ this.sales_data += newitem;
+ }
+
+ public string json {
+ owned get {
+ var result = "{\"profit\": {label: \"Profit\", data: [";
+ if(profit_data != null) {
+ for(int i=0; i < profit_data.length-1; i++)
+ result += profit_data[i].json + ", ";
+ result += profit_data[profit_data.length-1].json;
+ }
+ result += "]},\"sales\": {label: \"Sales\", data:[";
+ if(sales_data != null) {
+ for(int i=0; i < sales_data.length-1; i++)
+ result += sales_data[i].json + ", ";
+ result += sales_data[sales_data.length-1].json;
+ }
+ result += "]}}";
+
+ return result;
+ }
+ }
+}
+
+public class profit_per_weekday {
+ public Price[] day = new Price[7];
+
+ public profit_per_weekday() {
+ for(int i=0; i<day.length; i++)
+ day[i] = 0;
+ }
+
+ public string json {
+ owned get {
+ var result = "[";
+ result += @"{ label: \"Monday\", data: $(day[0]) },";
+ result += @"{ label: \"Tuesday\", data: $(day[1]) },";
+ result += @"{ label: \"Wednesday\", data: $(day[2]) },";
+ result += @"{ label: \"Thursday\", data: $(day[3]) },";
+ result += @"{ label: \"Friday\", data: $(day[4]) },";
+ result += @"{ label: \"Saturday\", data: $(day[5]) },";
+ result += @"{ label: \"Sunday\", data: $(day[6]) }";
+ result += "]";
+ return result;
+ }
+ }
+}
+
+public class profit_per_product {
+ Gee.HashMap<string,int> data;
+
+ public profit_per_product() {
+ data = new Gee.HashMap<string,int>();
+ }
+
+ public void add(string product, int amount) {
+ if(data.has_key(product))
+ data[product] = data[product] + amount;
+ else
+ data[product] = amount;
+ }
+
+ public string json {
+ owned get {
+ var result = "[";
+ foreach(var e in data.entries) {
+ result += @"{ label: \"$(e.key)\", data: $((Price) e.value) },";
+ }
+ if(result.length > 1) {
+ result = result.substring(0, result.length-1);
+ }
+ result += "]";
+ return result;
+ }
+ }
+}
diff --git a/src/main.vala b/src/main.vala
new file mode 100644
index 0000000..74736d2
--- /dev/null
+++ b/src/main.vala
@@ -0,0 +1,113 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public Device dev;
+public Database db;
+
+public static int main(string[] args) {
+ if(args.length < 2) {
+ stderr.printf("%s <device>\n", args[0]);
+ return 1;
+ }
+
+ dev = new Device(args[1], 9600, 8, 1);
+ db = new Database("shop.db");
+
+ dev.received_barcode.connect((data) => {
+ if(interpret(data))
+ dev.blink(10);
+ });
+
+ write_to_log("KtT Shop System has been started");
+
+ /* attach webserver to mainloop */
+ new WebServer();
+
+ /* run mainloop */
+ new MainLoop().run();
+
+ /* call destructors */
+ dev = null;
+ db = null;
+
+ return 0;
+}
+
+public void write_to_log(string format, ...) {
+ var arguments = va_list();
+ var message = format.vprintf(arguments);
+
+ stdout.printf(message + "\n");
+}
+
+public static bool interpret(string data) {
+ if(data.has_prefix("USER ")) {
+ string str_id = data.substring(5);
+ int32 id = int.parse(str_id);
+
+ /* check if data has valid format */
+ if(data != "USER %d".printf(id)) {
+ write_to_log("ungültige Benutzernummer: %s", data);
+ return false;
+ }
+
+ if(db.is_logged_in()) {
+ write_to_log("Last User forgot to logout!");
+ db.logout();
+ }
+
+ write_to_log("Login: %d", id);
+ return db.login(id);
+ } else if(data == "GUEST") {
+ if(db.is_logged_in()) {
+ write_to_log("Last User forgot to logout!");
+ db.logout();
+ }
+
+ write_to_log("Login: Guest");
+ return db.login(0);
+ } else if(data == "UNDO") {
+ if(!db.is_logged_in()) {
+ write_to_log("Can't undo if not logged in!");
+ return false;
+ } else {
+ write_to_log("Undo last purchase!");
+ return db.undo();
+ }
+ } else if(data == "LOGOUT") {
+ if(db.is_logged_in()) {
+ write_to_log("Logout!");
+ return db.logout();
+ }
+
+ return false;
+ } else {
+ uint64 id = uint64.parse(data);
+
+ /* check if data has valid format */
+ if(data != "%llu".printf(id)) {
+ write_to_log("ungültiges Produkt: %s", data);
+ return false;
+ }
+
+ if(db.buy(id)) {
+ write_to_log("gekaufter Artikel: %s (%d,%02d €)", db.get_product_name(id), db.get_product_price(id)/100, db.get_product_price(id) % 100);
+ return true;
+ } else {
+ write_to_log("Kauf fehlgeschlagen!");
+ return false;
+ }
+ }
+}
diff --git a/src/price.vapi b/src/price.vapi
new file mode 100644
index 0000000..b60e2a6
--- /dev/null
+++ b/src/price.vapi
@@ -0,0 +1,9 @@
+[SimpleType]
+[GIR (name = "gint")]
+[CCode (cname = "gint", cheader_filename = "glib.h", type_id = "G_TYPE_INT", marshaller_type_name = "INT", get_value_function = "g_value_get_int", set_value_function = "g_value_set_int", default_value = "0", type_signature = "i")]
+[IntegerType (rank = 6)]
+public struct Price : int {
+ public new string to_string() {
+ return "%d.%02d".printf(this / 100, this % 100);
+ }
+}
diff --git a/src/session.vala b/src/session.vala
new file mode 100644
index 0000000..da1a430
--- /dev/null
+++ b/src/session.vala
@@ -0,0 +1,141 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public errordomain WebSessionError {
+ SESSION_NOT_FOUND,
+ USER_NOT_FOUND
+}
+
+public class WebSession {
+ public int user {
+ get;
+ private set;
+ default = 0;
+ }
+ public string name {
+ get;
+ private set;
+ default = "Guest";
+ }
+ public bool failed {
+ get;
+ private set;
+ default = false;
+ }
+ public bool logged_in {
+ get;
+ private set;
+ default = false;
+ }
+ public bool superuser {
+ get;
+ private set;
+ default = false;
+ }
+ public bool disabled {
+ get;
+ private set;
+ default = false;
+ }
+
+ private string generate_session_id(int user) {
+ const string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
+ string result = "";
+
+ Random.set_seed((uint32) time_t() + (uint32) Posix.getpid() + (uint32) user);
+
+ for(int i=0; i<19; i++) {
+ int character_position = Random.int_range(0,charset.length);
+ string character = charset[character_position].to_string();
+ result += character;
+ }
+
+ /* TODO: make sure, that session id is unique */
+
+ return result;
+ }
+
+ private void setup_auth(int user) {
+ var auth = db.get_user_auth(user);
+ this.disabled = auth.disabled;
+ this.superuser = auth.superuser;
+ this.logged_in = true;
+ }
+
+ public void logout() {
+ if(logged_in) {
+ db.set_sessionid(user, "");
+ logged_in = false;
+ }
+ }
+
+ public WebSession(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client) {
+ var cookies = Soup.cookies_from_request(msg);
+
+ /* Check for existing session */
+ foreach(var cookie in cookies) {
+ if(cookie.name == "session") {
+ var sessionid = cookie.value;
+
+ try {
+ user = db.get_user_by_sessionid(sessionid);
+ name = db.get_username(user);
+ setup_auth(user);
+ return;
+ } catch(WebSessionError e) {
+ /* invalid session, ignore */
+ }
+ }
+ }
+
+ /* check for login query */
+ if(query == null || !query.contains("user") || !query.contains("password"))
+ return;
+
+ /* get credentials */
+ var userid = int.parse(query["user"]);
+ var password = query["password"];
+
+ /* check credentials */
+ if(db.check_user_password(userid, password)) {
+ /* generate session */
+ var sessionid = generate_session_id(userid);
+
+ /* set session in database */
+ db.set_sessionid(userid, sessionid);
+
+ /* set session in reply cookie */
+ cookies = new SList<Soup.Cookie>();
+ var sessioncookie = new Soup.Cookie("session", sessionid, "", "/", -1);
+ sessioncookie.domain = null;
+ cookies.append(sessioncookie);
+ Soup.cookies_to_response(cookies, msg);
+
+ /* login successful */
+ user = userid;
+ try {
+ name = db.get_username(user);
+ } catch(WebSessionError e) {
+ name = "Unknown User";
+ }
+
+ setup_auth(user);
+ } else {
+ /* login failed */
+ failed=true;
+ }
+ }
+}
+
diff --git a/src/template.vala b/src/template.vala
new file mode 100644
index 0000000..ef7e43d
--- /dev/null
+++ b/src/template.vala
@@ -0,0 +1,103 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public errordomain TemplateError {
+ NOT_FOUND,
+ NOT_LOADABLE,
+ NOT_ALLOWED,
+}
+
+public class WebTemplate {
+ private string template;
+ public uint8[] data { get { return template.data; } }
+
+ public WebTemplate(string file, WebSession login) throws TemplateError {
+ var b = File.new_for_path("templates/base.html");
+ var m = File.new_for_path("templates/menu.html");
+ var f = File.new_for_path("templates/"+file);
+ File fauth;
+
+ if(login.logged_in)
+ fauth = File.new_for_path("templates/menu_logout.html");
+ else
+ fauth = File.new_for_path("templates/menu_login.html");
+
+ uint8[] basis, menu, template, auth;
+
+ if(!b.query_exists())
+ throw new TemplateError.NOT_FOUND("templates/base.html not found!");
+
+ if(!m.query_exists())
+ throw new TemplateError.NOT_FOUND("templates/menu.html not found!");
+
+ if(!fauth.query_exists())
+ throw new TemplateError.NOT_FOUND(fauth.get_path()+" not found!");
+
+ if(!f.query_exists())
+ throw new TemplateError.NOT_FOUND("templates/"+file+" not found!");
+
+ try {
+ if(!b.load_contents(null, out basis, null))
+ throw new TemplateError.NOT_LOADABLE("templates/base.html could not be loaded!");
+ if(!m.load_contents(null, out menu, null))
+ throw new TemplateError.NOT_LOADABLE("templates/menu.html could not be loaded!");
+ if(!fauth.load_contents(null, out auth, null))
+ throw new TemplateError.NOT_LOADABLE(fauth.get_path()+" could not be loaded!");
+ if(!f.load_contents(null, out template, null))
+ throw new TemplateError.NOT_LOADABLE("templates/"+file+" could not be loaded!");
+ } catch(Error e) {
+ throw new TemplateError.NOT_LOADABLE("could not load templates!");
+ }
+
+ this.template = ((string) basis).replace("{{{NAVBAR}}}", ((string) menu));
+ this.template = this.template.replace("{{{AUTH}}}", ((string) auth));
+ this.template = this.template.replace("{{{CONTENT}}}", ((string) template));
+ this.template = this.template.replace("{{{USERNAME}}}", login.name);
+ this.template = this.template.replace("{{{USERID}}}", "%d".printf(login.user));
+ }
+
+ public WebTemplate.DATA(string file) throws TemplateError {
+ var f = File.new_for_path("templates/"+file);
+ uint8[] template;
+
+ if(!f.query_exists())
+ throw new TemplateError.NOT_FOUND("templates/"+file+" not found!");
+
+ try {
+ if(!f.load_contents(null, out template, null))
+ throw new TemplateError.NOT_LOADABLE("templates/"+file+" could not be loaded!");
+ } catch(Error e) {
+ throw new TemplateError.NOT_LOADABLE("could not load templates!");
+ }
+
+ this.template = (string) template;
+ }
+
+ public void replace(string key, string value) {
+ template = template.replace("{{{"+key+"}}}", value);
+ }
+
+ public void menu_set_active(string key) {
+ try {
+ var regex_active = new Regex("{{{MENU\\."+key+"}}}");
+ var regex_other = new Regex("{{{MENU\\..*}}}");
+
+ template = regex_active.replace(template, -1, 0, "active");
+ template = regex_other.replace(template, -1, 0, "");
+ } catch(RegexError e) {
+ warning ("%s", e.message);
+ }
+ }
+}
diff --git a/src/web.vala b/src/web.vala
new file mode 100644
index 0000000..562a1ba
--- /dev/null
+++ b/src/web.vala
@@ -0,0 +1,592 @@
+/* Copyright 2012, Sebastian Reichel <sre@ring0.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+public class WebServer {
+ private Soup.Server srv;
+
+ void handler_default(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("index.html", l);
+ t.replace("TITLE", "KtT Shop System");
+ t.menu_set_active("home");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_logout(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ l.logout();
+ var t = new WebTemplate("logout.html", l);
+ t.replace("TITLE", "KtT Shop System");
+ t.menu_set_active("home");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_users(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ string[] pathparts = path.split("/");
+
+ if(pathparts.length <= 2) {
+ handler_todo(server, msg, path, query, client);
+ } else {
+ int id = int.parse(pathparts[2]);
+
+ if(pathparts.length <= 3) {
+ handler_user_entry(server, msg, path, query, client, id);
+ } else {
+ switch(pathparts[3]) {
+ case "invoice":
+ uint16 selectedyear = (pathparts.length >= 5 && pathparts[4] != "") ? (uint16) int.parse(pathparts[4]) : (uint16) (new DateTime.now_local()).get_year();
+ uint8 selectedmonth = (pathparts.length >= 6 && pathparts[5] != "") ? (uint8) int.parse(pathparts[5]) : (uint8) (new DateTime.now_local()).get_month();
+ uint8 selectedday = (pathparts.length >= 7 && pathparts[6] != "") ? (uint8) int.parse(pathparts[6]) : (uint8) (new DateTime.now_local()).get_day_of_month();
+ handler_user_invoice(server, msg, path, query, client, id, selectedyear, selectedmonth, selectedday);
+ break;
+ case "stats":
+ handler_todo(server, msg, path, query, client);
+ break;
+ default:
+ handler_404(server, msg, path, query, client);
+ break;
+ }
+ }
+ }
+ }
+
+ void handler_user_entry(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, int id) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ if(id != l.user && !l.superuser) {
+ handler_403(server, msg, path, query, client);
+ return;
+ }
+ var t = new WebTemplate("users/entry.html", l);
+ t.replace("TITLE", "KtT Shop System: User Info %llu".printf(id));
+ t.menu_set_active("users");
+
+ var userinfo = db.get_user_info(id);
+
+ t.replace("UID", "%d".printf(userinfo.id));
+ t.replace("FIRSTNAME", userinfo.firstname);
+ t.replace("LASTNAME", userinfo.lastname);
+ t.replace("EMAIL", userinfo.email);
+ t.replace("GENDER", userinfo.gender);
+ t.replace("STREET", userinfo.street);
+ t.replace("POSTALCODE", "%d".printf(userinfo.plz));
+ t.replace("CITY", userinfo.city);
+
+ var userauth = db.get_user_auth(id);
+ t.replace("DISABLED", userauth.disabled ? "true" : "false");
+ t.replace("SUPERUSER", userauth.superuser ? "true" : "false");
+
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_user_invoice(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, int id, uint16 selectedyear, uint8 selectedmonth, uint8 selectedday) {
+ DateTime start, stop;
+
+ DateYear y = (DateYear) selectedyear;
+ if(!y.valid() || y > 8000) {
+ selectedyear = (uint16) new DateTime.now_local().get_year();
+ y = (DateYear) selectedyear;
+ }
+
+ DateMonth m = (DateMonth) selectedmonth;
+ if(selectedmonth != 0 && !m.valid()) {
+ selectedmonth = (uint8) new DateTime.now_local().get_month();
+ m = (DateMonth) selectedmonth;
+ }
+
+ DateDay d = (DateDay) selectedday;
+ if(selectedday != 0 && !d.valid()) {
+ selectedday = (uint8) new DateTime.now_local().get_day_of_month();
+ d = (DateDay) selectedday;
+ }
+
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ if(id != l.user && !l.superuser) {
+ handler_403(server, msg, path, query, client);
+ return;
+ }
+ var t = new WebTemplate("users/invoice.html", l);
+ t.replace("TITLE", "KtT Shop System: User Invoice %llu".printf(id));
+ t.menu_set_active("users");
+
+ /* years, in which something has been purchased by the user */
+ var first = db.get_first_purchase(id);
+ var last = db.get_last_purchase(id);
+ string years = "";
+ for(int i=first.get_year(); i <= last.get_year(); i++) {
+ years += @"<li><a href=\"/users/$id/invoice/$i/$selectedmonth/$selectedday\">$i</a></li>";
+ }
+ t.replace("YEARS", years);
+ t.replace("SELECTEDYEAR", @"$selectedyear");
+
+ /* months, in which something has been purchased by the user */
+ string[] monthnames = { "All Months", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
+ string months = @"<li><a href=\"/users/$id/invoice/$selectedyear/0/0\">All Months</a></li>";
+ for(int i=1; i<monthnames.length; i++) {
+ if(first.get_year() == selectedyear && i > 0 && i < first.get_month())
+ months += @"<li><a href=\"/users/$id/invoice/$selectedyear/$i/$selectedday\" class=\"disabled\"\">$(monthnames[i])</a></li>";
+ else if(selectedyear < first.get_year())
+ months += @"<li><a href=\"/users/$id/invoice/$selectedyear/$i/$selectedday\" class=\"disabled\"\">$(monthnames[i])</a></li>";
+ else if(last.get_year() == selectedyear && i > last.get_month())
+ months += @"<li><a href=\"/users/$id/invoice/$selectedyear/$i/$selectedday\" class=\"disabled\"\">$(monthnames[i])</a></li>";
+ else if(selectedyear > last.get_year())
+ months += @"<li><a href=\"/users/$id/invoice/$selectedyear/$i/$selectedday\" class=\"disabled\"\">$(monthnames[i])</a></li>";
+ else
+ months += @"<li><a href=\"/users/$id/invoice/$selectedyear/$i/$selectedday\">$(monthnames[i])</a></li>";
+ }
+ t.replace("MONTHS", months);
+ t.replace("SELECTEDMONTH", @"$(monthnames[selectedmonth])");
+
+ int dim = m.valid() ? Date.get_days_in_month(m, y) : 0;
+ string days = @"<li><a href=\"/users/$id/invoice/$selectedyear/$selectedmonth/0\">All Days</a></li>";
+ for(int i=1; i<=dim; i++) {
+ if(first.get_year() == selectedyear && first.get_month() == selectedmonth && i < first.get_day_of_month())
+ days += @"<li><a href=\"/users/$id/invoice/$selectedyear/$selectedmonth/$i\" class=\"disabled\">$i</a></li>";
+ else if(selectedyear < first.get_year() || (selectedyear == first.get_year() && selectedmonth < first.get_month()))
+ days += @"<li><a href=\"/users/$id/invoice/$selectedyear/$selectedmonth/$i\" class=\"disabled\">$i</a></li>";
+ else if(last.get_year() == selectedyear && last.get_month() == selectedmonth && i > last.get_day_of_month())
+ days += @"<li><a href=\"/users/$id/invoice/$selectedyear/$selectedmonth/$i\" class=\"disabled\">$i</a></li>";
+ else if(selectedyear > last.get_year() || (selectedyear == last.get_year() && selectedmonth > last.get_month()))
+ days += @"<li><a href=\"/users/$id/invoice/$selectedyear/$selectedmonth/$i\" class=\"disabled\">$i</a></li>";
+ else
+ days += @"<li><a href=\"/users/$id/invoice/$selectedyear/$selectedmonth/$i\">$i</a></li>";
+ }
+ t.replace("DAYS", days);
+ if(selectedday > 0)
+ t.replace("SELECTEDDAY", @"$selectedday");
+ else
+ t.replace("SELECTEDDAY", "All Days");
+
+ if(selectedday != 0) {
+ start = new DateTime.local(selectedyear, selectedmonth, selectedday, 8, 0, 0);
+ stop = start.add_days(1);
+ } else if(selectedmonth != 0) {
+ start = new DateTime.local(selectedyear, selectedmonth, 16, 8, 0, 0);
+ stop = start.add_months(1);
+ } else {
+ start = new DateTime.local(selectedyear, 1, 16, 8, 0, 0);
+ stop = start.add_years(1);
+ }
+
+ string table = "";
+ Price sum = 0;
+ foreach(var e in db.get_invoice(id, start.to_unix(), stop.to_unix())) {
+ var timestamp = new DateTime.from_unix_utc(e.timestamp);
+ var date = timestamp.format("%d.%m.%Y");
+ var time = timestamp.format("%H:%M:%S");
+ var product = e.product.name;
+ var price = e.price;
+ table += @"<tr><td>$date</td><td>$time</td><td>$product</td><td>$price€</td></tr>";
+ sum += e.price;
+ }
+
+ t.replace("DATA", table);
+ t.replace("SUM", @"$sum");
+
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_products(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ string[] pathparts = path.split("/");
+
+ if(pathparts.length <= 2 || pathparts[2] == "") {
+ handler_product_list(server, msg, path, query, client);
+ } else {
+ uint64 id = uint64.parse(pathparts[2]);
+
+ if(pathparts.length <= 3) {
+ handler_product_entry(server, msg, path, query, client, id);
+ } else {
+ switch(pathparts[3]) {
+ case "restock":
+ handler_product_restock(server, msg, path, query, client, id);
+ break;
+ default:
+ handler_product_entry(server, msg, path, query, client, id);
+ break;
+ }
+ }
+ }
+ }
+
+ void handler_product_list(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("products/index.html", l);
+ t.replace("TITLE", "KtT Shop System: Product List");
+ t.menu_set_active("products");
+
+ string table = "";
+ foreach(var e in db.get_stock()) {
+ table += "<tr><td><a href=\"/products/%s\">%s</a></td><td><a href=\"/products/%s\">%s</a></td><td>%d</td><td>%s€</td><td>%s€</td></tr>".printf(
+ e.id, e.id, e.id, e.name, e.amount, e.memberprice, e.guestprice
+ );
+ }
+
+ t.replace("DATA", table);
+
+ if(l.superuser)
+ t.replace("NEWPRODUCT", "block");
+ else
+ t.replace("NEWPRODUCT", "none");
+
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_product_entry(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, uint64 id) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("products/entry.html", l);
+ t.replace("TITLE", "KtT Shop System: Product %llu".printf(id));
+ t.menu_set_active("products");
+
+ /* ean */
+ t.replace("EAN", "%llu".printf(id));
+
+ /* name */
+ string name = db.get_product_name(id);
+ t.replace("NAME", name);
+
+ /* amount */
+ t.replace("AMOUNT", "%d".printf(db.get_product_amount(id)));
+
+ if(l.superuser)
+ t.replace("RESTOCK", "block");
+ else
+ t.replace("RESTOCK", "none");
+
+ /* prices */
+ string prices = "";
+ foreach(var e in db.get_prices(id)) {
+ var time = new DateTime.from_unix_local(e.valid_from);
+ prices += @"<tr><td>%s</td><td>$(e.memberprice)€</td><td>$(e.guestprice)€</td></tr>".printf(
+ time.format("%Y-%m-%d %H:%M")
+ );
+ }
+ t.replace("PRICES", prices);
+
+ /* restocks */
+ string restocks = "";
+ foreach(var e in db.get_restocks(id)) {
+ var time = new DateTime.from_unix_local(e.timestamp);
+ restocks += "<tr><td>%s</td><td>%d</td><td>%s€</td></tr>".printf(
+ time.format("%Y-%m-%d %H:%M"), e.amount, e.price
+ );
+ }
+ t.replace("RESTOCKS", restocks);
+
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_products_new(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client) {
+ try {
+ var session = new WebSession(server, msg, path, query, client);
+ var template = new WebTemplate("products/new.html", session);
+ template.replace("TITLE", "KtT Shop System: New Product");
+ template.menu_set_active("products");
+
+ if(!session.superuser) {
+ handler_403(server, msg, path, query, client);
+ return;
+ }
+
+ if(query != null && query.contains("name") && query.contains("id") && query.contains("memberprice") && query.contains("guestprice")) {
+ var name = query["name"];
+ var ean = uint64.parse(query["id"]);
+ Price memberprice = int.parse(query["memberprice"]);
+ Price guestprice = int.parse(query["guestprice"]);
+
+ if(ean > 0 && memberprice > 0 && guestprice > 0 && db.new_product(ean, name, memberprice, guestprice)) {
+ template.replace("NAME", name);
+ template.replace("EAN", @"$ean");
+ template.replace("MEMBERPRICE", @"$memberprice€");
+ template.replace("GUESTPRICE", @"$guestprice€");
+ template.replace("NEW.OK", "block");
+ template.replace("NEW.FAIL", "none");
+ } else {
+ template.replace("NAME", "...");
+ template.replace("NEW.OK", "none");
+ template.replace("NEW.FAIL", "block");
+ }
+ } else {
+ template.replace("NAME", "...");
+ template.replace("NEW.OK", "none");
+ template.replace("NEW.FAIL", "block");
+ }
+
+ msg.set_response("text/html", Soup.MemoryUse.COPY, template.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_product_restock(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client, uint64 id) {
+ try {
+ var session = new WebSession(server, msg, path, query, client);
+
+ if(!session.superuser) {
+ handler_403(server, msg, path, query, client);
+ return;
+ }
+
+ var template = new WebTemplate("products/restock.html", session);
+ template.replace("TITLE", "KtT Shop System: Restock Product %llu".printf(id));
+ template.replace("NAME", db.get_product_name(id));
+ template.menu_set_active("products");
+
+ if(query != null && query.contains("amount") && query.contains("price")) {
+ int amount = int.parse(query["amount"]);
+ Price price = int.parse(query["price"]);
+
+ if(amount >= 1 && price >= 1) {
+ if(db.restock(session.user, id, amount, price)) {
+ template.replace("AMOUNT", @"$amount");
+ template.replace("PRICE", @"$price");
+ template.replace("RESTOCK.OK", "block");
+ template.replace("RESTOCK.FAIL", "none");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, template.data);
+ return;
+ }
+ }
+ }
+
+ template.replace("RESTOCK.OK", "none");
+ template.replace("RESTOCK.FAIL", "block");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, template.data);
+ return;
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_stats(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("stats/index.html", l);
+ t.replace("TITLE", "KtT Shop System: Statistics");
+ t.menu_set_active("stats");
+
+ var stats = db.get_stats_info();
+
+ t.replace("NUMBER_OF_ARTICLES", @"$(stats.count_articles)");
+ t.replace("NUMBER_OF_USERS", @"$(stats.count_users)");
+ t.replace("STOCK_VALUE", @"$(stats.stock_value)€");
+ t.replace("TOTAL_SALES", @"$(stats.sales_total)€");
+ t.replace("TOTAL_PROFIT", @"$(stats.profit_total)€");
+ t.replace("SALES_TODAY", @"$(stats.sales_today)€");
+ t.replace("PROFIT_TODAY", @"$(stats.profit_today)€");
+ t.replace("SALES_THIS_MONTH", @"$(stats.sales_this_month)€");
+ t.replace("PROFIT_THIS_MONTH", @"$(stats.profit_this_month)€");
+ t.replace("SALES_PER_DAY", @"$(stats.sales_per_day)€");
+ t.replace("PROFIT_PER_DAY", @"$(stats.profit_per_day)€");
+ t.replace("SALES_PER_MONTH", @"$(stats.sales_per_month)€");
+ t.replace("PROFIT_PER_MONTH", @"$(stats.profit_per_month)€");
+
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_stats_stock(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("stats/stock.html", l);
+ string data = db.get_stats_stock().json;
+ t.replace("DATA", data);
+ t.replace("TITLE", "KtT Shop System: Statistics: Stock");
+ t.menu_set_active("stats");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_stats_profit_per_day(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("stats/profit_per_day.html", l);
+ string data = db.get_stats_profit_per_day().json;
+ t.replace("DATA", data);
+ t.replace("TITLE", "KtT Shop System: Statistics: Profit");
+ t.menu_set_active("stats");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_stats_profit_per_weekday(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("stats/profit_per_weekday.html", l);
+ string data = db.get_stats_profit_per_weekday().json;
+ t.replace("DATA", data);
+ t.replace("TITLE", "KtT Shop System: Statistics: Profit/Weekday");
+ t.menu_set_active("stats");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_stats_profit_per_product(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var l = new WebSession(server, msg, path, query, client);
+ var t = new WebTemplate("stats/profit_per_product.html", l);
+ string data = db.get_stats_profit_per_products().json;
+ t.replace("DATA", data);
+ t.replace("TITLE", "KtT Shop System: Statistics: Profit/Product");
+ t.menu_set_active("stats");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_js(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var t = new WebTemplate.DATA(path);
+ msg.set_response("text/javascript", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_css(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var t = new WebTemplate.DATA(path);
+ msg.set_response("text/css", Soup.MemoryUse.COPY, t.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ void handler_img(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var f = File.new_for_path("templates/"+path);
+ uint8[] data = null;
+
+ if(f.query_exists() && f.load_contents(null, out data, null)) {
+ msg.set_response("image/png", Soup.MemoryUse.COPY, data);
+ return;
+ }
+ } catch(Error e) {
+ error("there has been some error: %s!\n", e.message);
+ }
+
+ handler_404(server, msg, path, query, client);
+ return;
+ }
+
+ void handler_404(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ string result = "Page not Found\n";
+ msg.set_status(404);
+ msg.set_response("text/plain", Soup.MemoryUse.COPY, result.data);
+ }
+
+ void handler_403(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var session = new WebSession(server, msg, path, query, client);
+ var template = new WebTemplate("errors/403.html", session);
+ template.replace("TITLE", "Access Denied");
+ template.menu_set_active("");
+ msg.set_status(403);
+ msg.set_response("text/html", Soup.MemoryUse.COPY, template.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ } }
+
+ void handler_todo(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) {
+ try {
+ var session = new WebSession(server, msg, path, query, client);
+ var template = new WebTemplate("errors/todo.html", session);
+ template.replace("TITLE", "KtT Shop System: ToDo");
+ template.menu_set_active("");
+ msg.set_response("text/html", Soup.MemoryUse.COPY, template.data);
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ }
+ }
+
+ public WebServer(int port = 8080) {
+ srv = new Soup.Server(Soup.SERVER_PORT, port);
+
+ /* index */
+ srv.add_handler("/", handler_default);
+
+ /* logout */
+ srv.add_handler("/logout", handler_logout);
+
+ /* data (js, css, img) */
+ srv.add_handler("/js", handler_js);
+ srv.add_handler("/css", handler_css);
+ srv.add_handler("/img", handler_img);
+
+ /* products */
+ srv.add_handler("/products", handler_products);
+ srv.add_handler("/products/new", handler_products_new);
+
+ /* stats */
+ srv.add_handler("/stats", handler_stats);
+ srv.add_handler("/stats/stock", handler_stats_stock);
+ srv.add_handler("/stats/profit_per_day", handler_stats_profit_per_day);
+ srv.add_handler("/stats/profit_per_weekday", handler_stats_profit_per_weekday);
+ srv.add_handler("/stats/profit_per_product", handler_stats_profit_per_product);
+
+ /* users */
+ srv.add_handler("/users", handler_users);
+
+ srv.run();
+ }
+}