diff --git a/README.md b/README.md index da66284..4e88023 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PliWould - Measure And Price Sheet Good Merchandise +![PlyWould's ugly logo](./assets/images/pli-would-512.png) + Working behind the register at Habitat for Humanity ReStore I found sheet goods often came in partials. As a cashier, I would often have to determine the correct price for merchandise based on the area or length @@ -10,60 +12,87 @@ a manager to assist. Instead, I took it upon myself to find a solution. Hence, I created PliWould, an app that determines the cost of a product based on its dimensions. -By default my store's prices are in the database. However, the prices are editable and you can add or remove +By default my store's prices are in the database. +However, the prices are editable and you can add or remove products as needed. ## Usage -[[ TODO ]] +![App Overview](./doc/images/screenshots/index.png) + +### ![Scale Tab](./doc/images/icons/scale.png) Measure Tab + +This tab is used to determine the price based on measurements. + +Select a product from a list of products. + +![Plywood sheet selected](./doc/images/screenshots/plywood-sheet-4-by-8-inches.png) + +There are 2 different types of products: + +1. Length prodcuts +2. Area products. + +Typically length products have a Square button that measures per length, +but some do not, so I included them. + +Area products on the Square console are "partials," so they are listed in the product list. + +Select one you wish to price. + +Automatically the measurements for the "base measurement" will be filled in. + +Using a tape measure, measure the sheet's length and width. You can either put in +inches or feet (switch using the `in`/`ft` button selector). + +If the product is damaged, use the slider to select the amount of damage + +![Plywood sheet selected, with 25% damage](./doc/images/screenshots/plywood-sheet-4-by-8-feet-25-damage.png) + +If you select a length product, only the length field will be present. Proceed as you +would with area products. + +![Length product selected](./doc/images/screenshots/house-siding-length-input-feet.png) + +### ![Product Editor Tab](./doc/images/icons/list.png) Product Editor (WIP) + +In the product editor, you can add or remove products as needed. + +You can even edit or add attributes. + +Note that the `name` attribute is highly recommended as it's the name of the product. + +Otherwise, it will display as `Product `. # Development Docs -This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). +This is an [Expo](https://expo.dev) project. + +The `develop` branch is used to develop features until it's ready to be merged +into main. ## Get started +1. Clone the repository. + +``` +$ git clone https://gittea.dev/srcrr/PliWould +``` + +2. Install eas-cli **globally** + +``` +$ npm i -g eas-cli +``` + 1. Install dependencies ```bash - npm install + pnpm install ``` 2. Start the app ```bash - npx expo start - ``` - -In the output, you'll find options to open the app in a - -- [development build](https://docs.expo.dev/develop/development-builds/introduction/) -- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) -- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) -- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo - -You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). - -## Get a fresh project - -When you're ready, run: - -```bash -npm run reset-project -``` - -This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. - -## Learn more - -To learn more about developing your project with Expo, look at the following resources: - -- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). -- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. - -## Join the community - -Join our community of developers creating universal apps. - -- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. -- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. + pnpx expo start + ``` \ No newline at end of file diff --git a/app/store.ts b/app/store.ts index 31f11e1..acc22a6 100644 --- a/app/store.ts +++ b/app/store.ts @@ -3,7 +3,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { rememberReducer, rememberEnhancer } from 'redux-remember'; import reducers from "@/features/product/productSlice" import AsyncStorage from '@react-native-async-storage/async-storage'; -import { ProductData, } from "@/lib/product"; +import { ProductData } from "@/lib/dimensions_t"; import {Length} from "convert" const rememberedKeys = ['products']; diff --git a/assets/images/pli-would-512.png b/assets/images/pli-would-512.png new file mode 100644 index 0000000..afbb926 Binary files /dev/null and b/assets/images/pli-would-512.png differ diff --git a/components/AreaInput.tsx b/components/AreaInput.tsx index 929f0e0..2263984 100644 --- a/components/AreaInput.tsx +++ b/components/AreaInput.tsx @@ -1,5 +1,5 @@ import { MeasurementInput } from "./MeasurementInput"; -import { area_t, dimensions_t } from "@/lib/product"; +import { area_t, dimensions_t } from "@/lib/dimensions_t"; import { Length } from "convert"; import { useState } from "react"; import { StyleSheet, Text, View } from "react-native"; diff --git a/components/MeasurementInput.tsx b/components/MeasurementInput.tsx index 7479612..51517ea 100644 --- a/components/MeasurementInput.tsx +++ b/components/MeasurementInput.tsx @@ -1,4 +1,4 @@ -import { dimensions_t, length_t } from "@/lib/product"; +import { dimensions_t, length_t } from "@/lib/dimensions_t"; import { Length } from "convert"; import { useState } from "react"; import { StyleSheet, Text, TextInput, View } from "react-native"; diff --git a/components/ProductCalculatorSelector.tsx b/components/ProductCalculatorSelector.tsx index 68d997e..c6153c1 100644 --- a/components/ProductCalculatorSelector.tsx +++ b/components/ProductCalculatorSelector.tsx @@ -1,4 +1,5 @@ -import { Product, dimensions_t } from '@/lib/product'; +import { Product } from '@/lib/product'; +import { dimensions_t } from "@/lib/dimensions_t"; import { useState, useEffect } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/components/ProductEditor.tsx b/components/ProductEditor.tsx index 043c8bc..cf97ebe 100644 --- a/components/ProductEditor.tsx +++ b/components/ProductEditor.tsx @@ -1,6 +1,7 @@ import { useAppDispatch, useAppSelector } from "@/app/store" import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice" -import { Id, Product, dimensions_t } from "@/lib/product"; +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"; diff --git a/components/ProductEditorItem.tsx b/components/ProductEditorItem.tsx index 2b15c47..f73bff2 100644 --- a/components/ProductEditorItem.tsx +++ b/components/ProductEditorItem.tsx @@ -1,4 +1,5 @@ -import { Id, Product, dimensions_t } from "@/lib/product" +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 { ProductAttributeEditor } from "./ProductAttributeEditor"; diff --git a/doc/images/icons/list.png b/doc/images/icons/list.png new file mode 100644 index 0000000..310bbff Binary files /dev/null and b/doc/images/icons/list.png differ diff --git a/doc/images/icons/scale.png b/doc/images/icons/scale.png new file mode 100644 index 0000000..b8c7ea9 Binary files /dev/null and b/doc/images/icons/scale.png differ diff --git a/doc/images/screenshots/house-siding-length-input-feet.png b/doc/images/screenshots/house-siding-length-input-feet.png new file mode 100644 index 0000000..f584751 Binary files /dev/null and b/doc/images/screenshots/house-siding-length-input-feet.png differ diff --git a/doc/images/screenshots/index.png b/doc/images/screenshots/index.png new file mode 100644 index 0000000..eb4df5a Binary files /dev/null and b/doc/images/screenshots/index.png differ diff --git a/doc/images/screenshots/plywood-sheet-4-by-8-feet-25-damage.png b/doc/images/screenshots/plywood-sheet-4-by-8-feet-25-damage.png new file mode 100644 index 0000000..5e99427 Binary files /dev/null and b/doc/images/screenshots/plywood-sheet-4-by-8-feet-25-damage.png differ diff --git a/doc/images/screenshots/plywood-sheet-4-by-8-feet.png b/doc/images/screenshots/plywood-sheet-4-by-8-feet.png new file mode 100644 index 0000000..b941ff0 Binary files /dev/null and b/doc/images/screenshots/plywood-sheet-4-by-8-feet.png differ diff --git a/doc/images/screenshots/plywood-sheet-4-by-8-inches.png b/doc/images/screenshots/plywood-sheet-4-by-8-inches.png new file mode 100644 index 0000000..a4f7ad5 Binary files /dev/null and b/doc/images/screenshots/plywood-sheet-4-by-8-inches.png differ diff --git a/doc/images/screenshots/product-editor.png b/doc/images/screenshots/product-editor.png new file mode 100644 index 0000000..8037f96 Binary files /dev/null and b/doc/images/screenshots/product-editor.png differ diff --git a/features/product/productSlice.ts b/features/product/productSlice.ts index 594ae2e..3dd6a26 100644 --- a/features/product/productSlice.ts +++ b/features/product/productSlice.ts @@ -1,5 +1,6 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { dimensions_t, Id, Product, ProductData } from '@/lib/product'; +import { Id, Product } from '@/lib/product'; +import { dimensions_t, ProductData } from "@/lib/dimensions_t"; import uuid from "react-native-uuid"; import { RootState } from '@/app/store'; import { Length } from 'convert'; diff --git a/lib/__tests__/dimensions-test.ts b/lib/__tests__/dimensions-test.ts new file mode 100644 index 0000000..c007c64 --- /dev/null +++ b/lib/__tests__/dimensions-test.ts @@ -0,0 +1,14 @@ +import { diameterToLength, length_t } from '../dimensions'; + +describe('diameterToLength', () => { + it('should throw an error if the units of the outer and inner diameters do not match', () => { + expect(() => diameterToLength({ l: 10, u: 'inch' }, { l: 8, u: 'foot' }, 2)).toThrow('diameter units must match!'); + }); + + it('should return the correct length for multiple rings with different units', () => { + const outer : length_t = {l: 25, u: "in"}; + const inner: length_t = {l : 1, u: "in"}; + const l = diameterToLength(outer, inner, 12); + expect(l.l).toBeCloseTo(490, -1.0); + }); +}); diff --git a/lib/dimensions.ts b/lib/dimensions.ts new file mode 100644 index 0000000..18e912b --- /dev/null +++ b/lib/dimensions.ts @@ -0,0 +1,52 @@ +import convert from "convert"; +import { Length } from "convert"; + +export type length_t = { + l: number; u: Length; +}; + +export type area_t = length_t & { + w: number; +}; + +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)); +export const dimensionType = (d: dimensions_t) => isArea(d) ? "area" : "length"; export function matchDimensions(d1: dimensions_t, d2: dimensions_t) { + if (!( + (isArea(d1) && isArea(d2)) || + (isLength(d1) && isLength(d2)) + )) { + throw new Error(`Dimension mismatch: ${JSON.stringify(d1)} / ${JSON.stringify(d1)}`); + } + + return { + l: convert(d1.l, d1.u).to(d2.u), + u: d2.u, + ...( + "w" in d1 ? + { w: convert(d1.w, d1.u).to(d2.u), } + : {} + ) + }; +} + +/** + * Gets the total length of a carpet roll based on a diameter + * @param outerDiameter Outer diameter of the carpet roll + * @param innerDiameter Inner diameter of the carpet roll (the "hole") + * @param numRings Number of "rings" or "layers,"" just like a tree 🙂 🌲 + */ +export function diameterToLength(outerDiameter: length_t, innerDiameter: length_t, numRings: number) { + if (outerDiameter.u !== innerDiameter.u) { + throw new Error("diameter units must match!") + } + const thickness = (((outerDiameter.l - innerDiameter.l) / 2) / numRings); + return { + l: Math.PI * (Math.pow(outerDiameter.l, 2) / 4) - (Math.pow(innerDiameter.l, 2) / 4) / thickness, + u: innerDiameter.u, + }; +} \ No newline at end of file diff --git a/lib/product.ts b/lib/product.ts index b8c61eb..a2bcfff 100644 --- a/lib/product.ts +++ b/lib/product.ts @@ -1,6 +1,8 @@ import uuid from "react-native-uuid"; -import convert, { Area, Length } from "convert"; +import { Area } from "convert"; import { Transform } from "class-transformer"; +import { dimensions_t, area_t } from "./dimensions"; +import { matchDimensions } from "./dimensions"; export type Id = string; @@ -15,55 +17,18 @@ export type ProductAttributes = { currency?: Currency, // [index:string]: any, } -export type length_t = { - l: number, u: Length -} - -export type area_t = length_t & { - w: number, -} - -export type dimensions_t = area_t | length_t; - -export type ProductData = { - id?: Id, - pricePerUnit: number, - dimensions: dimensions_t, - attributes?: ProductAttributes, -}; - - -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)); -export const dimensionType = (d: dimensions_t) => isArea(d) ? "area" : "length" - -export function matchDimensions(d1: dimensions_t, d2: dimensions_t) { - if (! - ( - (isArea(d1) && isArea(d2)) || - (isLength(d1) && isLength(d2)) - ) - ) { - throw new Error(`Dimension mismatch: ${JSON.stringify(d1)} / ${JSON.stringify(d1)}`); - } - - return { - l: convert(d1.l, d1.u).to(d2.u), - u: d2.u, - ...( - "w" in d1 ? - { w: convert(d1.w, d1.u).to(d2.u), } - : {} - ) - } -} - export function dimensionArea(d: dimensions_t) { return "w" in d ? d.w * d.l : 0; } +export type ProductData = { + id?: Id; + pricePerUnit: number; + dimensions: dimensions_t; + attributes?: ProductAttributes; +}; + + export class Product { public id?: Id; diff --git a/lib/rendering.tsx b/lib/rendering.tsx index a7c1827..36bb709 100644 --- a/lib/rendering.tsx +++ b/lib/rendering.tsx @@ -2,7 +2,8 @@ import { RenderOptions, render } from "@testing-library/react-native"; import { PropsWithChildren, ReactElement } from "react"; import { Provider } from "react-redux"; import { setupStore, RootState } from "@/app/store"; -import { Product, ProductData } from "@/lib/product"; +import { Product } from "@/lib/product"; +import { ProductData } from "./product"; export interface ExtendedRenderOptions extends Omit { preloadedState?: Partial;