add screenshots and images for docs. add icon.
107
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 <UUID>`.
|
||||
|
||||
# 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
|
||||
```
|
@ -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'];
|
||||
|
BIN
assets/images/pli-would-512.png
Normal file
After Width: | Height: | Size: 259 KiB |
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
BIN
doc/images/icons/list.png
Normal file
After Width: | Height: | Size: 722 B |
BIN
doc/images/icons/scale.png
Normal file
After Width: | Height: | Size: 980 B |
BIN
doc/images/screenshots/house-siding-length-input-feet.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
doc/images/screenshots/index.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
doc/images/screenshots/plywood-sheet-4-by-8-feet-25-damage.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
doc/images/screenshots/plywood-sheet-4-by-8-feet.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
doc/images/screenshots/plywood-sheet-4-by-8-inches.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
doc/images/screenshots/product-editor.png
Normal file
After Width: | Height: | Size: 32 KiB |
@ -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';
|
||||
|
14
lib/__tests__/dimensions-test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
52
lib/dimensions.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
|
@ -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<RenderOptions, 'queries'> {
|
||||
preloadedState?: Partial<RootState>;
|
||||
|