the type construct of components display. TODO: add area rug calculator.
This commit is contained in:
parent
23d957824b
commit
dbba262044
@ -1,21 +1,106 @@
|
||||
import { Product } from "@/lib/product";
|
||||
import { Product } from "@/lib/product"
|
||||
import uuid from "react-native-uuid"
|
||||
|
||||
export const products = [
|
||||
// Sheet goods
|
||||
new Product(15, {l: 4, w : 8, u: "ft"}, { name: "Plywood 1/4\"" }),
|
||||
new Product(20, {l: 4, w : 8, u: "ft"}, { name: "Plywood 1/2\"" }),
|
||||
new Product(25, {l: 4, w : 8, u: "ft"}, { name: "Plywood 3/4\"" }),
|
||||
new Product(5, {l: 4, w : 8, u: "ft"}, { name: "Thin Panel Board" }),
|
||||
new Product(10, {l: 4, w : 8, u: "ft"}, { name: "Sheetrock" }),
|
||||
new Product(15, {l: 4, w : 8, u: "ft"}, { name: "OSB / Particle" }),
|
||||
new Product(20, {l: 4, w : 8, u: "ft"}, { name: "MDF" }),
|
||||
new Product(15, {l: 4, w : 8, u: "ft"}, { name: "Pegboard" }),
|
||||
new Product(5, {l: 3, w : 5, u: "ft"}, { name: "Cement" }),
|
||||
// trim
|
||||
new Product(1, {l: 0.50, u : "ft"}, { name: "trim <= 3 inches" }),
|
||||
new Product(1, {l: 0.75, u : "ft"}, { name: "trim > 3 inches" }),
|
||||
// siding
|
||||
new Product(1, {l: 1, u: "ft"}, {name: "house siding"}),
|
||||
new Product(1, {l: 1, u: "ft"}, {name: "metal / shelf bars"}),
|
||||
new Product(0.5, {l: 1, u: "ft"}, {name: "gutter spouts"}),
|
||||
];
|
||||
export default [
|
||||
// Sheet goods
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 15,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: 'Plywood 1/4"' },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 20,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: 'Plywood 1/2"' },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 25,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: 'Plywood 3/4"' },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 5,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "Thin Panel Board" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 10,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "Sheetrock" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 15,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "OSB / Particle" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 20,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "MDF" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 15,
|
||||
dimensions: { l: 4, w: 8, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "Pegboard" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 5,
|
||||
dimensions: { l: 3, w: 5, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "Cement" },
|
||||
},
|
||||
// trim
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 1,
|
||||
dimensions: { l: 0.5, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "trim <=3 inches" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 1,
|
||||
dimensions: { l: 0.75, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "trim > 3 inches" },
|
||||
},
|
||||
// siding
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 1,
|
||||
dimensions: { l: 1, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "house siding" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 1,
|
||||
dimensions: { l: 1, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "metal / shelf bars" },
|
||||
},
|
||||
{
|
||||
id: uuid.v4().valueOf(),
|
||||
pricePerUnit: 0.5,
|
||||
dimensions: { l: 1, u: "ft" },
|
||||
type: "lumber",
|
||||
attributes: { name: "gutter spouts" },
|
||||
},
|
||||
] as Array<Product>;
|
@ -4,13 +4,13 @@ import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||
import { Provider } from 'react-redux';
|
||||
import { products as fixtures } from "@/__fixtures__/initialProducts"
|
||||
import fixtures from "@/__fixtures__/initialProducts"
|
||||
import { setupStore } from '../store';
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const store = setupStore({
|
||||
products: fixtures.map(p => p.asObject),
|
||||
products: fixtures,
|
||||
units: "ft",
|
||||
});
|
||||
return (
|
||||
|
@ -1,68 +1,87 @@
|
||||
import { Product } from "@/lib/product";
|
||||
import { product_type_t } from "@/lib/dimensions";
|
||||
import { PRODUCT_TYPES, Product } from "@/lib/product";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { StyleSheet, Text, TextInput, TouchableHighlight, View } from "react-native";
|
||||
import { StyleSheet, TextInput, TouchableHighlight, View } from "react-native";
|
||||
import SelectDropdown from "react-native-select-dropdown";
|
||||
|
||||
export type ProductAttributeChangeFunc = (key: string, newValue: string) => any;
|
||||
export type ProductAttributeDeleteFunc = (key: string) => any;
|
||||
export type ChangeAttributeFunction = (oldKey : string, newKey : string) => any;
|
||||
export type ChangeAttributeFunction = (oldKey: string, newKey: string) => any;
|
||||
export type ProductTypeChangeFunc = (
|
||||
key: string,
|
||||
newProductType: product_type_t
|
||||
) => any;
|
||||
|
||||
export type ProductAttributeProps = {
|
||||
attributeKey: string,
|
||||
attributeValue: string,
|
||||
onChangeAttributeKey?: ChangeAttributeFunction,
|
||||
onChangeAttribute?: ProductAttributeChangeFunc,
|
||||
onDelete?: ProductAttributeChangeFunc,
|
||||
attributeKey: string;
|
||||
attributeValue: string;
|
||||
onProductTypeChange?: ProductTypeChangeFunc;
|
||||
onChangeAttributeKey?: ChangeAttributeFunction;
|
||||
onChangeAttribute?: ProductAttributeChangeFunc;
|
||||
onDelete?: ProductAttributeChangeFunc;
|
||||
};
|
||||
|
||||
export const ProductAttributeEditor = ({ attributeKey, attributeValue, onDelete, onChangeAttributeKey, onChangeAttribute }: ProductAttributeProps) => {
|
||||
const select_product_type_choices = PRODUCT_TYPES.map((p) => [p, p]);
|
||||
|
||||
const doChangeKey = (e: any) => {
|
||||
onChangeAttributeKey && onChangeAttributeKey(attributeKey, e);
|
||||
}
|
||||
export const ProductAttributeEditor = ({
|
||||
attributeKey,
|
||||
attributeValue,
|
||||
onDelete,
|
||||
onChangeAttributeKey,
|
||||
onChangeAttribute,
|
||||
}: ProductAttributeProps) => {
|
||||
const doChangeKey = (e: any) => {
|
||||
onChangeAttributeKey && onChangeAttributeKey(attributeKey, e);
|
||||
};
|
||||
|
||||
const doChangeValue = (e: any) => {
|
||||
onChangeAttribute && onChangeAttribute(attributeKey, e);
|
||||
}
|
||||
const doChangeValue = (e: any) => {
|
||||
onChangeAttribute && onChangeAttribute(attributeKey, e);
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.productAttributeRow}>
|
||||
<TextInput
|
||||
defaultValue={attributeKey}
|
||||
onChangeText={doChangeKey}
|
||||
style={styles.value}
|
||||
aria-label="Edit Key"
|
||||
/>
|
||||
<TextInput
|
||||
defaultValue={attributeValue}
|
||||
onChangeText={doChangeValue}
|
||||
style={styles.value}
|
||||
aria-label="Edit Value" />
|
||||
<TouchableHighlight
|
||||
onPress={() => onDelete && onDelete(attributeKey, attributeValue)}
|
||||
aria-label="Delete Attribute"
|
||||
style={{ backgroundColor: "darkred", borderRadius: 5, margin: 5, padding: 5, }}>
|
||||
<Ionicons name="trash-bin-outline" size={30} color={"white"} />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.productAttributeRow}>
|
||||
<TextInput
|
||||
defaultValue={attributeKey}
|
||||
onChangeText={doChangeKey}
|
||||
style={styles.value}
|
||||
aria-label="Edit Key"
|
||||
/>
|
||||
<TextInput
|
||||
defaultValue={attributeValue}
|
||||
onChangeText={doChangeValue}
|
||||
style={styles.value}
|
||||
aria-label="Edit Value"
|
||||
/>
|
||||
<TouchableHighlight
|
||||
onPress={() => onDelete && onDelete(attributeKey, attributeValue)}
|
||||
aria-label="Delete Attribute"
|
||||
style={{
|
||||
backgroundColor: "darkred",
|
||||
borderRadius: 5,
|
||||
margin: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-bin-outline" size={30} color={"white"} />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
productAttributeRow: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
key: {
|
||||
flex: 1,
|
||||
},
|
||||
value: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: "grey",
|
||||
borderStyle: "solid",
|
||||
padding: 10
|
||||
}
|
||||
productAttributeRow: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
key: {
|
||||
flex: 1,
|
||||
},
|
||||
value: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: "grey",
|
||||
borderStyle: "solid",
|
||||
padding: 10,
|
||||
},
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { Product } from '@/lib/product';
|
||||
import { dimensions_t } from "@/lib/dimensions_t";
|
||||
import { Product, productPriceFor } from '@/lib/product';
|
||||
import { dimensions_t } from "@/lib/dimensions";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
@ -23,7 +23,7 @@ export default function ProductCalculatorSelector() {
|
||||
const iv = setInterval(function () {
|
||||
if (!(activeProduct && measurement)) return;
|
||||
setPrice(
|
||||
activeProduct.priceFor(measurement, percentDamage)
|
||||
productPriceFor(activeProduct, measurement, percentDamage)
|
||||
)
|
||||
}, 50);
|
||||
return function () {
|
||||
@ -34,7 +34,7 @@ export default function ProductCalculatorSelector() {
|
||||
function onMeasurementSet(dimensions: dimensions_t) {
|
||||
setMeasurement(dimensions);
|
||||
activeProduct && setPrice(
|
||||
activeProduct.priceFor(measurement, percentDamage)
|
||||
productPriceFor(activeProduct, measurement, percentDamage)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice"
|
||||
import { Id, Product } from "@/lib/product";
|
||||
import { dimensions_t } from "@/lib/dimensions_t";
|
||||
import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native";
|
||||
import { ProductEditorItem } from "./ProductEditorItem";
|
||||
import { dimensions_t } from "@/lib/dimensions";
|
||||
|
||||
export const ProductEditor = ({}) => {
|
||||
const products = useAppSelector(selectProducts) as Product [];
|
||||
|
@ -1,231 +1,274 @@
|
||||
import { Id, Product } from "@/lib/product"
|
||||
import { dimensions_t } from "@/lib/dimensions_t";
|
||||
import { useState } from "react"
|
||||
import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native"
|
||||
import { Id, Product, product_type_t } from "@/lib/product";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
Touchable,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { ProductAttributeEditor } from "./ProductAttributeEditor";
|
||||
import { Dropdown } from 'react-native-element-dropdown';
|
||||
import { Dropdown } from "react-native-element-dropdown";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Length } from "convert";
|
||||
import { dimensions_t } from "@/lib/dimensions";
|
||||
|
||||
export type ProductAddedFunc = () => any;
|
||||
export type ProductDeletedFunc = (product_id: Id) => any;
|
||||
export type AttributeAddedFunc = (product_id: Id) => any;
|
||||
export type AttributeKeyUpdatedFunc = (product_id: Id, oldKey: string, newKey: string) => any;
|
||||
export type AttributeUpdatedFunc = (product_id: Id, attribute: string, value: string) => any;
|
||||
export type AttributeKeyUpdatedFunc = (
|
||||
product_id: Id,
|
||||
oldKey: string,
|
||||
newKey: string
|
||||
) => any;
|
||||
export type AttributeUpdatedFunc = (
|
||||
product_id: Id,
|
||||
attribute: string,
|
||||
value: string
|
||||
) => any;
|
||||
export type AttributeDeletedFunc = (product_id: Id, attribute: string) => any;
|
||||
export type PriceUpdatedFunc = (product_id: Id, price: number) => any;
|
||||
export type DimensionUpdatedFunc = (product_id: Id, dimension: dimensions_t) => any;
|
||||
export type DimensionUpdatedFunc = (
|
||||
product_id: Id,
|
||||
dimension: dimensions_t
|
||||
) => any;
|
||||
export type ProductTypeChangedFunc = (
|
||||
product_id: Id,
|
||||
product_type: product_type_t
|
||||
) => any;
|
||||
|
||||
export type ProductEditorItemProps = {
|
||||
product: Product,
|
||||
onProductAdded?: ProductAddedFunc,
|
||||
onProductDeleted?: ProductDeletedFunc,
|
||||
onAttributeAdded?: AttributeAddedFunc,
|
||||
onAttributeKeyChanged?: AttributeKeyUpdatedFunc,
|
||||
onAttributeUpdated?: AttributeUpdatedFunc,
|
||||
onAttributeDeleted?: AttributeDeletedFunc,
|
||||
onPriceUpdated?: PriceUpdatedFunc,
|
||||
onDimensionsUpdated?: DimensionUpdatedFunc,
|
||||
}
|
||||
product: Product;
|
||||
onProductAdded?: ProductAddedFunc;
|
||||
onProductDeleted?: ProductDeletedFunc;
|
||||
onAttributeAdded?: AttributeAddedFunc;
|
||||
onAttributeKeyChanged?: AttributeKeyUpdatedFunc;
|
||||
onAttributeUpdated?: AttributeUpdatedFunc;
|
||||
onAttributeDeleted?: AttributeDeletedFunc;
|
||||
onPriceUpdated?: PriceUpdatedFunc;
|
||||
onDimensionsUpdated?: DimensionUpdatedFunc;
|
||||
onProductTypeChanged?: ProductTypeChangedFunc;
|
||||
};
|
||||
|
||||
export const ProductEditorItem = (props: ProductEditorItemProps) => {
|
||||
const [showAttributes, setShowAttributes] = useState(false);
|
||||
const product = props.product;
|
||||
|
||||
const [showAttributes, setShowAttributes] = useState(false);
|
||||
const product = props.product;
|
||||
function onProductTypeChange(id: Id, newProductType: product_type_t) {
|
||||
props.onProductTypeChanged &&
|
||||
props.onProductTypeChanged(product.id as Id, newProductType);
|
||||
}
|
||||
|
||||
function onAttributeChanged(key: string, newValue: string) {
|
||||
props.onAttributeUpdated && props.onAttributeUpdated(product.id, key, newValue);
|
||||
}
|
||||
function onAttributeChanged(key: string, newValue: string) {
|
||||
props.onAttributeUpdated &&
|
||||
props.onAttributeUpdated(product.id as Id, key, newValue);
|
||||
}
|
||||
|
||||
function onAttributeKeyChanged(oldKey: string, newKey: string) {
|
||||
props.onAttributeKeyChanged && props.onAttributeKeyChanged(product.id, oldKey, newKey);
|
||||
}
|
||||
function onAttributeKeyChanged(oldKey: string, newKey: string) {
|
||||
props.onAttributeKeyChanged &&
|
||||
props.onAttributeKeyChanged(product.id as Id, oldKey, newKey);
|
||||
}
|
||||
|
||||
function onAttributeDelete(key: string) {
|
||||
props.onAttributeDeleted && props.onAttributeDeleted(product.id, key);
|
||||
}
|
||||
function onAttributeDelete(key: string) {
|
||||
props.onAttributeDeleted && props.onAttributeDeleted(product.id as Id, key);
|
||||
}
|
||||
|
||||
function onPricePerUnitChange(pricePerUnit: string) {
|
||||
props.onPriceUpdated && props.onPriceUpdated(product.id, parseFloat(pricePerUnit) || parseInt(pricePerUnit));
|
||||
}
|
||||
function onPricePerUnitChange(pricePerUnit: string) {
|
||||
props.onPriceUpdated &&
|
||||
props.onPriceUpdated(
|
||||
product.id as Id,
|
||||
parseFloat(pricePerUnit) || parseInt(pricePerUnit)
|
||||
);
|
||||
}
|
||||
|
||||
function onUnitsChanged(newUnits: Length) {
|
||||
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
|
||||
...(product.dimensions as dimensions_t),
|
||||
u: newUnits,
|
||||
})
|
||||
}
|
||||
function onUnitsChanged(newUnits: Length) {
|
||||
props.onDimensionsUpdated &&
|
||||
props.onDimensionsUpdated(product.id as Id, {
|
||||
...(product.dimensions as dimensions_t),
|
||||
u: newUnits,
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeLength(len: string) {
|
||||
const l = parseFloat(len) || parseInt(len);
|
||||
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
|
||||
...(product.dimensions as dimensions_t),
|
||||
l,
|
||||
})
|
||||
}
|
||||
function onChangeLength(len: string) {
|
||||
const l = parseFloat(len) || parseInt(len);
|
||||
props.onDimensionsUpdated &&
|
||||
props.onDimensionsUpdated(product.id as Id, {
|
||||
...(product.dimensions as dimensions_t),
|
||||
l,
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeWidth(width: string) {
|
||||
const w = width.length == 0 ? null : parseFloat(width) || parseInt(width);
|
||||
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
|
||||
...(product.dimensions as dimensions_t),
|
||||
...(w ? {w} : {}),
|
||||
})
|
||||
}
|
||||
function onChangeWidth(width: string) {
|
||||
const w = width.length == 0 ? null : parseFloat(width) || parseInt(width);
|
||||
props.onDimensionsUpdated &&
|
||||
props.onDimensionsUpdated(product.id as Id, {
|
||||
...(product.dimensions as dimensions_t),
|
||||
...(w ? { w } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function onDeleteProduct() {
|
||||
props.onProductDeleted && props.onProductDeleted(product.id);
|
||||
}
|
||||
function onDeleteProduct() {
|
||||
props.onProductDeleted && props.onProductDeleted(product.id as Id);
|
||||
}
|
||||
|
||||
const length = new String(product.dimensions.l || product.dimensions.l || "0") as string;
|
||||
const width = new String(product.dimensions.w || "") as string;
|
||||
const dimension = product.dimensions.u || product.dimensions.u || "foot";
|
||||
const length = new String(
|
||||
product.dimensions.l || product.dimensions.l || "0"
|
||||
) as string;
|
||||
const width = new String(product.dimensions.w || "") as string;
|
||||
const dimension = product.dimensions.u || product.dimensions.u || "foot";
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.productListHeader}>
|
||||
<TouchableHighlight
|
||||
onPress={() => setShowAttributes(!showAttributes)}
|
||||
aria-label="Product Item"
|
||||
style={styles.productItemName}
|
||||
>
|
||||
<Text style={styles.productNameText}>{product.attributes.name || `Product ${product.id}`}</Text>
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
onPress={() => onDeleteProduct()}
|
||||
aria-label="delete product"
|
||||
style={styles.deleteProductHighlight}
|
||||
>
|
||||
<Ionicons
|
||||
style={styles.deleteProductButton}
|
||||
name="trash-outline"
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
{showAttributes &&
|
||||
(
|
||||
<View style={styles.detailsWrapper}>
|
||||
<View style={styles.priceSpecWrapper}>
|
||||
<Text style={styles.priceLabel}>$</Text>
|
||||
<TextInput inputMode="decimal"
|
||||
defaultValue={new String(product.pricePerUnit) as string}
|
||||
aria-label="price per unit"
|
||||
onChangeText={onPricePerUnitChange}
|
||||
style={styles.priceInput}
|
||||
/>
|
||||
<Text style={styles.per}>per</Text>
|
||||
<Dropdown
|
||||
data={[
|
||||
{label: "feet", value: "ft"},
|
||||
{label: "inches", value: "in"},
|
||||
]}
|
||||
style={styles.unitsSelect}
|
||||
mode="modal"
|
||||
labelField="label"
|
||||
valueField="value"
|
||||
value={product.dimensions.u || "ft"}
|
||||
onChange={(item) => onUnitsChanged(item.value as Length)}
|
||||
/>
|
||||
<TextInput
|
||||
inputMode="decimal"
|
||||
defaultValue={length}
|
||||
onChangeText={onChangeLength}
|
||||
style={styles.lengthInput}
|
||||
aria-label="length"
|
||||
/>
|
||||
<Text style={{flex: 1,}}>x</Text>
|
||||
<TextInput
|
||||
inputMode="decimal"
|
||||
defaultValue={width}
|
||||
onChangeText={onChangeWidth}
|
||||
style={styles.widthInput}
|
||||
aria-label="width"
|
||||
/>
|
||||
</View>
|
||||
<Button title="+ Add Attribute" onPress={() => props.onAttributeAdded && props.onAttributeAdded(product.id)} />
|
||||
<FlatList
|
||||
style={styles.productAttributesList}
|
||||
data={Object.entries(product.attributes)}
|
||||
renderItem={({ item }) => (
|
||||
<ProductAttributeEditor
|
||||
attributeKey={item[0] || "some key"}
|
||||
attributeValue={item[1]}
|
||||
onChangeAttributeKey={onAttributeKeyChanged}
|
||||
onChangeAttribute={onAttributeChanged}
|
||||
onDelete={onAttributeDelete}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item, i) => `${product.id}-${i}`}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.productListHeader}>
|
||||
<TouchableHighlight
|
||||
onPress={() => setShowAttributes(!showAttributes)}
|
||||
aria-label="Product Item"
|
||||
style={styles.productItemName}
|
||||
>
|
||||
{product.attributes && (
|
||||
<Text style={styles.productNameText}>
|
||||
{product.attributes.name || `Product ${product.id}`}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
onPress={() => onDeleteProduct()}
|
||||
aria-label="delete product"
|
||||
style={styles.deleteProductHighlight}
|
||||
>
|
||||
<Ionicons style={styles.deleteProductButton} name="trash-outline" />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
{showAttributes && (
|
||||
<View style={styles.detailsWrapper}>
|
||||
<View style={styles.priceSpecWrapper}>
|
||||
<Text style={styles.priceLabel}>$</Text>
|
||||
<TextInput
|
||||
inputMode="decimal"
|
||||
defaultValue={new String(product.pricePerUnit).valueOf()}
|
||||
aria-label="price per unit"
|
||||
onChangeText={onPricePerUnitChange}
|
||||
style={styles.priceInput}
|
||||
/>
|
||||
<Text style={styles.per}>per</Text>
|
||||
<Dropdown
|
||||
data={[
|
||||
{ label: "feet", value: "ft" },
|
||||
{ label: "inches", value: "in" },
|
||||
]}
|
||||
style={styles.unitsSelect}
|
||||
mode="modal"
|
||||
labelField="label"
|
||||
valueField="value"
|
||||
value={product.dimensions.u || "ft"}
|
||||
onChange={(item) => onUnitsChanged(item.value as Length)}
|
||||
/>
|
||||
<TextInput
|
||||
inputMode="decimal"
|
||||
defaultValue={length}
|
||||
onChangeText={onChangeLength}
|
||||
style={styles.lengthInput}
|
||||
aria-label="length"
|
||||
/>
|
||||
<Text style={{ flex: 1 }}>x</Text>
|
||||
<TextInput
|
||||
inputMode="decimal"
|
||||
defaultValue={width}
|
||||
onChangeText={onChangeWidth}
|
||||
style={styles.widthInput}
|
||||
aria-label="width"
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
title="+ Add Attribute"
|
||||
onPress={() =>
|
||||
props.onAttributeAdded && props.onAttributeAdded(product.id as Id)
|
||||
}
|
||||
/>
|
||||
{product.attributes && (
|
||||
<FlatList
|
||||
style={styles.productAttributesList}
|
||||
data={Object.entries(product.attributes)}
|
||||
renderItem={({ item }) => (
|
||||
<ProductAttributeEditor
|
||||
onProductTypeChange={onProductTypeChange}
|
||||
attributeKey={item[0] || "some key"}
|
||||
attributeValue={item[1]}
|
||||
onChangeAttributeKey={onAttributeKeyChanged}
|
||||
onChangeAttribute={onAttributeChanged}
|
||||
onDelete={onAttributeDelete}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item, i) => `${product.id}-${i}`}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
deleteProductHighlight: {
|
||||
|
||||
padding: 5,
|
||||
borderWidth: 1,
|
||||
},
|
||||
deleteProductButton: {
|
||||
fontSize: 20,
|
||||
},
|
||||
detailsWrapper: {
|
||||
|
||||
},
|
||||
priceSpecWrapper: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
priceLabel: {
|
||||
},
|
||||
priceInput: {
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "lightgrey",
|
||||
borderStyle: "solid",
|
||||
},
|
||||
per: {
|
||||
padding: 5,
|
||||
},
|
||||
unitsLabel: {
|
||||
},
|
||||
unitsSelect: {
|
||||
flex: 1,
|
||||
padding: 5,
|
||||
},
|
||||
lengthInput: {
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "lightgrey",
|
||||
borderStyle: "solid",
|
||||
},
|
||||
widthInput: {
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "lightgrey",
|
||||
borderStyle: "solid",
|
||||
},
|
||||
productListHeader: {
|
||||
flexDirection: "row",
|
||||
padding: 5,
|
||||
},
|
||||
productNameText: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
productItemName: {
|
||||
flex: 1,
|
||||
backgroundColor: "lightgrey",
|
||||
padding: 4,
|
||||
margin: 4,
|
||||
},
|
||||
productAttributesList: {
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: "black",
|
||||
},
|
||||
})
|
||||
deleteProductHighlight: {
|
||||
padding: 5,
|
||||
borderWidth: 1,
|
||||
},
|
||||
deleteProductButton: {
|
||||
fontSize: 20,
|
||||
},
|
||||
detailsWrapper: {},
|
||||
priceSpecWrapper: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
priceLabel: {},
|
||||
priceInput: {
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "lightgrey",
|
||||
borderStyle: "solid",
|
||||
},
|
||||
per: {
|
||||
padding: 5,
|
||||
},
|
||||
unitsLabel: {},
|
||||
unitsSelect: {
|
||||
flex: 1,
|
||||
padding: 5,
|
||||
},
|
||||
lengthInput: {
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "lightgrey",
|
||||
borderStyle: "solid",
|
||||
},
|
||||
widthInput: {
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "lightgrey",
|
||||
borderStyle: "solid",
|
||||
},
|
||||
productListHeader: {
|
||||
flexDirection: "row",
|
||||
padding: 5,
|
||||
},
|
||||
productNameText: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
productItemName: {
|
||||
flex: 1,
|
||||
backgroundColor: "lightgrey",
|
||||
padding: 4,
|
||||
margin: 4,
|
||||
},
|
||||
productAttributesList: {
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: "black",
|
||||
},
|
||||
});
|
||||
|
@ -1,19 +1,22 @@
|
||||
import { FlatList, ScrollView, StyleSheet, Text, TouchableHighlight } from "react-native";
|
||||
import { ScrollView, StyleSheet } from "react-native";
|
||||
import { ProductTile } from "./ProductTile";
|
||||
import { Id, Product } from "@/lib/product";
|
||||
import { Key, useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Product } from "@/lib/product";
|
||||
import { useState } from "react";
|
||||
import { selectProducts } from "@/features/product/productSlice";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
|
||||
export type ProductSelectionProps = {
|
||||
onProductSelected?: (product: Product) => any;
|
||||
}
|
||||
|
||||
export default function ProductList({ onProductSelected }: ProductSelectionProps) {
|
||||
};
|
||||
|
||||
export default function ProductList({
|
||||
onProductSelected,
|
||||
}: ProductSelectionProps) {
|
||||
const [activeProduct, setActiveProduct] = useState(null as null | Product);
|
||||
const products = useAppSelector(selectProducts).filter(p => (!!p.dimensions));
|
||||
const products = useAppSelector(selectProducts).filter(p => !!p).filter((p) => {
|
||||
console.dir(p);
|
||||
return !!p.dimensions;
|
||||
});
|
||||
|
||||
function doOnProductSelected(product: Product) {
|
||||
setActiveProduct(product);
|
||||
@ -21,8 +24,8 @@ export default function ProductList({ onProductSelected }: ProductSelectionProps
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView scrollToOverflowEnabled={true}>
|
||||
{products.map(product => {
|
||||
<ScrollView scrollToOverflowEnabled={true} aria-label="product list">
|
||||
{products.map((product) => {
|
||||
return (
|
||||
<ProductTile
|
||||
product={product}
|
||||
@ -33,14 +36,12 @@ export default function ProductList({ onProductSelected }: ProductSelectionProps
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
productSelectorFlatList: {
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
},
|
||||
|
||||
})
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Product } from "@/lib/product"
|
||||
import { Product, priceDisplay, pricePerUnitDisplay } from "@/lib/product"
|
||||
import { ImageBackground, StyleProp, StyleSheet, Text, TouchableHighlight, View, ViewStyle } from "react-native";
|
||||
import { AnimatedStyle } from "react-native-reanimated";
|
||||
|
||||
@ -23,12 +23,14 @@ const FALLBACK_IMAGE = "";
|
||||
|
||||
export function ProductTile ({product, onProductSelected, isActive} : ProductTileProps) {
|
||||
const k = isActive ? "active" : "default";
|
||||
|
||||
const priceDisplay = pricePerUnitDisplay(product);
|
||||
return (
|
||||
|
||||
<TouchableHighlight
|
||||
style={styles[k].highlight}
|
||||
onPress={() => onProductSelected && onProductSelected(product)}>
|
||||
<Text style={styles[k].text}>{product.attributes.name || `Product ${product.id}`} ({product.pricePerUnitDisplay})</Text>
|
||||
<Text style={styles[k].text}>{product.attributes?.name || `Product ${product.id}`} ({priceDisplay})</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
@ -1,25 +1,16 @@
|
||||
import { Product } from "@/lib/product"
|
||||
import { LumberProduct, Product } from "@/lib/product"
|
||||
import {ProductAttributeEditor} from "../ProductAttributeEditor"
|
||||
import { area } from "enheter"
|
||||
import { fireEvent, render, screen } from '@testing-library/react-native';
|
||||
import React from "react";
|
||||
import { emitTypingEvents } from "@testing-library/react-native/build/user-event/type/type";
|
||||
|
||||
describe("Product editor tests", () => {
|
||||
const productName = "Fun Product";
|
||||
it("Product attributes can be deleted", async () => {
|
||||
const product = new Product(
|
||||
100,
|
||||
{l: 100, u: "foot"},
|
||||
{"name" : productName}
|
||||
);
|
||||
const onChange = jest.fn();
|
||||
const onDelete = jest.fn();
|
||||
render(
|
||||
<ProductAttributeEditor
|
||||
attributeKey="name"
|
||||
attributeValue="product"
|
||||
product={product}
|
||||
onChangeAttribute={onChange}
|
||||
onDelete={onDelete}
|
||||
/>);
|
||||
@ -28,11 +19,14 @@ describe("Product editor tests", () => {
|
||||
expect(onDelete).toHaveBeenCalled();
|
||||
});
|
||||
it("Product attributes can be modified", async () => {
|
||||
const product = new Product(
|
||||
100,
|
||||
{l: 100, u: "foot"},
|
||||
{"name" : productName}
|
||||
);
|
||||
const product : Product = {
|
||||
pricePerUnit: 10,
|
||||
dimensions: {
|
||||
l: 40,
|
||||
u: "ft",
|
||||
},
|
||||
type: "lumber",
|
||||
}
|
||||
const onChange = jest.fn();
|
||||
const onDelete = jest.fn();
|
||||
const onKeyChange = jest.fn();
|
||||
|
@ -2,50 +2,56 @@ import { renderWithProviders } from "@/lib/rendering";
|
||||
import { ProductEditor } from "@/components/ProductEditor";
|
||||
import { act, fireEvent, screen } from "@testing-library/react-native";
|
||||
import { selectProducts } from "@/features/product/productSlice";
|
||||
import { Product } from "@/lib/product";
|
||||
import { LumberProduct, Product } from "@/lib/product";
|
||||
|
||||
describe("ProductEditor", () => {
|
||||
const productName = "Flooring"
|
||||
const mockProduct = new Product(
|
||||
25,
|
||||
{ l: 4, w: 8, u: "foot" },
|
||||
{ name: productName },
|
||||
)
|
||||
it("renders correctly", async () => {
|
||||
const { store } = renderWithProviders(<ProductEditor />, {
|
||||
products: [
|
||||
mockProduct.asObject,
|
||||
],
|
||||
});
|
||||
|
||||
const state1 = store.getState();
|
||||
|
||||
let products = selectProducts(state1);
|
||||
|
||||
expect(products).toHaveLength(1);
|
||||
|
||||
// Check if the product names are rendered
|
||||
expect(screen.getByText(products[0].attributes.name as string)).toBeTruthy();
|
||||
|
||||
// Start to edit a product
|
||||
fireEvent.press(screen.getByText(productName));
|
||||
|
||||
// Change properties of the product to make sure it's updated in the store
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(screen.getByLabelText("length"), "16");
|
||||
})
|
||||
products = selectProducts(store.getState());
|
||||
expect(products[0].dimensions.l).toBe(16);
|
||||
act(() => {
|
||||
fireEvent.changeText(screen.getByLabelText("width"), "32");
|
||||
})
|
||||
products = selectProducts(store.getState());
|
||||
|
||||
expect(products[0].dimensions.w).toBe(32);
|
||||
|
||||
fireEvent.press(screen.getByLabelText("delete product"));
|
||||
products = selectProducts(store.getState());
|
||||
expect(products.length).toBe(0);
|
||||
const productName = "Flooring";
|
||||
const mockProduct: LumberProduct = {
|
||||
attributes: {
|
||||
name: productName,
|
||||
},
|
||||
pricePerUnit: 10,
|
||||
dimensions: {
|
||||
l: 40,
|
||||
u: "ft",
|
||||
},
|
||||
type: "lumber",
|
||||
};
|
||||
it("renders correctly", async () => {
|
||||
const { store } = renderWithProviders(<ProductEditor />, {
|
||||
products: [mockProduct],
|
||||
});
|
||||
|
||||
const state1 = store.getState();
|
||||
|
||||
let products = selectProducts(state1);
|
||||
|
||||
expect(products).toHaveLength(1);
|
||||
|
||||
// Check if the product names are rendered
|
||||
expect(
|
||||
screen.getByText(products[0].attributes.name as string)
|
||||
).toBeTruthy();
|
||||
|
||||
// Start to edit a product
|
||||
fireEvent.press(screen.getByText(productName));
|
||||
|
||||
// Change properties of the product to make sure it's updated in the store
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(screen.getByLabelText("length"), "16");
|
||||
});
|
||||
products = selectProducts(store.getState());
|
||||
expect(products[0].dimensions.l).toBe(16);
|
||||
act(() => {
|
||||
fireEvent.changeText(screen.getByLabelText("width"), "32");
|
||||
});
|
||||
products = selectProducts(store.getState());
|
||||
|
||||
expect(products[0].dimensions.w).toBe(32);
|
||||
|
||||
fireEvent.press(screen.getByLabelText("delete product"));
|
||||
products = selectProducts(store.getState());
|
||||
expect(products.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@ -4,14 +4,22 @@ import { ProductEditorItem } from '../ProductEditorItem';
|
||||
import { Product } from '@/lib/product';
|
||||
import { area } from 'enheter';
|
||||
import { renderWithProviders } from '@/lib/rendering';
|
||||
import { area_t } from '@/lib/dimensions';
|
||||
|
||||
describe('ProductEditorItem', () => {
|
||||
const productName = "Product 1";
|
||||
const mockProduct = new Product(
|
||||
25,
|
||||
{l: 4, u: 'feet'},
|
||||
{"name": productName},
|
||||
)
|
||||
const mockProduct : Product = {
|
||||
type: "area_rug",
|
||||
dimensions: {
|
||||
l: 1,
|
||||
w: 1,
|
||||
u: "feet",
|
||||
},
|
||||
pricePerUnit: 0.75,
|
||||
attributes: {
|
||||
name: productName,
|
||||
}
|
||||
}
|
||||
|
||||
const onAttributeAdded = jest.fn();
|
||||
const mockOnProductDeleted = jest.fn();
|
||||
@ -56,7 +64,7 @@ describe('ProductEditorItem', () => {
|
||||
}
|
||||
);
|
||||
fireEvent.press(screen.getByText("Product 1"));
|
||||
expect(screen.getByLabelText("units")).toBeTruthy();
|
||||
// expect(screen.getByLabelText("Units")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Edit Key")).toBeTruthy();
|
||||
expect(screen.getAllByLabelText("Edit Value").length).toEqual(1);
|
||||
|
||||
|
27
components/__tests__/ProductList-test.tsx
Normal file
27
components/__tests__/ProductList-test.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { renderWithProviders } from '@/lib/rendering';
|
||||
import { Product } from '@/lib/product';
|
||||
import ProductList from '@/components/ProductList';
|
||||
import initialProducts from '@/__fixtures__/initialProducts';
|
||||
import { screen } from '@testing-library/react-native';
|
||||
|
||||
describe('ProductList', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { getByTestId } = renderWithProviders(<ProductList />, {
|
||||
products: initialProducts,
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText('product list')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders products correctly', () => {
|
||||
const mockProduct = initialProducts[0];
|
||||
|
||||
const { getByText } = renderWithProviders(<ProductList />, {
|
||||
products: [mockProduct],
|
||||
});
|
||||
|
||||
expect(getByText(mockProduct.attributes.name)).toBeTruthy();
|
||||
expect(getByText(`$${mockProduct.pricePerUnit}`)).toBeTruthy();
|
||||
});
|
||||
});
|
@ -151,7 +151,7 @@ const productsState = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const selectProductsDatas = (state: RootState) => {
|
||||
export const selectProducts = (state: RootState) => {
|
||||
return state.products;
|
||||
}
|
||||
|
||||
@ -159,10 +159,6 @@ export const selectUnits = (state : RootState) => {
|
||||
return (state.units || "ft") as Length;
|
||||
}
|
||||
|
||||
export const selectProducts = createSelector([selectProductsDatas], productsData => {
|
||||
return productsData.map(d => Product.fromObject(d));
|
||||
})
|
||||
|
||||
export const selectProductIds = createSelector([selectProducts], products => {
|
||||
return products.map(p => p.id);
|
||||
})
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { length } from "enheter";
|
||||
import { Product } from "../product";
|
||||
|
||||
describe("Product tests", () => {
|
||||
@ -20,11 +19,4 @@ describe("Product tests", () => {
|
||||
const comparison = standard.priceFor({l : 24, u: "inch"});
|
||||
expect(comparison).toBeCloseTo(20, 4);
|
||||
});
|
||||
|
||||
it("Can convert to/from object", () => {
|
||||
const standard = new Product(10, {l: 1, u : "feet"});
|
||||
const obj = standard.asObject;
|
||||
const back = Product.fromObject(obj);
|
||||
expect(back).toEqual(standard);
|
||||
})
|
||||
});
|
@ -11,7 +11,6 @@ export type area_t = length_t & {
|
||||
|
||||
export type dimensions_t = area_t | length_t;
|
||||
|
||||
export type product_type_t = "area" | "length";
|
||||
|
||||
export const isArea = (d: dimensions_t) => ("width" in d);
|
||||
export const isLength = (d: dimensions_t) => (!("width" in d));
|
||||
|
@ -1,6 +1,7 @@
|
||||
import uuid from "react-native-uuid";
|
||||
import { dimensions_t, area_t } from "./dimensions";
|
||||
import { matchDimensions } from "./dimensions";
|
||||
import { Area, Length, Unit } from "convert";
|
||||
|
||||
export type Id = string;
|
||||
|
||||
@ -15,11 +16,18 @@ export type ProductAttributes = {
|
||||
currency?: Currency,
|
||||
[index:string]: any,
|
||||
}
|
||||
|
||||
export function dimensionArea(d: dimensions_t) {
|
||||
return "w" in d ? d.w * d.l : 0;
|
||||
}
|
||||
|
||||
export type product_type_t = "lumber" | "area_rug"
|
||||
export const PRODUCT_TYPES = [
|
||||
"lumber",
|
||||
"area_rug",
|
||||
] as const;
|
||||
|
||||
export type product_type_t = typeof PRODUCT_TYPES[number];
|
||||
|
||||
|
||||
export type Product = {
|
||||
id?: Id;
|
||||
|
Loading…
Reference in New Issue
Block a user