Compare commits
No commits in common. "develop" and "snapshot-2024-7-4-1" have entirely different histories.
develop
...
snapshot-2
107
README.md
@ -1,7 +1,5 @@
|
|||||||
# PliWould - Measure And Price Sheet Good Merchandise
|
# 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.
|
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
|
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.
|
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.
|
By default my store's prices are in the database. However, the prices are editable and you can add or remove
|
||||||
However, the prices are editable and you can add or remove
|
|
||||||
products as needed.
|
products as needed.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
![App Overview](./doc/images/screenshots/index.png)
|
[[ TODO ]]
|
||||||
|
|
||||||
### ![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
|
# Development Docs
|
||||||
|
|
||||||
This is an [Expo](https://expo.dev) project.
|
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
||||||
|
|
||||||
The `develop` branch is used to develop features until it's ready to be merged
|
|
||||||
into main.
|
|
||||||
|
|
||||||
## Get started
|
## 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
|
1. Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the app
|
2. Start the app
|
||||||
|
|
||||||
```bash
|
```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.
|
||||||
|
@ -3,7 +3,7 @@ import { configureStore } from '@reduxjs/toolkit';
|
|||||||
import { rememberReducer, rememberEnhancer } from 'redux-remember';
|
import { rememberReducer, rememberEnhancer } from 'redux-remember';
|
||||||
import reducers from "@/features/product/productSlice"
|
import reducers from "@/features/product/productSlice"
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { ProductData } from "@/lib/dimensions_t";
|
import { ProductData, } from "@/lib/product";
|
||||||
import {Length} from "convert"
|
import {Length} from "convert"
|
||||||
|
|
||||||
const rememberedKeys = ['products'];
|
const rememberedKeys = ['products'];
|
||||||
|
Before Width: | Height: | Size: 259 KiB |
@ -1,5 +1,5 @@
|
|||||||
import { MeasurementInput } from "./MeasurementInput";
|
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 { Length } from "convert";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
|
@ -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: {
|
|
||||||
},
|
|
||||||
})
|
|
@ -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 { Length } from "convert";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { StyleSheet, Text, TextInput, View } from "react-native";
|
import { StyleSheet, Text, TextInput, View } from "react-native";
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Product } from '@/lib/product';
|
import { Product, dimensions_t } from '@/lib/product';
|
||||||
import { dimensions_t } from "@/lib/dimensions_t";
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||||
import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice"
|
import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice"
|
||||||
import { Id, Product } from "@/lib/product";
|
import { Id, Product, dimensions_t } from "@/lib/product";
|
||||||
import { dimensions_t } from "@/lib/dimensions_t";
|
|
||||||
import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native";
|
import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native";
|
||||||
import { ProductEditorItem } from "./ProductEditorItem";
|
import { ProductEditorItem } from "./ProductEditorItem";
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Id, Product } from "@/lib/product"
|
import { Id, Product, dimensions_t } from "@/lib/product"
|
||||||
import { dimensions_t } from "@/lib/dimensions_t";
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native"
|
import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native"
|
||||||
import { ProductAttributeEditor } from "./ProductAttributeEditor";
|
import { ProductAttributeEditor } from "./ProductAttributeEditor";
|
||||||
|
Before Width: | Height: | Size: 722 B |
Before Width: | Height: | Size: 980 B |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB |
@ -1,6 +1,5 @@
|
|||||||
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { Id, Product } from '@/lib/product';
|
import { dimensions_t, Id, Product, ProductData } from '@/lib/product';
|
||||||
import { dimensions_t, ProductData } from "@/lib/dimensions_t";
|
|
||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { Length } from 'convert';
|
import { Length } from 'convert';
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,8 +1,6 @@
|
|||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
import { Area } from "convert";
|
import convert, { Area, Length } from "convert";
|
||||||
import { Transform } from "class-transformer";
|
import { Transform } from "class-transformer";
|
||||||
import { dimensions_t, area_t } from "./dimensions";
|
|
||||||
import { matchDimensions } from "./dimensions";
|
|
||||||
|
|
||||||
export type Id = string;
|
export type Id = string;
|
||||||
|
|
||||||
@ -17,18 +15,55 @@ export type ProductAttributes = {
|
|||||||
currency?: Currency,
|
currency?: Currency,
|
||||||
// [index:string]: any,
|
// [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) {
|
export function dimensionArea(d: dimensions_t) {
|
||||||
return "w" in d ? d.w * d.l : 0;
|
return "w" in d ? d.w * d.l : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductData = {
|
|
||||||
id?: Id;
|
|
||||||
pricePerUnit: number;
|
|
||||||
dimensions: dimensions_t;
|
|
||||||
attributes?: ProductAttributes;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export class Product {
|
export class Product {
|
||||||
|
|
||||||
public id?: Id;
|
public id?: Id;
|
||||||
|
@ -2,8 +2,7 @@ import { RenderOptions, render } from "@testing-library/react-native";
|
|||||||
import { PropsWithChildren, ReactElement } from "react";
|
import { PropsWithChildren, ReactElement } from "react";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { setupStore, RootState } from "@/app/store";
|
import { setupStore, RootState } from "@/app/store";
|
||||||
import { Product } from "@/lib/product";
|
import { Product, ProductData } from "@/lib/product";
|
||||||
import { ProductData } from "./product";
|
|
||||||
|
|
||||||
export interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
|
export interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
|
||||||
preloadedState?: Partial<RootState>;
|
preloadedState?: Partial<RootState>;
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
"@reduxjs/toolkit": "^2.2.6",
|
"@reduxjs/toolkit": "^2.2.6",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"convert": "^5.3.0",
|
"convert": "^5.3.0",
|
||||||
"dayjs": "^1.11.11",
|
|
||||||
"expo": "~51.0.18",
|
"expo": "~51.0.18",
|
||||||
"expo-asset": "^10.0.10",
|
"expo-asset": "^10.0.10",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
|
@ -38,9 +38,6 @@ dependencies:
|
|||||||
convert:
|
convert:
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0
|
version: 5.3.0
|
||||||
dayjs:
|
|
||||||
specifier: ^1.11.11
|
|
||||||
version: 1.11.11
|
|
||||||
expo:
|
expo:
|
||||||
specifier: ~51.0.18
|
specifier: ~51.0.18
|
||||||
version: 51.0.18(@babel/core@7.24.7)(@babel/preset-env@7.24.7)
|
version: 51.0.18(@babel/core@7.24.7)(@babel/preset-env@7.24.7)
|
||||||
|