Added a data entry page with initial content.

This commit is contained in:
2022-07-12 11:26:36 -07:00
parent 117bd8cd1a
commit c762581608
9 changed files with 440 additions and 183 deletions

View File

@@ -51,32 +51,37 @@ Meteor.methods({
check(assetTypeId, String);
check(assetId, String);
check(serial, String);
// Convert the asset ID's to uppercase for storage to make searching easier.
assetId = assetId.toUpperCase();
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
let assetType = AssetTypes.findOne({assetTypeId});
if(assetType.hasSerial && serial || !assetType.hasSerial && !serial) {
if(serial) {
Assets.insert({assetTypeId, assetId, serial});
}
else {
Assets.insert({assetTypeId, assetId});
}
if(serial) {
Assets.insert({assetTypeId, assetId, serial});
}
else {
//Should never get here due to client side validation.
console.log("Error: Must provide a serial for asset types marked as having one, and may not provide one for asset types not marked as having one.")
Assets.insert({assetTypeId, assetId});
}
}
},
'assets.update'(_id, assetId, serial) {
//TODO:
check(_id, String);
check(assetId, String);
if(serial) check(serial, String);
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
//TODO: Need to first verify there are no checked out assets to the staff member.
Assets.update({_id}, {$set: {assetId, serial}});
}
},
'assets.remove'(_id) {
check(_id, String);
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
//TODO: Need to first verify there are no checked out assets to the staff member.
Assets.remove({_id});
}
},
});

View File

@@ -237,7 +237,7 @@
<div class="container">
<h2>Sites</h2>
<GridTable bind:rows={sites} columns="{siteColumns}" actions="{actions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedSite}" on:selection={onSiteSelection}>
<GridTable bind:rows={sites} columns="{siteColumns}" actions="{siteActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedSite}" on:selection={onSiteSelection}>
{#if dirtySite}
<div class="editorContainer">
<div style="grid-column: 1/span 1">

View File

@@ -1,164 +1,26 @@
<script>
import Tab, { Label } from '@smui/tab';
import TabBar from '@smui/tab-bar';
import {Meteor} from "meteor/meteor";
import {onMount} from "svelte";
import {writable} from "svelte/store";
import TextField from '@smui/textfield';
import HelperText from '@smui/textfield/helper-text';
import Select, { Option } from '@smui/select';
import {AssetTypes} from "../api/asset-types";
import { useTracker } from 'meteor/rdb:svelte-meteor-data';
import GridTable from "./GridTable.svelte";
import {Assets} from "../api/assets";
import AssetList from "/imports/ui/Assets/AssetList.svelte";
import AssetDataEntry from "/imports/ui/Assets/AssetDataEntry.svelte";
onMount(async () => {
Meteor.subscribe('assetTypes');
});
let assetTypes;
//$m: assetTypes = AssetTypes.find({}).fetch();
//$: assetTypes = useTracker(() => AssetTypes.find({}).fetch());
$: assetTypes = AssetTypes.find({});
let selectedType = null;
const selectAssetType = (assetType) => {
selectedType = assetType;
}
// Asset Table //
const assetColumns = [
{
key: "assetId",
title: "Asset ID",
value: v => v.assetId,
minWidth: 100,
weight: 1,
cls: "assetId",
}, {
key: "serial",
title: "Serial",
value: v => v.serial,
minWidth: 100,
weight: 1,
cls: "serial",
},
];
const assetActions = {
title: "Actions",
headerWidgets: [
{icon: "add_box", action: () => {editedAsset.set({});}, tooltip: "Add a new asset."}
],
rowWidgets: [
{icon: "add_circle", action: (v) => {editedAsset.set(v)}},
{icon: "delete", action: (v) => {deleteAsset(v)}}
],
};
const deleteAsset = asset => {
//TODO:
};
// Create a holder for the site being edited. This allows us to clear the editor when the user finishes, and allows the table or parent view to setup the editor.
let editedAsset = writable(null);
let dirtyAsset = null;
// Copy the edited site when ever it changes, set some defaults for a new site object (to make the view happy).
editedAsset.subscribe(site => {dirtyAsset = Object.assign({serial: "", assetId: "", assetTypeId: ""}, site)});
// Load the sites (reactive).
let assets = Assets.find({});
const applyAssetChanges = () => {
if(dirtyAsset._id)
Meteor.call("assets.update", dirtyAsset._id, dirtyAsset.serial, dirtyAsset.assetId);
else
Meteor.call("assets.add", dirtyAsset.serial, dirtyAsset.assetId, dirtyAsset.assetTypeId);
editedAsset.set(null);
}
const rejectAssetChanges = () => {
editedAsset.set(null);
}
let selectedAsset = null;
const onAssetSelection = (e) => {
selectedAsset = Assets.findOne({_id: e.detail});
}
// End Asset Table //
let activeTab = null;
</script>
<div class="container">
<div class="listContainer">
<h2>Asset Types</h2>
<div class="list">
{#each $assetTypes as type}
<div class="listItem" class:selected={selectedType === type} on:click={(e) => {selectAssetType(type)}}>
<div class="listItemAssetTypeName">{type.name}</div>
<div class="listItemAssetTypeDescription">{type.description}</div>
</div>
{/each}
</div>
</div>
<div class="assetsContainer">
<h2>Assets</h2>
<GridTable bind:rows={assets} columns="{assetColumns}" actions="{assetActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedAsset}" on:selection={onAssetSelection}>
{#if dirtyAsset}
<div class="editorContainer">
<Select bind:value={dirtyAsset.assetTypeId} label="Select Menu">
{#each $assetTypes as assetType}
<Option value={assetType._id}>{assetType.name}</Option>
{/each}
</Select>
<div style="grid-column: 1/span 1">
<TextField type="text" style="width: 100%" bind:value={dirtyAsset.assetId} label="AssetId">
<HelperText slot="helper">Provide a unique asset ID string.</HelperText>
</TextField>
</div>
<div style="grid-column: 1/span 1">
<TextField type="text" style="width: 100%" bind:value={dirtyAsset.serial} label="Serial">
<HelperText slot="helper">Provide a unique serial string for the asset.</HelperText>
</TextField>
</div>
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applyAssetChanges}>
check
</button>
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectAssetChanges}>
close
</button>
</div>
{/if}
</GridTable>
</div>
<TabBar tabs={[{id:'list', label:'Asset List'}, {id:'entry', label:'Data Entry'}]} minWidth let:tab bind:active={activeTab}>
<Tab {tab}>
<Label>{tab.label}</Label>
</Tab>
</TabBar>
{#if activeTab && activeTab.id === 'list'}
<AssetList></AssetList>
{:else if activeTab && activeTab.id === 'entry'}
<AssetDataEntry></AssetDataEntry>
{/if}
</div>
<style>
.listContainer {
display: inline-block;
margin: 1rem;
max-width: 500px;
}
div.listContainer h2 {
margin: 0;
}
.list {
border: 2px solid #4f4e4e;
padding: 1rem;
}
.listItem:nth-child(even) {
/*background: #f6f6f6;*/
background: white;
}
.listItem {
margin: 4px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
border-bottom: 1px solid gray;
}
.listItem:last-child {
border: 0;
}
.listItem.selected {
background-color: rgba(255, 255, 0, 0.38);
}
.assetsContainer {
display: inline-block;
}
</style>

View File

@@ -0,0 +1,172 @@
<script>
import {Meteor} from "meteor/meteor";
import {onMount} from "svelte";
import {writable} from "svelte/store";
import Select, { Option } from '@smui/select';
import Dialog, { Title, Content, Actions } from '@smui/dialog';
import Button, { Label } from '@smui/button';
import List, {Item,Text,PrimaryText,SecondaryText} from '@smui/list';
import TextField from '@smui/textfield';
import u from 'umbrellajs';
import {AssetTypes} from "/imports/api/asset-types";
onMount(async () => {
Meteor.subscribe('assetTypes');
Meteor.subscribe('assets');
});
let assetTypes;
//$m: assetTypes = AssetTypes.find({}).fetch();
//$: assetTypes = useTracker(() => AssetTypes.find({}).fetch());
$: assetTypes = AssetTypes.find({});
let selectedAssetTypes = [];
let selectedType = null;
const selectAssetType = (assetType) => {
selectedType = assetType;
}
const showAddAssetType = () => {
}
let openAddAssetTypeButton;
let isOpen_AddAssetType = false;
const openAddAssetType = () => {
isOpen_AddAssetType = !isOpen_AddAssetType;
//openAddAssetTypeButton.innerHTML = isOpen_AddAssetType ? "close" : "add";
if(isOpen_AddAssetType) {
openAddAssetTypeButton.classList.add("reject-button");
}
else {
openAddAssetTypeButton.classList.remove("reject-button");
}
}
let selectedAddAssetType = null;
let openDialog = false;
let assetTypeDialogSelectedIndex;
let assetTypeDialogSelectedValue;
const openAssetTypesDialog = () => {
assetTypeDialogSelectedIndex = -1;
openDialog = true;
}
const dialogClosed = (e) => {
switch(e.detail.action) {
case 'accept':
if(!selectedAssetTypes.some(e => e._id === assetTypeDialogSelectedValue._id)) {
selectedAssetTypes = [...selectedAssetTypes, assetTypeDialogSelectedValue];
}
break;
default:
case 'cancel':
break;
}
}
let assetId = "";
let serial = "";
const addAsset = () => {
Meteor.call("assets.add", selectedAssetType._id, assetId, serial);
assetId = "";
serial = "";
}
let selectedAssetType = null;
</script>
<div class="listContainer">
<Dialog bind:open={openDialog} surface$style="width: 850px; max-width: calc(100vw - 32px);" on:SMUIDialog:closed={dialogClosed}>
<Title id="large-scroll-title">Asset Types</Title>
<Content id="large-scroll-content">
<List class="assetTypesList" twoLine singleSelection bind:selectedIndex={assetTypeDialogSelectedIndex}>
{#each $assetTypes as type}
<Item on:SMUI:action={() => (assetTypeDialogSelectedValue = type)} selected={assetTypeDialogSelectedValue === type}>
<Text>
<PrimaryText>{type.name}</PrimaryText>
<SecondaryText>{type.description}</SecondaryText>
</Text>
</Item>
{/each}
</List>
</Content>
<Actions>
<Button action="accept" default>
<Label>Ok</Label>
</Button>
<Button action="cancel">
<Label>Cancel</Label>
</Button>
</Actions>
</Dialog>
<div class="columnContainer">
<div class="listColumn">
<h3 style="display: inline-block">Asset Types</h3>
<Button class="addBtn" on:click={openAssetTypesDialog}>
<Label>Add...</Label>
</Button>
<List class="assetTypeList" singleSelection dense>
{#each selectedAssetTypes as type}
<Item on:SMUI:action={() => (selectedAssetType = type)} selected={selectedAssetType === type}>
<Text>{type.name}</Text>
</Item>
{/each}
</List>
</div>
<div class="fieldColumn">
<div style="grid-column: 1/span 1">
<TextField type="text" style="width: 100%" bind:value={assetId} label="AssetId">
</TextField>
</div>
<div style="grid-column: 1/span 1">
<TextField type="text" style="width: 100%" bind:value={serial} label="Serial">
</TextField>
</div>
</div>
</div>
<Button on:click={addAsset}>Add</Button>
</div>
<style>
.columnContainer {
display: grid;
grid-template-columns: minmax(20rem, 1fr) minmax(20rem, 1fr);
}
:global(.addBtn) {
margin-left: 4rem;
}
:global(.assetTypeList) {
--mdc-ripple-color: blue;
--mdc-ripple-selected-opacity: 0.2;
}
.listContainer {
display: inline-block;
margin: 1rem;
max-width: 500px;
}
div.listContainer h2 {
margin: 0;
}
.list {
//border: 2px solid #4f4e4e;
}
.listItem:nth-child(even) {
/*background: #f6f6f6;*/
background: white;
}
.listItem {
margin: 4px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
padding-bottom: 1rem;
padding-top: 0.4rem;
border-bottom: 1px solid gray;
}
.listItem:last-child {
border: 0;
}
.listItem.selected {
background-color: rgba(255, 255, 0, 0.38);
}
</style>

View File

@@ -0,0 +1,165 @@
<script>
import {Meteor} from "meteor/meteor";
import {onMount} from "svelte";
import {writable} from "svelte/store";
import TextField from '@smui/textfield';
import HelperText from '@smui/textfield/helper-text';
import Select, { Option } from '@smui/select';
import {AssetTypes} from "../../api/asset-types";
import { useTracker } from 'meteor/rdb:svelte-meteor-data';
import GridTable from "./../GridTable.svelte";
import {Assets} from "../../api/assets";
import u from 'umbrellajs';
onMount(async () => {
Meteor.subscribe('assetTypes');
Meteor.subscribe('assets');
});
let assetTypes;
$: assetTypes = AssetTypes.find({});
// Asset Table //
const assetColumns = [
{
key: "assetId",
title: "Asset ID",
value: v => v.assetId,
minWidth: 100,
weight: 1,
cls: "assetId",
}, {
key: "serial",
title: "Serial",
value: v => v.serial ? v.serial : "-",
minWidth: 100,
weight: 1,
cls: "serial",
},
];
const assetActions = {
title: "",
headerWidgets: [
{icon: "add_box", action: () => {editedAsset.update((v) => {return v ? null : {}})}, tooltip: "Add a new asset."}
],
rowWidgets: [
{icon: "add_circle", action: (v) => {editedAsset.set(v)}},
{icon: "delete", action: (v) => {deleteAsset(v)}}
],
};
const deleteAsset = asset => {
if(asset && asset._id) Meteor.call("assets.remove", asset._id);
};
// Create a holder for the site being edited. This allows us to clear the editor when the user finishes, and allows the table or parent view to setup the editor.
let editedAsset = writable(null);
let dirtyAsset = null;
// Copy the edited site when ever it changes, set some defaults for a new site object (to make the view happy).
editedAsset.subscribe(site => {
if(site) {
dirtyAsset = Object.assign({serial: "", assetId: "", assetTypeId: ""}, site);
//document.getElementsByClassName('select').focus();
}
else dirtyAsset = null;
});
// Load the sites (reactive).
let assets = Assets.find({});
const applyAssetChanges = () => {
if(dirtyAsset._id)
Meteor.call("assets.update", dirtyAsset._id, dirtyAsset.assetId, dirtyAsset.serial);
else
Meteor.call("assets.add", dirtyAsset.assetTypeId, dirtyAsset.assetId, dirtyAsset.serial);
editedAsset.set(null);
}
const rejectAssetChanges = () => {
editedAsset.set(null);
}
let selectedAsset = null;
const onAssetSelection = (e) => {
selectedAsset = Assets.findOne({_id: e.detail});
}
let editorFirstComponent = null;
$: if(editorFirstComponent) editorFirstComponent.focus();
const initEditor = (e) => {
//editorFirstComponent.focus();
//document.getElementsByClassName("focusMe")[0].children[0].children[2].focus();
// u('.focusMe .mdc-select__selected-text').first().focus();
//$('.select').focus();
}
// End Asset Table //
</script>
<div class="container">
<div class="assetsContainer">
<h2>Assets</h2>
<GridTable bind:rows={assets} columns="{assetColumns}" actions="{assetActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedAsset}" on:selection={onAssetSelection}>
{#if dirtyAsset}
<div class="editorContainer" use:initEditor>
<div style="grid-column: 1/span 1">
<Select class="focusMe select" bind:value={dirtyAsset.assetTypeId} label="Asset Type" bind:this={editorFirstComponent}>
{#each $assetTypes as assetType}
<Option value={assetType._id}>{assetType.name}</Option>
{/each}
</Select>
</div>
<div style="grid-column: 1/span 1">
<TextField type="text" style="width: 100%" bind:value={dirtyAsset.assetId} label="AssetId">
</TextField>
</div>
<div style="grid-column: 1/span 1">
<TextField type="text" style="width: 100%" bind:value={dirtyAsset.serial} label="Serial">
</TextField>
</div>
<div style="grid-column: 1/span 1; white-space: nowrap; text-align: right; margin-top: 0.5rem">
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applyAssetChanges}>
check
</button>
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectAssetChanges}>
close
</button>
</div>
</div>
{/if}
</GridTable>
</div>
</div>
<style>
.assetsContainer {
display: block;
}
.editorContainer {
display: grid;
grid-template-columns: minmax(10px, 1fr);
column-gap: 2rem;
background-color: #e0e3e7;
padding: 1rem;
}
:global(.select) {
width: 100%;
}
.accept-button, .reject-button {
font-size: .8rem;
padding: .6rem;
margin: auto;
color: white;
border-radius: 50%;
border: 0;
font-weight: 800;
alignment: center;
cursor: pointer;
}
.accept-button {
background-color: rgba(61, 148, 61, 0.91);
}
.reject-button {
background-color: rgba(176, 64, 64, 0.61);
}
.accept-button:hover {
background-color: rgb(61, 148, 61);
}
.reject-button:hover {
background-color: rgb(176, 64, 64);
}
</style>