Initial commit

Load this up somewhere where I can setup CI/CD
This commit is contained in:
2024-01-24 13:05:19 -05:00
commit 7119957c9e
458 changed files with 10153 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { ReactNode, useContext } from 'react';
import { Bin, binToLabel } from '../../../../lib/bin.enum';
import { BinnerContext } from '../../context/BinnerContext';
interface BinButtonProps {
bin: Bin;
className?: string;
icon?: ReactNode;
}
export function BinButton({ bin, className, icon }: BinButtonProps) {
const { item, setBin } = useContext(BinnerContext);
return (
<div className={className}>
<button disabled={!item} onClick={() => setBin(bin)}>
{icon && <span>{icon}</span>}
<strong>{binToLabel(bin)}</strong>
</button>
</div>
);
}

View File

@@ -0,0 +1,69 @@
.bins {
display: flex;
flex: 1 0 100%;
flex-direction: column;
gap: 1rem;
padding: 1rem;
.pdsBinWrap {
display: flex;
flex: 0 0 50%;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
.bin {
align-self: stretch;
display: flex;
flex: 1 1 33%;
button {
font-size: 1.3em;
flex: 0 0 100%;
}
}
}
.lossBinWrap {
display: flex;
flex: 0 0 50%;
button {
font-size: 1.3em;
flex: 0 0 100%;
}
}
}
.iconBottom, .iconLeft, .iconRight, .iconTop {
button {
align-items: center;
display: flex;
gap: 1em;
justify-content: center;
}
}
.iconBottom, .iconTop {
button {
flex-direction: column;
}
}
.iconLeft, .iconRight {
button {
flex-direction: row;
}
}
.iconRight, .iconBottom {
strong {
order: 1;
}
span {
order: 2
}
}

View File

@@ -0,0 +1,60 @@
import clsx from 'clsx';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowDown, faArrowLeft, faArrowRight, faArrowUp } from '@fortawesome/free-solid-svg-icons'
import { Bin } from '../../../../lib/bin.enum';
import { BinButton } from '../BinButton/BinButton';
import { useCallback, useContext, useEffect } from 'react';
import { BinnerContext } from '../../context/BinnerContext';
import styles from './Bins.module.scss';
export function Bins() {
const { bin, item, setBin, setItem } = useContext(BinnerContext);
const handleKeydown = useCallback((event: KeyboardEvent) => {
console.log('Bins Keydown!');
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
console.log('Loss!');
setBin(Bin.LOSS);
break;
case 'ArrowLeft':
event.preventDefault();
console.log('Process!');
setBin(Bin.PROCESS);
break;
case 'ArrowRight':
event.preventDefault();
console.log('Shoulder Tap!');
setBin(Bin.SHOULDER_TAP);
break;
case 'ArrowUp':
event.preventDefault();
console.log('Donate!');
setBin(Bin.DONATE);
break;
}
}, [bin, item, setBin, setItem]);
useEffect(() => {
window.addEventListener('keydown', handleKeydown);
return () => {
window.removeEventListener('keydown', handleKeydown);
};
}, []);
return (
<div className={styles.bins}>
<div className={styles.pdsBinWrap}>
<BinButton className={clsx(styles.bin, styles.pBin, styles.iconLeft)} bin={Bin.PROCESS} icon={<FontAwesomeIcon icon={faArrowLeft} />} />
<BinButton className={clsx(styles.bin, styles.dBin, styles.iconTop)} bin={Bin.DONATE} icon={<FontAwesomeIcon icon={faArrowUp} />} />
<BinButton className={clsx(styles.bin, styles.sBin, styles.iconRight)} bin={Bin.SHOULDER_TAP} icon={<FontAwesomeIcon icon={faArrowRight} />} />
</div>
<BinButton className={clsx(styles.lossBinWrap, styles.lBin, styles.iconBottom)} bin={Bin.LOSS} icon={<FontAwesomeIcon icon={faArrowDown} />} />
</div>
);
}

View File

@@ -0,0 +1,26 @@
.defectPanel {
height: 100%;
left: 0;
padding: 1rem;
position: absolute;
top: 0;
width: 100%;
}
.defectHeader {
align-items: center;
display: flex;
margin: 0.5rem 0;
padding: 0.5rem 0;
button {
flex: 0 0 auto;
width: 2em;
height: 2em;
}
span {
flex: 1 0 auto;
text-align: center;
}
}

View File

@@ -0,0 +1,26 @@
import { useContext } from 'react';
import { Bin } from '@/app/lib/bin.enum';
import { DetailsForm } from '../DetailsForm/DetailsForm';
import { BinnerContext } from '../../context/BinnerContext';
import styles from './DefectPanel.module.scss';
export function DefectPanel() {
const { bin, setBin } = useContext(BinnerContext);
if (bin === null) {
return;
}
return (
<div className={styles.defectPanel}>
<header className={styles.defectHeader}>
<button onClick={() => setBin(null)} aria-label="Press Escape to go back">&times;</button>
<span><strong>{Bin[bin]} Bin</strong></span>
</header>
<DetailsForm />
</div>
);
}

View File

@@ -0,0 +1,78 @@
.detailForm {
button, button.selected {
&:active {
background-color: #ff6900;
}
&:focus-visible {
outline-color: #ff6900;
background-color: rgb(24,48,40);
color: #fff;
}
}
select {
&:focus-visible {
outline-color: #ff6900;
}
}
}
.fieldset {
margin: 0.5rem 0;
padding: 1rem;
button {
padding: 0.5em;
}
label, legend {
display: inline-block;
font-weight: 700;
margin-bottom: 0.5em;
margin-right: 1em;
}
legend {
padding: 0 0.5em;
}
select {
padding: 0.5rem;
width: 30%;
}
}
.buttonWrap {
display: flex;
flex-flow: column wrap;
gap: 0.5rem;
}
.defectBtn {
font-size: 1.3em;
font-weight: 700;
padding: 0.5em;
&.selected {
background-color: #ff6900;
color: #000;
}
}
.selectWrap {
align-items: center;
display: flex;
gap: 0.5rem;
justify-content: center;
}
.submitBtn {
font-size: 1.3em;
margin: 1rem 0;
padding: 0.5em;
width: 100%;
}
// rgba(24,48,40,var(--tw-bg-opacity));

View File

@@ -0,0 +1,98 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import clsx from 'clsx';
import { useSupplyChainData } from '@/app/lib/context/SupplyChain/hooks';
import { Defect, defectToLabel } from '@/app/lib/defect.enum';
import { useBinHandler } from './hooks';
import styles from './DetailForm.module.scss';
export function DetailsForm() {
const { farms } = useSupplyChainData();
const [farm, setFarm] = useState<string | null>(null);
const [defect, setDefect] = useState<Defect | null>(null);
const binHandler = useBinHandler();
// Track keyboard focus state
const [focusedDefect, setFocusedDefect] = useState<number | null>(null);
// Refs for accessibility
const addFarmRef = useRef<HTMLButtonElement>(null);
const selectRef = useRef<HTMLSelectElement>(null);
const defectRefs = useRef<(HTMLButtonElement | null)[]>([]);
const defectsWrapRef = useRef<HTMLFieldSetElement>(null);
const submitRef = useRef<HTMLButtonElement>(null);
const waiReasonDescId = 'waiReasonDescription';
const defectCodes = Object.keys(Defect).slice(0, Object.keys(Defect).length / 2).map((code) => parseInt(code));
const handleDefectKeydown = useCallback((event: React.KeyboardEvent<HTMLFieldSetElement>) => {
if (focusedDefect !== null) {
if (event.key === 'ArrowDown') {
setFocusedDefect(focusedDefect + 1 > defectRefs.current.length - 1 ? 0 : focusedDefect + 1);
}
if (focusedDefect !== null && event.key === 'ArrowUp') {
setFocusedDefect(focusedDefect - 1 < 0 ? defectRefs.current.length - 1 : focusedDefect - 1);
}
} else {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
setFocusedDefect(0);
}
}
}, [focusedDefect]);
useEffect(() => {
if (submitRef.current) {
submitRef.current.focus();
}
}, [defect]);
useEffect(() => {
if (focusedDefect !== null && defectRefs.current[focusedDefect]) {
defectRefs.current[focusedDefect]?.focus();
}
}, [focusedDefect]);
useEffect(() => {
if (selectRef.current) {
selectRef.current.focus();
}
}, []);
return (
<div className={styles.detailForm}>
<fieldset className={styles.fieldset} onKeyDown={(e) => e.key === 'ArrowRight' && addFarmRef.current?.click()}>
<legend>Farm Information</legend>
<div className={styles.selectWrap}>
<label htmlFor="farm">Select source farm</label>
<select name="farm" id="farm" onChange={(event) => setFarm(event.target.value)} ref={selectRef} defaultValue="">
<option value="" disabled>Select a farm</option>
{farms.map((farm) => <option key={farm._id} value={farm._id}>{farm.name}</option>)}
</select>
<button onClick={() => alert('Users could add a farm where one does not exist!')} tabIndex={-1} ref={addFarmRef}>Add new farm</button>
</div>
</fieldset>
<fieldset className={styles.fieldset} onKeyDown={handleDefectKeydown} ref={defectsWrapRef}>
<legend id={waiReasonDescId}>Select a reason for binning</legend>
<div className={styles.buttonWrap}>
{defectCodes.map((code, i) => (
<button
key={code}
className={clsx(styles.defectBtn, defect === code && styles.selected)}
aria-describedby={waiReasonDescId}
onClick={() => setDefect(code)}
ref={(el) => defectRefs.current[code] = el}
tabIndex={i === 0 ? 0 : -1}
>
{defectToLabel(code)}
</button>
))}
</div>
</fieldset>
<button className={styles.submitBtn} disabled={defect === null} onClick={() => binHandler(farm, defect as Defect)} ref={submitRef}>Bin It!</button>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { useCallback, useContext } from 'react';
import { StringSchemaDefinition } from 'mongoose';
import { faker } from '@faker-js/faker';
import { BinnerContext } from '../../context/BinnerContext';
import { useGetProcessorIdByBarcodeId } from '../../hooks';
import { addToBin } from '@/app/lib/data/binned';
import { Defect } from '@/app/lib/defect.enum';
import { Product } from '@/app/lib/product.enum';
export const useBinHandler = () => {
const { bin, item, setBin, setItem } = useContext(BinnerContext);
const getProcessorIdByBarcodeId = useGetProcessorIdByBarcodeId();
return useCallback(async (farm: string | null, reason: Defect) => {
if (!item || bin === null) return;
const binnedItem = {
...item,
farm: farm as StringSchemaDefinition || undefined,
operator: faker.helpers.arrayElement(['Steve', 'Leighann', 'Lwanda', 'Xueliang', 'Jose']),
processor: getProcessorIdByBarcodeId(item.processor) as StringSchemaDefinition,
reason,
};
await addToBin(bin, binnedItem);
setBin(null);
setItem(null);
}, [bin, item, setBin, setItem]);
};

View File

@@ -0,0 +1,57 @@
.productInfo {
padding: 1rem;
label {
font-weight: 700;
font-size: 1.3em;
margin-bottom: 0.25rem;
}
input, select {
caret-color: transparent;
font-size: 1.36em;
margin-bottom: 1rem;
&[contenteditable=false] {
background-color: transparent;
border: none;
&:focus {
outline: none;
}
}
}
}
.attributes {
align-items: center;
display: flex;
flex-flow: row wrap;
gap: 0.5rem;
justify-content: center;
width: 100%;
}
.card {
display: flex;
flex-direction: column;
flex: 0 0 45%;
padding: 1rem;
}
.inputWithButton {
align-items: center;
display: flex;
flex-direction: row;
gap: 1rem;
input {
flex: 1 1 auto;
}
button {
flex: 0 0 auto;
padding: 0.5rem;
}
}

View File

@@ -0,0 +1,113 @@
import { useContext, useEffect, useRef, useState } from 'react';
import { faker } from '@faker-js/faker';
import { dateToOrdinal, ordinalToDate } from '@/app/lib/ordinalDate';
import { Product } from '@/app/lib/product.enum';
import { SupplyChainContext } from '@/app/lib/context/SupplyChain/SupplyChainContext';
import { ParsedBarcode, barcodeToProduct } from '@/app/lib/barcode';
import { BinnerContext } from '../../context/BinnerContext';
import { useGetProcessorByBarcodeId } from '../../hooks';
import styles from './ProductInfo.module.scss';
export function ProductInfo() {
const { item, setItem } = useContext(BinnerContext);
const { processors } = useContext(SupplyChainContext);
const [inputItem, setInputItem] = useState<Partial<ParsedBarcode> | null>(null);
const dateRef = useRef<HTMLInputElement>(null);
const weightRef = useRef<HTMLInputElement>(null);
const getProcessorByBarcodeId = useGetProcessorByBarcodeId();
const isCompleteItem = (item: Partial<ParsedBarcode> | null) => item && !!(item?.product && item?.date && item?.processor && item?.weight);
const [isEntryMode, setIsEntryMode] = useState(!isCompleteItem(item));
useEffect(() => {
setIsEntryMode(!isCompleteItem(item));
}, [item])
useEffect(() => {
if (isCompleteItem(inputItem)) {
setItem(barcodeToProduct(`${inputItem?.product}${inputItem?.date}${inputItem?.processor}${inputItem?.weight}`));
setInputItem(null);
}
setIsEntryMode(!isCompleteItem(inputItem));
}, [inputItem])
return (
<div className={styles.productInfo}>
<div className={styles.attributes}>
<div className={styles.card}>
<label htmlFor="product">Product</label>
{isEntryMode ? (
<select
id="product"
defaultValue=""
onChange={(e) => setInputItem({ ...inputItem || {}, product: e.currentTarget.value })}
>
<option value="">Select a product</option>
{Object.keys(Product).map((product) => (
<option key={product} value={Product[product]}>{product}</option>
))}
</select>
) : (
<input id="product" value={item?.product!} contentEditable={false} />
)}
</div>
<div className={styles.card}>
<label htmlFor="date">Packaged Date</label>
<input
id="date" value={!!item ? ordinalToDate(item.date).toDateString() : ''}
contentEditable={isEntryMode}
ref={dateRef}
onChange={(e) => {
dateRef.current.value = e.currentTarget.value;
setInputItem({ ...inputItem || {}, date: dateToOrdinal(new Date(e.currentTarget.value)) })
}}
/>
</div>
<div className={styles.card}>
<label htmlFor="processor">Processor</label>
{isEntryMode ? (
<select
id="processor"
defaultValue=""
onChange={(e) => setInputItem({ ...inputItem || {}, processor: e.currentTarget.value })}
>
<option value="">Select a processor</option>
{processors.map((processor) => (
<option key={processor._id} value={processor.barcode_id}>{processor.name}</option>
))}
</select>
) : (
<input id="processor" value={!!item ? (getProcessorByBarcodeId(item.processor)?.name ?? 'unknown/invalid processor') : ''} contentEditable={false} />
)}
</div>
<div className={styles.card}>
<label htmlFor="weight">Weight</label>
<div className={styles.inputWithButton}>
<input
id="weight"
value={!!item ? `${item.weight} lbs.` : ''}
contentEditable={false}
tabIndex={-1}
ref={weightRef}
/>
{isEntryMode && (
<button
onClick={(e) => {
const weight = faker.number.int({ min: 1000, max: 1999 });
weightRef.current.value = `${weight / 100} lbs`; setInputItem({ ...inputItem || {}, weight })
}}
tabIndex={0}
>
Weight from Scale
</button>
)}
</div>
</div>
</div>
</div>
);
}