Admin can manage shipments
parent
3a7a6959e9
commit
5b506417bc
@ -0,0 +1,19 @@
|
||||
const router = module.exports = require('express-promise-router')()
|
||||
const parseJSON = require('body-parser').json()
|
||||
const db = require('../db')
|
||||
const ensureAdmin = require('./middleware/ensureAdmin')
|
||||
|
||||
router.get('/', ensureAdmin, async (req, res) => {
|
||||
const shipments = await db.shipment.findAll()
|
||||
res.json(shipments)
|
||||
})
|
||||
|
||||
router.get('/:uuid', ensureAdmin, async (req, res) => {
|
||||
const shipment = await db.shipment.findByUUID(req.params.uuid)
|
||||
res.json(shipment)
|
||||
})
|
||||
|
||||
router.post('/', ensureAdmin, parseJSON, async (req, res) => {
|
||||
const shipment = await db.shipment.createShipment(req.body.description, req.body.items)
|
||||
res.json(shipment)
|
||||
})
|
@ -0,0 +1,22 @@
|
||||
module.exports = [{
|
||||
mapId: 'shipmentMap',
|
||||
idProperty: 'uuid',
|
||||
properties: [
|
||||
'date',
|
||||
'description'
|
||||
],
|
||||
collections: [
|
||||
{name: 'stockchanges', mapId: 'stockchangeBareMap', columnPrefix: 'stockchange_'}
|
||||
]
|
||||
},{
|
||||
mapId: 'stockchangeBareMap',
|
||||
idProperty: 'uuid',
|
||||
properties: [
|
||||
'type',
|
||||
'direction',
|
||||
'change'
|
||||
],
|
||||
associations: [
|
||||
{name: 'item', mapId: 'itemMap', columnPrefix: 'item_'}
|
||||
]
|
||||
}]
|
@ -0,0 +1,36 @@
|
||||
const pg = require('../pg')
|
||||
const joinjs = require('join-js').default;
|
||||
const debug = require('debug')('sos:db:category')
|
||||
const mappings = require('../mappings')
|
||||
const dbUtil = require('../util')
|
||||
|
||||
const shipment = module.exports = {}
|
||||
|
||||
shipment.findAll = () =>
|
||||
dbUtil.executeQuery({
|
||||
query: 'select * from sos.v_shipment',
|
||||
returnType: 'shipment',
|
||||
single: false
|
||||
})
|
||||
|
||||
shipment.findByUUID = uuid =>
|
||||
dbUtil.executeQuery({
|
||||
query: {
|
||||
text: 'select * from sos.v_shipment where shipment_uuid = $1',
|
||||
values: [uuid]
|
||||
},
|
||||
returnType: 'shipment',
|
||||
single: true
|
||||
})
|
||||
|
||||
shipment.createShipment = (description, items) =>
|
||||
dbUtil.executeFunction({
|
||||
name: 'create_shipment',
|
||||
params: [
|
||||
description,
|
||||
items.map(item => item.uuid),
|
||||
items.map(item => item.count)
|
||||
],
|
||||
returnType: 'shipment',
|
||||
single: true
|
||||
})
|
@ -0,0 +1,28 @@
|
||||
import router from 'next/router'
|
||||
|
||||
import Link from 'next/link'
|
||||
import {Button} from '@rmwc/button'
|
||||
import AdminToolbar from '~/components/admin/actionBar'
|
||||
import Table from '~/components/table'
|
||||
|
||||
ShipmentDetails.getInitialProps = async ({ctx: {axios, query: {uuid}}}) => {
|
||||
const {data: shipment} = await axios.get(`/api/shipments/${uuid}`)
|
||||
return {shipment}
|
||||
}
|
||||
|
||||
export default function ShipmentDetails({shipment}){
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title="Shipment Details"/>
|
||||
<Link href={`/admin/shipments`}><a><Button icon="arrow_left">Back to shipments</Button></a></Link>
|
||||
<p> </p>
|
||||
<Table
|
||||
columns={[
|
||||
{name: 'Item', extractor: ({item}) => item.name},
|
||||
{name: 'Count', extractor: (row) => row.change}
|
||||
]}
|
||||
rows={shipment.stockchanges.map(row => ({...row, id: row.item.uuid}))}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import router from 'next/router'
|
||||
|
||||
import Link from 'next/link'
|
||||
import {Button} from '@rmwc/button'
|
||||
import AdminToolbar from '~/components/admin/actionBar'
|
||||
import Table from '~/components/table'
|
||||
|
||||
AdminShipments.getInitialProps = async ({ctx: {axios}}) => {
|
||||
const {data: shipments} = await axios.get('/api/shipments')
|
||||
return {shipments}
|
||||
}
|
||||
|
||||
export default function AdminShipments({shipments}){
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title="Shipments">
|
||||
<Button outlined onClick={() => router.push('/admin/shipments/new')}>Add Shipment</Button>
|
||||
</AdminToolbar>
|
||||
<Table
|
||||
columns={[
|
||||
{name: 'Note', extractor: shipment => shipment.description},
|
||||
{name: 'Date', extractor: shipment => shipment.date},
|
||||
{name: 'Quantity', extractor: ({stockchanges}) => {
|
||||
const totalAdded = stockchanges
|
||||
.filter(stockchange => stockchange.direction === 'added')
|
||||
.map(stockchange => stockchange.change)
|
||||
.reduce((a,b) => (a+b), 0)
|
||||
const totalRemoved = stockchanges
|
||||
.filter(stockchange => stockchange.direction === 'subtracted')
|
||||
.map(stockchange => stockchange.change)
|
||||
.reduce((a,b) => (a+b), 0)
|
||||
const totalQuantity = totalAdded - totalRemoved
|
||||
const totalItems = stockchanges.length
|
||||
|
||||
return (
|
||||
<span>
|
||||
{totalQuantity} added across {totalItems} items
|
||||
</span>
|
||||
)
|
||||
}},
|
||||
{name: '', extractor: shipment => (
|
||||
<Link href={`/admin/shipments/${shipment.uuid}`}><a>Details</a></Link>
|
||||
)}
|
||||
]}
|
||||
// Map in an id property so the table can use array.map
|
||||
rows={shipments.map(shipment => ({id: shipment.uuid, ...shipment}))}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
import {useState} from 'react'
|
||||
|
||||
import Link from 'next/link'
|
||||
import AdminToolbar from '~/components/admin/actionBar'
|
||||
import {Button as RButton} from '@rmwc/button'
|
||||
import {FormController, Input, IntegerInput, Button} from '~/components/form'
|
||||
import Table from '~/components/table'
|
||||
import axios from 'axios'
|
||||
import router from 'next/router'
|
||||
|
||||
NewShipment.getInitialProps = async ({ctx: {axios}}) => {
|
||||
const {data: allItems} = await axios.get('/api/items')
|
||||
return {allItems}
|
||||
}
|
||||
|
||||
export default function NewShipment({allItems}){
|
||||
const [currentView, setView] = useState('description')
|
||||
const [description, setDescription] = useState()
|
||||
const [items, setItems] = useState([])
|
||||
const [currentItem, setCurrentItem] = useState()
|
||||
|
||||
const currentUUIDs = items.map(({item}) => item.uuid)
|
||||
const addableItems = allItems.filter(({uuid}) => !currentUUIDs.includes(uuid))
|
||||
|
||||
const handleRemoveItem = item => {
|
||||
setItems(items => items.filter(listItem => item.uuid !== listItem.item.uuid))
|
||||
}
|
||||
|
||||
const handleAddItem = ({count}) => {
|
||||
const newItem = {
|
||||
count,
|
||||
item: currentItem
|
||||
}
|
||||
|
||||
setCurrentItem(null)
|
||||
|
||||
setItems(items => [...items.filter(listItem => currentItem.uuid !== listItem.item.uuid), newItem])
|
||||
setView('summary')
|
||||
}
|
||||
|
||||
const saveShipment = async () => {
|
||||
const {data: shipment} = await axios.post('/api/shipments', {
|
||||
description,
|
||||
items: items.map(listItem => ({count: listItem.count, uuid: listItem.item.uuid}))
|
||||
})
|
||||
|
||||
router.push(`/admin/shipments/${shipment.uuid}`)
|
||||
}
|
||||
|
||||
switch(currentView){
|
||||
case 'description':
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title="Add Shipment"/>
|
||||
<Link href={`/admin/shipments`}><a><RButton icon="arrow_left">Cancel Shipment</RButton></a></Link>
|
||||
<FormController afterSubmit={({description}) => {setDescription(description); setView('summary')}}>
|
||||
<h2>Set shipment description</h2>
|
||||
<Input label="Description" type="text" name="description" initialValue={description || undefined} validate={value=>value.length > 0} hint="Enter a shipment description" />
|
||||
<Button type="submit">Next</Button>
|
||||
</FormController>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'summary':
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title={`Add Shipment`}/>
|
||||
<div style={{margin: '0 16px', marginBottom: '16px', display: 'flex', flexDirection: 'row', justifyContent: 'space-between'}}>
|
||||
<RButton icon="arrow_left" onClick={()=>setView('description')}>Edit Description</RButton>
|
||||
<RButton icon="add" onClick={() => setView('selectItem')}>Add Item</RButton>
|
||||
</div>
|
||||
<Table
|
||||
columns={[
|
||||
{name: 'Item', extractor: item => item.item.name},
|
||||
{name: 'Count', extractor: item => item.count},
|
||||
{name: '', extractor: ({item}) => (
|
||||
<span>
|
||||
<button onClick={()=>{setCurrentItem(item); setView('addItem')}} className="buttonLink">Adjust count</button>
|
||||
<button onClick={()=>{handleRemoveItem(item)}} className="buttonLink">Remove</button>
|
||||
</span>
|
||||
)}
|
||||
]}
|
||||
// Map in an id property so the table can use array.map
|
||||
rows={items.map(listItem => ({...listItem, id: listItem.item.uuid}))}
|
||||
/>
|
||||
|
||||
<FormController afterSubmit={saveShipment}>
|
||||
<Button enabled={items.length > 0}>Create Shipment</Button>
|
||||
</FormController>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'selectItem':
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title={`Add Shipment`}/>
|
||||
|
||||
<div style={{margin: '0 16px', display: 'flex', flexDirection: 'row', justifyContent: 'space-between'}}>
|
||||
<RButton icon="arrow_left" onClick={()=>setView('summary')}>Cancel Adding Item</RButton>
|
||||
</div>
|
||||
|
||||
<h2 style={{textAlign: 'center'}}>Select Item</h2>
|
||||
<Table
|
||||
columns={[
|
||||
{name: 'Name', extractor: item => item.name},
|
||||
{name: '', extractor: item => (
|
||||
<span>
|
||||
<button onClick={()=>{setCurrentItem(item); setView('addItem')}} className="buttonLink">Use this item</button>
|
||||
</span>
|
||||
)}
|
||||
]}
|
||||
|
||||
rows={addableItems}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'addItem':
|
||||
if(currentItem === null){
|
||||
setView('selectItem')
|
||||
return null
|
||||
}
|
||||
|
||||
const currentValue = items.find(listItem => currentItem.uuid === listItem.item.uuid)?.count
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title={`Add Shipment`}/>
|
||||
|
||||
<div style={{margin: '0 16px', display: 'flex', flexDirection: 'row', justifyContent: 'space-between'}}>
|
||||
<RButton icon="arrow_left" onClick={()=>setView('summary')}>Cancel Adding Item</RButton>
|
||||
</div>
|
||||
|
||||
<FormController afterSubmit={handleAddItem}>
|
||||
<h2 style={{textAlign: 'center'}}>{currentValue !== undefined ? 'Adjust Item' : 'Add Item'}</h2>
|
||||
<p>How many of “{currentItem.name}” are in this shipment?</p>
|
||||
<IntegerInput initialValue={currentValue || 1} label="Count" name="count" minimum={1}/>
|
||||
<Button type="submit">Add Item</Button>
|
||||
</FormController>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<AdminToolbar title="Add Shipment"/>
|
||||
<Link href={`/admin/shipments`}><a><RButton icon="arrow_left">Cancel Shipment</RButton></a></Link>
|
||||
<p>An error occurred trying to create this shipment. Please refresh the page.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue