diff --git a/api/orders.js b/api/orders.js index b427db5..a3c67b1 100644 --- a/api/orders.js +++ b/api/orders.js @@ -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) +}) diff --git a/components/admin/actionBar/actionBar.js b/components/admin/actionBar/actionBar.js index d4aa907..155a499 100644 --- a/components/admin/actionBar/actionBar.js +++ b/components/admin/actionBar/actionBar.js @@ -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 (

{title}

+ {actions && Array.isArray(actions) && actions.map(action => { + if(action.url) + return + if(action.onClick) + return + return + })} {children}
) diff --git a/components/admin/actionBar/actionBar.module.css b/components/admin/actionBar/actionBar.module.css index 079bb13..bb6c552 100644 --- a/components/admin/actionBar/actionBar.module.css +++ b/components/admin/actionBar/actionBar.module.css @@ -25,4 +25,5 @@ .actionBar button:global(.mdc-button--outlined):not(:disabled) { border-color: rgba(255,255,255,.44); + margin-left: 8px; } diff --git a/components/form/button.js b/components/form/button.js index edae49c..6ca71ca 100644 --- a/components/form/button.js +++ b/components/form/button.js @@ -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) return (
- diff --git a/components/form/dateInput.js b/components/form/dateInput.js new file mode 100644 index 0000000..f5eedeb --- /dev/null +++ b/components/form/dateInput.js @@ -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 ( +
+ {label && } +
+ setModal(true)} type="text" name={name} value={displayedValue} onBlur={onBlur} /> +
+ {(hint || error) && {error || (isValid ? '' : hint)}} + + + +
+ ) +} diff --git a/components/form/form.js b/components/form/form.js index 67c5aa2..b37fb96 100644 --- a/components/form/form.js +++ b/components/form/form.js @@ -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) { diff --git a/components/modal/modal.js b/components/modal/modal.js new file mode 100644 index 0000000..72d0245 --- /dev/null +++ b/components/modal/modal.js @@ -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(( +
+
+ {visible && ( + +
+ +
+

{title}

+ + +
+ {children} +
+
+
+ )} +
+ ), window.document.body) +} diff --git a/components/modal/modal.module.css b/components/modal/modal.module.css new file mode 100644 index 0000000..5edc284 --- /dev/null +++ b/components/modal/modal.module.css @@ -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; +} + diff --git a/db/mappings/order.js b/db/mappings/order.js index 20ea632..629770e 100644 --- a/db/mappings/order.js +++ b/db/mappings/order.js @@ -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' + ] }] diff --git a/db/models/order.js b/db/models/order.js index 470c2bd..bdaaa9f 100644 --- a/db/models/order.js +++ b/db/models/order.js @@ -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 + }) diff --git a/db/sql/1-tables.sql b/db/sql/1-tables.sql index f3f49b8..2337ca6 100644 --- a/db/sql/1-tables.sql +++ b/db/sql/1-tables.sql @@ -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" ( diff --git a/db/sql/2-views.sql b/db/sql/2-views.sql index 610150b..c7d9040 100644 --- a/db/sql/2-views.sql +++ b/db/sql/2-views.sql @@ -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 diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql index a73ac6d..25cb61e 100644 --- a/db/sql/3-functions.sql +++ b/db/sql/3-functions.sql @@ -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$; diff --git a/package-lock.json b/package-lock.json index 68c8aad..929f4ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 12f6062..4584c61 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/_app.js b/pages/_app.js index 6d996a0..1b72cb6 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -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 diff --git a/pages/admin/orders/[id].js b/pages/admin/orders/[id].js deleted file mode 100644 index f2cbc01..0000000 --- a/pages/admin/orders/[id].js +++ /dev/null @@ -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 ( - <> - -
{JSON.stringify(order, null, 2)}
- - ) -} diff --git a/pages/admin/orders/[id]/index.js b/pages/admin/orders/[id]/index.js new file mode 100644 index 0000000..27df8a8 --- /dev/null +++ b/pages/admin/orders/[id]/index.js @@ -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 ( + <> + + +

Order for {capitalizeName(order.address.name)}

+

Purchased {DateTime.fromISO(lastTransaction.completion_time).toFormat('LLLL dd, h:mm a')}

+ +
{JSON.stringify(order, null, 2)}
+ + ) +} + +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) +} diff --git a/pages/admin/orders/[id]/ship/tracking.js b/pages/admin/orders/[id]/ship/tracking.js new file mode 100644 index 0000000..2246bd5 --- /dev/null +++ b/pages/admin/orders/[id]/ship/tracking.js @@ -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 ( + <> + + + Router.push(`/admin/orders/${order.uuid}`)} url={`/api/orders/${order.uuid}/ship/tracking`}> + val.length > 0} type="text" name="code" hint="Please enter the USPS Tracking code" /> + + + + + ) +} diff --git a/pages/admin/orders/index.js b/pages/admin/orders/index.js index 7abc502..df905de 100644 --- a/pages/admin/orders/index.js +++ b/pages/admin/orders/index.js @@ -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 ( <> +

Need to ship:

+ + + } + ]} + rows={unshippedOrders.map(order => ({id: order.uuid, ...order}))} + /> + +

Shipped:

Router.push(`/admin/orders/${row.id}`)}>Details } ]} - rows={orders.map(order => ({id: order.uuid, ...order}))} + rows={shippedOrders.map(order => ({id: order.uuid, ...order}))} /> )