Compare commits

...
12 Commits
35 changed files with 1411 additions and 594 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 KiB

After

Width:  |  Height:  |  Size: 912 KiB

+24 -15
View File
@@ -52,7 +52,7 @@ Clone this repository by running:
git clone --branch v0.1 --single-branch https://github.com/theis-js/stockhome.git
```
You can replace `v0.1` with the latest release tag to get the latest version of the stack. You can also use `dev` to get
You can replace `v0.1` with any other release tag to get your desired version of the repo. You can also use `dev` to get
the latest development version, but keep in mind that it may contain bugs and errors.
View the available version tags here: [Releases](https://github.com/theis-js/stockhome/tags)
@@ -69,7 +69,7 @@ AUTH_SIGNATURE=
BACKEND_HOST=stockhome-backend
```
Make sure that you have set an secure root password and a secure signature.
Make sure that you have set a secure root password and a secure signature.
> **Note** These three values cannot contain special characters.
@@ -87,7 +87,7 @@ First, navigate into the root directory of this repository and run:
docker compose up -d --build
```
The database and all necessary services are started and initialised automatically.
The database and all necessary services are started and initialized automatically.
### 5. First login
@@ -98,6 +98,12 @@ Username: admin
Password: admin
```
The docker config exposes the frontend on port `80`. Therefore, you can access the web page either on
`http://localhost`, if you hosted it on your local machine, or under the IP-Adress or hostname of your server.
If port `80` is occupied, you can change the docker-compose. Therefore, take a look
at [Change Port Config](.github/docs/docker/README.md#ports).
_Keep in mind that you should change the password later in the settings page._
> **Note** If errors occur get yourself some help in the [error code page](./.github/docs/error/README.md).
@@ -109,22 +115,25 @@ repository! [Report a bug](https://github.com/theis-js/stockhome/issues/new/choo
## Update
To update stockhome to the latest version, navigate into the root directory of this repository and run:
To update Stockhome to the latest version, navigate into the root directory of this repository and run:
```shell
git fetch --all --tags && git checkout "$(git describe --tags "$(git rev-list --tags --max-count=1)")"
```
To rebuild the containers run:
```shell
git pull
latesttag=$(git describe --tags)
git checkout ${latesttag}
docker compose up -d --build
```
To update to a specific version, replace `latesttag` with the version tag you want to update to. You can view the
available version tags here: [Releases](https://github.com/theis-js/stockhome/tags)
To update/downgrade to a specific version, run the command below. You can replace `v0.1` with the tag/version you want.
You can find all Releases her: [Releases](https://github.com/theis-js/stockhome/tags).
**Example to downgrade/update to version `v0.1`:**
```shell
git pull
git fetch --all
git checkout v0.1
docker compose up -d --build
```
@@ -137,20 +146,20 @@ docker compose up -d --build
- Visual Studio Code (or any other IDE)
- Cloned the repository
### Start backend and database
### Initialize repository dependencies
First, navigate into the root directory of this repository and run:
```shell
docker compose -f docker-compose.dev.yml up -d --build
npm install
```
### Start frontend
### Start development environment
Navigate into the `frontend` directory and run:
Stay in the root directory and run:
```shell
npm install
npm run build:shared
npm run dev
```
+140 -104
View File
@@ -1,133 +1,169 @@
-- =============================================================
-- STOCKHOME MOCK DATA
-- Generates: 5 Storage Locations, ~100 Products
-- STOCKHOME MOCK DATA (EN)
-- Generates: 5 Storage Locations, 94 Products
--
-- Safe to re-run: storage locations are upserted by name, and
-- products reference the locations by looked-up UUID rather
-- than a hardcoded literal. This avoids the FK error 1452 that
-- occurs when a location with the same name already exists
-- (storage_locations.name is UNIQUE) with a different UUID.
--
-- Uncomment the DELETE below if you want a clean product table.
-- =============================================================
USE stockhome;
-- Optional clean slate for products only (keeps user/app settings):
-- DELETE FROM products;
-- =============================================
-- Storage Locations (5)
-- =============================================
INSERT INTO storage_locations (uuid, name, description) VALUES
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), 'Main Freezer', 'Large chest freezer in the basement'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), 'Kitchen Freezer', 'Freezer compartment of the kitchen fridge'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), 'Pantry', 'Dry goods storage in the hallway'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), 'Kitchen Fridge', 'Main refrigerator in the kitchen'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), 'Cellar Shelf', 'Cool storage shelves in the cellar');
INSERT INTO storage_locations (uuid, name, description)
VALUES (UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), 'Main Freezer', 'Large chest freezer in the basement'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), 'Kitchen Freezer',
'Freezer compartment of the kitchen fridge'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), 'Pantry', 'Dry goods storage in the hallway'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), 'Kitchen Fridge', 'Main refrigerator in the kitchen'),
(UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), 'Cellar Shelf', 'Cool storage shelves in the cellar')
ON DUPLICATE KEY UPDATE description = VALUES(description);
-- =============================================
-- Products (~100)
-- Resolve the actual UUIDs (works whether the rows were just
-- inserted or already existed with different UUIDs)
-- =============================================
SET @loc_main_freezer = (SELECT uuid
FROM storage_locations
WHERE name = 'Main Freezer');
SET @loc_kitchen_freezer = (SELECT uuid
FROM storage_locations
WHERE name = 'Kitchen Freezer');
SET @loc_pantry = (SELECT uuid
FROM storage_locations
WHERE name = 'Pantry');
SET @loc_kitchen_fridge = (SELECT uuid
FROM storage_locations
WHERE name = 'Kitchen Fridge');
SET @loc_cellar_shelf = (SELECT uuid
FROM storage_locations
WHERE name = 'Cellar Shelf');
-- =============================================
-- Products (94)
-- uuid, picture, created_at, updated_at and deleted rely on
-- their column defaults.
-- Self-frozen items have a bottling_date.
-- Store-bought items have bottling_date = NULL.
-- =============================================
INSERT INTO products (uuid, name, description, price, amount, storage_location, expiry_date, bottling_date, picture, deleted) VALUES
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date)
VALUES
-- ── Self-frozen items (Main Freezer) ──
(UUID_TO_BIN(UUID()), 'Strawberry Puree', 'Blended fresh strawberries', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-15', '2026-06-01', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Tomato Sauce', 'Homemade marinara sauce', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-10', '2026-05-20', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Chicken Broth', 'Slow-cooked bone broth', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-30', '2026-04-15', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Blanched Green Beans', 'Garden green beans, blanched', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-02-01', '2026-05-28', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Pesto Cubes', 'Basil pesto in ice cube trays', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-01', '2026-05-10', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Raspberry Jam', 'Homemade seedless raspberry jam', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-03-01', '2026-06-02', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Beef Stew', 'Leftover beef stew, portioned', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-15', '2026-04-20', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Blanched Spinach', 'Fresh spinach, blanched and pressed', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-20', '2026-05-15', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Mango Chunks', 'Diced ripe mangoes', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-20', '2026-05-25', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Carrot Soup', 'Pureed carrot ginger soup', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-01', '2026-04-10', NULL, FALSE),
('Strawberry Puree', 'Blended fresh strawberries', NULL, 4, @loc_main_freezer, '2026-12-15', '2026-06-01'),
('Tomato Sauce', 'Homemade marinara sauce', NULL, 6, @loc_main_freezer, '2027-01-10', '2026-05-20'),
('Chicken Broth', 'Slow-cooked bone broth', NULL, 3, @loc_main_freezer, '2026-11-30', '2026-04-15'),
('Blanched Green Beans', 'Garden green beans, blanched', NULL, 5, @loc_main_freezer, '2027-02-01', '2026-05-28'),
('Pesto Cubes', 'Basil pesto in ice cube trays', NULL, 2, @loc_main_freezer, '2026-12-01', '2026-05-10'),
('Raspberry Jam', 'Homemade seedless raspberry jam', NULL, 3, @loc_main_freezer, '2027-03-01', '2026-06-02'),
('Beef Stew', 'Leftover beef stew, portioned', NULL, 4, @loc_main_freezer, '2026-10-15', '2026-04-20'),
('Blanched Spinach', 'Fresh spinach, blanched and pressed', NULL, 6, @loc_main_freezer, '2027-01-20', '2026-05-15'),
('Mango Chunks', 'Diced ripe mangoes', NULL, 3, @loc_main_freezer, '2026-12-20', '2026-05-25'),
('Carrot Soup', 'Pureed carrot ginger soup', NULL, 4, @loc_main_freezer, '2026-11-01', '2026-04-10'),
-- ── Self-frozen items (Kitchen Freezer) ──
(UUID_TO_BIN(UUID()), 'Blueberry Mix', 'Mixed blueberries from the garden', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-10', '2026-06-03', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Zucchini Slices', 'Sliced and blanched zucchini', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-15', '2026-05-18', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Apple Sauce', 'Homemade cinnamon apple sauce', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-10', '2026-05-22', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Bolognese Sauce', 'Classic meat sauce, portioned', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-11-20', '2026-04-25', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Herb Butter', 'Parsley and garlic butter logs', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-30', '2026-05-30', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Peach Slices', 'Frozen ripe peach slices', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-05', '2026-06-01', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Corn Kernels', 'Fresh sweet corn, cut off the cob', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-20', '2026-05-12', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Pumpkin Puree', 'Roasted and pureed butternut squash', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-03-10', '2026-05-08', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Lemon Juice Cubes', 'Fresh lemon juice frozen in cubes', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-04-01', '2026-06-02', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Minced Garlic', 'Peeled and minced garlic cloves', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-25', '2026-05-14', NULL, FALSE),
('Blueberry Mix', 'Mixed blueberries from the garden', NULL, 2, @loc_kitchen_freezer, '2026-12-10', '2026-06-03'),
('Zucchini Slices', 'Sliced and blanched zucchini', NULL, 3, @loc_kitchen_freezer, '2027-01-15', '2026-05-18'),
('Apple Sauce', 'Homemade cinnamon apple sauce', NULL, 5, @loc_kitchen_freezer, '2027-02-10', '2026-05-22'),
('Bolognese Sauce', 'Classic meat sauce, portioned', NULL, 3, @loc_kitchen_freezer, '2026-11-20', '2026-04-25'),
('Herb Butter', 'Parsley and garlic butter logs', NULL, 4, @loc_kitchen_freezer, '2026-12-30', '2026-05-30'),
('Peach Slices', 'Frozen ripe peach slices', NULL, 2, @loc_kitchen_freezer, '2027-01-05', '2026-06-01'),
('Corn Kernels', 'Fresh sweet corn, cut off the cob', NULL, 3, @loc_kitchen_freezer, '2027-02-20', '2026-05-12'),
('Pumpkin Puree', 'Roasted and pureed butternut squash', NULL, 4, @loc_kitchen_freezer, '2027-03-10', '2026-05-08'),
('Lemon Juice Cubes', 'Fresh lemon juice frozen in cubes', NULL, 6, @loc_kitchen_freezer, '2027-04-01', '2026-06-02'),
('Minced Garlic', 'Peeled and minced garlic cloves', NULL, 2, @loc_kitchen_freezer, '2027-01-25', '2026-05-14'),
-- ── Self-frozen items (split across storages) ──
(UUID_TO_BIN(UUID()), 'Vegetable Stock', 'Homemade veggie stock', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-05', '2026-04-30', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Rhubarb Compote', 'Stewed rhubarb with sugar', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-30', '2026-05-05', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Cherry Halves', 'Pitted sour cherries', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-25', '2026-06-01', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Cauliflower Florets', 'Blanched cauliflower pieces', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-15', '2026-05-19', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Plum Jam', 'Homemade plum jam', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-03-20', '2026-05-27', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Mushroom Duxelles', 'Finely chopped sauteed mushrooms', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-11-10', '2026-04-18', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Chili Con Carne', 'Portioned homemade chili', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-30', '2026-04-12', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Banana Slices', 'Ripe banana slices for smoothies', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-08', '2026-06-03', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Parsley Cubes', 'Chopped parsley in olive oil cubes', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-04-15', '2026-05-31', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Lentil Soup', 'Thick homemade lentil soup', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-25', '2026-04-22', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Blackberry Puree', 'Strained wild blackberries', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-12', '2026-06-02', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Roasted Bell Peppers', 'Peeled roasted red peppers', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-28', '2026-05-16', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Goulash', 'Hungarian-style beef goulash', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-20', '2026-04-08', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Mixed Berry Smoothie Pack', 'Pre-portioned smoothie mix', NULL, 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-18', '2026-06-04', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Leek Cream Soup', 'Pureed leek and potato soup', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-15', '2026-04-28', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Diced Onions', 'Pre-diced yellow onions', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-03-05', '2026-05-24', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Grape Juice Concentrate', 'Reduced grape juice for desserts', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-02-05', '2026-05-11', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Currant Jelly', 'Red currant jelly', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-04-10', '2026-06-01', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Broccoli Florets', 'Blanched broccoli pieces', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-08', '2026-05-20', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Sweet Potato Mash', 'Roasted and mashed sweet potatoes', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-12-28', '2026-05-17', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Apricot Halves', 'Frozen ripe apricot halves', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-02-12', '2026-06-03', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Minestrone', 'Chunky Italian vegetable soup', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-11-08', '2026-04-14', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Pea Puree', 'Fresh garden peas, pureed', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-03-15', '2026-05-26', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Cranberry Sauce', 'Homemade whole berry cranberry sauce', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-01-18', '2026-05-09', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Egg Noodles', 'Homemade egg noodles, pre-cooked', NULL, 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2026-12-12', '2026-05-29', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Gooseberry Compote', 'Stewed gooseberries', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2027-02-22', '2026-06-02', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Creamed Corn', 'Homemade creamed corn', NULL, 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-01-28', '2026-05-21', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Chicken Curry', 'Portioned Thai green curry', NULL, 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000001'), '2026-10-25', '2026-04-16', NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Dill Cubes', 'Chopped dill frozen in water cubes', NULL, 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000002'), '2027-04-20', '2026-06-01', NULL, FALSE),
('Vegetable Stock', 'Homemade veggie stock', NULL, 5, @loc_main_freezer, '2026-12-05', '2026-04-30'),
('Rhubarb Compote', 'Stewed rhubarb with sugar', NULL, 3, @loc_main_freezer, '2027-01-30', '2026-05-05'),
('Cherry Halves', 'Pitted sour cherries', NULL, 2, @loc_main_freezer, '2026-12-25', '2026-06-01'),
('Cauliflower Florets', 'Blanched cauliflower pieces', NULL, 4, @loc_kitchen_freezer, '2027-02-15', '2026-05-19'),
('Plum Jam', 'Homemade plum jam', NULL, 3, @loc_main_freezer, '2027-03-20', '2026-05-27'),
('Mushroom Duxelles', 'Finely chopped sauteed mushrooms', NULL, 2, @loc_kitchen_freezer, '2026-11-10', '2026-04-18'),
('Chili Con Carne', 'Portioned homemade chili', NULL, 3, @loc_main_freezer, '2026-10-30', '2026-04-12'),
('Banana Slices', 'Ripe banana slices for smoothies', NULL, 5, @loc_kitchen_freezer, '2026-12-08', '2026-06-03'),
('Parsley Cubes', 'Chopped parsley in olive oil cubes', NULL, 4, @loc_kitchen_freezer, '2027-04-15', '2026-05-31'),
('Lentil Soup', 'Thick homemade lentil soup', NULL, 3, @loc_main_freezer, '2026-11-25', '2026-04-22'),
('Blackberry Puree', 'Strained wild blackberries', NULL, 2, @loc_main_freezer, '2027-01-12', '2026-06-02'),
('Roasted Bell Peppers', 'Peeled roasted red peppers', NULL, 3, @loc_kitchen_freezer, '2027-02-28', '2026-05-16'),
('Goulash', 'Hungarian-style beef goulash', NULL, 2, @loc_main_freezer, '2026-10-20', '2026-04-08'),
('Mixed Berry Smoothie Pack', 'Pre-portioned smoothie mix', NULL, 6, @loc_kitchen_freezer, '2026-12-18', '2026-06-04'),
('Leek Cream Soup', 'Pureed leek and potato soup', NULL, 3, @loc_main_freezer, '2026-11-15', '2026-04-28'),
('Diced Onions', 'Pre-diced yellow onions', NULL, 4, @loc_kitchen_freezer, '2027-03-05', '2026-05-24'),
('Grape Juice Concentrate', 'Reduced grape juice for desserts', NULL, 2, @loc_main_freezer, '2027-02-05', '2026-05-11'),
('Currant Jelly', 'Red currant jelly', NULL, 3, @loc_main_freezer, '2027-04-10', '2026-06-01'),
('Broccoli Florets', 'Blanched broccoli pieces', NULL, 4, @loc_kitchen_freezer, '2027-01-08', '2026-05-20'),
('Sweet Potato Mash', 'Roasted and mashed sweet potatoes', NULL, 3, @loc_main_freezer, '2026-12-28', '2026-05-17'),
('Apricot Halves', 'Frozen ripe apricot halves', NULL, 2, @loc_kitchen_freezer, '2027-02-12', '2026-06-03'),
('Minestrone', 'Chunky Italian vegetable soup', NULL, 3, @loc_main_freezer, '2026-11-08', '2026-04-14'),
('Pea Puree', 'Fresh garden peas, pureed', NULL, 4, @loc_kitchen_freezer, '2027-03-15', '2026-05-26'),
('Cranberry Sauce', 'Homemade whole berry cranberry sauce', NULL, 2, @loc_main_freezer, '2027-01-18', '2026-05-09'),
('Egg Noodles', 'Homemade egg noodles, pre-cooked', NULL, 5, @loc_kitchen_freezer, '2026-12-12', '2026-05-29'),
('Gooseberry Compote', 'Stewed gooseberries', NULL, 2, @loc_main_freezer, '2027-02-22', '2026-06-02'),
('Creamed Corn', 'Homemade creamed corn', NULL, 3, @loc_kitchen_freezer, '2027-01-28', '2026-05-21'),
('Chicken Curry', 'Portioned Thai green curry', NULL, 2, @loc_main_freezer, '2026-10-25', '2026-04-16'),
('Dill Cubes', 'Chopped dill frozen in water cubes', NULL, 4, @loc_kitchen_freezer, '2027-04-20', '2026-06-01'),
-- ── Store-bought items (no bottling_date) ──
-- Kitchen Fridge
(UUID_TO_BIN(UUID()), 'Whole Milk', 'Full-fat milk, 1 liter', '1.19', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-14', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Greek Yogurt', 'Plain Greek yogurt, 500g', '2.49', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-20', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Butter', 'Unsalted butter, 250g', '2.29', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-07-15', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Cheddar Cheese', 'Mature cheddar block, 400g', '3.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-08-10', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Cream Cheese', 'Philadelphia original, 200g', '1.89', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-25', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Orange Juice', 'Fresh squeezed, 1 liter', '2.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-12', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Eggs', 'Free-range eggs, pack of 10', '3.29', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-18', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Ham Slices', 'Cooked ham, 200g pack', '2.19', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-10', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Mozzarella', 'Fresh mozzarella ball, 125g', '1.49', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-09', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Hummus', 'Classic chickpea hummus, 300g', '2.29', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-15', NULL, NULL, FALSE),
('Whole Milk', 'Full-fat milk, 1 liter', '1.19', 2, @loc_kitchen_fridge, '2026-06-14', NULL),
('Greek Yogurt', 'Plain Greek yogurt, 500g', '2.49', 3, @loc_kitchen_fridge, '2026-06-20', NULL),
('Butter', 'Unsalted butter, 250g', '2.29', 2, @loc_kitchen_fridge, '2026-07-15', NULL),
('Cheddar Cheese', 'Mature cheddar block, 400g', '3.49', 1, @loc_kitchen_fridge, '2026-08-10', NULL),
('Cream Cheese', 'Philadelphia original, 200g', '1.89', 2, @loc_kitchen_fridge, '2026-06-25', NULL),
('Orange Juice', 'Fresh squeezed, 1 liter', '2.99', 1, @loc_kitchen_fridge, '2026-06-12', NULL),
('Eggs', 'Free-range eggs, pack of 10', '3.29', 1, @loc_kitchen_fridge, '2026-06-18', NULL),
('Ham Slices', 'Cooked ham, 200g pack', '2.19', 2, @loc_kitchen_fridge, '2026-06-10', NULL),
('Mozzarella', 'Fresh mozzarella ball, 125g', '1.49', 3, @loc_kitchen_fridge, '2026-06-09', NULL),
('Hummus', 'Classic chickpea hummus, 300g', '2.29', 1, @loc_kitchen_fridge, '2026-06-15', NULL),
-- Pantry
(UUID_TO_BIN(UUID()), 'Pasta Spaghetti', 'Durum wheat spaghetti, 500g', '1.29', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-03-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Basmati Rice', 'Long grain basmati, 1kg', '2.99', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-06-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Canned Tomatoes', 'Chopped tomatoes, 400g tin', '0.89', 6, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-01-15', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Canned Chickpeas', 'Chickpeas in water, 400g', '0.99', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-04-10', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Olive Oil', 'Extra virgin olive oil, 500ml', '5.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-09-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Peanut Butter', 'Crunchy peanut butter, 350g', '3.49', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-02-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Honey', 'Raw wildflower honey, 500g', '6.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-12-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Oats', 'Rolled oats, 1kg', '1.99', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-08-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Flour', 'All-purpose wheat flour, 1kg', '0.99', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-06-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Sugar', 'White granulated sugar, 1kg', '1.49', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2029-01-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Canned Tuna', 'Tuna chunks in sunflower oil, 185g', '1.79', 5, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-07-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Coconut Milk', 'Full-fat coconut milk, 400ml', '1.59', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-02-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Soy Sauce', 'Naturally brewed soy sauce, 250ml', '2.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-05-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Balsamic Vinegar', 'Aged balsamic vinegar, 250ml', '3.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2029-06-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Dried Lentils', 'Green lentils, 500g', '1.69', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-09-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Cornflakes', 'Classic cornflakes, 500g', '2.19', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-04-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Black Beans', 'Canned black beans, 400g', '1.09', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-08-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Mustard', 'Dijon mustard, 200g jar', '1.89', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2027-11-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Dried Pasta Penne', 'Penne rigate, 500g', '1.19', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-05-15', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Maple Syrup', 'Pure Canadian maple syrup, 250ml', '7.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000003'), '2028-10-01', NULL, NULL, FALSE),
('Pasta Spaghetti', 'Durum wheat spaghetti, 500g', '1.29', 4, @loc_pantry, '2028-03-01', NULL),
('Basmati Rice', 'Long grain basmati, 1kg', '2.99', 2, @loc_pantry, '2028-06-01', NULL),
('Canned Tomatoes', 'Chopped tomatoes, 400g tin', '0.89', 6, @loc_pantry, '2028-01-15', NULL),
('Canned Chickpeas', 'Chickpeas in water, 400g', '0.99', 4, @loc_pantry, '2028-04-10', NULL),
('Olive Oil', 'Extra virgin olive oil, 500ml', '5.99', 1, @loc_pantry, '2027-09-01', NULL),
('Peanut Butter', 'Crunchy peanut butter, 350g', '3.49', 2, @loc_pantry, '2027-02-01', NULL),
('Honey', 'Raw wildflower honey, 500g', '6.99', 1, @loc_pantry, '2028-12-01', NULL),
('Oats', 'Rolled oats, 1kg', '1.99', 2, @loc_pantry, '2027-08-01', NULL),
('Flour', 'All-purpose wheat flour, 1kg', '0.99', 3, @loc_pantry, '2027-06-01', NULL),
('Sugar', 'White granulated sugar, 1kg', '1.49', 2, @loc_pantry, '2029-01-01', NULL),
('Canned Tuna', 'Tuna chunks in sunflower oil, 185g', '1.79', 5, @loc_pantry, '2028-07-01', NULL),
('Coconut Milk', 'Full-fat coconut milk, 400ml', '1.59', 3, @loc_pantry, '2028-02-01', NULL),
('Soy Sauce', 'Naturally brewed soy sauce, 250ml', '2.49', 1, @loc_pantry, '2028-05-01', NULL),
('Balsamic Vinegar', 'Aged balsamic vinegar, 250ml', '3.99', 1, @loc_pantry, '2029-06-01', NULL),
('Dried Lentils', 'Green lentils, 500g', '1.69', 2, @loc_pantry, '2028-09-01', NULL),
('Cornflakes', 'Classic cornflakes, 500g', '2.19', 1, @loc_pantry, '2027-04-01', NULL),
('Black Beans', 'Canned black beans, 400g', '1.09', 3, @loc_pantry, '2028-08-01', NULL),
('Mustard', 'Dijon mustard, 200g jar', '1.89', 1, @loc_pantry, '2027-11-01', NULL),
('Dried Pasta Penne', 'Penne rigate, 500g', '1.19', 3, @loc_pantry, '2028-05-15', NULL),
('Maple Syrup', 'Pure Canadian maple syrup, 250ml', '7.49', 1, @loc_pantry, '2028-10-01', NULL),
-- Cellar Shelf
(UUID_TO_BIN(UUID()), 'Canned Corn', 'Sweet corn kernels, 340g', '1.09', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-03-15', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Canned Peas', 'Garden peas, 400g tin', '0.89', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-06-10', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Apple Cider Vinegar', 'Organic apple cider vinegar, 500ml', '3.29', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-11-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Sunflower Oil', 'Refined sunflower oil, 1 liter', '2.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-10-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Tomato Paste', 'Double concentrated, 200g tube', '1.29', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-01-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Canned Sardines', 'Sardines in olive oil, 120g', '2.19', 4, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-09-15', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Pickled Cucumbers', 'Whole pickled gherkins, 670g jar', '1.99', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-12-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Jam Strawberry', 'Store-bought strawberry jam, 450g', '2.79', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-07-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Canned Peaches', 'Peach halves in syrup, 420g', '1.69', 3, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2028-04-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Crackers', 'Whole wheat crackers, 200g', '1.89', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000005'), '2027-05-01', NULL, NULL, FALSE),
('Canned Corn', 'Sweet corn kernels, 340g', '1.09', 4, @loc_cellar_shelf, '2028-03-15', NULL),
('Canned Peas', 'Garden peas, 400g tin', '0.89', 3, @loc_cellar_shelf, '2028-06-10', NULL),
('Apple Cider Vinegar', 'Organic apple cider vinegar, 500ml', '3.29', 2, @loc_cellar_shelf, '2028-11-01', NULL),
('Sunflower Oil', 'Refined sunflower oil, 1 liter', '2.49', 1, @loc_cellar_shelf, '2027-10-01', NULL),
('Tomato Paste', 'Double concentrated, 200g tube', '1.29', 3, @loc_cellar_shelf, '2028-01-01', NULL),
('Canned Sardines', 'Sardines in olive oil, 120g', '2.19', 4, @loc_cellar_shelf, '2028-09-15', NULL),
('Pickled Cucumbers', 'Whole pickled gherkins, 670g jar', '1.99', 2, @loc_cellar_shelf, '2027-12-01', NULL),
('Jam Strawberry', 'Store-bought strawberry jam, 450g', '2.79', 1, @loc_cellar_shelf, '2027-07-01', NULL),
('Canned Peaches', 'Peach halves in syrup, 420g', '1.69', 3, @loc_cellar_shelf, '2028-04-01', NULL),
('Crackers', 'Whole wheat crackers, 200g', '1.89', 2, @loc_cellar_shelf, '2027-05-01', NULL),
-- Additional Kitchen Fridge items
(UUID_TO_BIN(UUID()), 'Salami', 'Italian salami slices, 100g', '2.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-20', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Parmesan', 'Parmigiano Reggiano wedge, 200g', '4.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-09-01', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Smoked Salmon', 'Sliced smoked salmon, 100g', '3.99', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-08', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Heavy Cream', 'Whipping cream 35%, 200ml', '1.29', 2, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-16', NULL, NULL, FALSE),
(UUID_TO_BIN(UUID()), 'Tofu', 'Firm organic tofu, 400g', '2.49', 1, UUID_TO_BIN('a1000000-0000-0000-0000-000000000004'), '2026-06-22', NULL, NULL, FALSE);
('Salami', 'Italian salami slices, 100g', '2.99', 1, @loc_kitchen_fridge, '2026-06-20', NULL),
('Parmesan', 'Parmigiano Reggiano wedge, 200g', '4.99', 1, @loc_kitchen_fridge, '2026-09-01', NULL),
('Smoked Salmon', 'Sliced smoked salmon, 100g', '3.99', 1, @loc_kitchen_fridge, '2026-06-08', NULL),
('Heavy Cream', 'Whipping cream 35%, 200ml', '1.29', 2, @loc_kitchen_fridge, '2026-06-16', NULL),
('Tofu', 'Firm organic tofu, 400g', '2.49', 1, @loc_kitchen_fridge, '2026-06-22', NULL);
+7 -1
View File
@@ -2,6 +2,8 @@ import { createRouter, RouterProvider } from "@tanstack/react-router";
import "./App.css";
import { routeTree } from "./routeTree.gen";
import { NotFound } from "./components/NotFound.tsx";
import { CssVarsProvider } from "@mui/joy";
import { theme } from "./theme.ts";
const router = createRouter({ routeTree, defaultNotFoundComponent: NotFound });
@@ -12,7 +14,11 @@ declare module "@tanstack/react-router" {
}
function App() {
return <RouterProvider router={router} />;
return (
<CssVarsProvider theme={theme} defaultMode={"system"}>
<RouterProvider router={router} />
</CssVarsProvider>
);
}
export default App;
+47 -9
View File
@@ -3,7 +3,7 @@ import { Button, Input } from "@mui/joy";
import { useMutation } from "@tanstack/react-query";
import { signInUser } from "../utils/api/auth";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import type { AlertInterface } from "../misc/interfaces";
import { MyAlert } from "./MyAlert.tsx";
@@ -11,6 +11,7 @@ import { MyAlert } from "./MyAlert.tsx";
export const LoginCard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const search: { loggedOut?: boolean } = useSearch({ from: "/login" });
const [alert, setAlert] = useState<AlertInterface>({
isAlert: false,
type: "neutral",
@@ -19,12 +20,15 @@ export const LoginCard = () => {
});
useEffect(() => {
if (search.loggedOut) {
setAlert({
isAlert: true,
type: "primary",
header: t("success"),
text: t("logout-success-text"),
});
}
void navigate({ to: "/login" });
}, []);
const form = useForm({
@@ -53,7 +57,7 @@ export const LoginCard = () => {
header: "",
text: "",
});
navigate({ to: "/app/inventory" });
void navigate({ to: "/app/inventory" });
}
},
onError: (error: unknown) => {
@@ -68,14 +72,34 @@ export const LoginCard = () => {
});
return (
<div className="flex min-h-screen w-full items-center justify-center bg-[radial-gradient(1200px_circle_at_20%_10%,#e6f2ff_0%,#f7f9fc_40%,#f1f5fb_100%)] px-6 py-10">
<div
className="flex min-h-screen w-full items-center justify-center px-6 py-10"
style={{
background:
"radial-gradient(1200px circle at 20% 10%, var(--joy-palette-primary-100) 0%, var(--joy-palette-background-body) 40%, var(--joy-palette-background-level1) 100%)",
}}
>
<div className="mx-auto flex w-full max-w-4xl items-center justify-center">
<div className="w-full max-w-md rounded-3xl border border-white/70 bg-white/80 p-8 shadow-[0_24px_60px_rgba(12,38,78,0.18)] backdrop-blur">
<div
className="w-full max-w-md rounded-3xl p-8 backdrop-blur"
style={{
border: "1px solid var(--joy-palette-divider)",
backgroundColor: "var(--joy-palette-background-surface)",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 18%, transparent)",
}}
>
<div className="mb-8 space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-[#0b6bcb]">
<p
className="text-xs font-semibold uppercase tracking-[0.3em]"
style={{ color: "var(--joy-palette-primary-solidBg)" }}
>
Stockhome
</p>
<h1 className="text-3xl font-semibold text-slate-900">
<h1
className="text-3xl font-semibold"
style={{ color: "var(--joy-palette-text-primary)" }}
>
{t("login")}
</h1>
</div>
@@ -101,7 +125,11 @@ export const LoginCard = () => {
placeholder={t("username")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
@@ -114,7 +142,11 @@ export const LoginCard = () => {
placeholder={t("password")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
@@ -122,7 +154,13 @@ export const LoginCard = () => {
type="submit"
loading={isPending}
size="lg"
className="w-full rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="w-full rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("login")}
</Button>
+4 -1
View File
@@ -9,7 +9,10 @@ export const NotFound = () => {
<Typography color={"primary"} level={"h1"}>
{t("not-found-header")}
</Typography>
<Typography className={"text-slate-900"} level={"body-lg"}>
<Typography
sx={{ color: "var(--joy-palette-text-primary)" }}
level={"body-lg"}
>
{t("not-found-body")}
</Typography>
</div>
+76 -15
View File
@@ -7,42 +7,59 @@ import StorageIcon from "@mui/icons-material/Storage";
import SettingsIcon from "@mui/icons-material/Settings";
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import TranslateIcon from "@mui/icons-material/Translate";
import Brightness4Icon from "@mui/icons-material/Brightness4";
import MenuIcon from "@mui/icons-material/Menu";
import CloseIcon from "@mui/icons-material/Close";
import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import Cookies from "js-cookie";
import { changeTranslation } from "../utils/uxFncs";
import { useColorScheme } from "@mui/joy/styles";
import { useLogout } from "../hooks/useLogout.ts";
export const Sidebar = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { logout } = useLogout();
const matchRoute = useMatchRoute();
const { mode, setMode } = useColorScheme();
const [isOpen, setIsOpen] = useState(false);
const btnClass =
"h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!";
"h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold transition [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!";
const variant = (to: string) =>
!!matchRoute({ to, fuzzy: false }) ? "soft" : "plain";
const handleNavigate = (to: string) => {
navigate({ to });
void navigate({ to });
setIsOpen(false);
};
return (
<aside className="flex w-full flex-col gap-6 border-b border-white/80 bg-linear-to-b from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] px-4 py-5 shadow-[0_20px_60px_rgba(11,107,203,0.08)] sm:px-5 lg:h-full lg:min-h-screen lg:max-w-70 lg:border-b-0 lg:border-r lg:px-6 lg:py-8">
<aside
className="flex w-full flex-col gap-6 px-4 py-5 sm:px-5 lg:h-full lg:min-h-screen lg:max-w-70 lg:border-b-0 lg:border-r lg:px-6 lg:py-8"
style={{
borderBottom: "1px solid var(--joy-palette-divider)",
borderRightColor: "var(--joy-palette-divider)",
background:
"linear-gradient(to bottom, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow:
"0 20px 60px color-mix(in srgb, var(--joy-palette-primary-solidBg) 8%, transparent)",
}}
>
<div className="flex items-center justify-between gap-4">
<div className="space-y-2">
<Typography
level="h2"
className="text-[22px] font-semibold text-[#0b6bcb]"
className="text-[22px] font-semibold"
sx={{ color: "var(--joy-palette-primary-solidBg)" }}
>
{t("app-title")}
</Typography>
<Typography
level="body-lg"
className="text-sm font-medium text-slate-500"
className="text-sm font-medium"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("app-name") ? Cookies.get("app-name") : ""}
</Typography>
@@ -50,7 +67,14 @@ export const Sidebar = () => {
<Button
variant="soft"
size="sm"
className="transition-transform duration-200 ease-out"
sx={{
display: "inline-flex",
"@media (min-width: 1024px)": { display: "none" },
"&:hover": {
backgroundColor: "var(--joy-palette-primary-900)",
color: "var(--joy-palette-primary-50)",
},
}}
onClick={() => setIsOpen((open) => !open)}
startDecorator={
<span
@@ -61,10 +85,6 @@ export const Sidebar = () => {
{isOpen ? <CloseIcon /> : <MenuIcon />}
</span>
}
sx={{
display: "inline-flex",
"@media (min-width: 1024px)": { display: "none" },
}}
>
{isOpen ? t("close") : t("menu")}
</Button>
@@ -83,6 +103,13 @@ export const Sidebar = () => {
variant={variant("/app/inventory")}
startDecorator={<InventoryIcon />}
className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
>
{t("inventory")}
</Button>
@@ -91,6 +118,13 @@ export const Sidebar = () => {
variant={variant("/app/add-product")}
startDecorator={<AddBoxIcon />}
className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
>
{t("add")}
</Button>
@@ -99,6 +133,13 @@ export const Sidebar = () => {
variant={variant("/app/storages")}
startDecorator={<StorageIcon />}
className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
>
{t("storages")}
</Button>
@@ -107,14 +148,18 @@ export const Sidebar = () => {
variant={variant("/app/app-settings")}
startDecorator={<SettingsIcon />}
className={btnClass}
sx={{
color: "var(--joy-palette-text-secondary)",
"&:hover": {
bgcolor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
},
}}
>
{t("settings")}
</Button>
<Button
onClick={() => {
Cookies.remove("token");
handleNavigate("/login?logout=true");
}}
onClick={logout}
color="danger"
startDecorator={<ExitToAppIcon />}
className={btnClass}
@@ -132,9 +177,25 @@ export const Sidebar = () => {
>
{t("change-translation")}
</Button>
<Button
onClick={() => setMode(mode === "dark" ? "light" : "dark")}
color="neutral"
startDecorator={<Brightness4Icon />}
className={btnClass}
>
{mode === "dark" ? "Light Mode" : "Dark Mode"}
</Button>
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/70 bg-white/80 px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em] text-[#0b6bcb] shadow-[0_12px_30px_rgba(12,38,78,0.12)]">
<div
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em]"
style={{
border: "1px solid var(--joy-palette-divider)",
backgroundColor: "var(--joy-palette-background-surface)",
color: "var(--joy-palette-primary-solidBg)",
boxShadow: "0 12px 30px var(--joy-palette-divider)",
}}
>
<img
src="/favicon.png"
alt="Stockhome"
+21 -5
View File
@@ -50,7 +50,7 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
(values.description ?? "") !== (storage.description ?? "");
return (
<tr key={storage.uuid} className="border-t border-slate-200 align-top">
<tr key={storage.uuid} className="align-top">
<td className="px-6 py-5">
<form.Field name="name">
{(field) => (
@@ -60,7 +60,12 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
onBlur={field.handleBlur}
size="sm"
variant="outlined"
className="rounded-xl bg-slate-50/80 text-slate-700 shadow-none ring-1 ring-inset ring-slate-200 focus-within:ring-2 focus-within:ring-slate-300"
className="rounded-xl shadow-none"
sx={{
bgcolor: "var(--joy-palette-background-level1)",
color: "var(--joy-palette-text-secondary)",
"--Input-focusedHighlight": "var(--joy-palette-neutral-300)",
}}
/>
)}
</form.Field>
@@ -74,15 +79,26 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
onBlur={field.handleBlur}
size="sm"
variant="outlined"
className="rounded-xl bg-slate-50/80 text-slate-700 shadow-none ring-1 ring-inset ring-slate-200 focus-within:ring-2 focus-within:ring-slate-300"
className="rounded-xl shadow-none"
sx={{
bgcolor: "var(--joy-palette-background-level1)",
color: "var(--joy-palette-text-secondary)",
"--Input-focusedHighlight": "var(--joy-palette-neutral-300)",
}}
/>
)}
</form.Field>
</td>
<td className="px-6 py-5 text-sm text-slate-500">
<td
className="px-6 py-5 text-sm"
style={{ color: "var(--joy-palette-text-tertiary)" }}
>
{formatDate(storage.created_at)}
</td>
<td className="px-6 py-5 text-sm text-slate-500">
<td
className="px-6 py-5 text-sm"
style={{ color: "var(--joy-palette-text-tertiary)" }}
>
{formatDate(storage.updated_at)}
</td>
<td className="px-6 py-5 text-right">
@@ -51,11 +51,20 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
return (
<>
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
<ModalDialog className="rounded-3xl border border-white/70 bg-white/90 p-6 shadow-[0_30px_70px_rgba(12,38,78,0.2)] backdrop-blur">
<DialogTitle className="text-slate-900">
<ModalDialog
className="rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 30px 70px color-mix(in srgb, var(--joy-palette-primary-800) 20%, transparent)",
}}
>
<DialogTitle sx={{ color: "var(--joy-palette-text-primary)" }}>
{t("new-storage-title")}
</DialogTitle>
<DialogContent className="text-slate-500">
<DialogContent sx={{ color: "var(--joy-palette-text-tertiary)" }}>
{t("new-storage-content")}
</DialogContent>
<form
@@ -73,7 +82,11 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
placeholder={t("storage-name")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
@@ -85,14 +98,24 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
placeholder={t("description")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
<Button
type="submit"
size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("submit")}
</Button>
@@ -37,8 +37,6 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
newPasswordRep: "",
},
onSubmit: async ({ value }) => {
console.log(value);
if (
value.newPassword === value.newPasswordRep &&
value.newPassword !== ""
@@ -66,7 +64,6 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
});
},
onSuccess: () => {
// Sets the success alert in the settings page via component props
props.alert({
isAlert: true,
type: "success",
@@ -74,7 +71,6 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
text: t("SU005"),
});
// Sets the success alert locally (in the modal itself)
setAlert({
isAlert: true,
type: "success",
@@ -88,8 +84,17 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
return (
<>
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
<ModalDialog className="rounded-3xl border border-white/70 bg-white/90 p-6 shadow-[0_30px_70px_rgba(12,38,78,0.2)] backdrop-blur">
<DialogTitle className="text-slate-900">
<ModalDialog
className="rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 30px 70px color-mix(in srgb, var(--joy-palette-primary-800) 20%, transparent)",
}}
>
<DialogTitle sx={{ color: "var(--joy-palette-text-primary)" }}>
{t("new-password-title")}
</DialogTitle>
<form
@@ -108,7 +113,11 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
placeholder={t("current-password")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
@@ -121,7 +130,11 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
placeholder={t("new-password")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
@@ -134,14 +147,24 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
placeholder={t("new-password-rep")}
variant="outlined"
size="lg"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
<Button
type="submit"
size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("change")}
</Button>
+13
View File
@@ -0,0 +1,13 @@
import Cookies from "js-cookie";
import { useNavigate } from "@tanstack/react-router";
export const useLogout = () => {
const navigate = useNavigate();
const logout = () => {
Cookies.remove("token");
void navigate({ to: "/login", search: { loggedOut: true } });
};
return { logout };
};
+104 -22
View File
@@ -90,10 +90,16 @@ export const AddProduct = () => {
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-3">
<div className="space-y-1">
<Typography level="h2" className="text-slate-900">
<Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("add-product")}
</Typography>
<Typography level="body-lg" className="text-slate-500">
<Typography
level="body-lg"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("add-product-subtitle")}
</Typography>
</div>
@@ -106,7 +112,16 @@ export const AddProduct = () => {
</Chip>
</div>
</div>
<Box className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur">
<Box
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
}}
>
<form
className="space-y-6"
onSubmit={(e) => {
@@ -117,7 +132,10 @@ export const AddProduct = () => {
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-4">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-name")}
</Typography>
<form.Field name="name">
@@ -130,13 +148,20 @@ export const AddProduct = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("description")}
</Typography>
<form.Field name="description">
@@ -148,14 +173,21 @@ export const AddProduct = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("expiry-date")}
</Typography>
<form.Field name="expiry_date">
@@ -167,13 +199,19 @@ export const AddProduct = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("bottling-date")}
</Typography>
<form.Field name="bottling_date">
@@ -185,7 +223,10 @@ export const AddProduct = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
@@ -193,14 +234,28 @@ export const AddProduct = () => {
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]">
<Typography level="title-md" className="text-slate-900">
<div
className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("inventory")}
</Typography>
<Divider className="my-3" />
<div className="grid gap-4">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("amount")}
</Typography>
<form.Field name="amount">
@@ -220,13 +275,19 @@ export const AddProduct = () => {
);
}}
onBlur={field.handleBlur}
className="rounded-2xl bg-white/80"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("price")}
</Typography>
<form.Field name="price">
@@ -238,16 +299,25 @@ export const AddProduct = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("currency")}
</Typography>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storage-place")}
</Typography>
<form.Field name="storage_location_uuid">
@@ -260,7 +330,10 @@ export const AddProduct = () => {
}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
>
{storages?.map((storage) => (
<Option key={storage.uuid} value={storage.uuid}>
@@ -276,7 +349,10 @@ export const AddProduct = () => {
</div>
</div>
<div className="flex gap-3 items-center">
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("product-details")}
</Typography>
<div className="grow"></div>
@@ -284,7 +360,13 @@ export const AddProduct = () => {
type="submit"
loading={isPending}
size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("save")}
</Button>
+37 -18
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { ChangeEvent, MouseEvent, useEffect, useState } from "react";
import { Avatar, Button, Checkbox, Chip, CircularProgress, Sheet, Table, Typography, } from "@mui/joy";
import { useNavigate } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
@@ -9,7 +9,7 @@ import { formatDate } from "../utils/uxFncs";
import Cookies from "js-cookie";
import type { AlertInterface, ProductRow } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError";
import CategoryIcon from "@mui/icons-material/category";
import CategoryIcon from "@mui/icons-material/Category";
import { MyAlert } from "../components/MyAlert.tsx";
export const InventoryPage = () => {
@@ -73,12 +73,12 @@ export const InventoryPage = () => {
mutationFn: (values: string[]) => deleteSelectedProducts(values),
onSuccess: () => {
setSelected([]);
queryClient.invalidateQueries({ queryKey: ["products"] });
void queryClient.invalidateQueries({ queryKey: ["products"] });
},
onError: showError,
});
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleSelectAllClick = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setSelected(rows.map((row) => row.id));
return;
@@ -86,7 +86,7 @@ export const InventoryPage = () => {
setSelected([]);
};
const handleClick = (_event: React.MouseEvent<unknown>, id: string) => {
const handleClick = (_event: MouseEvent<unknown>, id: string) => {
const selectedIndex = selected.indexOf(id);
let newSelected: readonly string[] = [];
if (selectedIndex === -1) {
@@ -124,9 +124,20 @@ export const InventoryPage = () => {
<Sheet
variant="outlined"
className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm sm:h-[calc(100vh-260px)]"
className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl shadow-sm sm:h-[calc(100vh-260px)]"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
}}
>
<div
className="flex items-center justify-between px-6 py-4"
style={{
borderBottom: "1px solid var(--joy-palette-divider)",
color: "var(--joy-palette-text-secondary)",
}}
>
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700">
<Typography level="body-lg" fontWeight="bold">
{t("inventory")}
</Typography>
@@ -147,24 +158,26 @@ export const InventoryPage = () => {
stripe="odd"
variant="plain"
hoverRow
className="w-full text-slate-700"
className="w-full"
sx={{
tableLayout: "fixed",
color: "var(--joy-palette-text-secondary)",
"--TableCell-headBackground":
"var(--joy-palette-background-surface)",
"var(--joy-palette-background-level1)",
"& thead": {
position: "sticky",
top: 0,
zIndex: 3,
backgroundColor: "rgb(248 250 252)",
backgroundColor: "var(--joy-palette-background-level1)",
},
"& thead tr": {
backgroundColor: "rgb(248 250 252)",
backgroundColor: "var(--joy-palette-background-level1)",
},
"& thead th": {
zIndex: 2,
backgroundColor: "rgb(248 250 252)",
backgroundColor: "var(--joy-palette-background-level1)",
backgroundImage: "none",
color: "var(--joy-palette-text-secondary)",
},
"& thead th:nth-child(1)": {
width: "44px",
@@ -198,10 +211,13 @@ export const InventoryPage = () => {
minWidth: "100px",
},
"& tr > *:nth-child(n+4)": { textAlign: "left" },
"& tbody tr": {
borderTop: "1px solid var(--joy-palette-divider)",
},
}}
>
<thead>
<tr className="text-slate-600">
<tr>
<th className="px-2 py-4">
<Checkbox
checked={rows.length > 0 && selected.length === rows.length}
@@ -229,7 +245,6 @@ export const InventoryPage = () => {
return (
<tr
key={row.id}
className="border-t border-slate-200"
onClick={(event) => handleClick(event, row.id)}
role="checkbox"
aria-checked={isItemSelected}
@@ -254,13 +269,15 @@ export const InventoryPage = () => {
<div className="min-w-0">
<Typography
level="title-md"
className="text-slate-900 truncate"
className="truncate"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{row.name}
</Typography>
<Typography
level="body-sm"
className="text-slate-500 truncate"
className="truncate"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{row.description}
</Typography>
@@ -273,7 +290,8 @@ export const InventoryPage = () => {
</Typography>
<Typography
level="body-sm"
className="truncate text-slate-400"
className="truncate"
sx={{ color: "var(--joy-palette-neutral-400)" }}
>
{Cookies.get("currency")}
</Typography>
@@ -294,7 +312,8 @@ export const InventoryPage = () => {
</Typography>
<Typography
level="body-sm"
className="truncate text-slate-500"
className="truncate"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{row.locationDetail}
</Typography>
+103 -38
View File
@@ -1,27 +1,12 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages.ts";
import {
Box,
Button,
Chip,
CircularProgress,
Divider,
Input,
Option,
Select,
Typography,
} from "@mui/joy";
import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form";
import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
import { toInputDate } from "../utils/uxFncs";
import type {
AlertInterface,
productDetailsInterface,
ProductFormValues,
Storage,
} from "../misc/interfaces";
import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError";
import Cookies from "js-cookie";
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
@@ -140,7 +125,11 @@ export const ProductQuickView = () => {
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-3">
<div className="space-y-1">
<Typography level="h2" fontWeight="1000" className="text-slate-900">
<Typography
level="h2"
fontWeight="1000"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
<ElectricBoltIcon color="primary" />
{t("product-details-quick")}
<ElectricBoltIcon color="primary" />
@@ -157,7 +146,16 @@ export const ProductQuickView = () => {
{productDetailsLoading && <CircularProgress size="sm" />}
</div>
{isSuccess && (
<Box className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur">
<Box
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
}}
>
<form
className="space-y-6"
onSubmit={(e) => {
@@ -168,7 +166,10 @@ export const ProductQuickView = () => {
<div className="gap-5 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-4">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-name")}
</Typography>
<form.Field name="name">
@@ -180,13 +181,20 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("description")}
</Typography>
<form.Field name="description">
@@ -198,14 +206,21 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("expiry-date")}
</Typography>
<form.Field name="expiry_date">
@@ -217,13 +232,19 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("bottling-date")}
</Typography>
<form.Field name="bottling_date">
@@ -235,7 +256,10 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
@@ -243,14 +267,28 @@ export const ProductQuickView = () => {
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]">
<Typography level="title-lg" className="text-slate-900">
<div
className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-lg"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("inventory")}
</Typography>
<Divider className="my-3" />
<div className="grid gap-4">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("amount")}
</Typography>
<form.Field name="amount">
@@ -270,13 +308,19 @@ export const ProductQuickView = () => {
);
}}
onBlur={field.handleBlur}
className="rounded-2xl bg-white/80"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("price")}
</Typography>
<form.Field name="price">
@@ -288,16 +332,25 @@ export const ProductQuickView = () => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("currency")}
</Typography>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storage-place")}
</Typography>
<form.Field name="storage_location_uuid">
@@ -309,7 +362,10 @@ export const ProductQuickView = () => {
}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
>
{storages?.map((storage) => (
<Option key={storage.uuid} value={storage.uuid}>
@@ -325,14 +381,23 @@ export const ProductQuickView = () => {
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("product-details")}
</Typography>
<Button
type="submit"
loading={isPending}
size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("save")}
</Button>
+73 -16
View File
@@ -91,10 +91,16 @@ export const Settings = () => {
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-3">
<div className="space-y-1">
<Typography level="h2" className="text-slate-900">
<Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("settings")}
</Typography>
<Typography level="body-lg" className="text-slate-500">
<Typography
level="body-lg"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("settings-sub")}
</Typography>
</div>
@@ -108,7 +114,16 @@ export const Settings = () => {
</div>
</div>
<Sheet className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur">
<Sheet
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px rgba(var(--joy-palette-primary-900, 12 38 78) / 0.12)",
}}
>
{alert.isAlert && (
<MyAlert type={alert.type} header={alert.header} text={alert.text} />
)}
@@ -124,13 +139,19 @@ export const Settings = () => {
form.handleSubmit();
}}
>
<div className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
<div className="grid gap-6 lg:grid-cols-[1.1fr\_0.9fr]">
<div className="space-y-5">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("app-name")}
</Typography>
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("app-name-sub")}
</Typography>
<form.Field name="app-name">
@@ -141,16 +162,26 @@ export const Settings = () => {
size="lg"
variant="outlined"
placeholder="Stockhome"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("currency")}
</Typography>
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("currency-sub")}
</Typography>
<form.Field name="currency">
@@ -161,18 +192,36 @@ export const Settings = () => {
size="lg"
variant="outlined"
placeholder="EUR"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
</div>
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]">
<Typography level="title-lg" className="text-slate-900">
<div
className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-lg"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("quick-tips")}
</Typography>
<Divider className="my-3" />
<div className="space-y-3 text-md text-slate-600">
<div
className="space-y-3 text-md"
style={{ color: "var(--joy-palette-text-secondary)" }}
>
<p>{t("quick-tips-1")}</p>
<p>{t("quick-tips-2")}</p>
</div>
@@ -183,15 +232,23 @@ export const Settings = () => {
type="submit"
size="lg"
color="primary"
className="rounded-2xl text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("save")}
</Button>
<Button
onClick={() => setModal(true)}
size="lg"
color="warning"
className="rounded-2xl text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("change-password")}
</Button>
+32 -10
View File
@@ -50,10 +50,16 @@ export const Storages = () => {
<>
<div className="flex flex-col gap-4">
<div className="min-w-65 space-y-2">
<Typography level="h2" className="text-slate-900">
<Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storages")}
</Typography>
<Typography level="body-lg" className="text-slate-500">
<Typography
level="body-lg"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("storage-delete-info")}
</Typography>
</div>
@@ -74,7 +80,12 @@ export const Storages = () => {
<Sheet
variant="outlined"
className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm sm:h-[calc(100vh-260px)]"
className="mt-6 flex min-h-0 w-full max-w-full flex-col overflow-hidden rounded-2xl shadow-sm sm:h-[calc(100vh-260px)]"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
}}
>
<AddStorageModal isOpen={modal} setOpen={setModal} />
{isLoading ? (
@@ -83,7 +94,13 @@ export const Storages = () => {
</div>
) : (
<>
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700">
<div
className="flex items-center justify-between px-6 py-4"
style={{
borderBottom: "1px solid var(--joy-palette-divider)",
color: "var(--joy-palette-text-secondary)",
}}
>
<Typography level="body-lg" fontWeight="bold">
{t("storages")}
</Typography>
@@ -94,24 +111,26 @@ export const Storages = () => {
stripe="odd"
variant="plain"
hoverRow
className="w-full text-slate-700"
className="w-full"
sx={{
tableLayout: "fixed",
color: "var(--joy-palette-text-secondary)",
"--TableCell-headBackground":
"var(--joy-palette-background-surface)",
"var(--joy-palette-background-level1)",
"& thead": {
position: "sticky",
top: 0,
zIndex: 3,
backgroundColor: "rgb(248 250 252)",
backgroundColor: "var(--joy-palette-background-level1)",
},
"& thead tr": {
backgroundColor: "rgb(248 250 252)",
backgroundColor: "var(--joy-palette-background-level1)",
},
"& thead th": {
zIndex: 2,
backgroundColor: "rgb(248 250 252)",
backgroundColor: "var(--joy-palette-background-level1)",
backgroundImage: "none",
color: "var(--joy-palette-text-secondary)",
},
"& thead th:nth-child(1)": {
width: "26%",
@@ -134,10 +153,13 @@ export const Storages = () => {
minWidth: "80px",
},
"& tr > *:nth-child(n+3)": { textAlign: "left" },
"& tbody tr": {
borderTop: "1px solid var(--joy-palette-divider)",
},
}}
>
<thead>
<tr className="text-slate-600">
<tr>
<th className="px-3 py-4">{t("storage-name")}</th>
<th className="px-3 py-4">{t("description")}</th>
<th className="px-3 py-4">{t("created-at")}</th>
+109 -39
View File
@@ -1,27 +1,12 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages.ts";
import {
Box,
Button,
Chip,
CircularProgress,
Divider,
Input,
Option,
Select,
Typography,
} from "@mui/joy";
import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form";
import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
import { toInputDate } from "../utils/uxFncs";
import type {
AlertInterface,
productDetailsInterface,
ProductFormValues,
Storage,
} from "../misc/interfaces";
import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError";
import QrCodeIcon from "@mui/icons-material/QrCode";
import Cookies from "js-cookie";
@@ -160,7 +145,10 @@ export const ViewProduct = (props: ViewProductProps) => {
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-3">
<div className="space-y-1">
<Typography level="h2" className="text-slate-900">
<Typography
level="h2"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-details")}
</Typography>
</div>
@@ -175,7 +163,16 @@ export const ViewProduct = (props: ViewProductProps) => {
{productDetailsLoading && <CircularProgress size="sm" />}
</div>
{isSuccess && (
<Box className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur">
<Box
className="mt-6 rounded-3xl p-6 backdrop-blur"
sx={{
border: "1px solid",
borderColor: "divider",
bgcolor: "background.surface",
boxShadow:
"0 24px 60px color-mix(in srgb, var(--joy-palette-primary-800) 12%, transparent)",
}}
>
<form
className="space-y-6"
onSubmit={(e) => {
@@ -186,7 +183,10 @@ export const ViewProduct = (props: ViewProductProps) => {
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
<div className="space-y-4">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("product-name")}
</Typography>
<form.Field name="name">
@@ -198,13 +198,20 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("description")}
</Typography>
<form.Field name="description">
@@ -216,14 +223,21 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
boxShadow: "0 10px 24px var(--joy-palette-divider)",
}}
/>
)}
</form.Field>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("expiry-date")}
</Typography>
<form.Field name="expiry_date">
@@ -235,13 +249,19 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("bottling-date")}
</Typography>
<form.Field name="bottling_date">
@@ -253,7 +273,10 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
@@ -261,14 +284,28 @@ export const ViewProduct = (props: ViewProductProps) => {
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl border border-white/70 bg-linear-to-br from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] p-5 shadow-[0_16px_40px_rgba(12,38,78,0.08)]">
<Typography level="title-lg" className="text-slate-900">
<div
className="rounded-2xl p-5"
style={{
border: "1px solid var(--joy-palette-divider)",
background:
"linear-gradient(to bottom right, var(--joy-palette-primary-50), var(--joy-palette-background-level1), var(--joy-palette-background-level2))",
boxShadow: "0 16px 40px var(--joy-palette-divider)",
}}
>
<Typography
level="title-lg"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("inventory")}
</Typography>
<Divider className="my-3" />
<div className="grid gap-4">
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("amount")}
</Typography>
<form.Field name="amount">
@@ -288,13 +325,19 @@ export const ViewProduct = (props: ViewProductProps) => {
);
}}
onBlur={field.handleBlur}
className="rounded-2xl bg-white/80"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("price")}
</Typography>
<form.Field name="price">
@@ -306,16 +349,25 @@ export const ViewProduct = (props: ViewProductProps) => {
onBlur={field.handleBlur}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
/>
)}
</form.Field>
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{Cookies.get("currency")}
</Typography>
</div>
<div className="space-y-1">
<Typography level="title-md" className="text-slate-900">
<Typography
level="title-md"
sx={{ color: "var(--joy-palette-text-primary)" }}
>
{t("storage-place")}
</Typography>
<form.Field name="storage_location_uuid">
@@ -327,7 +379,10 @@ export const ViewProduct = (props: ViewProductProps) => {
}
size="lg"
variant="outlined"
className="rounded-2xl bg-white/90"
className="rounded-2xl"
sx={{
bgcolor: "var(--joy-palette-background-surface)",
}}
>
{storages?.map((storage) => (
<Option key={storage.uuid} value={storage.uuid}>
@@ -344,7 +399,10 @@ export const ViewProduct = (props: ViewProductProps) => {
</div>
<div>
<div className="flex gap-3 items-center">
<Typography level="body-sm" className="text-slate-500">
<Typography
level="body-sm"
sx={{ color: "var(--joy-palette-text-tertiary)" }}
>
{t("product-details")}
</Typography>
<div className="grow"></div>
@@ -353,7 +411,13 @@ export const ViewProduct = (props: ViewProductProps) => {
loading={isPending}
onClick={() => downloadQRcode()}
size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("download-qr-code")}
</Button>
@@ -361,7 +425,13 @@ export const ViewProduct = (props: ViewProductProps) => {
type="submit"
loading={isPending}
size="lg"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
color="primary"
variant="solid"
className="rounded-2xl transition hover:-translate-y-0.5"
sx={{
boxShadow:
"0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
}}
>
{t("save")}
</Button>
+125 -97
View File
@@ -12,12 +12,13 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout'
import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product'
import { Route as AppHiddenLayoutStoragesRouteImport } from './routes/app/_hiddenLayout/storages'
import { Route as AppHiddenLayoutInventoryRouteImport } from './routes/app/_hiddenLayout/inventory'
import { Route as AppHiddenLayoutAppSettingsRouteImport } from './routes/app/_hiddenLayout/app-settings'
import { Route as AppHiddenLayoutAddProductRouteImport } from './routes/app/_hiddenLayout/add-product'
import { Route as AppHiddenLayoutAuthedRouteImport } from './routes/app/_hiddenLayout/_authed'
import { Route as AppHiddenLayoutQuickViewProductRouteImport } from './routes/app/_hiddenLayout/quick-view/product'
import { Route as AppHiddenLayoutAuthedViewProductRouteImport } from './routes/app/_hiddenLayout/_authed/view-product'
import { Route as AppHiddenLayoutAuthedStoragesRouteImport } from './routes/app/_hiddenLayout/_authed/storages'
import { Route as AppHiddenLayoutAuthedInventoryRouteImport } from './routes/app/_hiddenLayout/_authed/inventory'
import { Route as AppHiddenLayoutAuthedAppSettingsRouteImport } from './routes/app/_hiddenLayout/_authed/app-settings'
import { Route as AppHiddenLayoutAuthedAddProductRouteImport } from './routes/app/_hiddenLayout/_authed/add-product'
const LoginRoute = LoginRouteImport.update({
id: '/login',
@@ -34,62 +35,67 @@ const AppHiddenLayoutRoute = AppHiddenLayoutRouteImport.update({
path: '/app',
getParentRoute: () => rootRouteImport,
} as any)
const AppHiddenLayoutViewProductRoute =
AppHiddenLayoutViewProductRouteImport.update({
id: '/view-product',
path: '/view-product',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutStoragesRoute = AppHiddenLayoutStoragesRouteImport.update({
id: '/storages',
path: '/storages',
const AppHiddenLayoutAuthedRoute = AppHiddenLayoutAuthedRouteImport.update({
id: '/_authed',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutInventoryRoute =
AppHiddenLayoutInventoryRouteImport.update({
id: '/inventory',
path: '/inventory',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutAppSettingsRoute =
AppHiddenLayoutAppSettingsRouteImport.update({
id: '/app-settings',
path: '/app-settings',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutAddProductRoute =
AppHiddenLayoutAddProductRouteImport.update({
id: '/add-product',
path: '/add-product',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutQuickViewProductRoute =
AppHiddenLayoutQuickViewProductRouteImport.update({
id: '/quick-view/product',
path: '/quick-view/product',
getParentRoute: () => AppHiddenLayoutRoute,
} as any)
const AppHiddenLayoutAuthedViewProductRoute =
AppHiddenLayoutAuthedViewProductRouteImport.update({
id: '/view-product',
path: '/view-product',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedStoragesRoute =
AppHiddenLayoutAuthedStoragesRouteImport.update({
id: '/storages',
path: '/storages',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedInventoryRoute =
AppHiddenLayoutAuthedInventoryRouteImport.update({
id: '/inventory',
path: '/inventory',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedAppSettingsRoute =
AppHiddenLayoutAuthedAppSettingsRouteImport.update({
id: '/app-settings',
path: '/app-settings',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
const AppHiddenLayoutAuthedAddProductRoute =
AppHiddenLayoutAuthedAddProductRouteImport.update({
id: '/add-product',
path: '/add-product',
getParentRoute: () => AppHiddenLayoutAuthedRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/app': typeof AppHiddenLayoutRouteWithChildren
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/storages': typeof AppHiddenLayoutStoragesRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
'/app': typeof AppHiddenLayoutAuthedRouteWithChildren
'/app/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
'/app/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
'/app/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
'/app/storages': typeof AppHiddenLayoutAuthedStoragesRoute
'/app/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/app': typeof AppHiddenLayoutRouteWithChildren
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/storages': typeof AppHiddenLayoutStoragesRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
'/app': typeof AppHiddenLayoutAuthedRouteWithChildren
'/app/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
'/app/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
'/app/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
'/app/storages': typeof AppHiddenLayoutAuthedStoragesRoute
'/app/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
}
export interface FileRoutesById {
@@ -97,11 +103,12 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren
'/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/_hiddenLayout/app-settings': typeof AppHiddenLayoutAppSettingsRoute
'/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/_hiddenLayout/storages': typeof AppHiddenLayoutStoragesRoute
'/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute
'/app/_hiddenLayout/_authed': typeof AppHiddenLayoutAuthedRouteWithChildren
'/app/_hiddenLayout/_authed/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
'/app/_hiddenLayout/_authed/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
'/app/_hiddenLayout/_authed/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
'/app/_hiddenLayout/_authed/storages': typeof AppHiddenLayoutAuthedStoragesRoute
'/app/_hiddenLayout/_authed/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
'/app/_hiddenLayout/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
}
export interface FileRouteTypes {
@@ -132,11 +139,12 @@ export interface FileRouteTypes {
| '/'
| '/login'
| '/app/_hiddenLayout'
| '/app/_hiddenLayout/add-product'
| '/app/_hiddenLayout/app-settings'
| '/app/_hiddenLayout/inventory'
| '/app/_hiddenLayout/storages'
| '/app/_hiddenLayout/view-product'
| '/app/_hiddenLayout/_authed'
| '/app/_hiddenLayout/_authed/add-product'
| '/app/_hiddenLayout/_authed/app-settings'
| '/app/_hiddenLayout/_authed/inventory'
| '/app/_hiddenLayout/_authed/storages'
| '/app/_hiddenLayout/_authed/view-product'
| '/app/_hiddenLayout/quick-view/product'
fileRoutesById: FileRoutesById
}
@@ -169,39 +177,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppHiddenLayoutRouteImport
parentRoute: typeof rootRouteImport
}
'/app/_hiddenLayout/view-product': {
id: '/app/_hiddenLayout/view-product'
path: '/view-product'
fullPath: '/app/view-product'
preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/storages': {
id: '/app/_hiddenLayout/storages'
path: '/storages'
fullPath: '/app/storages'
preLoaderRoute: typeof AppHiddenLayoutStoragesRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/inventory': {
id: '/app/_hiddenLayout/inventory'
path: '/inventory'
fullPath: '/app/inventory'
preLoaderRoute: typeof AppHiddenLayoutInventoryRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/app-settings': {
id: '/app/_hiddenLayout/app-settings'
path: '/app-settings'
fullPath: '/app/app-settings'
preLoaderRoute: typeof AppHiddenLayoutAppSettingsRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/add-product': {
id: '/app/_hiddenLayout/add-product'
path: '/add-product'
fullPath: '/app/add-product'
preLoaderRoute: typeof AppHiddenLayoutAddProductRouteImport
'/app/_hiddenLayout/_authed': {
id: '/app/_hiddenLayout/_authed'
path: ''
fullPath: '/app'
preLoaderRoute: typeof AppHiddenLayoutAuthedRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/quick-view/product': {
@@ -211,24 +191,72 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppHiddenLayoutQuickViewProductRouteImport
parentRoute: typeof AppHiddenLayoutRoute
}
'/app/_hiddenLayout/_authed/view-product': {
id: '/app/_hiddenLayout/_authed/view-product'
path: '/view-product'
fullPath: '/app/view-product'
preLoaderRoute: typeof AppHiddenLayoutAuthedViewProductRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/storages': {
id: '/app/_hiddenLayout/_authed/storages'
path: '/storages'
fullPath: '/app/storages'
preLoaderRoute: typeof AppHiddenLayoutAuthedStoragesRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/inventory': {
id: '/app/_hiddenLayout/_authed/inventory'
path: '/inventory'
fullPath: '/app/inventory'
preLoaderRoute: typeof AppHiddenLayoutAuthedInventoryRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/app-settings': {
id: '/app/_hiddenLayout/_authed/app-settings'
path: '/app-settings'
fullPath: '/app/app-settings'
preLoaderRoute: typeof AppHiddenLayoutAuthedAppSettingsRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
'/app/_hiddenLayout/_authed/add-product': {
id: '/app/_hiddenLayout/_authed/add-product'
path: '/add-product'
fullPath: '/app/add-product'
preLoaderRoute: typeof AppHiddenLayoutAuthedAddProductRouteImport
parentRoute: typeof AppHiddenLayoutAuthedRoute
}
}
}
interface AppHiddenLayoutAuthedRouteChildren {
AppHiddenLayoutAuthedAddProductRoute: typeof AppHiddenLayoutAuthedAddProductRoute
AppHiddenLayoutAuthedAppSettingsRoute: typeof AppHiddenLayoutAuthedAppSettingsRoute
AppHiddenLayoutAuthedInventoryRoute: typeof AppHiddenLayoutAuthedInventoryRoute
AppHiddenLayoutAuthedStoragesRoute: typeof AppHiddenLayoutAuthedStoragesRoute
AppHiddenLayoutAuthedViewProductRoute: typeof AppHiddenLayoutAuthedViewProductRoute
}
const AppHiddenLayoutAuthedRouteChildren: AppHiddenLayoutAuthedRouteChildren = {
AppHiddenLayoutAuthedAddProductRoute: AppHiddenLayoutAuthedAddProductRoute,
AppHiddenLayoutAuthedAppSettingsRoute: AppHiddenLayoutAuthedAppSettingsRoute,
AppHiddenLayoutAuthedInventoryRoute: AppHiddenLayoutAuthedInventoryRoute,
AppHiddenLayoutAuthedStoragesRoute: AppHiddenLayoutAuthedStoragesRoute,
AppHiddenLayoutAuthedViewProductRoute: AppHiddenLayoutAuthedViewProductRoute,
}
const AppHiddenLayoutAuthedRouteWithChildren =
AppHiddenLayoutAuthedRoute._addFileChildren(
AppHiddenLayoutAuthedRouteChildren,
)
interface AppHiddenLayoutRouteChildren {
AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute
AppHiddenLayoutAppSettingsRoute: typeof AppHiddenLayoutAppSettingsRoute
AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute
AppHiddenLayoutStoragesRoute: typeof AppHiddenLayoutStoragesRoute
AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute
AppHiddenLayoutAuthedRoute: typeof AppHiddenLayoutAuthedRouteWithChildren
AppHiddenLayoutQuickViewProductRoute: typeof AppHiddenLayoutQuickViewProductRoute
}
const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = {
AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute,
AppHiddenLayoutAppSettingsRoute: AppHiddenLayoutAppSettingsRoute,
AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute,
AppHiddenLayoutStoragesRoute: AppHiddenLayoutStoragesRoute,
AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute,
AppHiddenLayoutAuthedRoute: AppHiddenLayoutAuthedRouteWithChildren,
AppHiddenLayoutQuickViewProductRoute: AppHiddenLayoutQuickViewProductRoute,
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { Sidebar } from "../../components/Sidebar";
export const Route = createFileRoute("/app/_hiddenLayout")({
@@ -7,7 +7,10 @@ export const Route = createFileRoute("/app/_hiddenLayout")({
function AppLayout() {
return (
<div className="flex min-h-screen w-full flex-col bg-[#f7f9fc] lg:flex-row">
<div
className="flex min-h-screen w-full flex-col lg:flex-row"
style={{ backgroundColor: "var(--joy-palette-background-body)" }}
>
<Sidebar />
<main className="flex-1 px-4 py-5 sm:px-6 lg:px-8 lg:py-6">
<Outlet />
@@ -0,0 +1,9 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { verifyLogin } from "../../../utils/api/auth";
export const Route = createFileRoute("/app/_hiddenLayout/_authed")({
beforeLoad: async () => {
await verifyLogin();
},
component: () => <Outlet />,
});
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { AddProduct } from "../../../../pages/AddProduct";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/add-product")({
component: RouteComponent,
});
function RouteComponent() {
return <AddProduct />;
}
@@ -0,0 +1,12 @@
import { createFileRoute } from "@tanstack/react-router";
import { Settings } from "../../../../pages/Settings";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/app-settings")(
{
component: RouteComponent,
},
);
function RouteComponent() {
return <Settings />;
}
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { InventoryPage } from "../../../../pages/Inventory";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/inventory")({
component: RouteComponent,
});
function RouteComponent() {
return <InventoryPage />;
}
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { Storages } from "../../../../pages/Storages";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/storages")({
component: RouteComponent,
});
function RouteComponent() {
return <Storages />;
}
@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { ViewProduct } from "../../../../pages/ViewProduct";
export const Route = createFileRoute("/app/_hiddenLayout/_authed/view-product")(
{
validateSearch: z.object({
product: z.string(),
}),
component: RouteComponent,
},
);
function RouteComponent() {
const { product } = Route.useSearch();
return <ViewProduct uuid={product} />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { AddProduct } from "../../../pages/AddProduct";
export const Route = createFileRoute("/app/_hiddenLayout/add-product")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <AddProduct />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { Settings } from "../../../pages/Settings";
export const Route = createFileRoute("/app/_hiddenLayout/app-settings")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <Settings />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { InventoryPage } from "../../../pages/Inventory";
export const Route = createFileRoute("/app/_hiddenLayout/inventory")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <InventoryPage />;
}
@@ -1,18 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "../../../utils/api/auth";
import { Storages } from "../../../pages/Storages";
export const Route = createFileRoute("/app/_hiddenLayout/storages")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
component: RouteComponent,
});
function RouteComponent() {
return <Storages />;
}
@@ -1,23 +0,0 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { z } from "zod";
import { isAuthenticated } from "../../../utils/api/auth";
import { ViewProduct } from "../../../pages/ViewProduct";
export const Route = createFileRoute("/app/_hiddenLayout/view-product")({
beforeLoad: async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
});
}
},
validateSearch: z.object({
product: z.string(),
}),
component: RouteComponent,
});
function RouteComponent() {
const { product } = Route.useSearch();
return <ViewProduct uuid={product} />;
}
+1
View File
@@ -6,6 +6,7 @@ export const Route = createFileRoute("/")({
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
search: { loggedOut: true },
});
} else {
throw redirect({
+4
View File
@@ -1,7 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { LoginCard } from "../components/LoginCard";
import { checkLogin } from "../utils/api/auth.ts";
export const Route = createFileRoute("/login")({
beforeLoad: async () => {
await checkLogin();
},
component: RouteComponent,
});
+265
View File
@@ -0,0 +1,265 @@
import { extendTheme } from "@mui/joy/styles";
// I generated this theme with AI (Claude Opus 4.6)
export const theme = extendTheme({
cssVarPrefix: "joy",
colorSchemes: {
light: {
palette: {
primary: {
50: "#eef7ff",
100: "#dcedff",
200: "#b3d9ff",
300: "#7fbdff",
400: "#469cf5",
500: "#0b6bcb",
600: "#0958a8",
700: "#08478a",
800: "#0a3a6e",
900: "#0c2f59",
solidBg: "#0b6bcb",
solidHoverBg: "#095aa7",
solidActiveBg: "#08478a",
outlinedBorder: "#7fbdff",
outlinedColor: "#08478a",
softBg: "#dcedff", // Color in sidebar for selected view
softColor: "#08478a",
},
success: {
50: "#ecfdf5",
100: "#d1faee",
200: "#a6f4dc",
300: "#6ee7c9",
400: "#34d0ac",
500: "#0d9488",
600: "#0b7c73",
700: "#0a655e",
800: "#0b514c",
900: "#0a423f",
solidBg: "#0d9488",
solidHoverBg: "#0b7c73",
softBg: "#d1faee",
softColor: "#0a655e",
},
warning: {
50: "#fffaeb",
100: "#fef0c7",
200: "#fedf89",
300: "#fdc74a",
400: "#fbaf24",
500: "#f59e0b",
600: "#d67e06",
700: "#b25f08",
800: "#8f4a0e",
900: "#763d10",
solidBg: "#f59e0b",
solidHoverBg: "#d67e06",
softBg: "#fef0c7",
softColor: "#8f4a0e",
},
danger: {
50: "#fff1f2",
100: "#ffe1e4",
200: "#ffc7cd",
300: "#ff9ba7",
400: "#fb6b7d",
500: "#e11d48",
600: "#be123c",
700: "#9b1038",
800: "#7f1230",
900: "#6a122b",
solidBg: "#e11d48",
solidHoverBg: "#be123c",
softBg: "#ffe1e4",
softColor: "#9b1038",
},
neutral: {
50: "#f8fafc",
100: "#f1f5f9",
200: "#e2e8f0",
300: "#cbd5e1",
400: "#94a3b8",
500: "#64748b",
600: "#475569",
700: "#334155",
800: "#1e293b",
900: "#0f172a",
},
background: {
body: "#f7f9fc",
surface: "#ffffff",
level1: "#f1f5f9",
level2: "#e2e8f0",
level3: "#cbd5e1",
},
text: {
primary: "#0f172a",
secondary: "#475569",
tertiary: "#64748b",
},
divider: "rgba(15, 23, 42, 0.08)",
},
},
dark: {
palette: {
primary: {
50: "#0c2f59",
100: "#0a3a6e",
200: "#08478a",
300: "#0958a8",
400: "#0b6bcb",
500: "#3b8ce0",
600: "#6ba9ea",
700: "#9cc6f2",
800: "#c7defa",
900: "#eef7ff",
solidBg: "#3b8ce0",
solidHoverBg: "#6ba9ea",
solidActiveBg: "#0958a8",
outlinedBorder: "#0958a8",
outlinedColor: "#9cc6f2",
softBg: "rgba(59, 140, 224, 0.16)", // Color in sidebar for selected view
softColor: "#9cc6f2",
},
success: {
50: "#0a423f",
100: "#0b514c",
200: "#0a655e",
300: "#0b7c73",
400: "#0d9488",
500: "#2dc8b1",
600: "#6ee7c9",
700: "#a6f4dc",
800: "#d1faee",
900: "#ecfdf5",
solidBg: "#0d9488",
solidHoverBg: "#2dc8b1",
softBg: "rgba(13, 148, 136, 0.18)",
softColor: "#6ee7c9",
},
warning: {
50: "#763d10",
100: "#8f4a0e",
200: "#b25f08",
300: "#d67e06",
400: "#f59e0b",
500: "#fbaf24",
600: "#fdc74a",
700: "#fedf89",
800: "#fef0c7",
900: "#fffaeb",
solidBg: "#f59e0b",
solidHoverBg: "#fbaf24",
softBg: "rgba(245, 158, 11, 0.16)",
softColor: "#fdc74a",
},
danger: {
50: "#6a122b",
100: "#7f1230",
200: "#9b1038",
300: "#be123c",
400: "#e11d48",
500: "#f04463",
600: "#fb6b7d",
700: "#ff9ba7",
800: "#ffc7cd",
900: "#fff1f2",
solidBg: "#e11d48",
solidHoverBg: "#f04463",
softBg: "rgba(225, 29, 72, 0.18)",
softColor: "#fb6b7d",
},
neutral: {
50: "#0b1220",
100: "#0f172a",
200: "#1e293b",
300: "#334155",
400: "#475569",
500: "#64748b",
600: "#94a3b8",
700: "#cbd5e1",
800: "#e2e8f0",
900: "#f1f5f9",
},
background: {
body: "#0b1220",
surface: "#111a2e",
level1: "#16213a",
level2: "#1e293b",
level3: "#334155",
},
text: {
primary: "#f1f5f9",
secondary: "#cbd5e1",
tertiary: "#94a3b8",
},
divider: "rgba(226, 232, 240, 0.08)",
},
},
},
fontFamily: {
body: "'Inter', 'Segoe UI', system-ui, sans-serif",
display: "'Inter', 'Segoe UI', system-ui, sans-serif",
},
radius: {
xs: "6px",
sm: "10px",
md: "14px",
lg: "20px",
xl: "28px",
},
components: {
JoyInput: {
styleOverrides: {
input: ({ theme }) => ({
color: theme.vars.palette.text.primary,
"&::placeholder": {
color: theme.vars.palette.text.tertiary,
opacity: 1,
},
}),
},
},
JoySelect: {
styleOverrides: {
button: ({ theme }) => ({
color: theme.vars.palette.text.primary,
}),
listbox: ({ theme }) => ({
color: theme.vars.palette.text.primary,
backgroundColor: theme.vars.palette.background.surface,
}),
},
},
JoySheet: {
styleOverrides: {
root: ({ theme }) => ({
backgroundColor: theme.vars.palette.background.surface,
borderColor: theme.vars.palette.divider,
}),
},
},
JoyOption: {
styleOverrides: {
root: ({ theme }) => ({
color: theme.vars.palette.text.primary,
backgroundColor: theme.vars.palette.background.surface,
"&:hover": {
backgroundColor: theme.vars.palette.background.level1,
},
'&[aria-selected="true"]': {
backgroundColor: theme.vars.palette.background.level2,
},
}),
},
},
JoyButton: {
styleOverrides: {
root: {
transition: "all 0.15s ease",
},
},
},
},
});
+31 -18
View File
@@ -1,11 +1,12 @@
import {API_BASE} from "../../config/api.config";
import { API_BASE } from "../../config/api.config";
import Cookies from "js-cookie";
import type {TFunction} from "i18next";
import {fetchSettings} from "./settings";
import type {ChangePasswordIntf} from "../../misc/interfaces";
import {createApiError} from "./apiError";
import type { TFunction } from "i18next";
import { fetchSettings } from "./settings";
import type { ChangePasswordIntf } from "../../misc/interfaces";
import { createApiError } from "./apiError";
import { redirect } from "@tanstack/react-router";
export async function isAuthenticated() {
export const isAuthenticated = async () => {
if (Cookies.get("token")) {
const result = await fetch(`${API_BASE}/users/verify-token`, {
method: "POST",
@@ -23,13 +24,13 @@ export async function isAuthenticated() {
Cookies.remove("token");
return false;
}
};
export async function signInUser(
export const signInUser = async (
username: string,
password: string,
t: TFunction,
) {
) => {
const result = await fetch(`${API_BASE}/users/login`, {
method: "POST",
headers: {
@@ -37,7 +38,7 @@ export async function signInUser(
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({username, password}),
body: JSON.stringify({ username, password }),
});
const response = await result.json();
@@ -49,17 +50,12 @@ export async function signInUser(
Cookies.set("app-name", settings?.data[0].value);
Cookies.set("currency", settings?.data[1].value);
return {ok: true as const};
return { ok: true as const };
}
Cookies.remove("token");
throw createApiError(response.code, t(response.code || "unknown-error"));
}
export function signOutUser() {
Cookies.remove("token");
return {ok: true as const};
}
};
export const mutatePassword = async (payload: ChangePasswordIntf) => {
const result = await fetch(`${API_BASE}/users/change-password`, {
@@ -78,8 +74,25 @@ export const mutatePassword = async (payload: ChangePasswordIntf) => {
const response = await result.json();
if (response.code === "SU005") {
return {code: response.code};
return { code: response.code };
}
throw createApiError(response.code, "Change password failed");
};
export const verifyLogin = async () => {
if (!(await isAuthenticated())) {
throw redirect({
to: "/login",
search: { loggedOut: true },
});
}
};
export const checkLogin = async () => {
if (await isAuthenticated()) {
throw redirect({
to: "/app/inventory",
});
}
};
-13
View File
@@ -6331,19 +6331,6 @@
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT"
},
"node_modules/react-toastify": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.1.0.tgz",
"integrity": "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",