Merge branch 'dev' into 'main'

Merge to add Menu Item to List Item linking

See merge request bmcgonag/get_my!1
This commit is contained in:
Brian McGonagill 2024-07-29 22:39:50 +00:00
commit bc04c509a5
18 changed files with 453 additions and 14 deletions

View file

@ -5,6 +5,52 @@ Open source shopping / list app.
If you were using this previously, the new updates change the data structure. Please remove your old database if you find odd behavior or errors. The data structures may simply be incompatible.
I apologize for any inconvenince as I simplify the app a bit.
## Install and Run
NOTE: if you have a cpu without the AVX instruction set, you can change the line 'image: mongo' to be 'image: mongo:4.4'in the docker compose below.
### Docker / Docker Compose
You can use this docker compose file to get the system up and running.
```
---
services:
get_my:
container_name: get_my
image: bmcgonag/get_my:latest
ports:
- "80:3000" # you can change the left side to a less common port number if needed
healthcheck:
test: curl --fail -s http://localhost:3000/ || exit 1
interval: 30s
timeout: 10s
retries: 3
links:
- mongo
depends_on:
- mongo
restart: unless-stopped
mongo:
container_name: get_my-mongo
image: mongo
volumes:
- ./data:/data/db # this will store the application data in the same directory where you have the docker-compose.yml file.
restart: unless-stopped
```
### On Host using NodeJS
1. You need to have a mongo database instance installed and ready to use. You also need to know the port it uses, as well as create a database in mongo for the 'Get My' application. I recommend naming the database 'get-my'.
2. You need to have NodeJS installed (the hard part is that this application is build in MeteorJS, and currently uses a node version of 14.21.4 - which you'll find isn't available for download from node themselves anymore). You need 14.21.4 and can get a tar.gz file of it here: https://guide.meteor.com/using-node-v14.21.4
3. Once setup. you need to set some environment variables
- `ROOT_URL=`(whatever you want your node application to accessed at, e.g. `http://localhost`, etc)
- `MONGO_URL=`(ip and port for your mongodb in this format `mongodb://localhost:27017/get-my`) This is the url for the application to reach the mongo database, and does not have to exist on the same machine, but must be reachable by IP or hostname, and port. The 'get-my' in this url is the database name you setup.
- `PORT=3000`
- `MAIL_URL=` not a working feature yet, but if you want smtp email to work you'd need to set this to be something like `smtp://USERNAME:PASSWORD@HOST:PORT`
4. Download the source bundle (I'll add a url to release bundle soon)
5. unzip the source bundle
5. Move into the bundle/program/server folder, and run `npm install`. This should install all needed dependancies.
6. move back to the bundle folder, and run `node main.js`. This will start the application running.
7. There are multiple node applications to help keep a node js app running in the background, like PM2, etc...but I'll leave that up to you to get going.
## Keeping it Simple
- Registration / Login System built in
- 1st user to register is the system admin by default.

View file

@ -19,6 +19,10 @@ Template.taskForm.onRendered(function() {
secondaryPlaceholder: '+Task Name',
});
setTimeout(function() {
instances = M.FormSelect.init(elems, {});
}, 350);
Session.set("taskNameErr", false);
Session.set("taskUserErr", false);
Session.set("taskDateErr", false);

View file

@ -11,7 +11,7 @@
{{itemName}}
{{/if}}
</span>
<i class="material-icons clickable deleteListItem right">delete</i>
<i class="material-icons clickable deleteListItem right modal-trigger" href="#modalDelete">delete</i>
<i class="material-icons clickable markListItemReceived right">check</i>
</li>
{{/each}}

View file

@ -1,4 +1,5 @@
import { ListItems } from '../../imports/api/listItems.js';
import { M } from '../lib/assets/materialize.js';
Template.listItemsTbl.onCreated(function() {
this.autorun( () => {
@ -7,8 +8,9 @@ Template.listItemsTbl.onCreated(function() {
});
Template.listItemsTbl.onRendered(function() {
// new modal init here
// $('.modal').modal();
var elems = document.querySelectorAll('.modal');
var instances = M.Modal.init(elems, {});
Session.set("showReceivedItems", false);
Session.set("searchVal", "");
});

View file

@ -0,0 +1,47 @@
<template name="addProdToListModal">
<div class="modal" id="addProdToList">
<div class="modal-content">
<h2>Add Items to List</h2>
<form class="row" style="gap: 1em;">
<div class="col s12 input-field outlined" id="chooseListDiv">
<select name="chooseList" id="chooseList">
{{#each setOfLists}}
<option value="{{_id}}">{{listName}}</option>
{{/each}}
</select>
</div>
<div class="col s12">
<ul class="collection with-header">
<li class="collection-header">
<h4>Products to Add</h4>
</li>
{{#each productToChoose}}
<li class="collection-item" id="{{prodId}}">
<div>
<p>
<label>
<input type="checkbox" class="productListing" id="{{prodId}}" />
<span class="my-text">{{prodName}}</span>
</label>
</p>
</div>
</li>
{{/each}}
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<div class="row">
<div class="col s12 m6 l6">
<a class="left btn waves-effect waves-light orange white-text modal-close">Cancel</a>
</div>
<div class="col s12 m6 l6">
<a class="btn waves-effect waves-light green white-text saveProdsToList" id="saveProdsToList">Save to List</a>
</div>
</div>
</div>
</div>
{{> snackbar}}
</template>

View file

@ -0,0 +1,64 @@
import { M } from '../lib/assets/materialize.js';
import { MenuProdLinks } from '../../imports/api/menuProdLinks';
import { Products } from '../../imports/api/products.js';
import { Lists } from '../../imports/api/lists.js';
import { ListItems } from '../../imports/api/listItems';
Template.addProdToListModal.onCreated(function() {
this.subscribe("myProducts");
this.subscribe("myLists");
this.subscribe("myListItems");
this.subscribe("menuProdLinkData");
});
Template.addProdToListModal.onRendered(function() {
var elems = document.querySelectorAll('select');
var instances = M.FormSelect.init(elems, {});
Session.set("itemsSelected", []);
});
Template.addProdToListModal.helpers({
assocProds: function() {
let menuItemId = Session.get("menuItemId");
let assocProds = MenuProdLinks.find({ menuItemId: menuItemId });
if (typeof assocProds != 'undefined' && assocProds != '' && assocProds != null) {
return assocProds;
}
},
setOfLists: function() {
return Lists.find({});
},
productToChoose: function() {
let prodLinkLIst = MenuProdLinks.find({ menuId: Session.get("menuItemId")});
if (typeof prodLinkLIst != 'undefined' && prodLinkLIst != "" && prodLinkLIst != null) {
return prodLinkLIst;
}
},
});
Template.addProdToListModal.events({
'click .productListing' (event) {
let itemId = event.currentTarget.id;
let selected = Session.get("itemsSelected");
console.log("Item clicked: " + itemId);
selected.push(itemId);
console.dir(selected);
Session.set("itemsSelected", selected);
},
'click #saveProdsToList' (event) {
event.preventDefault();
let selectedItems = Session.get("itemsSelected");
let listId = $("#chooseList").val();
// console.log(" calling meteor method with items: ");
// console.dir(selectedItems);
// console.log(" and list id: "+ listId);
Meteor.call('add.itemsFromMenuItem', selectedItems, listId, function(err, result) {
if (err) {
console.log(" ERROR adding menu components to list: " + err);
} else {
showSnackbar("Items Added to List!", "green");
}
});
}
});

View file

@ -5,7 +5,7 @@ import { M } from '../lib/assets/materialize.js';
Template.menuItemsForm.onCreated(function() {
this.subscribe("myMenus");
this.subscribe("myMenuItems", Session.get("menuId"));
this.subscribe("allMenuItems", Session.get("menuId"));
});
Template.menuItemsForm.onRendered(function() {
@ -13,6 +13,15 @@ Template.menuItemsForm.onRendered(function() {
var instances = M.Datepicker.init(elems, {});
Session.set("menuItemErr", false);
Session.set("menuListItems", {});
this.autorun(() => {
var elema = document.querySelectorAll('.autocomplete');
var instancea = M.Autocomplete.init(elema, {
minlength: 0,
data: Session.get("menuListItems"),
});
});
});
Template.menuItemsForm.helpers({
@ -65,5 +74,20 @@ Template.menuItemsForm.events({
}
});
}
},
'keyup #menuItemInp' (event) {
if (event.which != 13) {
let findMenuItem = $("#menuItemInp").val();
let menuItemInfo = MenuItems.find({ itemName: {$regex: findMenuItem + '.*', $options: 'i' }}).fetch();
if (typeof menuItemInfo != 'undefined' && menuItemInfo != '' && menuItemInfo != null) {
getMenuItemList(menuItemInfo);
}
}
},
});
getMenuItemList = function(menuItemInfo) {
let menuItemObjArray = [];
menuItemObjArray = menuItemInfo.map(info => ({ id: info._id, text: info.itemName }));
Session.set("menuListItems", menuItemObjArray);
}

View file

@ -2,8 +2,16 @@
<div class="row">
<div class="col s12">
<table>
<thead>
<tr>
<th>Menu Item</th>
<th>Date to Serve</th>
<th>Has Product Links</th>
<th>Actions</th>
</tr>
</thead>
{{#each thisMenuItems}}
<tr class="clickable">
<tr>
<td>
<span>
{{#if $eq itemMade true}}
@ -17,12 +25,20 @@
{{serveDate}}
</td>
<td>
<i class="material-icons clickable deleteMenuItem right modal-trigger" data-target="modalDelete">delete</i>
{{#if $eq isLinked true}}<a class="waves-effect waves-light blue darken-3 white-text btn addProdsToList modal-trigger" href="#addProdToList">+ Shopping List</a>{{/if}}
</td>
<td>
<i class="material-icons tooltipped modal-trigger deleteMenuItem clickable" href="#modalDelete" data-position="top" data-tooltip-id="deleteMenuTip">delete</i>
<i class="material-icons tooltipped modal-trigger linkToProducts clickable" href="#modalLinkProducts" data-position="top" data-tooltip-id="linkMenuTip">link</i>
</td>
</tr>
{{/each}}
</table>
<div class="tooltip-content" style="display: none;" id="deleteMenuTip">Delete this menu item</div>
<div class="tooltip-content" style="display: none;" id="linkMenuTip">Link Products that make up this menu item</div>
</div>
</div>
{{> deleteConfirmationModal}}
{{> modalLinkProducts}}
{{> addProdToListModal}}
</template>

View file

@ -10,11 +10,19 @@ Template.menuItemsTbl.onCreated(function() {
Template.menuItemsTbl.onRendered(function() {
var elems = document.querySelectorAll('.modal');
var instances = M.Modal.init(elems, {});
var elemt = document.querySelectorAll('.tooltipped');
var instancet = M.Tooltip.init(elemt, {});
Meteor.setTimeout(function() {
var instances = M.Modal.init(elems, {});
var instancet = M.Tooltip.init(elemt, {});
}, 500);
});
Template.menuItemsTbl.helpers({
thisMenuItems: function() {
return MenuItems.find({}, { sort: { serveDateActual: 1 }});
return MenuItems.find({ menuId: Session.get("menuId") }, { sort: { serveDateActual: 1 }});
}
});
@ -26,4 +34,12 @@ Template.menuItemsTbl.events({
Session.set("item", this.itemName);
Session.set("view", "Menu Items");
},
'click .linkToProducts' (event) {
event.preventDefault();
Session.set("menuItemId", this._id);
},
'click .addProdsToList' (event) {
event.preventDefault();
Session.set("menuItemId", this._id);
}
});

View file

@ -2,8 +2,11 @@
<h5>{{menuName}}</h5>
<form class="menuItemFrm row" style="gap: 1em;" id="menuItemFrm">
<div class="col s12 m6 l9 input-field outlined">
<input type="text" class="menuItemInp" style="{{#if $eq menuItemErr true}}border: 2px solid red{{/if}}" id="menuItemInp" />
<label for="menuItemInp">Add Menu Item</label>
<input type="text" class="autocomplete" id="menuItemInp" autocomplete="off" />
<label for="menuItemInp">Item</label>
<!-- <input type="text" class="menuItemInp" style="{{#if $eq menuItemErr true}}border: 2px solid red{{/if}}" id="menuItemInp" />
<label for="menuItemInp">Add Menu Item</label> -->
</div>
<div class="col s12 m6 l3 input-field outlined">
<input type="text" class="datepicker" id="dateServed" />

View file

@ -0,0 +1,29 @@
<template name="modalLinkProducts">
<div id="modalLinkProducts" class="modal">
<div class="modal-content">
<p class="flow-text">Choose Product Items below that are used to make this menu item.</p>
<div class="col s12 input-field outlined" id="prouctsForMenudiv">
<select name="" id="prodForMenu" class="prodForMenu" multiple>
<option value="" disabled>Choose...</option>
{{#each products}}
<option value="{{prodName}}">{{prodName}}</option>
{{/each}}
</select>
</div>
</div>
<br>
<hr>
<div class="modal-footer">
<div class="row">
<div class="col s6">
<a id="cancelLInk" class="btn waves-effect wave-light orange white-text left modal-close">Cancel</a>
</div>
<div class="col s6">
<a id="saveLink" class="btn waves-effect waves-light green white-text right modal-close">Save</a>
</div>
</div>
</div>
</div>
{{> snackbar}}
</template>

View file

@ -0,0 +1,57 @@
import { M } from '../lib/assets/materialize.js';
import { Products } from '../../imports/api/products.js';
import { MenuItems } from '../../imports/api/menuItems';
import { MenuProdLinks } from '../../imports/api/menuProdLinks.js';
Template.modalLinkProducts.onCreated(function() {
this.subscribe("myMenuItems");
this.subscribe("myProducts");
this.subscribe("menuProdLinkData");
});
Template.modalLinkProducts.onRendered(function() {
var elems = document.querySelectorAll('.modal');
var instances = M.Modal.init(elems, {});
var elemse = document.querySelectorAll('select');
var instancese = M.FormSelect.init(elemse, {
dropdownOptions: 4,
});
setTimeout(function() {
var instances = M.Modal.init(elems, {});
var instancese = M.FormSelect.init(elemse, {
dropdownOptions: 4,
});
}, 250);
});
Template.modalLinkProducts.helpers({
products: function() {
return Products.find({});
}
});
Template.modalLinkProducts.events({
'click #saveLink' (event) {
event.preventDefault();
let menuItemId = Session.get("menuItemId");
let linkSelect = document.getElementById('prodForMenu');
let links = M.FormSelect.getInstance(linkSelect).getSelectedValues();
if (typeof links != undefined && links != [] && links != null) {
Meteor.call("add.menuProdLinks", menuItemId, links, function(err, result) {
if (err) {
console.log(" ERROR adding product links to this menu item: " + err);
} else {
Meteor.call('update.menuItemLinked', menuItemId, true, function(err, result) {
if (err) {
console.log(" ERROR adding link exists to menu item: " + err);
} else {
showSnackbar("Products added to Menu Item successfully!", "green");
}
});
}
});
}
}
});

View file

@ -25,3 +25,7 @@ strike {
text-decoration-thickness: 3px;
text-decoration-color: red;
}
.my-text {
font-size: 1.3em !important;
}

View file

@ -54,9 +54,39 @@ Meteor.methods({
dateAddedToList: new Date(),
});
}
},
'add.itemsFromMenuItem' (itemIds, listId) {
check(itemIds, [String]);
check(listId, String);
if (!this.userId) {
throw new Meteor.Error('You are not allowed to add items from a menu. Make sure you are logged in with valid user credentials.');
}
console.dir(itemIds);
for (i=0; i < itemIds.length; i++) {
// let's check and make sure the product isn't already on the list
let onList = ListItems.find({ listId: listId, prodId: itemIds[i] }).count();
console.log("Number On List: " + onList);
if (onList == 0) {
// now pull the product
let prodInfo = Products.findOne({ _id: itemIds[i] });
ListItems.insert({
itemName: prodInfo.prodName,
listId: listId,
prodId: prodInfo._id,
addedBy: this.userId,
itemStore: prodInfo.prodStore,
itemOrdered: false,
itemReceived: false,
dateAddedToList: new Date(),
});
} else {
// product exists on list, move on to next
console.log("Product Exists on Selected List.");
}
}
},
'setOrdered.listItem' (itemId) {
check(itemId, String);

View file

@ -31,6 +31,21 @@ Meteor.methods({
addedBy: this.userId,
itemMade: false,
dateAddedtoMenu: new Date(),
isLinked: false,
});
},
'update.menuItemLinked' (itemId, isLinked) {
check(itemId, String);
check(isLinked, Boolean);
if (!this.userId) {
throw new Meteor.Error('You are not allowed to set this menu item as linked to products. Make sure you are logged in with valid user credentials.');
}
return MenuItems.update({ _id: itemId }, {
$set: {
isLinked: isLinked,
}
});
},
'setMade.menuItem' (itemId) {

View file

@ -0,0 +1,44 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import { Products } from './products';
export const MenuProdLinks = new Mongo.Collection('menuProdLinks');
MenuProdLinks.allow({
insert: function(userId, doc){
// if use id exists, allow insert
return !!userId;
},
});
Meteor.methods({
'add.menuProdLinks' (menuItemId, prodNameArray) {
check(menuItemId, String);
check(prodNameArray, [String]);
if (!this.userId) {
throw new Meteor.Error('You are not allowed to add menu and product links. Make sure you are logged in with valid user credentials.');
}
let productObj = {};
let prodNameLength = prodNameArray.length;
for (i=0; i<prodNameLength; i++) {
let product = Products.findOne({ prodName: prodNameArray[i] });
if (typeof product != 'undefined' && product != null && product != "") {
let prodId = product._id;
MenuProdLinks.insert({
menuItemId: menuItemId,
prodName: prodNameArray[i],
prodId: prodId,
dateCreated: Date(),
createdBy: this.userId
});
} else {
console.log(" ERROR - unable to find a matching product by name: " + prodName[i] + ".");
}
}
}
});

View file

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { SysConfig } from '../imports/api/systemConfig';
import { MenuItems } from '../imports/api/menuItems';
Meteor.startup(() => {
// code to run on server at startup
@ -18,4 +19,24 @@ Meteor.startup(() => {
} else {
console.log("Registration policy already set.");
}
// check if the isLInked item exists on menuitems, and if not, add it (data cleanup task)
let itemInfoNoLink = MenuItems.find({ isLinked: { $exists: false } }).fetch();
console.log("No Ites with isLinked not set: " + itemInfoNoLink.length);
if (itemInfoNoLink.length > 0) {
console.log("found items with isLinked not set.");
console.dir(itemInfoNoLink);
let infoLength = itemInfoNoLink.length;
for (i=0; i < infoLength; i++) {
MenuItems.update({ _id: itemInfoNoLink[i]._id }, {
$set: {
isLinked: false,
}
});
}
} else {
// this will show if all items are found to have isLInked set.
console.log("No itesm with isLinked not set.");
}
});

View file

@ -9,6 +9,7 @@ import { MenuItems } from '../imports/api/menuItems.js';
import moment from 'moment';
import { TaskItems } from '../imports/api/tasks.js';
import { UserConfig } from '../imports/api/userConfig.js';
import { MenuProdLinks } from '../imports/api/menuProdLinks.js';
Meteor.publish("SystemConfig", function() {
try {
@ -82,10 +83,18 @@ Meteor.publish("myMenuItems", function(menuId) {
try {
return MenuItems.find({ menuId: menuId });
} catch (error) {
console.log(" ERROR pulling list items for this list: " + error);
console.log(" ERROR pulling menu items for this list: " + error);
}
});
Meteor.publish("allMenuItems", function() {
try {
return MenuItems.find({});
} catch (error) {
console.log(" ERROR pulling all menu items from collection: " + error);
}
})
Meteor.publish("todayMenuItems", function() {
try {
let todayDate = new Date();
@ -118,4 +127,12 @@ Meteor.publish("rolesAvailable", function() {
} catch (error) {
console.log(" ERROR publishing roles: " + error);
}
})
});
Meteor.publish("menuProdLinkData", function() {
try {
return MenuProdLinks.find({});
} catch (error) {
console.log(" ERROR publishing menu product links: " + error);
}
});