Typica is a free program for professional coffee roasters. https://typica.us
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

navigation.xml 51KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. <window id="navwindow">
  2. <layout type="horizontal">
  3. <layout type="vertical">
  4. <layout type="grid">
  5. <row>
  6. <column>
  7. <button name="Roasting Schedule" id="schedule" type="push" />
  8. </column>
  9. </row>
  10. <row>
  11. <column>
  12. <sqldrop id="machineselector" />
  13. </column>
  14. <column>
  15. <button name="Roast Coffee" id="roast" type="push" />
  16. </column>
  17. </row>
  18. <row>
  19. <column>
  20. <button name="Manual Roasting Log Entry" id="manual" type="push" />
  21. </column>
  22. </row>
  23. <row>
  24. <column>
  25. <button name="Purchase Green Coffee" id="green" type="push" />
  26. </column>
  27. </row>
  28. <row>
  29. <column>
  30. <button name="Manage Roasted Coffee Items" id="newroasted" type="push" />
  31. </column>
  32. </row>
  33. <row>
  34. <column>
  35. <button name="Edit Roasting Specification" id="roastspec" type="push" />
  36. </column>
  37. </row>
  38. <row>
  39. <column>
  40. <button name="Update Inventory" id="inventory" type="push" />
  41. </column>
  42. </row>
  43. <row>
  44. <column>
  45. <button name="New Cupping Session" id="createcupping" type="push" />
  46. </column>
  47. </row>
  48. <row>
  49. <column>
  50. <button name="Join Cupping Session" id="joincupping" type="push" />
  51. </column>
  52. </row>
  53. <row>
  54. <column>
  55. <button name="Summarize Cupping Session" id="sumcupping" type="push" />
  56. </column>
  57. </row>
  58. <row>
  59. <column>
  60. <button name="View Target Roast Profiles" id="profilehistory" type="push" />
  61. </column>
  62. </row>
  63. <row>
  64. <column>
  65. <button name="Import Target Roast Profiles" id="target" type="push" />
  66. </column>
  67. </row>
  68. <row>
  69. <column>
  70. <button name="Enter Green Coffee Sales" id="greensales" type="push" />
  71. </column>
  72. </row>
  73. </layout>
  74. <stretch />
  75. </layout>
  76. <webview id="dashboard" stretch="1" />
  77. </layout>
  78. <menu name="Reports" type="reports" src="Reports" />
  79. <menu name="Users">
  80. <item id="switchuser">Switch User</item>
  81. <item id="createuser">Create New Users</item>
  82. </menu>
  83. <menu name="Settings">
  84. <item id="resetconnection">Forget Database Connection</item>
  85. <item id="configure">Preferences</item>
  86. </menu>
  87. <menu name="Lists">
  88. <item id="departmentlist">Department List</item>
  89. </menu>
  90. <program>
  91. var window = this;
  92. var navigationwindow = window;
  93. window.loggingWindow = undefined;
  94. var roasterlist = findChildObject(this, 'machineselector');
  95. var model = new DeviceTreeModel;
  96. roasterlist.setModel(model);
  97. roasterlist.currentIndex = QSettings.value("machineSelection", 0);
  98. roasterlist['currentIndexChanged(int)'].connect(function() {
  99. QSettings.setValue("machineSelection", roasterlist.currentIndex);
  100. });
  101. var resetdbconnection = findChildObject(this, 'resetconnection');
  102. resetdbconnection.triggered.connect(function() {
  103. QSettings.setValue("database/exists", false);
  104. QSettings.setValue("database/hostname", "");
  105. QSettings.setValue("database/dbname", "");
  106. QSettings.setValue("database/user", "");
  107. QSettings.setValue("database/password", "");
  108. });
  109. var schedule = findChildObject(this, 'schedule');
  110. schedule.clicked.connect(function() {
  111. createWindow("schedule");
  112. });
  113. var manual = findChildObject(this, 'manual');
  114. manual.clicked.connect(function() {
  115. createWindow("manualLogEntry");
  116. });
  117. var profilehistory = findChildObject(this, 'profilehistory');
  118. profilehistory.clicked.connect(function() {
  119. createWindow("profilehistory");
  120. });
  121. var greensalesbutton = findChildObject(this, 'greensales');
  122. greensalesbutton.clicked.connect(function() {
  123. createWindow("greensales");
  124. });
  125. var sumcup = findChildObject(this, 'sumcupping');
  126. sumcup.clicked.connect(function() {
  127. var sessionlist = createWindow("finsessionlist");
  128. sessionlist.windowTitle = TTR("navwindow", "Typica - Summarize Cupping Session");
  129. });
  130. var ncsbutton = findChildObject(this, 'createcupping')
  131. ncsbutton.clicked.connect(function() {
  132. var ncswindow = createWindow("session");
  133. ncswindow.windowTitle = TTR("navwindow", "Typica - New Cupping Session");
  134. });
  135. var jcsbutton = findChildObject(this, 'joincupping')
  136. jcsbutton.clicked.connect(function() {
  137. var jcswindow = createWindow("sessionlist");
  138. jcswindow.windowTitle = TTR("navwindow", "Typica - Join Cupping Session");
  139. });
  140. /*
  141. var nrbutton = findChildObject(this, 'newroaster');
  142. nrbutton.clicked.connect(function() {
  143. var nrwindow = createWindow("newroaster");
  144. nrwindow.windowTitle = TTR("navwindow", "Typica - New Roaster");
  145. });
  146. */
  147. var inventory = findChildObject(this, 'inventory');
  148. inventory.clicked.connect(function() {
  149. var invwin = createWindow("inventory");
  150. invwin.windowTitle = TTR("navwindow", "Typica - Inventory");
  151. });
  152. var roastspecbutton = findChildObject(this, 'roastspec');
  153. roastspecbutton.clicked.connect(function() {
  154. var specwindow = createWindow("roastspec");
  155. });
  156. var gbutton = findChildObject(this, 'green');
  157. gbutton.clicked.connect(function() {
  158. var purchasewindow = createWindow("purchase");
  159. });
  160. var nrbutton = findChildObject(this, 'newroasted');
  161. nrbutton.clicked.connect(function() {
  162. var nrwindow = createWindow("newroasted");
  163. nrwindow.windowTitle = TTR("navwindow", "Manage Roasted Coffee Items");
  164. });
  165. var importb = findChildObject(this, 'target');
  166. importb.clicked.connect(function() {
  167. var importWindow = createWindow("importTargets");
  168. importWindow.windowTitle = TTR("navwindow", "Typica - Import Target Roast Profiles");
  169. });
  170. var roastbutton = findChildObject(this, 'roast');
  171. roastbutton.clicked.connect(function() {
  172. if(typeof(window.loggingWindow) == "undefined")
  173. {
  174. window.loggingWindow = createWindow("basicWindow");
  175. window.loggingWindow.windowTitle = "Typica [*]";
  176. window.loggingWindow.navigationWindow = window;
  177. }
  178. else
  179. {
  180. window.loggingWindow.raise();
  181. window.loggingWindow.activateWindow();
  182. }
  183. });
  184. var configure = findChildObject(this, 'configure');
  185. configure.triggered.connect(function() {
  186. var confwindow = new SettingsWindow;
  187. confwindow.show();
  188. });
  189. var departmentlist = findChildObject(this, 'departmentlist');
  190. departmentlist.triggered.connect(function() {
  191. createWindow("departmentlist");
  192. });
  193. <![CDATA[
  194. var DBCreateBase = function() {
  195. var query = new QSqlQuery();
  196. query.exec("CREATE TABLE IF NOT EXISTS certifications (item bigint NOT NULL, certification text NOT NULL)");
  197. query.exec("CREATE TABLE IF NOT EXISTS cupping_samples (session bigint NOT NULL, sample text NOT NULL, position bigint NOT NULL, type text NOT NULL, \"time\" timestamp without time zone, machine bigint, point text, item bigint)");
  198. query.exec("CREATE TABLE cupping_sessions (id bigserial NOT NULL, event text, name text NOT NULL, \"time\" timestamp without time zone NOT NULL, blind boolean NOT NULL, open boolean NOT NULL, note text)");
  199. query.exec("CREATE TABLE IF NOT EXISTS cuppingforms (session bigint NOT NULL, sample text NOT NULL, position bigint NOT NULL, grader text, finalscore numeric, notes text, serialization text)");
  200. query.exec("CREATE TABLE IF NOT EXISTS cuppingform_t1 (aroma numeric, flavor numeric, aftertaste numeric, acidity numeric, body numeric, uniformity numeric, balance numeric, cleancup numeric, sweetness numeric, overall numeric, total numeric) INHERITS (cuppingforms)");
  201. query.exec("CREATE TABLE IF NOT EXISTS invoices (id bigserial PRIMARY KEY NOT NULL, invoice text, vendor text NOT NULL, \"time\" timestamp without time zone NOT NULL)");
  202. query.exec("CREATE TABLE IF NOT EXISTS invoice_items (invoice_id bigint NOT NULL, record_type text NOT NULL, item_id bigint, description text NOT NULL, cost numeric NOT NULL)");
  203. query.exec("CREATE TABLE IF NOT EXISTS transactions (\"time\" timestamp without time zone NOT NULL, item bigint NOT NULL)");
  204. query.exec("CREATE TABLE IF NOT EXISTS inventory (quantity numeric NOT NULL) INHERITS (transactions)");
  205. query.exec("CREATE TABLE IF NOT EXISTS loss (quantity numeric NOT NULL, reason text) INHERITS (transactions)");
  206. query.exec("CREATE TABLE IF NOT EXISTS make (quantity numeric NOT NULL) INHERITS (transactions)");
  207. query.exec("CREATE TABLE IF NOT EXISTS purchase (quantity numeric NOT NULL, cost numeric NOT NULL, vendor text NOT NULL) INHERITS (transactions)");
  208. query.exec("CREATE TABLE IF NOT EXISTS sale (quantity numeric NOT NULL, customer text) INHERITS (transactions)");
  209. query.exec("CREATE TABLE IF NOT EXISTS use (quantity numeric NOT NULL) INHERITS (transactions)");
  210. query.exec("CREATE VIEW all_transactions AS ((((SELECT purchase.\"time\", purchase.item, purchase.quantity, purchase.cost, purchase.vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'PURCHASE' AS type FROM purchase UNION SELECT use.\"time\", use.item, use.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'USE' AS type FROM use) UNION SELECT inventory.\"time\", inventory.item, inventory.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'INVENTORY' AS type FROM inventory) UNION SELECT loss.\"time\", loss.item, loss.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, loss.reason, NULL::unknown AS customer, 'LOSS' AS type FROM loss) UNION SELECT make.\"time\", make.item, make.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'MAKE' AS type FROM make) UNION SELECT sale.\"time\", sale.item, sale.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, sale.customer, 'SALE' AS type FROM sale");
  211. query.exec("CREATE FUNCTION time_range(bigint) RETURNS integer AS $$ BEGIN IF (SELECT quantity FROM items WHERE id = $1) > 0 THEN RETURN (SELECT current_date - min(time)::date + 1 FROM use WHERE item = $1); ELSE RETURN (SELECT max(time)::date - min(time)::date + 1 FROM use WHERE item = $1); END IF; END; $$ LANGUAGE plpgsql STRICT");
  212. query.exec("CREATE TABLE IF NOT EXISTS items(id bigint NOT NULL, name text NOT NULL, reference text, unit text NOT NULL, quantity numeric DEFAULT 0, category text)");
  213. query.exec("CREATE SEQUENCE items_id_seq INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1");
  214. query.exec("CREATE TABLE IF NOT EXISTS coffees(origin text NOT NULL, region text, producer text, grade text, milling text, drying text) INHERITS (items)");
  215. query.exec("CREATE VIEW coffee_history AS SELECT coffees.id, coffees.name, coffees.origin, coffees.quantity AS stock, (SELECT sum(use.quantity) AS sum FROM use WHERE (use.item = coffees.id)) AS used, time_range(coffees.id) AS \"interval\", ((SELECT (sum(use.quantity) / (time_range(use.item))::numeric) FROM use WHERE (use.item = coffees.id) GROUP BY use.item))::numeric(10,2) AS rate, (SELECT (('now'::text)::date + ((coffees.quantity / (SELECT (sum(use.quantity) / (time_range(use.item))::numeric) FROM use WHERE (use.item = coffees.id) GROUP BY use.item)))::integer)) AS \"out\" FROM coffees WHERE (coffees.id IN (SELECT use.item FROM use)) ORDER BY coffees.origin");
  216. query.exec("CREATE TABLE IF NOT EXISTS current_items (item bigint NOT NULL)");
  217. query.exec("CREATE TABLE IF NOT EXISTS decaf_coffees (decaf_method text NOT NULL) INHERITS (coffees)");
  218. query.exec("CREATE TABLE IF NOT EXISTS files (id bigint NOT NULL, name text NOT NULL, type text NOT NULL, note text, file bytea NOT NULL)");
  219. query.exec("CREATE TABLE IF NOT EXISTS item_files(\"time\" timestamp without time zone NOT NULL, item bigint NOT NULL, files bigint[] NOT NULL)");
  220. query.exec("CREATE TYPE item_transaction_with_balance AS (\"time\" timestamp without time zone, item bigint, quantity numeric, cost numeric, vendor text, reason text, customer text, type text, balance numeric)");
  221. query.exec("CREATE TABLE IF NOT EXISTS lb_bag_conversion (item bigint NOT NULL, conversion numeric NOT NULL)");
  222. query.exec("CREATE TABLE IF NOT EXISTS machine (id bigint NOT NULL, name text NOT NULL)");
  223. query.exec("CREATE VIEW regular_coffees AS SELECT coffees.id, coffees.name, coffees.reference, coffees.unit, coffees.quantity, coffees.category, coffees.origin, coffees.region, coffees.producer, coffees.grade, coffees.milling, coffees.drying FROM coffees WHERE (NOT (coffees.id IN (SELECT decaf_coffees.id FROM decaf_coffees)))");
  224. query.exec("CREATE TABLE IF NOT EXISTS roasting_log (\"time\" timestamp without time zone NOT NULL, unroasted_id bigint[], unroasted_quantity numeric[], unroasted_total_quantity numeric, roasted_id bigint, roasted_quantity numeric, transaction_type text NOT NULL, annotation text, machine bigint NOT NULL, duration interval, approval boolean, humidity numeric, barometric numeric, indoor_air numeric, outdoor_air numeric, files bigint[])");
  225. query.exec("CREATE VIEW short_log AS SELECT roasting_log.\"time\", (SELECT items.name FROM items WHERE (items.id = roasting_log.roasted_id)) AS name, roasting_log.unroasted_total_quantity, roasting_log.roasted_quantity, ((((roasting_log.unroasted_total_quantity - roasting_log.roasted_quantity) / roasting_log.unroasted_total_quantity) * (100)::numeric))::numeric(12,2) AS weight_loss, roasting_log.duration FROM roasting_log ORDER BY roasting_log.\"time\"");
  226. query.exec("CREATE FUNCTION add_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = quantity + NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
  227. query.exec("CREATE FUNCTION bags_in_stock(bigint) RETURNS numeric AS $_$SELECT quantity / (SELECT conversion FROM lb_bag_conversion WHERE item = id) FROM items WHERE id = $1;$_$ LANGUAGE sql IMMUTABLE STRICT");
  228. query.exec("CREATE FUNCTION log_use() RETURNS trigger AS $$ DECLARE i integer := array_lower(NEW.unroasted_id, 1); u integer := array_upper(NEW.unroasted_id, 1); BEGIN WHILE i <= u LOOP INSERT INTO use VALUES(NEW.time, NEW.unroasted_id[i], NEW.unroasted_quantity[i]); i := i + 1; END LOOP; RETURN NEW; END; $$ LANGUAGE plpgsql");
  229. query.exec("CREATE FUNCTION replace_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
  230. query.exec("CREATE FUNCTION subtract_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = quantity - NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
  231. query.exec("CREATE SEQUENCE files_id_seq START WITH 1 INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1");
  232. query.exec("ALTER TABLE files ALTER COLUMN id SET DEFAULT nextval('files_id_seq'::regclass)");
  233. query.exec("ALTER TABLE items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass)");
  234. query.exec("ALTER TABLE ONLY files ADD CONSTRAINT file_pkey PRIMARY KEY (id)");
  235. query.exec("ALTER TABLE ONLY items ADD CONSTRAINT items_pkey PRIMARY KEY (id)");
  236. query.exec("ALTER TABLE ONLY lb_bag_conversion ADD CONSTRAINT lb_bag_conversion_item_key UNIQUE (item)");
  237. query.exec("ALTER TABLE ONLY roasting_log ADD CONSTRAINT roasting_log_pkey PRIMARY KEY (\"time\", machine)");
  238. query.exec("CREATE INDEX itemcategories ON items USING btree (category)");
  239. query.exec("CREATE INDEX itemnames ON items USING btree (name)");
  240. query.exec("CREATE INDEX roasting_log_index ON roasting_log USING btree (\"time\")");
  241. query.exec("CREATE INDEX transactionitems ON transactions USING btree (item)");
  242. query.exec("CREATE INDEX transactiontimes ON transactions USING btree (\"time\")");
  243. query.exec("CREATE TRIGGER add_inventory_trigger AFTER INSERT ON purchase FOR EACH ROW EXECUTE PROCEDURE add_inventory()");
  244. query.exec("CREATE TRIGGER add_inventory_trigger AFTER INSERT ON make FOR EACH ROW EXECUTE PROCEDURE add_inventory()");
  245. query.exec("CREATE TRIGGER log_use_trigger AFTER INSERT ON roasting_log FOR EACH ROW EXECUTE PROCEDURE log_use()");
  246. query.exec("CREATE TRIGGER replace_inventory_trigger AFTER INSERT ON inventory FOR EACH ROW EXECUTE PROCEDURE replace_inventory()");
  247. query.exec("CREATE TRIGGER subtract_inventory_trigger AFTER INSERT ON loss FOR EACH ROW EXECUTE PROCEDURE subtract_inventory()");
  248. query.exec("CREATE TRIGGER subtract_inventory_trigger AFTER INSERT ON sale FOR EACH ROW EXECUTE PROCEDURE subtract_inventory()");
  249. query.exec("CREATE TRIGGER subtract_inventory_trigger AFTER INSERT ON use FOR EACH ROW EXECUTE PROCEDURE subtract_inventory()");
  250. query.exec("ALTER TABLE ONLY item_files ADD CONSTRAINT item_files_item_fkey FOREIGN KEY (item) REFERENCES items(id)");
  251. query.exec("ALTER TABLE ONLY transactions ADD CONSTRAINT transactions_item_fkey FOREIGN KEY (item) REFERENCES items(id)");
  252. query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('base-features', TRUE, 1)");
  253. query = query.invalidate();
  254. };
  255. /* Some changes to the database are required for sample roasting features in
  256. Typica 1.6 and later. */
  257. var DBCreateSampleRoasting = function() {
  258. var query = new QSqlQuery;
  259. query.exec("CREATE TABLE IF NOT EXISTS item_attributes (id bigserial PRIMARY KEY NOT NULL, name text NOT NULL)");
  260. query.exec("CREATE TABLE IF NOT EXISTS coffee_sample_items(arrival timestamp without time zone, vendor text, attribute_ids bigint[], attribute_values text[], item_id bigint) INHERITS (items)");
  261. query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('sample-roasting', TRUE, 1)");
  262. query = query.invalidate();
  263. };
  264. /* Some changes to the database are required to log who performed what tasks.
  265. This adds logging for all transaction types and also for the roasting log.*/
  266. var DBUpdateMultiUser = function() {
  267. var query = new QSqlQuery;
  268. query.exec("ALTER TABLE transactions ADD COLUMN person text DEFAULT NULL");
  269. query.exec("ALTER TABLE roasting_log ADD COLUMN person text DEFAULT NULL");
  270. query.exec("CREATE OR REPLACE FUNCTION log_session_user() RETURNS trigger AS $$ BEGIN NEW.person := session_user; RETURN NEW; END; $$ LANGUAGE plpgsql");
  271. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON inventory FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  272. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON loss FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  273. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON make FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  274. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON purchase FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  275. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON sale FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  276. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON use FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  277. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON roasting_log FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  278. query.exec("UPDATE TypicaFeatures SET version = 2 WHERE feature = 'base-features'");
  279. query = query.invalidate();
  280. };
  281. /* Bug fix and optimization for item_history */
  282. var DBUpdateHistory = function() {
  283. var query = new QSqlQuery;
  284. query.exec("CREATE TYPE transaction_type AS (type text, quantity numeric)");
  285. query.exec("CREATE FUNCTION update_balance(numeric, transaction_type) RETURNS numeric AS $$ BEGIN CASE $2.type WHEN 'PURCHASE', 'MAKE' THEN RETURN $1 + $2.quantity; WHEN 'INVENTORY' THEN RETURN $2.quantity; WHEN 'USE', 'SALE', 'LOSS' THEN RETURN $1 - $2.quantity; END CASE; END; $$ LANGUAGE plpgsql STRICT");
  286. query.exec("CREATE AGGREGATE transaction_balance (BASETYPE = transaction_type, SFUNC = update_balance, STYPE = numeric, INITCOND = '0')");
  287. query.exec("CREATE OR REPLACE FUNCTION item_history(bigint) RETURNS SETOF item_transaction_with_balance AS $$ SELECT time, item, quantity, cost, vendor, reason, customer, type, transaction_balance((type, quantity)::transaction_type) OVER (PARTITION BY item ORDER BY time ASC) AS balance FROM all_transactions WHERE item = $1; $$ LANGUAGE SQL");
  288. query.exec("DROP FUNCTION calculate_inventory_balance()");
  289. query = query.invalidate();
  290. };
  291. /* Asynchronous notifications */
  292. var DBUpdateNotifications = function() {
  293. var query = new QSqlQuery;
  294. query.exec("CREATE OR REPLACE FUNCTION notify_roasting_log_change() RETURNS TRIGGER AS $$ BEGIN NOTIFY RoastingLogChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  295. query.exec("CREATE TRIGGER notify_roasting_log_change AFTER INSERT OR UPDATE ON roasting_log FOR EACH STATEMENT EXECUTE PROCEDURE notify_roasting_log_change()");
  296. query = query.invalidate();
  297. };
  298. /* Update trigger functions to make column names explicit */
  299. var DBUpdateTriggers = function() {
  300. var query = new QSqlQuery;
  301. query.exec("CREATE OR REPLACE FUNCTION log_make_update() RETURNS trigger AS $$ BEGIN IF NEW.roasted_quantity <> OLD.roasted_quantity AND NEW.roasted_quantity IS NOT NULL THEN INSERT INTO make (time, item, quantity) VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  302. query.exec("UPDATE TypicaFeatures SET version = 4 WHERE feature = 'base-features'");
  303. };
  304. var DBUpdateReminders = function() {
  305. var query = new QSqlQuery;
  306. query.exec("CREATE TABLE IF NOT EXISTS reminders (id bigserial PRIMARY KEY NOT NULL, reminder text NOT NULL)");
  307. query.exec("UPDATE TypicaFeatures SET version = 5 WHERE feature = 'base-features'");
  308. query = query.invalidate();
  309. };
  310. var DBUpdateSpecification = function() {
  311. var query = new QSqlQuery;
  312. query.exec("CREATE TABLE IF NOT EXISTS roasting_specification (\"time\" timestamp without time zone NOT NULL, item bigint NOT NULL, loss numeric, tolerance numeric, notes text)");
  313. query.exec("UPDATE TypicaFeatures SET version = 6 WHERE feature = 'base-features'");
  314. query = query.invalidate();
  315. };
  316. /* Updates for Typica version 1.8 */
  317. var DBUpdate18 = function() {
  318. var query = new QSqlQuery;
  319. /* Create a table for Typica users login data */
  320. query.exec("CREATE TABLE IF NOT EXISTS typica_users(name TEXT PRIMARY KEY NOT NULL, password TEXT, active boolean NOT NULL, auto_login boolean NOT NULL)");
  321. /* Update session user logging to only use the database user when a Typica user is not explicitly provided. This maintains compatibility for mixed version use. */
  322. query.exec("CREATE OR REPLACE FUNCTION log_session_user() RETURNS trigger AS $$ BEGIN IF NEW.person IS NULL THEN NEW.person := session_user; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  323. query.exec("CREATE OR REPLACE FUNCTION log_use() RETURNS trigger AS $$ DECLARE i integer := array_lower(NEW.unroasted_id, 1); u integer := array_upper(NEW.unroasted_id, 1); BEGIN IF NEW.transaction_type = 'ROAST' THEN WHILE i <= u LOOP INSERT INTO use (time, item, quantity, person) VALUES(NEW.time, NEW.unroasted_id[i], NEW.unroasted_quantity[i], NEW.person); i := i + 1; END LOOP; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  324. query.exec("CREATE OR REPLACE FUNCTION log_make() RETURNS trigger AS $$ BEGIN IF NEW.roasted_quantity IS NOT NULL THEN INSERT INTO make (time, item, quantity, person) VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity, NEW.person); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  325. query.exec("CREATE TABLE IF NOT EXISTS sample_roast_profiles(\"time\" timestamp without time zone NOT NULL, profile_name TEXT NOT NULL, file bigint NOT NULL)");
  326. query.exec("UPDATE TypicaFeatures SET version = 7 WHERE feature = 'base-features'");
  327. query = query.invalidate();
  328. };
  329. /* Updates for Typica version 1.9 */
  330. var DBUpdate19 = function() {
  331. var query = new QSqlQuery;
  332. query.exec("ALTER TABLE roasting_specification ADD COLUMN spec jsonb");
  333. query.exec("ALTER TABLE roasting_log ADD COLUMN additional_data jsonb");
  334. query.exec("CREATE TABLE IF NOT EXISTS scheduled_roasts (id bigserial PRIMARY KEY, machine bigint, \"time\" timestamp without time zone, data jsonb NOT NULL)");
  335. query.exec("CREATE OR REPLACE FUNCTION notify_scheduled_roasts_changed() RETURNS trigger AS $$ BEGIN NOTIFY ScheduledRoastsChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  336. query.exec("CREATE TRIGGER notify_scheduled_roasts_changed AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON scheduled_roasts FOR EACH STATEMENT EXECUTE PROCEDURE notify_scheduled_roasts_changed()");
  337. query.exec("CREATE OR REPLACE FUNCTION notify_transactions_changed() RETURNS trigger AS $$ BEGIN NOTIFY TransactionsChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  338. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON transactions FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  339. query.exec("CREATE OR REPLACE FUNCTION notify_purchase_changed() RETURNS trigger AS $$ BEGIN NOTIFY PurchaseChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  340. query.exec("CREATE TRIGGER notify_purchase_changed AFTER INSERT OR UPDATE OR DELETE ON purchase FOR EACH STATEMENT EXECUTE PROCEDURE notify_purchase_changed()");
  341. query.exec("CREATE OR REPLACE FUNCTION notify_sale_changed() RETURNS trigger AS $$ BEGIN NOTIFY SaleChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  342. query.exec("CREATE TRIGGER notify_sale_changed AFTER INSERT OR UPDATE OR DELETE ON sale FOR EACH STATEMENT EXECUTE PROCEDURE notify_sale_changed()");
  343. query.exec("CREATE OR REPLACE FUNCTION notify_invoices_changed() RETURNS trigger AS $$ BEGIN NOTIFY InvoicesChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  344. query.exec("CREATE TRIGGER nofify_invoices_changed AFTER INSERT OR UPDATE OR DELETE ON invoices FOR EACH STATEMENT EXECUTE PROCEDURE notify_invoices_changed()");
  345. query.exec("CREATE TRIGGER notify_invoices_changed AFTER INSERT OR UPDATE OR DELETE ON invoice_items FOR EACH STATEMENT EXECUTE PROCEDURE notify_invoices_changed()");
  346. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON inventory FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  347. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON loss FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  348. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON make FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  349. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON purchase FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  350. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON sale FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  351. query.exec("CREATE TRIGGER notify_transactions_changed AFTER INSERT OR UPDATE OR DELETE ON use FOR EACH STATEMENT EXECUTE PROCEDURE notify_transactions_changed()");
  352. query.exec("CREATE OR REPLACE FUNCTION notify_reminders_changed() RETURNS trigger AS $$ BEGIN NOTIFY RemindersChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  353. query.exec("CREATE TRIGGER notify_reminders_changed AFTER INSERT OR UPDATE OR DELETE ON reminders FOR EACH STATEMENT EXECUTE PROCEDURE notify_reminders_changed()");
  354. query.exec("UPDATE TypicaFeatures SET version = 8 WHERE feature = 'base-features'");
  355. query = query.invalidate();
  356. };
  357. if(Application.databaseConnected()) {
  358. query = new QSqlQuery();
  359. /* A table keeps track of database versioning information. This
  360. table is created
  361. if required. */
  362. query.exec("CREATE TABLE IF NOT EXISTS TypicaFeatures (feature TEXT PRIMARY KEY, enabled boolean, version bigint)");
  363. query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'base-features'");
  364. if(query.next())
  365. {
  366. if(query.value(2) < 1)
  367. {
  368. DBCreateBase();
  369. }
  370. if(query.value(2) < 2)
  371. {
  372. DBUpdateMultiUser();
  373. DBUpdateHistory();
  374. }
  375. if(query.value(2) < 3)
  376. {
  377. DBUpdateNotifications();
  378. }
  379. if(query.value(2) < 4)
  380. {
  381. DBUpdateTriggers();
  382. }
  383. if(query.value(2) < 5)
  384. {
  385. DBUpdateReminders();
  386. }
  387. if(query.value(2) < 6)
  388. {
  389. DBUpdateSpecification();
  390. }
  391. if(query.value(2) < 7) {
  392. DBUpdate18();
  393. }
  394. if(query.value(2) < 8) {
  395. DBUpdate19();
  396. }
  397. }
  398. else
  399. {
  400. DBCreateBase();
  401. DBUpdateMultiUser();
  402. DBUpdateHistory();
  403. DBUpdateNotifications();
  404. DBUpdateTriggers();
  405. DBUpdateReminders();
  406. DBUpdateSpecification();
  407. DBUpdate18();
  408. DBUpdate19();
  409. }
  410. query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'sample-roasting'");
  411. if(query.next())
  412. {
  413. if(query.value(2) < 1)
  414. {
  415. DBCreateSampleRoasting();
  416. }
  417. }
  418. else
  419. {
  420. DBCreateSampleRoasting();
  421. }
  422. var promptNewUsers = true;
  423. query.exec("SELECT count(1) FROM typica_users");
  424. if(query.next()) {
  425. if(Number(query.value(0)) > 0) {
  426. promptNewUsers = false;
  427. }
  428. }
  429. if(promptNewUsers) {
  430. var newUserDialog = new NewTypicaUser();
  431. newUserDialog.exec();
  432. }
  433. if(!Application.autoLogin()) {
  434. var loginDialog = new LoginDialog();
  435. loginDialog.exec();
  436. }
  437. query = query.invalidate();
  438. }
  439. var switchuser = findChildObject(this, 'switchuser');
  440. switchuser.triggered.connect(function() {
  441. var loginDialog = new LoginDialog();
  442. loginDialog.exec();
  443. });
  444. var createuser = findChildObject(this, 'createuser');
  445. createuser.triggered.connect(function() {
  446. var newUserDialog = new NewTypicaUser();
  447. newUserDialog.exec();
  448. });
  449. var dashboard = findChildObject(this, 'dashboard');
  450. var refresh = function() {
  451. var buffer = new QBuffer;
  452. buffer.open(3);
  453. var output = new XmlWriter(buffer);
  454. output.writeStartDocument("1.0");
  455. output.writeDTD('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg.dtd">');
  456. output.writeStartElement("html");
  457. output.writeAttribute("xmlns", "http://www.w3.org/1999/xhtml");
  458. output.writeStartElement("head");
  459. output.writeStartElement("link");
  460. output.writeAttribute("rel", "stylesheet");
  461. output.writeAttribute("href", QSettings.value("config") + "/Scripts/dashboard.css");
  462. output.writeEndElement();
  463. output.writeEndElement(); // head
  464. output.writeStartElement("body");
  465. output.writeStartElement("div");
  466. output.writeAttribute("class", "container");
  467. var query = new QSqlQuery;
  468. drawReminders(output, query);
  469. drawSchedule(output, query);
  470. drawProductionTrends(output, query);
  471. drawLatest(output, query);
  472. drawRecentlyOut(output, query);
  473. drawLeastAvailable(output, query);
  474. drawUnused(output, query);
  475. drawMostRoasted(output, query);
  476. drawLeastRoasted(output, query);
  477. query = query.invalidate();
  478. output.writeEndElement(); // End of container
  479. output.writeEndElement(); // body
  480. output.writeEndElement(); // html
  481. output.writeEndDocument();
  482. dashboard.setContent(buffer);
  483. buffer.close();
  484. }
  485. var startCell = function(output, title) {
  486. output.writeStartElement("div");
  487. output.writeAttribute("class", "cell");
  488. output.writeStartElement("div");
  489. output.writeAttribute("class", "cell-wrapper");
  490. output.writeStartElement("div");
  491. output.writeAttribute("class", "cell-title");
  492. output.writeCharacters(title);
  493. output.writeEndElement();
  494. }
  495. var startStage = function(output) {
  496. output.writeStartElement("div");
  497. output.writeAttribute("class", "cell-stage");
  498. }
  499. var endStage = function(output) {
  500. output.writeEndElement();
  501. }
  502. var endCell = function(output, summary) {
  503. if(arguments.length > 1) {
  504. output.writeStartElement("div");
  505. output.writeAttribute("class", "cell-notes");
  506. output.writeCharacters(summary);
  507. output.writeEndElement();
  508. }
  509. output.writeEndElement();
  510. output.writeEndElement();
  511. }
  512. var drawReminders = function(output, query) {
  513. query.exec("SELECT id, reminder FROM reminders");
  514. e = new Array();
  515. while(query.next()) {
  516. var reminder = JSON.parse(query.value(1));
  517. reminder.dbid = query.value(0);
  518. var start_time = "" + reminder.start_year + "-" + reminder.start_month + "-" + reminder.start_day + " " + reminder.start_time;
  519. if(reminder.condition == "PRODUCTIONWEIGHT") {
  520. var convert = 1;
  521. var unittext = TTR("navwindow", " Lb");
  522. if(reminder.unit == "KG") {
  523. convert = 2.2;
  524. unittext = TTR("navwindow", " Kg");
  525. }
  526. var dq = new QSqlQuery;
  527. dq.prepare("SELECT sum(roasted_quantity)/:conversion FROM roasting_log WHERE time > :since");
  528. dq.bind(":conversion", convert);
  529. dq.bind(":since", start_time);
  530. dq.exec();
  531. dq.next();
  532. var proportion;
  533. var remain;
  534. if(reminder.value == 0 || (reminder.value < Number(dq.value(0)))) {
  535. proportion = 1;
  536. } else {
  537. proportion = Number(dq.value(0)) / reminder.value;
  538. }
  539. remain = (reminder.value - Number(dq.value(0))).toFixed(0);
  540. reminder.completion = proportion;
  541. reminder.detail = remain + unittext;
  542. dq = dq.invalidate();
  543. } else if (reminder.condition == "DAYS") {
  544. var dq = new QSqlQuery;
  545. dq.prepare("SELECT 'now'::date - :since::date");
  546. dq.bind(":since", start_time);
  547. dq.exec();
  548. dq.next();
  549. var proportion;
  550. var remain;
  551. if(reminder.value == 0 || (reminder.value < Number(dq.value(0)))) {
  552. proportion = 1;
  553. } else {
  554. proportion = Number(dq.value(0)) / reminder.value;
  555. }
  556. remain = reminder.value - Number(dq.value(0));
  557. reminder.completion = proportion;
  558. reminder.detail = remain + TTR("navwindow", " Days");
  559. dq = dq.invalidate();
  560. } else if (reminder.condition == "PRODUCTIONBATCHES") {
  561. var dq = new QSqlQuery;
  562. dq.prepare("SELECT count(1) FROM roasting_log WHERE time > :since");
  563. dq.bind(":since", start_time);
  564. dq.exec();
  565. dq.next();
  566. var proportion;
  567. var remain;
  568. if(reminder.value == 0 || (reminder.value < Number(dq.value(0)))) {
  569. proportion = 1;
  570. } else {
  571. proportion = Number(dq.value(0)) / reminder.value;
  572. }
  573. remain = reminder.value - Number(dq.value(0));
  574. reminder.completion = proportion;
  575. reminder.detail = remain + TTR("navwindow", " Batches");
  576. dq = dq.invalidate();
  577. } else if (reminder.condition == "PRODUCTIONHOURS") {
  578. var dq = new QSqlQuery;
  579. dq.prepare("SELECT extract(epoch FROM (SELECT sum(duration) FROM roasting_log WHERE time > :since) / 3600)");
  580. dq.bind(":since", start_time);
  581. dq.exec();
  582. dq.next();
  583. var proportion;
  584. var remain;
  585. if(reminder.value == 0 || (reminder.value < Number(dq.value(0)))) {
  586. proportion = 1;
  587. } else {
  588. proportion = Number(dq.value(0)) / reminder.value;
  589. }
  590. remain = reminder.value - Number(dq.value(0));
  591. reminder.completion = proportion;
  592. reminder.detail = remain.toFixed(1) + TTR("navwindow", " Hours");
  593. dq = dq.invalidate();
  594. }
  595. e[reminder.dbid] = reminder;
  596. }
  597. var s = e.filter(function(n) {
  598. return n.hasOwnProperty("completion")}).sort(function(a, b) {
  599. return b.completion - a.completion});
  600. var c = 0;
  601. var so = 0;
  602. s.forEach(function(item) {
  603. if(item.completion >= 1) {
  604. c += 1;
  605. } else if (item.completion >= 0.8) {
  606. so += 1;
  607. }
  608. });
  609. if(c > 0 || so > 0) {
  610. output.writeStartElement("a");
  611. output.writeAttribute("href", "typica://script/reminders");
  612. startCell(output, TTR("navwindow", "Reminders"));
  613. startStage(output);
  614. var summaryText;
  615. if(c > 0) {
  616. summaryText = "" + c + TTR("navwindow", " reminders due");
  617. for(var i = 0; i < c; i++) {
  618. output.writeStartElement("div");
  619. output.writeAttribute("class", "reminder");
  620. output.writeTextElement("p", s[i].title);
  621. output.writeTextElement("p", Math.floor(s[i].completion * 100) + "%");
  622. output.writeTextElement("p", s[i].detail);
  623. output.writeEndElement();
  624. }
  625. } else {
  626. summaryText = "" + so + TTR("navwindow", " reminders due soon");
  627. for(var i = 0; i < so; i++) {
  628. output.writeStartElement("div");
  629. output.writeAttribute("class", "reminder");
  630. output.writeTextElement("p", s[i].title);
  631. output.writeTextElement("p", Math.floor(s[i].completion * 100) + "%");
  632. output.writeTextElement("p", s[i].detail);
  633. output.writeEndElement();
  634. }
  635. }
  636. endStage(output);
  637. endCell(output, summaryText);
  638. output.writeEndElement();
  639. }
  640. }
  641. var kilounit = TTR("navwindow", "Kg");
  642. var poundunit = TTR("navwindow", "Lb");
  643. var unitData = function() {
  644. var retval = new Object;
  645. if(Number(QSettings.value("script/report_unit")) == 0) {
  646. retval.conversion = 2.2;
  647. retval.unittext = "Kg";
  648. } else {
  649. retval.conversion = 1;
  650. retval.unittext = "Lb";
  651. }
  652. return retval;
  653. }
  654. var drawSchedule = function(output, query) {
  655. var c = 0;
  656. var u = unitData();
  657. query.prepare("SELECT (SELECT name FROM items WHERE id = (data#>>'{roasted}')::numeric), (data#>>'{green_weight}')::numeric/:conversion FROM scheduled_roasts WHERE machine IS NULL");
  658. query.bind(":conversion", u.conversion);
  659. query.exec();
  660. if(query.next()) {
  661. output.writeStartElement("a");
  662. output.writeAttribute("href", "typica://script/schedule");
  663. startCell(output, TTR("navwindow", "Scheduled Roasts"));
  664. startStage(output);
  665. do {
  666. c += 1;
  667. output.writeTextElement("p", query.value(1) + u.unittext + " " + query.value(0));
  668. } while(query.next());
  669. endStage(output);
  670. endCell(output, "" + c + TTR("navwindow", " batches scheduled"));
  671. output.writeEndElement();
  672. }
  673. }
  674. var drawLatest = function(output, query) {
  675. var u = unitData();
  676. query.prepare("SELECT time, roasted_quantity/:conversion, (SELECT name FROM items WHERE id = roasted_id), approval FROM roasting_log ORDER BY time DESC LIMIT 5");
  677. query.bind(":conversion", u.conversion);
  678. query.exec();
  679. if(query.next()) {
  680. output.writeStartElement("a");
  681. output.writeAttribute("href", "typica://script/log");
  682. startCell(output, TTR("navwindow", "Latest Batches"));
  683. startStage(output);
  684. do {
  685. output.writeStartElement("p");
  686. if(query.value(3) == "false") {
  687. output.writeAttribute("style", "color: #FF0000;");
  688. }
  689. output.writeCharacters(query.value(0).replace("T", " ") + " " + query.value(1) + u.unittext + " " + query.value(2));
  690. output.writeEndElement();
  691. } while(query.next());
  692. endStage(output);
  693. endCell(output);
  694. output.writeEndElement();
  695. }
  696. }
  697. var drawMostRoasted = function(output, query) {
  698. var u = unitData();
  699. query.prepare("SELECT (sum(roasted_quantity)/:conversion) AS sum, (SELECT name FROM items WHERE id = roasted_id) FROM roasting_log WHERE time > 'now'::date - '28 days'::interval AND approval = true GROUP BY roasted_id ORDER BY sum DESC LIMIT 5");
  700. query.bind(":conversion", u.conversion);
  701. query.exec();
  702. if(query.next()) {
  703. startCell(output, TTR("navwindow", "Most Roasted Coffees (Last 28 Days)"));
  704. startStage(output);
  705. do {
  706. output.writeTextElement("p", query.value(0) + u.unittext + " " + query.value(1));
  707. } while(query.next());
  708. endStage(output);
  709. endCell(output);
  710. }
  711. }
  712. var drawLeastRoasted = function(output, query) {
  713. var u = unitData();
  714. query.prepare("SELECT (sum(roasted_quantity)/:conversion) AS sum, (SELECT name FROM items WHERE id = roasted_id) FROM roasting_log WHERE time > 'now'::date - '28 days'::interval AND approval = true GROUP BY roasted_id ORDER BY sum ASC LIMIT 5");
  715. query.bind(":conversion", u.conversion);
  716. query.exec();
  717. if(query.next()) {
  718. startCell(output, TTR("navwindow", "Least Roasted Coffees (Last 28 Days)"));
  719. startStage(output);
  720. do {
  721. output.writeTextElement("p", query.value(0) + u.unittext + " " + query.value(1));
  722. } while(query.next());
  723. endStage(output);
  724. endCell(output);
  725. }
  726. }
  727. var drawLeastAvailable = function(output, query) {
  728. var u = unitData();
  729. query.prepare("SELECT name, (quantity / :conversion)::numeric(12,2) AS quantity, (SELECT out FROM coffee_history WHERE coffee_history.id = items.id) AS out FROM items WHERE quantity > 0 ORDER BY out ASC LIMIT 5");
  730. query.bind(":conversion", u.conversion);
  731. query.exec();
  732. if(query.next()) {
  733. startCell(output, TTR("navwindow", "Least Available Coffes"));
  734. startStage(output);
  735. output.writeStartElement("table");
  736. do {
  737. output.writeStartElement("tr");
  738. output.writeTextElement("td", query.value(1) + u.unittext);
  739. output.writeTextElement("td", query.value(0));
  740. output.writeTextElement("td", query.value(2));
  741. output.writeEndElement();
  742. } while(query.next());
  743. output.writeEndElement();
  744. endStage(output);
  745. endCell(output);
  746. }
  747. }
  748. var drawRecentlyOut = function(output, query) {
  749. query.exec("SELECT (SELECT name FROM items WHERE id = item), max(time)::date AS last_transaction FROM transactions WHERE item IN (SELECT id FROM items WHERE quantity = 0) GROUP BY item ORDER BY last_transaction DESC LIMIT 5");
  750. if(query.next()) {
  751. startCell(output, TTR("navwindow", "Latest Out of Stock Coffees"));
  752. startStage(output);
  753. output.writeStartElement("table");
  754. do {
  755. output.writeStartElement("tr");
  756. output.writeTextElement("td", query.value(1));
  757. output.writeTextElement("td", query.value(0));
  758. output.writeEndElement();
  759. } while(query.next());
  760. output.writeEndElement();
  761. endStage(output);
  762. endCell(output);
  763. }
  764. }
  765. var drawUnused = function(output, query) {
  766. query.exec("SELECT name FROM coffees WHERE id NOT IN (SELECT item FROM use) AND quantity > 0");
  767. var c = 0;
  768. if(query.next()) {
  769. startCell(output, TTR("navwindow", "Unused Coffees"));
  770. startStage(output);
  771. do {
  772. c += 1;
  773. output.writeTextElement("p", query.value(0));
  774. } while(query.next());
  775. endStage(output);
  776. endCell(output, "" + c + TTR("navwindow", " unused coffees"));
  777. }
  778. }
  779. var calculateChange = function(current, previous) {
  780. var difference = Number(current) - Number(previous);
  781. var retval;
  782. if(difference > 0) {
  783. retval = "+";
  784. } else if(difference < 0) {
  785. retval = "";
  786. }
  787. retval += ((difference/Number(previous)) * 100).toFixed(1);
  788. retval += "%";
  789. return retval;
  790. }
  791. var outputChange = function(output, value) {
  792. output.writeStartElement("td");
  793. if(value[0] == '+') {
  794. output.writeAttribute("style", "color: #008B00");
  795. } else if(value[0] == '-') {
  796. output.writeAttribute("style", "color: #8B0000");
  797. }
  798. output.writeCharacters(value);
  799. output.writeEndElement();
  800. }
  801. var outputChangeRow = function(output, query, p1, p2) {
  802. var u = unitData();
  803. query.prepare("SELECT sum(roasted_quantity)/:conversion FROM roasting_log WHERE approval = true AND time > 'now'::date - '" + p1 + " days'::interval");
  804. query.bind(":conversion", u.conversion);
  805. query.exec();
  806. query.next();
  807. var current = Number(query.value(0));
  808. output.writeTextElement("td", "" + current.toFixed(0) + u.unittext);
  809. query.prepare("SELECT sum(roasted_quantity)/:conversion FROM roasting_log WHERE approval = true AND time > 'now'::date - '" + p2 + " days'::interval AND time < 'now'::date - '" + p1 + " days'::interval");
  810. query.bind(":conversion", u.conversion);
  811. query.exec();
  812. query.next();
  813. var previous = Number(query.value(0));
  814. output.writeTextElement("td", "" + previous.toFixed(0) + u.unittext);
  815. outputChange(output, calculateChange(current, previous));
  816. }
  817. var drawProductionTrends= function(output, query) {
  818. startCell(output, TTR("navwindow", "Production Trends"));
  819. startStage(output);
  820. output.writeStartElement("table");
  821. output.writeStartElement("tr");
  822. output.writeTextElement("th", TTR("navwindow", "Period"));
  823. output.writeTextElement("th", TTR("navwindow", "Production"));
  824. output.writeTextElement("th", TTR("navwindow", "Previous"));
  825. output.writeTextElement("th", TTR("navwindow", "Change"));
  826. output.writeEndElement();
  827. output.writeStartElement("tr");
  828. output.writeTextElement("th", TTR("navwindow", "Today"));
  829. outputChangeRow(output, query, 0, 1);
  830. output.writeEndElement();
  831. output.writeStartElement("tr");
  832. output.writeTextElement("th", TTR("navwindow", "Last 7 Days"));
  833. outputChangeRow(output, query, 6, 13);
  834. output.writeEndElement();
  835. output.writeStartElement("tr");
  836. output.writeTextElement("th", TTR("navwindow", "Last 30 Days"));
  837. outputChangeRow(output, query, 29, 59);
  838. output.writeEndElement();
  839. output.writeStartElement("tr");
  840. output.writeTextElement("th", TTR("navwindow", "Last 90 Days"));
  841. outputChangeRow(output, query, 89, 179);
  842. output.writeEndElement();
  843. output.writeStartElement("tr");
  844. output.writeTextElement("th", TTR("navwindow", "Last 365 Days"));
  845. outputChangeRow(output, query, 364, 729);
  846. output.writeEndElement();
  847. output.writeEndElement();
  848. endStage(output);
  849. endCell(output);
  850. }
  851. refresh();
  852. dashboard.scriptLinkClicked.connect(function(url) {
  853. if(url == "reminders") {
  854. createReport("reminders.xml");
  855. } else if(url == "log") {
  856. createReport("historyreport.xml");
  857. } else if(url == "schedule") {
  858. createWindow("schedule");
  859. }
  860. });
  861. var remindernotification = Application.subscribe("reminderschange");
  862. remindernotification.notify.connect(function() {
  863. refresh();
  864. });
  865. var schedulenotification = Application.subscribe("scheduledroastschange");
  866. schedulenotification.notify.connect(function() {
  867. refresh();
  868. });
  869. var lognotification = Application.subscribe("transactionschange");
  870. lognotification.notify.connect(function() {
  871. refresh();
  872. });
  873. ]]>
  874. </program>
  875. </window>