Admin can manage shipments

main
Ashelyn Dawn 4 years ago
parent 3a7a6959e9
commit 5b506417bc

@ -23,6 +23,7 @@ router.use('/items/', require('./items'))
router.use('/images/', require('./images'))
router.use('/categories/', require('./categories'))
router.use('/orders/', require('./orders'))
router.use('/shipments/', require('./shipments'))
router.use((req, res, next)=>{
const err = new Error('Not found')

@ -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)
})

@ -31,7 +31,7 @@ export default function Card({item}) {
<li className={styles['number-in-stock']}>
{
item.number_in_stock > 0
? `${item.number_in_stock} pair${item.number_in_stock > 1 ? 's':''} in stock`
? `${item.number_in_stock} in stock`
: 'Currently out of stock'
}
</li>

@ -12,7 +12,7 @@ export default function Table({columns, rows, foot}) {
</thead>
{rows && rows.length > 0 && <tbody>
{rows.map(row=>
<tr key={row.id} className={row.class}>
<tr key={row.id || row.uuid} className={row.class}>
{columns.map(column=>
<td key={column.name}>{column.extractor(row)}</td>
)}

@ -8,5 +8,6 @@ module.exports = {
order: require('./models/order'),
address: require('./models/address'),
coupon: require('./models/coupon'),
config: require('./models/config')
config: require('./models/config'),
shipment: require('./models/shipment')
}

@ -2,5 +2,6 @@ module.exports = [
...require('./config'),
...require('./user'),
...require('./item'),
...require('./order')
...require('./order'),
...require('./stockchange')
]

@ -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
})

@ -130,3 +130,18 @@ create or replace view sos.v_config as
select * from sos."config"
left join sos."user" on config_updated_by = user_uuid
where config_date_updated = (select max(config_date_updated) from sos."config");
create or replace view sos.v_stockchange as
select
item_stockchange.*,
item_stockchange_shipment.stockchange_shipment_uuid,
item_stockchange_purchase.stockchange_transaction_uuid,
item_stockchange_admin.stockchange_withdrawal_uuid
from sos."item_stockchange"
left join sos."item_stockchange_shipment" on item_stockchange.stockchange_uuid = item_stockchange_shipment.stockchange_uuid
left join sos."item_stockchange_purchase" on item_stockchange.stockchange_uuid = item_stockchange_purchase.stockchange_uuid
left join sos."item_stockchange_admin" on item_stockchange.stockchange_uuid = item_stockchange_admin.stockchange_uuid;
create or replace view sos.v_shipment as
select * from sos."shipment"
left join sos.v_stockchange on v_stockchange.stockchange_shipment_uuid = shipment_uuid;

@ -848,3 +848,51 @@ begin
return query select * from sos.find_order_for_transaction(_transaction_uuid);
end; $function$;
create or replace function sos.create_shipment(_shipment_description text, _item_uuids uuid[], _counts integer[])
returns setof sos.v_shipment
language plpgsql
as $function$
declare
_shipment_uuid uuid;
_stockchange_uuid uuid;
begin
-- Check that the arrays are the same length
if array_length(_item_uuids, 1) != array_length(_counts, 1) then
raise 'Arrays are not of the same length';
end if;
-- Create shipment
insert into sos."shipment" (
shipment_description
) values (
_shipment_description
) returning shipment_uuid into _shipment_uuid;
-- Create all the item stockchanges
for i in array_lower(_item_uuids, 1) .. array_upper(_item_uuids, 1) loop
insert into sos."item_stockchange" (
stockchange_type,
stockchange_item_uuid,
stockchange_change,
stockchange_direction
) values (
'shipment',
_item_uuids[i],
_counts[i],
'added'
) returning stockchange_uuid into _stockchange_uuid;
insert into sos."item_stockchange_shipment" (
stockchange_uuid,
stockchange_type,
stockchange_shipment_uuid
) values (
_stockchange_uuid,
'shipment',
_shipment_uuid
);
end loop;
return query select * from sos.v_shipment where shipment_uuid = _shipment_uuid;
end; $function$;

@ -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 &ldquo;{currentItem.name}&rdquo; 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…
Cancel
Save