Many changes aded to system.

This commit is contained in:
Brian McGonagill 2026-01-24 13:49:38 -06:00
parent e0571d14b7
commit 5ba618f471
22 changed files with 640 additions and 57 deletions

242
Outline.md Normal file
View file

@ -0,0 +1,242 @@
### Handled at the master level
Tenants:
- Tenant 000000: Master Tenant
- Tenant: 000001 - zzzzzz (all other tenants)
- Tenant Name
- Tenant ID
- Tenant Description
- Tenant Email (primary contact)
- Tenant Phone (primary Contact)
### Handled at the Tenant Level:
#### Tenant Locations:
Any locations defined through a parent - child hierarchy. This can best reflect the actual structure of any location where assets may be located
```
location {
_id: location_id, # req
tenant_id: tenant_id, # req
loc_name: location_name, # req
loc_desc: location_description,
loc_type: code_table, # req
loc_info: {
location_info
},
loc_is_child: t/f, # req
loc_parent_id: location_id,
}
```
__Code Table: location type__
```
location_types: {
_id: loc_type_id
loc_type: type,
tenant_id: tenant_id,
}
```
__Code Table: location info__
```
location_details: {
_id: loc_detail_id,
tenant_id: tenant_id,
loc_type: type_id,
loc_detail: detail1,
loc_detail_type: (text, number, t/f, select),
loc_detail_sel_options: {
option: option1,
option: option2,
}
}
```
----
### Assets
An asset is any item, whether permanent fixture, high dollar equiprment, consumable requiring inventory and replenishment, or otherwise.
```
item: {
_id: item_id,
tenant_id: tenant_id,
location_id: location_id,
location_path: path / separated / out,
item_name: item name,
item_desc: item desc,
images: {
image_name: image name,
image_storage_location: path to image,
}
item_qty: qunatity,
item_type: code_table,
expiration_date: item_expires
createdDate: new Date(),
createdBy: user_id,
modified: {
modified_on: new Date(),
modified_by: user_id,
modified_reason: text_input,
},
item_archived: t/f,
item_archive_info: {
archived_on: new Date(),
archived_by: user_id,
archive_reason: text_input,
},
item_deleted: t/f,
item_delete_info: {
deleted_on: new Date(),
deleted_by: user_id,
deleted_reason: text_input,
},
item_dispo_info: {
item_dispo: code_table,
dispo_date: new Date(),
dispo_by: user_id,
dispo_reason: text_input,
dispo_description: text_input,
},
item_barcode: barcode detail (autogenerate 1d),
item_req_inspection: t/f
item_req_inventory: t/f
item_cost: number,
item_value: number,
item_type_attributes: {
item_attribute: {
attribute: entry_type,
attribute: entry_type,
},
item_attribute: {
attribute: entry_type,
attribute: entry_type,
},
}.
is_parent_of_multipart_asset: t/f
is_part_of_multipart_asset: t/f
multipart_asset_parent_id
}
```
__Code Table: Asset Types__
```
asset_types: {
_id: asset_type_id,
tenant_id: tenant_id:
asset_type_name: name,
assett_type_desc: asset_type_description,
asset_type_req_more_details: t/f
}
```
__Code Table: Asset Type Details__
```
asset_type_details: {
_id: asset_type_details_id,
asset_type_id: asset_type_id,
tenant_id: tenant_id,
detail: detail_to_collect,
field_type: number, text, t/f, select,
asset_type_detail_sel_options: {
option: option1,
option: option2,
}
}
```
__Code Table: Asset Disposition__
```
asset_dispoitions: {
_id: dispo_id,
tenant_id: tenant_id,
disposition: disposition,
is_final: is_a_final_dispo (t/f),
archive_with_this_disp: t/f,
}
```
__Code Table: Asset Inspection Types__
```
asset_inspection_type: {
_id: inspection_id,
tenant_id: tenant_id,
insp_type_name: name
}
```
__Code Table: Asset Inspection Options__
```
asset_inspection_name: {
_id: inspection_id,
tenant_id: tenant_id,
insp_name: name,
insp_type: item_inspection_id
}
```
__Code Table: Asset Attribute__
```
asset_attribute_name: {
_id: attribute_id,
tenant_id: tenant_id,
attribute_name: name, # e.g. color, texture, etc
attribute_req: t/f,
attribute_applies_to_asset_types: <multi-select types>
}
```
----
## Asset Inspections
Inspection Type
Inspection Steps
- Step Type (e.g. pass, fail, measure, grade, etc)
- Step instructions
- Observations
- Evidence (images, notes, video, data, etc.)
Multi-part Asset Inspection?
Signature of completion
Signature of acceptance
Signature of reviewer
Singature of secondary inspectors
----
## Asset Inventory
----
## Asset Movements
Movements covers a wide array of actions, including but not limited to moving an item from 1 place to another, moving an item in a way that the item is used up (e.g. using a needle, flex-cuff, etc), movement for destruction, movement for retirement, etc. These types can be defined by the tenant.
#### Movement Record
```
asset_movement: {
_id: asset_move_id,
tenant_id: tenant_id,
asset_id: asset_id,
movement_type: movement_type_id,
movement_end
}
```
```
movement_type: {
_id: movement_id,
tenant_id: tenant_id,
movement_name: name,
movement_desc: description,
req_end_location: t/f,
req_usage_qty: t/f,
}
```
----
##

View file

@ -33,5 +33,16 @@
</div>
</article>
</div>
<div class="grid">
<article>
<h4>Code Setup</h4>
<p>Setup Code Tables (pre-defined values)</p>
<div class="grid">
<div>
<button class="locationTypes navSetup" id="locationTypes">Location Types</button>
</div>
</div>
</article>
</div>
{{> snackbar}}
</template>

View file

@ -1,6 +1,7 @@
import { Roles } from 'meteor/roles';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { SysConfig } from "../../../imports/api/systemConfig";
Template.systemAdmin.onCreated(function() {
this.subscribe("SystemConfig");
this.subscribe("rolesAvailable");
@ -59,4 +60,8 @@ Template.systemAdmin.events({
updateInfo();
},
'click .navSetup' (event) {
let target = event.target.id;
FlowRouter.go('/' + target);
}
});

View file

@ -1,6 +1,6 @@
<template name="home">
{{#if currentUser}}
<h1>This is Home.</h1>
<h1>Dashboard</h1>
{{else}}
{{#if $eq loginOrReg 'login'}}
{{> login}}

View file

@ -0,0 +1,62 @@
/* Basic styling for the navigation panel */
.sidenav {
height: 100%;
width: 250px;
position: fixed;
z-index: 1;
top: 0;
left: -250px;
background-color: #333;
overflow-x: hidden;
transition: 0.5s;
}
.sidenav a {
padding: 16px 8px 16px 32px;
text-decoration: none;
font-size: 20px;
color: #f1f1f1;
display: block;
transition: 0.3s;
}
.sidenav a:hover {
background-color: #555;
}
/* Button to toggle the sidenav */
.openbtn {
cursor: pointer;
}
.openbtn:hover {
background-color: #444;
}
.closeBtn {
position: absolute;
top: 10px;
right: 25px;
font-size: 24px !important;
margin-left: 50px;
cursor: pointer;
}
.closeBtn:hover {
background-color: #444;
}
/* Top section for title and other information */
.top-section {
padding: 30px;
text-align: center;
background-color: #555;
color: white;
font-size: 24px;
}
/* On smaller screens, reduce the size of the sidenav button */
@media screen and (max-height: 450px) {
.sidenav {padding-top: 15px;}
.sidenav a {font-size: 18px;}
}

View file

@ -1,24 +1,35 @@
<template name="headerBar">
<nav>
<ul>
<li><h2>[Project Name]</h2></li>
<li><i class="material-icons openbtn" id="openNav">menu</i></li>
</ul>
<ul>
{{#if currentUser}}
{{#if isInRole "systemadmin"}}
<li><a href="#" id="manage" class="navBtn">Manage</a></li>
{{/if}}
<li class="signOut"><a href="#" class="signOut">Log Out</a></li>
{{#if $eq updateExists true}}
<li><a href="#!"><i class="material-icons" id='dashboard'>notifications</i></a></li>
{{/if}}
{{else}}
{{#if $eq loginOrReg 'login'}}
<li><a href="#!" id="reg" class="navBtn">Register</a></li>
{{else}}
<li><a href="#!" id="login" class="navBtn">Sign In</a></li>
{{/if}}
{{/if}}
<li><h1>Open Asset</h1></li>
</ul>
<ul>
<li></li>
</ul>
</nav>
<div id="mySidenav" class="sidenav">
<div class="top-section">Menu<i class="material-icons closeBtn" id="closeNav">close</i></div>
{{#if currentUser}}
<a href="#" class="navBtn" id="home">Home</a>
{{#if isInRole "systemadmin"}}
<a href="#" id="manage" class="navBtn">Manage</a>
{{/if}}
<a href="#">Clients</a>
<hr>
<a href="#" class="signOut">Sign Out</a>
{{else}}
{{#if $eq loginOrReg 'login'}}
<a href="#!" id="reg" class="navBtn">Register</a>
{{else}}
<a href="#!" id="login" class="navBtn">Sign In</a>
{{/if}}
{{/if}}
</div>
</template>

View file

@ -56,5 +56,13 @@ Template.headerBar.events({
'click #brandLogo' (event) {
event.preventDefault();
// FlowRouter.go('/dashboard');
}
},
'click #openNav' (event) {
let mySidenav = document.getElementById("mySidenav");
mySidenav.style.left = "0";
},
'click #closeNav' (event) {
let mySidenav = document.getElementById("mySidenav");
mySidenav.style.left = "-250px";
},
});

View file

@ -0,0 +1,23 @@
<template name="locationTypeTbl">
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#each types}}
<tr>
<td>{{locationTypeName}}</td>
<td>{{locationTypeDesc}}</td>
<td>
<i class="material-icons">edit</i>
<i class="material-icons">delete</i>
</td>
</tr>
{{/each}}
</tbody>
</table>
</template>

View file

@ -0,0 +1,21 @@
import { Roles } from 'meteor/roles';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { LocationTypes } from '../../imports/api/locationTypes.js';
Template.locationTypeTbl.onCreated(function() {
this.subscribe("LocationTypes");
});
Template.locationTypeTbl.onRendered(function() {
});
Template.locationTypeTbl.helpers({
types: function() {
return LocationTypes.find({});
},
});
Template.locationTypeTbl.events({
});

View file

@ -0,0 +1,21 @@
<template name="locationTypes">
<h1>Location Type Setup</h1>
<div class="grid">
<div>
<label for="typeName">Location Type Name *</label>
<input type="text" class="typeName" id="typeName" required />
</div>
<div>
<label for="typeDesc">Location Type Description</label>
<input type="text" class="typeDesc" id="typeDesc" />
</div>
</div>
<div class="grid">
<div>
<button class="primary right" id="saveLocationType">Save</button>
</div>
</div>
<hr>
{{> locationTypeTbl}}
{{> snackbar}}
</template>

View file

@ -0,0 +1,38 @@
import { Roles } from 'meteor/roles';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { LocationTypes } from '../../imports/api/locationTypes.js';
Template.locationTypes.onCreated(function() {
this.subscribe("LocationTypes");
});
Template.locationTypes.onRendered(function() {
});
Template.locationTypes.helpers({
});
Template.locationTypes.events({
'click #saveLocationType' (event) {
event.preventDefault();
let typeName = $("#typeName").val();
let typeDesc = $("#typeDesc").val();
if (typeName == null || typeName == "") {
return;
} else {
const addLocation = async() => {
const result = await Meteor.callAsync("add.locationType", typeName, typeDesc);
if (!result) {
showSnackbar("Location Type Failed!", "red");
} else {
showSnackbar("Location Type Added!", "green");
}
}
addLocation();
}
}
});

View file

@ -0,0 +1,4 @@
<template name="locations">
<h1>Locations</h1>
</template>

View file

@ -0,0 +1,19 @@
import { Roles } from 'meteor/roles';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
// import { Locations } from '../../imports/api/locations.js';
Template.locations.onCreated(function() {
});
Template.locations.onRendered(function() {
});
Template.locations.helpers({
});
Template.locations.events({
});

View file

@ -3,7 +3,6 @@
<div class="container">
{{> headerBar}}
{{> Template.dynamic template=main}}
</div>
{{/if}}
</template>

28
imports/api/assets.js Normal file
View file

@ -0,0 +1,28 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
export const Assets = new Mongo.Collection('assets');
Assets.allow({
insert: function(userId, doc){
// if use id exists, allow insert
return !!userId;
},
});
Meteor.methods({
async 'add.asset' (assetName {
check(assetName, String);
if (!this.userId) {
throw new Meteor.Error('You are not allowed to add assets. Make sure you are logged in with valid user credentials.');
}
return await Assets.insertAsync({
assetName: assetName,
tenant_id: "000000",
});
},
});

View file

@ -0,0 +1,30 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
export const LocationTypes = new Mongo.Collection('locationTypes');
LocationTypes.allow({
insert: function(userId, doc){
// if use id exists, allow insert
return !!userId;
},
});
Meteor.methods({
async "add.locationType" (typeName, typeDesc) {
check(typeName, String);
check(typeDesc, String);
if (!this.userId) {
throw new Meteor.Error('You are not allowed to add location types. Make sure you are logged in with valid user credentials.');
}
return await LocationTypes.insertAsync({
locationTypeName: typeName,
locationTypeDesc: typeDesc,
dateAdded: new Date(),
addedBy: this.userId,
});
}
});

16
imports/api/locations.js Normal file
View file

@ -0,0 +1,16 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
export const Locations = new Mongo.Collection('locations');
Locations.allow({
insert: function(userId, doc){
// if use id exists, allow insert
return !!userId;
},
});
Meteor.methods({
});

0
imports/api/tenants.js Normal file
View file

View file

@ -6,11 +6,23 @@ export const UserInfo = new Mongo.Collection('userInfo');
UserInfo.allow({
insert: function(userId, doc){
// if use id exists, allow insert
// if user id exists, allow insert
return !!userId;
},
});
Meteor.methods({
async 'addUserToTenant' (userId, tenant_id) {
check(userId, String);
check(tenant_id, String);
if (!this.userId) {
throw new Meteor.Error('You are not allowed to add tenants for users. Make sure you are logged in with valid user credentials.');
}
return await UserInfo.insertAsync({
userId: userId,
tenant_id: tenant_id,
});
},
});

View file

@ -48,3 +48,17 @@ FlowRouter.route('/userMgmt', {
this.render('MainLayout', { main: "userMgmt" });
}
});
FlowRouter.route('/locations', {
name: 'locations',
action() {
this.render('MainLayout', { main: "locations" });
}
});
FlowRouter.route('/locationTypes', {
name: 'locationtypes',
action() {
this.render('MainLayout', { main: "locationTypes" });
}
});

View file

@ -11,9 +11,12 @@ Meteor.methods({
if (user) {
let userId = user._id;
if (countOfUsers > 1) {
await Roles.addUsersToRolesAsync(userId, role);
let addedRole = await Roles.addUsersToRolesAsync(userId, role);
} else if (countOfUsers == 1) {
await Roles.addUsersToRolesAsync(userId, "systemadmin");
let addedRole = await Roles.addUsersToRolesAsync(userId, "systemadmin");
if (addedRole) {
await Meteor.callAsync('addUserToTenant', userId, "000001");
}
} else {
console.log("The count of users didn't seem to work when adding a new user.");
}

View file

@ -2,6 +2,8 @@ import { SysConfig } from "../imports/api/systemConfig";
import { UpdateInfo } from "../imports/api/updateInfo";
import { MScripts } from "../imports/api/mScripts";
import { UserInfo } from "../imports/api/userInfo";
import { Locations } from "../imports/api/locations";
import { LocationTypes } from "../imports/api/locationTypes";
import { Roels } from "meteor/roles";
Meteor.publish("SystemConfig", function() {
@ -34,3 +36,17 @@ Meteor.publish(null, function () {
}
this.ready();
});
Meteor.publish("Locations", function () {
if (this.userId) {
return Locations.find({});
}
this.ready();
});
Meteor.publish("LocationTypes", function () {
if (this.userId) {
return LocationTypes.find({});
}
this.ready();
});