Compare commits

..

No commits in common. "develop" and "snapshot-2024-7-4-1" have entirely different histories.

24 changed files with 93 additions and 215 deletions

105
README.md
View File

@ -1,7 +1,5 @@
# 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
@ -12,87 +10,60 @@ 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
![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>`.
[[ TODO ]]
# Development Docs
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.
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
## 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
pnpm install
npm install
```
2. Start the app
```bash
pnpx expo start
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.

View File

@ -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/dimensions_t";
import { ProductData, } from "@/lib/product";
import {Length} from "convert"
const rememberedKeys = ['products'];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

View File

@ -1,5 +1,5 @@
import { MeasurementInput } from "./MeasurementInput";
import { area_t, dimensions_t } from "@/lib/dimensions_t";
import { area_t, dimensions_t } from "@/lib/product";
import { Length } from "convert";
import { useState } from "react";
import { StyleSheet, Text, View } from "react-native";

View File

@ -1,53 +0,0 @@
import { area_t } from "@/lib/dimensions";
import convert, { Area, Length } from "convert";
import dayjs, { Dayjs } from "dayjs";
import { StyleSheet, Text, View } from "react-native";
export type AreaRugTagProps = {
dimensions: area_t,
price_per_area: {
price: number,
per: {
n: number,
u: Area,
}
},
date?: Dayjs
currencySymbol?: string
};
export const AreaRugTag = (props: AreaRugTagProps) => {
const date = props.date || dayjs();
const square = props.dimensions.l * props.dimensions.w;
const areaUnits = `square ${props.dimensions.u}`;
const square2 = convert(square, areaUnits as Area).to(props.price_per_area.per.u)
const price = (square2 / props.price_per_area.per.n) * props.price_per_area.price;
const sPrice = price.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
const currencySymbol = props.currencySymbol || "$";
return (
<View style={styles.component}>
<Text aria-label="area rug dimensions" style={styles.dimensions}>{props.dimensions.l} x {props.dimensions.w}</Text>
<Text aria-label="area rug price" style={styles.price}>{currencySymbol} {sPrice}</Text>
<Text aria-label="area rug date" style={styles.date}>{date.format("YYYY/MM/DD")}</Text>
<Text aria-label="this week's color" style={styles.tagColor}>[Curent Tag Color]</Text>
</View>
)
};
const styles = StyleSheet.create({
component: {
paddingVertical: 100,
flex: 1,
},
dimensions: {
},
price: {
},
date: {
},
tagColor: {
},
})

View File

@ -1,4 +1,4 @@
import { dimensions_t, length_t } from "@/lib/dimensions_t";
import { dimensions_t, length_t } from "@/lib/product";
import { Length } from "convert";
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";

View File

@ -1,5 +1,4 @@
import { Product } from '@/lib/product';
import { dimensions_t } from "@/lib/dimensions_t";
import { Product, dimensions_t } from '@/lib/product';
import { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

View File

@ -1,7 +1,6 @@
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 { Id, Product, dimensions_t } from "@/lib/product";
import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native";
import { ProductEditorItem } from "./ProductEditorItem";

View File

@ -1,5 +1,4 @@
import { Id, Product } from "@/lib/product"
import { dimensions_t } from "@/lib/dimensions_t";
import { Id, Product, dimensions_t } from "@/lib/product"
import { useState } from "react"
import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native"
import { ProductAttributeEditor } from "./ProductAttributeEditor";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,6 +1,5 @@
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Id, Product } from '@/lib/product';
import { dimensions_t, ProductData } from "@/lib/dimensions_t";
import { dimensions_t, Id, Product, ProductData } from '@/lib/product';
import uuid from "react-native-uuid";
import { RootState } from '@/app/store';
import { Length } from 'convert';

View File

@ -1,14 +0,0 @@
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);
});
});

View File

@ -1,52 +0,0 @@
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,
};
}

View File

@ -1,8 +1,6 @@
import uuid from "react-native-uuid";
import { Area } from "convert";
import convert, { Area, Length } from "convert";
import { Transform } from "class-transformer";
import { dimensions_t, area_t } from "./dimensions";
import { matchDimensions } from "./dimensions";
export type Id = string;
@ -17,18 +15,55 @@ 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;

View File

@ -2,8 +2,7 @@ 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 } from "@/lib/product";
import { ProductData } from "./product";
import { Product, ProductData } from "@/lib/product";
export interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: Partial<RootState>;

View File

@ -24,7 +24,6 @@
"@reduxjs/toolkit": "^2.2.6",
"class-transformer": "^0.5.1",
"convert": "^5.3.0",
"dayjs": "^1.11.11",
"expo": "~51.0.18",
"expo-asset": "^10.0.10",
"expo-constants": "~16.0.2",

View File

@ -38,9 +38,6 @@ dependencies:
convert:
specifier: ^5.3.0
version: 5.3.0
dayjs:
specifier: ^1.11.11
version: 1.11.11
expo:
specifier: ~51.0.18
version: 51.0.18(@babel/core@7.24.7)(@babel/preset-env@7.24.7)