Packages can be marked shipped (manual tracking)

main
Ashelyn Dawn 5 years ago
parent 9081f2b5dc
commit 5648a1b31a

@ -157,3 +157,8 @@ router.post('/current/checkout/verify', ensureCart, parseJSON, async (req, res)
res.json({status: payment.status, order})
})
router.post('/:uuid/ship/tracking', ensureAdmin, parseJSON, async (req, res) => {
const order = await db.order.setTracking(req.params.uuid, req.body.code, req.body.date || null)
res.json(order)
})

@ -1,9 +1,18 @@
import Router from 'next/router'
import {Button} from '@rmwc/button'
import styles from './actionBar.module.css'
export default function AdminActionBar({title, children}) {
export default function AdminActionBar({title, actions, children}) {
return (
<div className={styles.actionBar}>
<h2>{title}</h2>
{actions && Array.isArray(actions) && actions.map(action => {
if(action.url)
return <Button outlined onClick={()=>Router.push(action.url)}>{action.label}</Button>
if(action.onClick)
return <Button outlined onClick={action.onClick}>{action.label}</Button>
return <Button outlined>{action.label}</Button>
})}
{children}
</div>
)

@ -25,4 +25,5 @@
.actionBar button:global(.mdc-button--outlined):not(:disabled) {
border-color: rgba(255,255,255,.44);
margin-left: 8px;
}

@ -3,12 +3,12 @@ import {Icon} from '@rmwc/icon'
import styles from './styles.module.css'
export default function Button({outline, onClick, enabled, type, children, icon: _icon}){
export default function Button({outline, onClick, enabled, type, children, icon: _icon, style}){
const icon = _icon && (typeof _icon === 'string' ? <Icon icon={_icon}/> : _icon)
return (
<div className={styles.formElementContainer + (outline ? ' ' + styles.outline : '')}>
<button className={icon && styles.buttonWithIcon} disabled={enabled === undefined ? false : !enabled} onClick={onClick} type={type}>
<button style={style} className={icon && styles.buttonWithIcon} disabled={enabled === undefined ? false : !enabled} onClick={onClick} type={type}>
{icon && <span className={styles.buttonIcon}>{icon}</span>}
{children}
</button>

@ -0,0 +1,50 @@
import React, {useState, useRef} from 'react'
import {Button} from './form'
import {DateTime} from 'luxon'
import InfiniteCalendar from 'react-infinite-calendar';
import Modal from '~/components/modal'
import styles from './styles.module.css'
// TODO: At some point make the date input better for accessibility
// (currently just skips tab index)
export default function Input({placeholder: _placeholder, label: _label, error, hint, name, value, onChange, onBlur, isValid}){
const label = (_label === undefined) ? name.replace(name[0], name[0].toUpperCase()) : _label
const displayedValue = value ? DateTime.fromISO(value).toFormat('LLLL dd, yyyy') : _placeholder
const inputRef = useRef()
const [modalShown, setModal] = useState(false)
const handleDateSelect = date => {
onChange({target: {value: DateTime.fromJSDate(date).toISO()}})
handleCancel()
}
const handleCancel = () => {
setModal(false)
setImmediate(()=>inputRef.current?.blur())
}
return (
<div className={styles.formElementContainer}>
{label && <label htmlFor={name}>{label}:</label>}
<div className={styles.complexInput + ((isValid && !error)?'':' ' + styles.invalid)}>
<input readOnly tabIndex={-1} style={{opacity: value ? 1 : .4}} ref={inputRef} onClick={()=>setModal(true)} type="text" name={name} value={displayedValue} onBlur={onBlur} />
</div>
{(hint || error) && <span className={styles.hint}>{error || (isValid ? '' : hint)}</span>}
<Modal title={`Select ${label}`} visible={modalShown} onDeactivate={handleCancel}>
<InfiniteCalendar
width={600}
height={400}
displayOptions={{
layout: 'landscape',
showTodayHelper: false
}}
maxDate={new Date()}
selected={value ? DateTime.fromISO(value).toJSDate() : undefined}
onSelect={handleDateSelect}
/>
</Modal>
</div>
)
}

@ -11,6 +11,7 @@ export const DecimalInput = require('./decimalInput.js').default
export const Button = require('./button.js').default
export const Checkbox = require('./checkbox.js').default
export const TextArea = require('./textArea.js').default
export const DateInput = require('./dateInput.js').default
const errorReducer = (errors, action) => {
switch (action.type) {

@ -0,0 +1,32 @@
import ReactDOM from 'react-dom'
import {Icon} from '@rmwc/icon'
import FocusTrap from 'focus-trap-react'
import styles from './modal.module.css'
export default function Modal({visible, title, children, onDeactivate}){
if(typeof window === 'undefined')
return null
return ReactDOM.createPortal((
<div className={styles.modalContainer + (visible?' ' + styles.modalShown:'')}>
<div className={styles.modalBackground}/>
{visible && (
<FocusTrap active={visible} focusTrapOptions={{onDeactivate}}>
<div className={styles.modalScroll} onClick={onDeactivate}>
<dialog open className={styles.modal}>
<div className={styles.titleContainer}>
<h2>{title}</h2>
<button className="buttonLink" onClick={onDeactivate}>
<Icon icon="close"/>
</button>
</div>
{children}
</dialog>
</div>
</FocusTrap>
)}
</div>
), window.document.body)
}

@ -0,0 +1,74 @@
.modalContainer, .modalBackground, .modalScroll {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
z-index: 10;
}
.modalContainer {
opacity: 0;
transition: opacity .1s;
pointer-events: none;
}
.modalShown {
opacity: 1;
transition: opacity .5s;
pointer-events: initial;
}
.modalBackground {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,.7);
}
.modalScroll {
overflow-y: auto;
cursor: pointer;
}
.modal {
border: none;
margin: 100px auto;
max-width: 600px;
width: calc(100vw - 50px);
background: white;
min-height: 100px;
animation: slidein .4s;
box-shadow: 0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);
}
@keyframes slidein {
0% {
margin-top: 80px;
}
100% {
margin-top: 100px;
}
}
.titleContainer {
height: 60px;
display: flex;
flex-direction: row;
border-bottom: solid 1px rgba(0,0,0,.1);
padding: 0 8px;
color: black;
}
.titleContainer > h2:first-child {
flex: 1;
}
.titleContainer > button:last-child {
color: black;
}

@ -7,7 +7,8 @@ module.exports = [{
],
associations: [
{name: 'user', mapId: 'userMap', columnPrefix: 'user_'},
{name: 'address', mapId: 'addressMap', columnPrefix: 'address_'}
{name: 'address', mapId: 'addressMap', columnPrefix: 'address_'},
{name: 'delivery', mapId: 'deliveryMap', columnPrefix: 'delivery_'}
],
collections: [
{name: 'transactions', mapId: 'transactionMap', columnPrefix: 'transaction_'}
@ -76,4 +77,15 @@ module.exports = [{
properties: [
'reciept_email'
]
},{
mapId: 'deliveryMap',
idProperty: 'uuid',
properties: [
'type',
'tracking_number',
'date_shipped',
'easypost_id',
'description',
'date_delivered'
]
}]

@ -242,3 +242,11 @@ order.addPayment = async function(transaction, paymentIntent){
return order
}
order.setTracking = (uuid, trackingCode, shipDate) =>
dbUtil.executeFunction({
name: 'set_delivery_tracking',
params: [uuid, trackingCode, shipDate],
returnType: 'order',
single: true
})

@ -116,7 +116,7 @@ create table sos."delivery_hand_shipped" (
foreign key (delivery_uuid, delivery_type) references sos."delivery" (delivery_uuid, delivery_type),
delivery_tracking_number text not null,
delivery_date_shipped timestamptz not null
delivery_date_shipped timestamptz not null default now()
);
create table sos."delivery_easypost" (

@ -102,7 +102,19 @@ create or replace view sos.v_transaction_paid as
left join sos.v_payment on transaction_uuid = payment_transaction_uuid
group by transaction_uuid;
-- TODO: add coupon, delivery
create or replace view sos.v_delivery as
select
"delivery".*,
coalesce("delivery_hand_shipped".delivery_tracking_number, "delivery_easypost".delivery_tracking_number) as delivery_tracking_number,
coalesce("delivery_hand_shipped".delivery_date_shipped, "delivery_easypost".delivery_date_shipped) as delivery_date_shipped,
delivery_easypost_id,
delivery_description,
delivery_date_delivered
from sos."delivery"
left join sos."delivery_hand_shipped" on "delivery".delivery_uuid = "delivery_hand_shipped".delivery_uuid
left join sos."delivery_hand_delivered" on "delivery".delivery_uuid = "delivery_hand_delivered".delivery_uuid
left join sos."delivery_easypost" on "delivery".delivery_uuid = "delivery_easypost".delivery_uuid;
create or replace view sos.v_order as
select
"order".*,
@ -117,13 +129,15 @@ create or replace view sos.v_order as
"address".*,
v_transaction_paid.transaction_amount_paid_cents,
v_payment.*,
v_cart.*
v_cart.*,
v_delivery.*
from sos."order"
left join sos."transaction" on transaction_order_uuid = order_uuid
left join sos."coupon" on transaction_coupon_uuid = coupon_uuid
left join sos."address" on order_address_uuid = address_uuid
left join sos.v_transaction_paid on "transaction".transaction_uuid = v_transaction_paid.transaction_uuid
left join sos.v_payment on "transaction".transaction_uuid = payment_transaction_uuid
left join sos.v_delivery on "order".order_delivery_uuid = delivery_uuid
left join sos.v_cart on cart_uuid = transaction_cart_uuid;
create or replace view sos.v_config as

@ -896,3 +896,50 @@ begin
return query select * from sos.v_shipment where shipment_uuid = _shipment_uuid;
end; $function$;
create or replace function sos.set_delivery_tracking(_order_uuid uuid, _tracking_number text, _date_shipped timestamptz)
returns setof sos.v_order
language plpgsql
as $function$
declare
_delivery_uuid uuid;
begin
-- Ensure order has no delivery
select order_delivery_uuid into _delivery_uuid
from sos."order" where order_uuid = _order_uuid;
if _delivery_uuid is not null then
raise 'Order already has a delivery record';
end if;
-- Create delivery
insert into sos."delivery" (
delivery_type
) values (
'hand_shipped'
) returning delivery_uuid into _delivery_uuid;
-- Default date
if _date_shipped is null then
_date_shipped := now();
end if;
-- Create delivery subtype record
insert into sos."delivery_hand_shipped" (
delivery_uuid,
delivery_type,
delivery_tracking_number,
delivery_date_shipped
) values (
_delivery_uuid,
'hand_shipped',
_tracking_number,
_date_shipped
);
-- Update order
update sos."order" set
order_delivery_uuid = _delivery_uuid
where order_uuid = _order_uuid;
return query select * from sos.v_order where order_uuid = _order_uuid;
end; $function$;

162
package-lock.json generated

@ -2369,6 +2369,11 @@
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz",
"integrity": "sha512-7xvKeErvXZFtUItTHgNtLgS9RJpVnwBlWX8jSo/BO8VsF6deszemZSkJJJA1KOKrXuzZH4WALpAJdq5EyfgMLg=="
},
"chain-function": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz",
"integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg=="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -2379,6 +2384,11 @@
"supports-color": "^5.3.0"
}
},
"change-emitter": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz",
"integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU="
},
"chokidar": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
@ -3068,6 +3078,11 @@
"type": "^1.0.1"
}
},
"date-fns": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -3253,6 +3268,14 @@
"randombytes": "^2.0.0"
}
},
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@ -3326,6 +3349,14 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"requires": {
"iconv-lite": "~0.4.13"
}
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -3734,6 +3765,27 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fbjs": {
"version": "0.8.17",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
"integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
"requires": {
"core-js": "^1.0.0",
"isomorphic-fetch": "^2.1.1",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.18"
},
"dependencies": {
"core-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
}
}
},
"figgy-pudding": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
@ -3849,6 +3901,23 @@
"readable-stream": "^2.3.6"
}
},
"focus-trap": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-4.0.2.tgz",
"integrity": "sha512-HtLjfAK7Hp2qbBtLS6wEznID1mPT+48ZnP2nkHzgjpL4kroYHg0CdqJ5cTXk+UO5znAxF5fRUkhdyfgrhh8Lzw==",
"requires": {
"tabbable": "^3.1.2",
"xtend": "^4.0.1"
}
},
"focus-trap-react": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-6.0.0.tgz",
"integrity": "sha512-mvEYxmP75PMx0vOqoIAmJHO/qUEvdTAdz6gLlEZyxxODnuKQdnKea2RWTYxghAPrV+ibiIq2o/GTSgQycnAjcw==",
"requires": {
"focus-trap": "^4.0.2"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
@ -4199,6 +4268,11 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
"integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs="
},
"hosted-git-info": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
@ -4538,6 +4612,26 @@
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
"isomorphic-fetch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"requires": {
"node-fetch": "^1.0.1",
"whatwg-fetch": ">=0.10.0"
},
"dependencies": {
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"requires": {
"encoding": "^0.1.11",
"is-stream": "^1.0.1"
}
}
}
},
"jest-worker": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz",
@ -6775,11 +6869,45 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
"integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
},
"react-infinite-calendar": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/react-infinite-calendar/-/react-infinite-calendar-2.3.1.tgz",
"integrity": "sha1-l/Y5g7PuHsDfR2s2uCJ8fbDtv4U=",
"requires": {
"classnames": "^2.2.5",
"date-fns": "^1.27.2",
"dom-helpers": "^3.2.1",
"prop-types": "^15.5.7",
"react-tiny-virtual-list": "^2.0.0",
"react-transition-group": "^1.1.3",
"recompose": "^0.22.0"
}
},
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-tiny-virtual-list": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-tiny-virtual-list/-/react-tiny-virtual-list-2.2.0.tgz",
"integrity": "sha512-MDiy2xyqfvkWrRiQNdHFdm36lfxmcLLKuYnUqcf9xIubML85cmYCgzBJrDsLNZ3uJQ5LEHH9BnxGKKSm8+C0Bw==",
"requires": {
"prop-types": "^15.5.7"
}
},
"react-transition-group": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz",
"integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==",
"requires": {
"chain-function": "^1.0.0",
"dom-helpers": "^3.2.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.5.6",
"warning": "^3.0.0"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -6823,6 +6951,17 @@
"source-map": "~0.6.1"
}
},
"recompose": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/recompose/-/recompose-0.22.0.tgz",
"integrity": "sha1-8JnRg4WILKQdnuyJFxja3dwyBO8=",
"requires": {
"change-emitter": "^0.1.2",
"fbjs": "^0.8.1",
"hoist-non-react-statics": "^1.0.0",
"symbol-observable": "^1.0.4"
}
},
"reflect.ownkeys": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
@ -7821,6 +7960,16 @@
}
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"tabbable": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-3.1.2.tgz",
"integrity": "sha512-wjB6puVXTYO0BSFtCmWQubA/KIn7Xvajw0x0l6eJUudMG/EAiJvIUnyNX6xO4NpGrJ16lbD0eUseB9WxW0vlpQ=="
},
"tapable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@ -8139,6 +8288,11 @@
"is-typedarray": "^1.0.0"
}
},
"ua-parser-js": {
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ=="
},
"unfetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz",
@ -8369,6 +8523,14 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "2.0.0-beta.5",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.0.0-beta.5.tgz",

@ -28,6 +28,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"express-validator": "^6.4.0",
"focus-trap-react": "^6.0.0",
"join-js": "^1.0.2",
"luxon": "^1.22.0",
"multer": "^1.4.2",
@ -36,6 +37,7 @@
"pg": "^7.18.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-infinite-calendar": "^2.3.1",
"sharp": "^0.24.1",
"stripe": "^8.44.0",
"use-measure": "^0.3.0",

@ -11,6 +11,7 @@ import AdminNav from '~/components/admin/nav'
import ErrorBoundary from '~/components/errorBoundary'
import "~/styles/layout.css"
import 'react-infinite-calendar/styles.css'
Layout.getInitialProps = async ({Component, ctx}) => {
// Configure axios instance

@ -1,19 +0,0 @@
import React from 'react'
import Router from 'next/router'
import ActionBar from '~/components/admin/actionBar'
import Table from '~/components/table'
import {DateTime} from 'luxon'
Order.getInitialProps = async ({ctx: {axios, query: {id}}}) => {
const {data: order} = await axios.get(`/api/orders/${id}`)
return {order}
}
export default function Order({order}){
return (
<>
<ActionBar title="Order Details"/>
<pre>{JSON.stringify(order, null, 2)}</pre>
</>
)
}

@ -0,0 +1,54 @@
import React from 'react'
import Router from 'next/router'
import ActionBar from '~/components/admin/actionBar'
import Table from '~/components/table'
import {DateTime} from 'luxon'
import {Button} from '@rmwc/button'
Order.getInitialProps = async ({ctx: {axios, query: {id}}}) => {
const {data: order} = await axios.get(`/api/orders/${id}`)
return {order}
}
export default function Order({order}){
const lastTransaction = getLastTransaction(order)
return (
<>
<ActionBar title="Order Details" actions={[
// {label: 'Ship via Easypost', url: `/admin/orders/${order.uuid}/ship/easypost`},
{label: 'Enter Tracking', url: `/admin/orders/${order.uuid}/ship/tracking`},
// {label: 'Mark Delivered', url: `/admin/orders/${order.uuid}/ship/delivery`}
]}/>
<h2>Order for {capitalizeName(order.address.name)}</h2>
<h3>Purchased {DateTime.fromISO(lastTransaction.completion_time).toFormat('LLLL dd, h:mm a')}</h3>
<pre>{JSON.stringify(order, null, 2)}</pre>
</>
)
}
function capitalizeName(string){
return string
.split(' ')
.map(word => word[0].toUpperCase() + word.slice(1).toLowerCase())
.join(' ')
}
function getLastTransaction(order){
return order.transactions.sort(sortTransactions)[0]
}
function sortTransactions({completion_time: a}, {completion_time: b}){
const timeA = DateTime.fromISO(a)
const timeB = DateTime.fromISO(b)
return timeB.diff(timeA).as('seconds')
}
const formatMoney = money => {
if (money === undefined || money === null) return null;
return '$' + (money / 100).toFixed(2)
}

@ -0,0 +1,24 @@
import React, {useState} from 'react'
import Router from 'next/router'
import ActionBar from '~/components/admin/actionBar'
import {FormController, DateInput, Input, Button} from '~/components/form'
EnterTracking.getInitialProps = async ({ctx: {axios, query: {id}}}) => {
const {data: order} = await axios.get(`/api/orders/${id}`)
return {order}
}
export default function EnterTracking({order}){
return (
<>
<ActionBar title="Enter Tracking"/>
<FormController afterSubmit={() => Router.push(`/admin/orders/${order.uuid}`)} url={`/api/orders/${order.uuid}/ship/tracking`}>
<Input label="Tracking Code" validate={val => val.length > 0} type="text" name="code" hint="Please enter the USPS Tracking code" />
<DateInput label="Ship Date" placeholder="Today" name="date" />
<Button type="submit">Submit</Button>
</FormController>
</>
)
}

@ -10,9 +10,28 @@ Orders.getInitialProps = async ({ctx}) => {
}
export default function Orders({orders}){
const unshippedOrders = orders.filter(order => !order.delivery)
const shippedOrders = orders.filter(order => order.delivery).reverse()
return (
<>
<ActionBar title="Orders"/>
<h4>Need to ship:</h4>
<Table
columns={[
{name: 'Purchased', extractor: getPurchaseTime},
{name: 'Items', extractor: getNumberItems},
{name: 'Price', extractor: getItemPrice},
{name: 'Shipping', extractor: getShippingEstimate},
{name: 'Amount Paid', extractor: getAmountPaid},
{name: '', extractor: row =>
<button className="buttonLink" onClick={() => Router.push(`/admin/orders/${row.id}`)}>Details</button>
}
]}
rows={unshippedOrders.map(order => ({id: order.uuid, ...order}))}
/>
<h4>Shipped:</h4>
<Table
columns={[
{name: 'Purchased', extractor: getPurchaseTime},
@ -24,7 +43,7 @@ export default function Orders({orders}){
<button className="buttonLink" onClick={() => Router.push(`/admin/orders/${row.id}`)}>Details</button>
}
]}
rows={orders.map(order => ({id: order.uuid, ...order}))}
rows={shippedOrders.map(order => ({id: order.uuid, ...order}))}
/>
</>
)

Loading…
Cancel
Save