Compare commits
35
Commits
v0.1
...
9df8570e5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9df8570e5a | ||
|
|
b5a4f39c6f | ||
|
|
262c2b665d | ||
|
|
bde647aa08 | ||
|
|
e2a7d2e560 | ||
|
|
99246e3e3d | ||
|
|
3153076d72 | ||
|
|
ef5fb3e1da | ||
|
|
aa87442557 | ||
|
|
580db6afe9 | ||
|
|
bbca5a36cc | ||
|
|
0d4a048d16 | ||
|
|
57fe6dc9ac | ||
|
|
ef905fb30b | ||
|
|
925cb73c47 | ||
|
|
99a1097053 | ||
|
|
73dd672fdb | ||
|
|
92b5fd92bc | ||
|
|
d486655f54 | ||
|
|
018a7aefc7 | ||
|
|
6be34c93aa | ||
|
|
9c50c3f300 | ||
|
|
3a5c16031a | ||
|
|
c6bba8a40c | ||
|
|
87bed4e1c7 | ||
|
|
84fa2e0ab0 | ||
|
|
36ec8da1a0 | ||
|
|
7247a78809 | ||
|
|
9ecae324a3 | ||
|
|
4a73c43f80 | ||
|
|
8ebdbc14d6 | ||
|
|
1928068499 | ||
|
|
ea18228299 | ||
|
|
6aa00da60b | ||
|
|
0315aa4a40 |
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report something that isn't working correctly in Stockhome
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: bug
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Bug Description
|
||||||
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
<!-- How can we reliably reproduce the issue? -->
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
<!-- What did you expect to happen? -->
|
||||||
|
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
<!-- What actually happened instead? -->
|
||||||
|
|
||||||
|
|
||||||
|
## Screenshots / Logs
|
||||||
|
<!-- If applicable, add screenshots or paste relevant logs (e.g. from `docker compose logs`). -->
|
||||||
|
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|--------------------|-------------------------------|
|
||||||
|
| Stockhome version | <!-- e.g. v1.0.0 or commit hash --> |
|
||||||
|
| Deployment | <!-- Docker / docker-compose / manual --> |
|
||||||
|
| Browser | <!-- e.g. Chrome 125, Firefox 126 --> |
|
||||||
|
| OS (host) | <!-- e.g. Ubuntu 24.04, Raspberry Pi OS --> |
|
||||||
|
| `BACKEND_HOST` | <!-- localhost / IP / domain --> |
|
||||||
|
|
||||||
|
## Docker Logs
|
||||||
|
<!-- Run `docker compose logs` and paste any relevant output here. -->
|
||||||
|
<details>
|
||||||
|
<summary>docker compose logs</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
paste logs here
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Anything else that might help us understand the issue. -->
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or improvement for Stockhome
|
||||||
|
title: "[FEATURE] "
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Feature Description
|
||||||
|
<!-- A clear and concise description of the feature you'd like to see. -->
|
||||||
|
|
||||||
|
|
||||||
|
## Problem / Motivation
|
||||||
|
<!-- Is your feature request related to a problem? Describe it.
|
||||||
|
e.g. "I always have to manually check expiry dates because..." -->
|
||||||
|
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
<!-- Describe how you imagine this feature working. Be as specific as you like. -->
|
||||||
|
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
<!-- Have you thought of other approaches or workarounds? Why are they not ideal? -->
|
||||||
|
|
||||||
|
|
||||||
|
## Affected Area
|
||||||
|
<!-- Which part of Stockhome does this relate to? Check all that apply. -->
|
||||||
|
- [ ] Inventory / Product management
|
||||||
|
- [ ] Storage locations
|
||||||
|
- [ ] Expiry / Bottling date tracking
|
||||||
|
- [ ] QR code generation / scanning
|
||||||
|
- [ ] Costs tracking
|
||||||
|
- [ ] Settings (app name, currency, …)
|
||||||
|
- [ ] Authentication / User management
|
||||||
|
- [ ] API / Backend
|
||||||
|
- [ ] Frontend / UI
|
||||||
|
- [ ] Docker / Deployment
|
||||||
|
- [ ] Other: <!-- describe -->
|
||||||
|
|
||||||
|
## Additional Context / Mockups
|
||||||
|
<!-- Add any other context, screenshots, or mockups that help illustrate the idea. -->
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
.LSOverride
|
.LSOverride
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ Temporary Items
|
|||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
ToDo.txt
|
ToDo.txt
|
||||||
|
.idea
|
||||||
|
dist
|
||||||
|
index.ts
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.docker/volumes
|
.docker/volumes
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
The problem solver for every household in the world: **STOCKHOME**! Have you ever spent too long searching for things in your freezer or pantry? If so, this open-source project is just the right thing for your home server!
|
The problem solver for every household in the world: **STOCKHOME**! Have you ever spent too long searching for things in
|
||||||
|
your freezer or pantry? If so, this open-source project is just the right thing for your home server!
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./.github/assets/Arc_Inventory.low.jpg" width="802" alt="Inventory Screenshot" />
|
<img src="./.github/assets/Arc_Inventory.low.jpg" width="802" alt="Inventory Screenshot" />
|
||||||
@@ -43,19 +44,20 @@ exit
|
|||||||
|
|
||||||
### 2. Clone repository
|
### 2. Clone repository
|
||||||
|
|
||||||
If you haven't installed git, download the source code from this repository from the release page and unpack the `.zip` file.
|
**_Recommended because it is easy to update_**
|
||||||
|
|
||||||
**OR**
|
Clone this repository by running:
|
||||||
|
|
||||||
**_Recommended because it is easier to update then:_**
|
|
||||||
|
|
||||||
If you want to be always up to date, you can clone this repository by running:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://git.the1s.de/theis.gaedigk/stockhome.git
|
git clone --branch v0.1 --single-branch https://github.com/theis-js/stockhome.git
|
||||||
```
|
```
|
||||||
|
|
||||||
> **NOTE:** To do this, you must have git installed. [How to install git?](https://git-scm.com/install/)
|
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)
|
||||||
|
|
||||||
|
> **Note** To do this, you must have git installed. [How to install git?](https://git-scm.com/install/)
|
||||||
|
|
||||||
### 3. Create `.env` file
|
### 3. Create `.env` file
|
||||||
|
|
||||||
@@ -64,16 +66,18 @@ In the root directory of this repository create an `.env` file and enter the fol
|
|||||||
```txt
|
```txt
|
||||||
MYSQL_ROOT_PASSWORD=
|
MYSQL_ROOT_PASSWORD=
|
||||||
AUTH_SIGNATURE=
|
AUTH_SIGNATURE=
|
||||||
BACKEND_HOST=localhost
|
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.
|
> **Note** These three values cannot contain special characters.
|
||||||
|
|
||||||
The `BACKEND_HOST` can be set to the IP address of your server or to `localhost` if you are running the app on your local machine.
|
The `BACKEND_HOST` can be set to the IP address of your server or to `localhost` if you are running the app on your
|
||||||
|
local machine.
|
||||||
|
|
||||||
> **Change Docker Config:** If you want to run the stack for example in a docker network or want to change something at the docker config, visit the [Docker docs](./.github/docs/docker/README.md) for this project.
|
> **Change Docker Config:** If you want to run the stack for example in a docker network or want to change something at
|
||||||
|
> the docker config, visit the [Docker docs](./.github/docs/docker/README.md) for this project.
|
||||||
|
|
||||||
### 4. Start Stockhome
|
### 4. Start Stockhome
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ First, navigate into the root directory of this repository and run:
|
|||||||
docker compose up -d --build
|
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
|
### 5. First login
|
||||||
|
|
||||||
@@ -94,18 +98,43 @@ Username: admin
|
|||||||
Password: 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._
|
_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).
|
> **Note** If errors occur get yourself some help in the [error code page](./.github/docs/error/README.md).
|
||||||
|
|
||||||
> _I'd be grateful if you could report any bugs as issues here in the repository!_
|
## Feedback and bug reports - Troubleshooting
|
||||||
|
|
||||||
|
I'd be grateful if you could report any bugs as issues here in the
|
||||||
|
repository! [Report a bug](https://github.com/theis-js/stockhome/issues/new/choose)
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
To update the stack, 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
|
```shell
|
||||||
docker compose pull
|
git fetch --all --tags && git checkout "$(git describe --tags "$(git rev-list --tags --max-count=1)")"
|
||||||
|
```
|
||||||
|
|
||||||
|
To rebuild the containers run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
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 fetch --all
|
||||||
|
git checkout v0.1
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -114,23 +143,23 @@ docker compose up -d --build
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
- Visual Studio Code
|
- Visual Studio Code (or any other IDE)
|
||||||
- Cloned the repository
|
- Cloned the repository
|
||||||
|
|
||||||
### Start backend and database
|
### Initialize repository dependencies
|
||||||
|
|
||||||
First, navigate into the root directory of this repository and run:
|
First, navigate into the root directory of this repository and run:
|
||||||
|
|
||||||
```shell
|
```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
|
```shell
|
||||||
npm install
|
npm run build:shared
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+40
-5
@@ -1,12 +1,47 @@
|
|||||||
FROM node:20-alpine
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Workspace manifests (npm workspaces need these present)
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY backend/package.json backend/
|
||||||
|
COPY frontend/package.json frontend/
|
||||||
|
COPY shared/package.json shared/
|
||||||
|
|
||||||
|
# Install all deps for the monorepo (includes devDeps for building)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy sources required to build shared + backend
|
||||||
|
COPY shared/ shared/
|
||||||
|
COPY backend/ backend/
|
||||||
|
|
||||||
|
# Build shared (now produces dist/esm + dist/cjs + dist/types) and then backend
|
||||||
|
RUN npm run build --workspace=shared
|
||||||
|
RUN npm run build --workspace=backend
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
WORKDIR /backend
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
# Workspace manifests again (so npm can install prod deps for backend + shared)
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY backend/package.json backend/
|
||||||
|
COPY frontend/package.json frontend/
|
||||||
|
COPY shared/package.json shared/
|
||||||
|
|
||||||
|
# Install prod deps only
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
COPY . .
|
# Copy runtime artifacts
|
||||||
|
COPY --from=builder /app/backend/dist backend/dist
|
||||||
|
COPY --from=builder /app/backend/database.scheme.sql backend/database.scheme.sql
|
||||||
|
|
||||||
|
# Copy the full shared dist output (esm/cjs/types)
|
||||||
|
COPY --from=builder /app/shared/dist shared/dist
|
||||||
|
COPY shared/package.json shared/package.json
|
||||||
|
|
||||||
EXPOSE 8004
|
EXPOSE 8004
|
||||||
CMD ["npm", "start"]
|
WORKDIR /app/backend
|
||||||
|
CMD ["node", "dist/server.js"]
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type {AuthTokenPayload} from "../services/tokenService.js";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: AuthTokenPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type ErrorCode = readonly [code: string, message: string];
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean;
|
||||||
|
code: string;
|
||||||
|
data: any;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
Generated
-1096
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -1,22 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "1.0.0",
|
"version": "0.0.0-dev",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "server.js",
|
"main": "server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"dev": "tsx --watch server.ts",
|
||||||
"start": "node server.js"
|
"build": "tsc -b && node scripts/copy-assets.cjs",
|
||||||
|
"start": "node dist/server.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@stockhome/shared": "1.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.16.0"
|
"mysql2": "^3.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/node": "^26.1.0",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"tsx": "^4.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import mysql from "mysql2";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const pool = mysql
|
|
||||||
.createPool({
|
|
||||||
host: process.env.DB_HOST,
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_NAME,
|
|
||||||
})
|
|
||||||
.promise();
|
|
||||||
|
|
||||||
export const newProduct = async (
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
price,
|
|
||||||
amount,
|
|
||||||
storage_location,
|
|
||||||
expiry_date,
|
|
||||||
bottling_date,
|
|
||||||
) => {
|
|
||||||
let newPrice;
|
|
||||||
if (price == "") {
|
|
||||||
newPrice = null;
|
|
||||||
} else {
|
|
||||||
newPrice = price;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES (?, ?, ?, ?, UUID_TO_BIN(?), ?, ?)",
|
|
||||||
[
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
newPrice,
|
|
||||||
amount,
|
|
||||||
storage_location,
|
|
||||||
expiry_date,
|
|
||||||
bottling_date,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "sp001" }; // success
|
|
||||||
} else {
|
|
||||||
return { code: "ep001" }; // error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const productDetails = async (uuid) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`SELECT
|
|
||||||
BIN_TO_UUID(p.uuid) AS uuid,
|
|
||||||
p.name,
|
|
||||||
p.description,
|
|
||||||
p.price,
|
|
||||||
p.amount,
|
|
||||||
BIN_TO_UUID(s.uuid) AS storage_location_uuid,
|
|
||||||
s.name AS storage_location_name,
|
|
||||||
p.expiry_date,
|
|
||||||
p.bottling_date,
|
|
||||||
p.picture
|
|
||||||
FROM products p
|
|
||||||
JOIN storage_locations s ON p.storage_location = s.uuid
|
|
||||||
WHERE p.uuid = UUID_TO_BIN(?)`,
|
|
||||||
[uuid],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
return { code: "sp003", data: result[0] };
|
|
||||||
} else {
|
|
||||||
return { code: "ep003" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const allProducts = async () => {
|
|
||||||
const [result] = await pool.query(`
|
|
||||||
SELECT
|
|
||||||
BIN_TO_UUID(p.uuid) AS uuid,
|
|
||||||
p.name,
|
|
||||||
p.description,
|
|
||||||
p.price,
|
|
||||||
p.amount,
|
|
||||||
BIN_TO_UUID(s.uuid) AS storage_location_uuid,
|
|
||||||
s.name AS storage_location_name,
|
|
||||||
p.expiry_date,
|
|
||||||
p.bottling_date,
|
|
||||||
p.picture,
|
|
||||||
p.created_at,
|
|
||||||
p.updated_at
|
|
||||||
FROM products p
|
|
||||||
JOIN storage_locations s ON p.storage_location = s.uuid
|
|
||||||
WHERE p.deleted = 0
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
return { code: "sp002", data: result };
|
|
||||||
} else {
|
|
||||||
return { code: "ep002" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setAmount = async (itemUUID, amount) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`
|
|
||||||
UPDATE products SET amount = ? WHERE uuid = UUID_TO_BIN(?)
|
|
||||||
`,
|
|
||||||
[amount, itemUUID],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "sp004" }; // success
|
|
||||||
} else {
|
|
||||||
return { code: "ep004" }; // error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateItem = async (itemUUID, newValues) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`
|
|
||||||
UPDATE products SET name = ?, description = ?, price = ?, amount = ?, storage_location = UUID_TO_BIN(?), expiry_date = ?, bottling_date = ? WHERE uuid = UUID_TO_BIN(?);
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
newValues.name,
|
|
||||||
newValues.description,
|
|
||||||
newValues.price,
|
|
||||||
newValues.amount,
|
|
||||||
newValues.storage_location_uuid,
|
|
||||||
newValues.expiry_date,
|
|
||||||
newValues.bottling_date,
|
|
||||||
itemUUID,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "sp005" }; // success
|
|
||||||
} else {
|
|
||||||
return { code: "ep005" }; // error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteProduct = async (uuid) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`UPDATE products SET deleted = 1 WHERE uuid = UUID_TO_BIN(?);`,
|
|
||||||
[uuid],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "sp006" }; // success
|
|
||||||
} else {
|
|
||||||
return { code: "ep006" }; // error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import mysql, {
|
||||||
|
type ResultSetHeader,
|
||||||
|
type RowDataPacket,
|
||||||
|
} from "mysql2/promise";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { returnErrorCode } from "../../../services/helperFuncs.js";
|
||||||
|
import { PRODUCT_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST!,
|
||||||
|
user: process.env.DB_USER!,
|
||||||
|
password: process.env.DB_PASSWORD!,
|
||||||
|
database: process.env.DB_NAME!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const newProduct = async (
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
price: string,
|
||||||
|
amount: string,
|
||||||
|
storage_location: string,
|
||||||
|
expiry_date: string,
|
||||||
|
bottling_date: string,
|
||||||
|
): Promise<
|
||||||
|
boolean | { success: boolean; code: string; data: null; message: string }
|
||||||
|
> => {
|
||||||
|
const newPrice = price !== "" ? price : null;
|
||||||
|
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
"INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES (?, ?, ?, ?, UUID_TO_BIN(?), ?, ?)",
|
||||||
|
[
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
newPrice,
|
||||||
|
amount,
|
||||||
|
storage_location,
|
||||||
|
expiry_date,
|
||||||
|
bottling_date,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SP001",
|
||||||
|
data: null,
|
||||||
|
message: "New Product successfully created.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_CREATED);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const productDetails = async (uuid: string) => {
|
||||||
|
const [result] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT BIN_TO_UUID(p.uuid) AS uuid,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.price,
|
||||||
|
p.amount,
|
||||||
|
BIN_TO_UUID(s.uuid) AS storage_location_uuid,
|
||||||
|
s.name AS storage_location_name,
|
||||||
|
p.expiry_date,
|
||||||
|
p.bottling_date,
|
||||||
|
p.picture
|
||||||
|
FROM products p
|
||||||
|
JOIN storage_locations s ON p.storage_location = s.uuid
|
||||||
|
WHERE p.uuid = UUID_TO_BIN(?)`,
|
||||||
|
[uuid],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SP003",
|
||||||
|
data: result[0],
|
||||||
|
message: "Product details successfully loaded.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_FOUND);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const allProducts = async () => {
|
||||||
|
const [result] = await pool.query<RowDataPacket[]>(`
|
||||||
|
SELECT BIN_TO_UUID(p.uuid) AS uuid,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.price,
|
||||||
|
p.amount,
|
||||||
|
BIN_TO_UUID(s.uuid) AS storage_location_uuid,
|
||||||
|
s.name AS storage_location_name,
|
||||||
|
p.expiry_date,
|
||||||
|
p.bottling_date,
|
||||||
|
p.picture,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM products p
|
||||||
|
JOIN storage_locations s ON p.storage_location = s.uuid
|
||||||
|
WHERE p.deleted = 0
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SP002",
|
||||||
|
data: result,
|
||||||
|
message: "All products successfully loaded.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(PRODUCT_ERROR_CODE.NO_PRODUCTS_FOUND);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setAmount = async (itemUUID: string, amount: number) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
`UPDATE products
|
||||||
|
SET amount = ?
|
||||||
|
WHERE uuid = UUID_TO_BIN(?)`,
|
||||||
|
[amount, itemUUID],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SP004",
|
||||||
|
data: null,
|
||||||
|
message: "Amount successfully updated.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_AMOUNT_NOT_UPDATED);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateItem = async (
|
||||||
|
itemUUID: string,
|
||||||
|
newValues: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: string;
|
||||||
|
amount: string;
|
||||||
|
storage_location_uuid: string;
|
||||||
|
expiry_date: string;
|
||||||
|
bottling_date: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
`UPDATE products
|
||||||
|
SET name = ?,
|
||||||
|
description = ?,
|
||||||
|
price = ?,
|
||||||
|
amount = ?,
|
||||||
|
storage_location = UUID_TO_BIN(?),
|
||||||
|
expiry_date = ?,
|
||||||
|
bottling_date = ?
|
||||||
|
WHERE uuid = UUID_TO_BIN(?);`,
|
||||||
|
[
|
||||||
|
newValues.name,
|
||||||
|
newValues.description,
|
||||||
|
newValues.price,
|
||||||
|
newValues.amount,
|
||||||
|
newValues.storage_location_uuid,
|
||||||
|
newValues.expiry_date,
|
||||||
|
newValues.bottling_date,
|
||||||
|
itemUUID,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SP005",
|
||||||
|
data: null,
|
||||||
|
message: "Item updated successfully.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_UPDATED);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteProduct = async (uuid: string) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
`UPDATE products
|
||||||
|
SET deleted = 1
|
||||||
|
WHERE uuid = UUID_TO_BIN(?);`,
|
||||||
|
[uuid],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SP006",
|
||||||
|
data: null,
|
||||||
|
message: "Product deleted successfully.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED);
|
||||||
|
};
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import mysql from "mysql2";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const pool = mysql
|
|
||||||
.createPool({
|
|
||||||
host: process.env.DB_HOST,
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_NAME,
|
|
||||||
})
|
|
||||||
.promise();
|
|
||||||
|
|
||||||
export const allStorages = async () => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"SELECT BIN_TO_UUID(uuid) AS uuid, name, description, created_at, updated_at FROM storage_locations;",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
return { code: "ss001", data: result };
|
|
||||||
} else {
|
|
||||||
return { code: "es001" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newStorage = async (name, description) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"INSERT INTO storage_locations (name, description) VALUES (?, ?)",
|
|
||||||
[name, description],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "ss002" };
|
|
||||||
} else {
|
|
||||||
return { code: "es002" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateStorage = async (uuid, values) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE storage_locations SET name = ?, description = ? WHERE uuid = UUID_TO_BIN(?);",
|
|
||||||
[values.name, values.description, uuid],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "ss003" };
|
|
||||||
} else {
|
|
||||||
return { code: "es003" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteStorage = async (uuid) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"DELETE FROM storage_locations WHERE uuid = UUID_TO_BIN(?);",
|
|
||||||
[uuid],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "ss004" };
|
|
||||||
} else {
|
|
||||||
return { code: "es004" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import mysql, {
|
||||||
|
type ResultSetHeader,
|
||||||
|
type RowDataPacket,
|
||||||
|
} from "mysql2/promise";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { STORAGE_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
import { returnErrorCode } from "../../../services/helperFuncs.js";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST!,
|
||||||
|
user: process.env.DB_USER!,
|
||||||
|
password: process.env.DB_PASSWORD!,
|
||||||
|
database: process.env.DB_NAME!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const allStorages = async () => {
|
||||||
|
const [result] = await pool.query<RowDataPacket[]>(
|
||||||
|
"SELECT BIN_TO_UUID(uuid) AS uuid, name, description, created_at, updated_at FROM storage_locations;",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SS001",
|
||||||
|
data: result,
|
||||||
|
message: "Successfully fetched all storage locations.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(STORAGE_ERROR_CODE.NO_STORAGE_LOCATIONS_FOUND);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newStorage = async (name: string, description: string) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
"INSERT INTO storage_locations (name, description) VALUES (?, ?)",
|
||||||
|
[name, description],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SS002",
|
||||||
|
data: null,
|
||||||
|
message: "Successfully created new storage location.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_CREATED);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateStorage = async (
|
||||||
|
uuid: string,
|
||||||
|
values: { name: string; description: string },
|
||||||
|
) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
"UPDATE storage_locations SET name = ?, description = ? WHERE uuid = UUID_TO_BIN(?);",
|
||||||
|
[values.name, values.description, uuid],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SS003",
|
||||||
|
data: null,
|
||||||
|
message: "Successfully updated storage location.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_UPDATED);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteStorage = async (uuid: string) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
"DELETE FROM storage_locations WHERE uuid = UUID_TO_BIN(?);",
|
||||||
|
[uuid],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SS004",
|
||||||
|
data: null,
|
||||||
|
message: "Successfully deleted storage location.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_DELETED);
|
||||||
|
};
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import mysql from "mysql2";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const pool = mysql
|
|
||||||
.createPool({
|
|
||||||
host: process.env.DB_HOST,
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_NAME,
|
|
||||||
})
|
|
||||||
.promise();
|
|
||||||
|
|
||||||
export const findUser = async (username, password) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"SELECT BIN_TO_UUID(uuid) AS uuid, username, first_name, last_name, email, is_admin, is_active, last_login FROM users WHERE username = ? AND password = ?;",
|
|
||||||
[username, password],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length <= 0) {
|
|
||||||
return { code: "eu001" }; // username or password is wrong
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result[0].is_active) {
|
|
||||||
return { code: "eu002" }; // user is deactivated
|
|
||||||
}
|
|
||||||
|
|
||||||
return { code: "su001", data: result[0] }; // user found
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loginUser = async (username) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE users SET last_login = NOW() WHERE username = ?;",
|
|
||||||
[username],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "su002" };
|
|
||||||
} else {
|
|
||||||
return { code: "eu003" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSettings = async (payload) => {
|
|
||||||
const appName = payload["app-name"];
|
|
||||||
const currency = payload.currency;
|
|
||||||
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`UPDATE app_settings
|
|
||||||
SET value = CASE name
|
|
||||||
WHEN "app-name" THEN ?
|
|
||||||
WHEN "currency" THEN ?
|
|
||||||
ELSE value
|
|
||||||
END
|
|
||||||
WHERE name IN ("app-name", "currency");`,
|
|
||||||
[appName, currency],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "su003" };
|
|
||||||
} else {
|
|
||||||
return { code: "eu004" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSettings = async () => {
|
|
||||||
const [result] = await pool.query(`SELECT * FROM app_settings;`);
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
return { code: "su004", result };
|
|
||||||
} else {
|
|
||||||
return { code: "eu005" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changePassword = async (
|
|
||||||
username,
|
|
||||||
currentPasswordUser,
|
|
||||||
newPassword,
|
|
||||||
) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`UPDATE users SET password = ? WHERE username = ? AND password = ?;`,
|
|
||||||
[newPassword, username, currentPasswordUser],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { code: "su005" };
|
|
||||||
} else {
|
|
||||||
return { code: "eu006" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import mysql, {
|
||||||
|
type ResultSetHeader,
|
||||||
|
type RowDataPacket,
|
||||||
|
} from "mysql2/promise";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { GENERAL_ERROR_CODE, USER_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
import { returnErrorCode } from "../../../services/helperFuncs.js";
|
||||||
|
import type { AuthTokenPayload } from "../../../services/tokenService";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST!,
|
||||||
|
user: process.env.DB_USER!,
|
||||||
|
password: process.env.DB_PASSWORD!,
|
||||||
|
database: process.env.DB_NAME!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const findUser = async (username: string, password: string) => {
|
||||||
|
const [result] = await pool.query<RowDataPacket[]>(
|
||||||
|
"SELECT BIN_TO_UUID(uuid) AS uuid, username, first_name, last_name, email, is_admin, is_active, last_login FROM users WHERE username = ? AND password = ?;",
|
||||||
|
[username, password],
|
||||||
|
);
|
||||||
|
|
||||||
|
const userRow = result[0];
|
||||||
|
|
||||||
|
if (!userRow) {
|
||||||
|
return returnErrorCode(USER_ERROR_CODE.WRONG_USERNAME_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast DB row to AuthTokenPayload for token generation / typing
|
||||||
|
const user = userRow as unknown as AuthTokenPayload;
|
||||||
|
|
||||||
|
if (!user.is_active) {
|
||||||
|
return returnErrorCode(USER_ERROR_CODE.USER_IS_DEACTIVATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SU001",
|
||||||
|
data: user,
|
||||||
|
message: "Successfully found user.",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginUser = async (username: string) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
"UPDATE users SET last_login = NOW() WHERE username = ?;",
|
||||||
|
[username],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SU002",
|
||||||
|
data: null,
|
||||||
|
message: "Successfully logged in user.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSettings = async (payload: {
|
||||||
|
"app-name": string;
|
||||||
|
currency: string;
|
||||||
|
}) => {
|
||||||
|
const appName = payload["app-name"];
|
||||||
|
const currency = payload.currency;
|
||||||
|
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
`UPDATE app_settings
|
||||||
|
SET value = CASE name
|
||||||
|
WHEN "app-name" THEN ?
|
||||||
|
WHEN "currency" THEN ?
|
||||||
|
ELSE value
|
||||||
|
END
|
||||||
|
WHERE name IN ("app-name", "currency");`,
|
||||||
|
[appName, currency],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SU003",
|
||||||
|
data: null,
|
||||||
|
message: "Successfully updated settings.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSettings = async () => {
|
||||||
|
const [result] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT *
|
||||||
|
FROM app_settings;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SU004",
|
||||||
|
data: result,
|
||||||
|
message: "Successfully fetched settings.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changePassword = async (
|
||||||
|
username: string,
|
||||||
|
currentPasswordUser: string,
|
||||||
|
newPassword: string,
|
||||||
|
) => {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
`UPDATE users
|
||||||
|
SET password = ?
|
||||||
|
WHERE username = ?
|
||||||
|
AND password = ?;`,
|
||||||
|
[newPassword, username, currentPasswordUser],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
code: "SU005",
|
||||||
|
data: null,
|
||||||
|
message: "Successfully fetched settings.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return returnErrorCode(USER_ERROR_CODE.PASSWORD_CHANGE_FAILED);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
import { authenticate } from "../../services/tokenService.js";
|
|
||||||
import {
|
|
||||||
allProducts,
|
|
||||||
deleteProduct,
|
|
||||||
newProduct,
|
|
||||||
productDetails,
|
|
||||||
setAmount,
|
|
||||||
updateItem,
|
|
||||||
} from "./database/products.database.js";
|
|
||||||
dotenv.config();
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.post("/new-product", authenticate, async (req, res) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
price,
|
|
||||||
amount,
|
|
||||||
storage_location,
|
|
||||||
expiry_date,
|
|
||||||
bottling_date,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const result = await newProduct(
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
price,
|
|
||||||
amount,
|
|
||||||
storage_location,
|
|
||||||
expiry_date,
|
|
||||||
bottling_date,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.code === "ep001") {
|
|
||||||
res.status(406).json({
|
|
||||||
success: false,
|
|
||||||
code: "ep001",
|
|
||||||
data: null,
|
|
||||||
message: "Error while creating product",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "sp001") {
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
code: "sp001",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/all-products", authenticate, async (req, res) => {
|
|
||||||
const result = await allProducts();
|
|
||||||
|
|
||||||
if (result.code === "ep002") {
|
|
||||||
res.status(406).json({
|
|
||||||
success: false,
|
|
||||||
code: "ep002",
|
|
||||||
data: null,
|
|
||||||
message: "Error while fetching products",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "sp002") {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
code: "sp002",
|
|
||||||
data: result.data,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/view", async (req, res) => {
|
|
||||||
const uuid = req.query.uuid;
|
|
||||||
|
|
||||||
const result = await productDetails(uuid);
|
|
||||||
|
|
||||||
if (result.code === "ep003") {
|
|
||||||
res.status(406).json({
|
|
||||||
success: false,
|
|
||||||
code: "ep003",
|
|
||||||
data: null,
|
|
||||||
message: "Error while fetching product",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "sp003") {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
code: "sp003",
|
|
||||||
data: result.data,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put("/mutate/set-amount", authenticate, async (req, res) => {
|
|
||||||
const amount = req.query.amount;
|
|
||||||
const itemUUID = req.query.item;
|
|
||||||
|
|
||||||
const result = await setAmount(itemUUID, amount);
|
|
||||||
|
|
||||||
if (result.code === "ep004") {
|
|
||||||
res.status(406).json({
|
|
||||||
success: false,
|
|
||||||
code: "ep004",
|
|
||||||
data: null,
|
|
||||||
message: "Error while updating product amount",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "sp004") {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
code: "sp004",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/mutate/update-item", async (req, res) => {
|
|
||||||
const itemUUID = req.query.item;
|
|
||||||
const newValues = req.body;
|
|
||||||
|
|
||||||
const result = await updateItem(itemUUID, newValues);
|
|
||||||
|
|
||||||
if (result.code === "ep005") {
|
|
||||||
res.status(406).json({
|
|
||||||
success: false,
|
|
||||||
code: "ep005",
|
|
||||||
data: null,
|
|
||||||
message: "Error while updating product",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "sp005") {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
code: "sp005",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/delete-selection", authenticate, async (req, res) => {
|
|
||||||
let isError = false;
|
|
||||||
const uuidArray = req.body;
|
|
||||||
|
|
||||||
for (const uuid of uuidArray) {
|
|
||||||
const response = await deleteProduct(uuid);
|
|
||||||
if (response.code === "ep006" || !response) {
|
|
||||||
isError = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError === false) {
|
|
||||||
res.status(202).json({
|
|
||||||
success: true,
|
|
||||||
code: "sp006",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "ep006",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import express from "express";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { authenticate } from "../../services/tokenService";
|
||||||
|
import {
|
||||||
|
allProducts,
|
||||||
|
deleteProduct,
|
||||||
|
newProduct,
|
||||||
|
productDetails,
|
||||||
|
setAmount,
|
||||||
|
updateItem,
|
||||||
|
} from "./database/products.database";
|
||||||
|
import { GENERAL_ERROR_CODE, PRODUCT_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/new-product", authenticate, async (req, res) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
storage_location,
|
||||||
|
expiry_date,
|
||||||
|
bottling_date,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[0],
|
||||||
|
data: null,
|
||||||
|
message: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await newProduct(
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
storage_location,
|
||||||
|
expiry_date,
|
||||||
|
bottling_date,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result || typeof result !== "object") {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SP001") {
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/all-products", authenticate, async (req, res) => {
|
||||||
|
const result = await allProducts();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SP002") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: result.data,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === PRODUCT_ERROR_CODE.NO_PRODUCTS_FOUND[0]) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/view", async (req, res) => {
|
||||||
|
const uuid = req.query.uuid;
|
||||||
|
|
||||||
|
if (typeof uuid !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0],
|
||||||
|
data: null,
|
||||||
|
message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await productDetails(uuid);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SP003") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: result.data,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === PRODUCT_ERROR_CODE.PRODUCT_NOT_FOUND[0]) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/mutate/set-amount", authenticate, async (req, res) => {
|
||||||
|
const amount = req.query.amount;
|
||||||
|
const itemUUID = req.query.item;
|
||||||
|
|
||||||
|
if (typeof itemUUID !== "string" || typeof amount !== "number") {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0],
|
||||||
|
data: null,
|
||||||
|
message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await setAmount(itemUUID, amount);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SP004") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/mutate/update-item", authenticate, async (req, res) => {
|
||||||
|
const itemUUID = req.query.item;
|
||||||
|
const newValues = req.body;
|
||||||
|
|
||||||
|
if (typeof itemUUID !== "string" || !newValues) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0],
|
||||||
|
data: null,
|
||||||
|
message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateItem(itemUUID, newValues);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SP005") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/delete-selection", authenticate, async (req, res) => {
|
||||||
|
const uuidArray = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(uuidArray) || uuidArray.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[0],
|
||||||
|
data: null,
|
||||||
|
message: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const uuid of uuidArray) {
|
||||||
|
const response = await deleteProduct(uuid);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.code !== "SP006") {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: response.code || PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED[0],
|
||||||
|
data: null,
|
||||||
|
message: response.message || PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
code: "SP006",
|
||||||
|
data: null,
|
||||||
|
message: "All selected products deleted successfully.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
import { authenticate } from "../../services/tokenService.js";
|
|
||||||
import {
|
|
||||||
allStorages,
|
|
||||||
newStorage,
|
|
||||||
updateStorage,
|
|
||||||
deleteStorage,
|
|
||||||
} from "./database/storage.database.js";
|
|
||||||
dotenv.config();
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/all-storages", authenticate, async (req, res) => {
|
|
||||||
const result = await allStorages();
|
|
||||||
|
|
||||||
if (result.code === "es001") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "es001",
|
|
||||||
data: null,
|
|
||||||
message: "unexpected server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "ss001") {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
code: "ss001",
|
|
||||||
data: result.data,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/new-storage", authenticate, async (req, res) => {
|
|
||||||
const { name, description } = req.body;
|
|
||||||
|
|
||||||
let desc = description;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
code: "es000",
|
|
||||||
data: null,
|
|
||||||
message: "invalid request body",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description == "") {
|
|
||||||
desc = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await newStorage(name, desc);
|
|
||||||
|
|
||||||
if (result.code === "es002") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "es002",
|
|
||||||
data: null,
|
|
||||||
message: "unexpected server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "ss002") {
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
code: "ss002",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/update-storage", authenticate, async (req, res) => {
|
|
||||||
const storageUUID = req.query.storageUUID;
|
|
||||||
const values = req.body;
|
|
||||||
|
|
||||||
const result = await updateStorage(storageUUID, values);
|
|
||||||
|
|
||||||
if (result.code === "es003") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "es003",
|
|
||||||
data: null,
|
|
||||||
message: "unexpected server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "ss003") {
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
code: "ss003",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete("/delete", authenticate, async (req, res) => {
|
|
||||||
const uuid = req.query.uuid;
|
|
||||||
|
|
||||||
const result = await deleteStorage(uuid);
|
|
||||||
|
|
||||||
if (result.code === "es004") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "es004",
|
|
||||||
data: null,
|
|
||||||
message: "unexpected server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "ss004") {
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
code: "ss004",
|
|
||||||
data: null,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import express from "express";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { authenticate } from "../../services/tokenService";
|
||||||
|
import {
|
||||||
|
allStorages,
|
||||||
|
deleteStorage,
|
||||||
|
newStorage,
|
||||||
|
updateStorage,
|
||||||
|
} from "./database/storage.database";
|
||||||
|
import { GENERAL_ERROR_CODE, STORAGE_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/all-storages", authenticate, async (req, res) => {
|
||||||
|
const result = await allStorages();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR,
|
||||||
|
data: null,
|
||||||
|
message: "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SS001") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: result.data,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === STORAGE_ERROR_CODE.NO_STORAGE_LOCATIONS_FOUND[0]) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/new-storage", authenticate, async (req, res) => {
|
||||||
|
const { name, description } = req.body;
|
||||||
|
|
||||||
|
let desc = description;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: STORAGE_ERROR_CODE.INVALID_REQUEST_BODY[0],
|
||||||
|
data: null,
|
||||||
|
message: STORAGE_ERROR_CODE.INVALID_REQUEST_BODY[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description == "") {
|
||||||
|
desc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await newStorage(name, desc);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SS002") {
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/update-storage", authenticate, async (req, res) => {
|
||||||
|
const storageUUID = req.query.storageUUID;
|
||||||
|
const values = req.body;
|
||||||
|
|
||||||
|
if (!storageUUID || !values) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
|
||||||
|
data: null,
|
||||||
|
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof storageUUID !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
|
||||||
|
data: null,
|
||||||
|
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateStorage(storageUUID, values);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SS003") {
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/delete", authenticate, async (req, res) => {
|
||||||
|
const uuid = req.query.uuid;
|
||||||
|
|
||||||
|
if (typeof uuid !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
|
||||||
|
data: null,
|
||||||
|
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deleteStorage(uuid);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SS004") {
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
import { authenticate, generateToken } from "../../services/tokenService.js";
|
|
||||||
import {
|
|
||||||
findUser,
|
|
||||||
loginUser,
|
|
||||||
updateSettings,
|
|
||||||
getSettings,
|
|
||||||
changePassword,
|
|
||||||
} from "./database/users.database.js";
|
|
||||||
dotenv.config();
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.post("/verify-token", authenticate, async (req, res) => {
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/update-app-settings", authenticate, async (req, res) => {
|
|
||||||
const appName = req.body.appName;
|
|
||||||
const currency = req.body.currency;
|
|
||||||
|
|
||||||
const result = await updateSettings(req.body);
|
|
||||||
|
|
||||||
if (result.code === "su003") {
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
code: "su003",
|
|
||||||
data: result.data,
|
|
||||||
message: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "eu004") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "eu004",
|
|
||||||
data: null,
|
|
||||||
message: "Unexpected server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/settings", authenticate, async (req, res) => {
|
|
||||||
const result = await getSettings();
|
|
||||||
|
|
||||||
if (result.code === "su004") {
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
code: "su004",
|
|
||||||
data: result.result,
|
|
||||||
message: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "eu005") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "eu005",
|
|
||||||
data: null,
|
|
||||||
message: "Unexpected server error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
|
||||||
const username = req.body.username;
|
|
||||||
const password = req.body.password;
|
|
||||||
|
|
||||||
const result = await findUser(username, password);
|
|
||||||
|
|
||||||
if (result.code === "eu001") {
|
|
||||||
res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
code: "eu001",
|
|
||||||
data: null,
|
|
||||||
message: "username oder password is wrong",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "eu002") {
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
code: "eu002",
|
|
||||||
data: null,
|
|
||||||
message: "user is deactivated",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "su001") {
|
|
||||||
const token = await generateToken(result.data);
|
|
||||||
const login = await loginUser(result.data.username);
|
|
||||||
|
|
||||||
if (login.code === "eu003") {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
code: "eu003",
|
|
||||||
data: null,
|
|
||||||
message: "Unexpected server error. Please contact system admin.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(202).json({
|
|
||||||
success: true,
|
|
||||||
code: "su001",
|
|
||||||
data: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
message: "User token generated successfully",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/change-password", authenticate, async (req, res) => {
|
|
||||||
const currentPassword = req.body.currentPassword;
|
|
||||||
const newPassword = req.body.newPassword;
|
|
||||||
const username = req.user.username;
|
|
||||||
|
|
||||||
const result = await changePassword(username, currentPassword, newPassword);
|
|
||||||
|
|
||||||
if (result.code === "su005") {
|
|
||||||
res.status(202).json({
|
|
||||||
success: true,
|
|
||||||
code: result.code,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.code === "eu006") {
|
|
||||||
res.status(406).json({
|
|
||||||
success: false,
|
|
||||||
code: result.code,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import express from "express";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { authenticate, generateToken } from "../../services/tokenService";
|
||||||
|
import {
|
||||||
|
changePassword,
|
||||||
|
findUser,
|
||||||
|
getSettings,
|
||||||
|
loginUser,
|
||||||
|
updateSettings,
|
||||||
|
} from "./database/users.database";
|
||||||
|
import { GENERAL_ERROR_CODE, USER_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/verify-token", authenticate, async (req, res) => {
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/update-app-settings", authenticate, async (req, res) => {
|
||||||
|
const values = req.body;
|
||||||
|
|
||||||
|
if (!values || Object.keys(values).length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
|
||||||
|
data: null,
|
||||||
|
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateSettings(values);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SU003") {
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: result.data,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/settings", authenticate, async (req, res) => {
|
||||||
|
const result = await getSettings();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SU004") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: result.data,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/login", async (req, res) => {
|
||||||
|
const username = req.body.username;
|
||||||
|
const password = req.body.password;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
|
||||||
|
data: null,
|
||||||
|
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await findUser(username, password);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === USER_ERROR_CODE.WRONG_USERNAME_PASSWORD[0]) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === USER_ERROR_CODE.USER_IS_DEACTIVATED[0]) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SU001") {
|
||||||
|
if (!result.data) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateToken(result.data);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: USER_ERROR_CODE.TOKEN_GENERATION_FAILED[0],
|
||||||
|
data: null,
|
||||||
|
message: USER_ERROR_CODE.TOKEN_GENERATION_FAILED[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = await loginUser(result.data.username);
|
||||||
|
|
||||||
|
if (!login) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (login.code !== "SU002") {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: login.code || USER_ERROR_CODE.LOGIN_FAILED[0],
|
||||||
|
data: null,
|
||||||
|
message: login.message || USER_ERROR_CODE.LOGIN_FAILED[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
code: "SU001",
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
message: "User token generated successfully",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/change-password", authenticate, async (req, res) => {
|
||||||
|
const currentPassword = req.body.currentPassword;
|
||||||
|
const newPassword = req.body.newPassword;
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = req.user.username;
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
|
||||||
|
data: null,
|
||||||
|
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await changePassword(username, currentPassword, newPassword);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "SU005") {
|
||||||
|
return res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
data: null,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
|
||||||
|
data: null,
|
||||||
|
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// scripts/copy-assets.cjs
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const rootDir = path.join(__dirname, "..");
|
||||||
|
const distDir = path.join(rootDir, "dist");
|
||||||
|
|
||||||
|
const sqlFiles = fs
|
||||||
|
.readdirSync(rootDir)
|
||||||
|
.filter((f) => f.endsWith(".scheme.sql"));
|
||||||
|
|
||||||
|
for (const file of sqlFiles) {
|
||||||
|
const srcPath = path.join(rootDir, file);
|
||||||
|
const distPath = path.join(distDir, file);
|
||||||
|
fs.copyFileSync(srcPath, distPath);
|
||||||
|
console.log(`Copied ${srcPath} -> ${distPath}`);
|
||||||
|
}
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import cors from "cors";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
import mysql from "mysql2";
|
|
||||||
import { readFile } from "fs/promises";
|
|
||||||
dotenv.config();
|
|
||||||
const app = express();
|
|
||||||
app.set("view engine", "ejs");
|
|
||||||
|
|
||||||
const port = 8004;
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
const pool = mysql
|
|
||||||
.createPool({
|
|
||||||
host: process.env.DB_HOST,
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_NAME,
|
|
||||||
})
|
|
||||||
.promise();
|
|
||||||
|
|
||||||
// frontend routes
|
|
||||||
import userRouter from "./routes/app/users.route.js";
|
|
||||||
app.use("/users", userRouter);
|
|
||||||
|
|
||||||
import productRouter from "./routes/app/products.route.js";
|
|
||||||
app.use("/products", productRouter);
|
|
||||||
|
|
||||||
import storageRouter from "./routes/app/storage.route.js";
|
|
||||||
app.use("/storage", storageRouter);
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
runStartup(port);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Startup code
|
|
||||||
const runStartup = async (port) => {
|
|
||||||
// Check if database is configured; create schema if app_settings is missing.
|
|
||||||
let firstStartupValue = null;
|
|
||||||
try {
|
|
||||||
const [firstResponse] = await pool.query(
|
|
||||||
`SELECT value FROM app_settings WHERE name = "first-startup";`,
|
|
||||||
);
|
|
||||||
firstStartupValue = firstResponse[0]?.value ?? null;
|
|
||||||
} catch (err) {
|
|
||||||
if (err?.code !== "ER_NO_SUCH_TABLE") {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstStartupValue !== "false") {
|
|
||||||
const schemaPath = new URL("./database.scheme.sql", import.meta.url);
|
|
||||||
const schemaSql = await readFile(schemaPath, "utf8");
|
|
||||||
const statements = schemaSql
|
|
||||||
.split(";")
|
|
||||||
.map((statement) => statement.trim())
|
|
||||||
.filter((statement) => statement.length > 0);
|
|
||||||
|
|
||||||
for (const statement of statements) {
|
|
||||||
await pool.query(statement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create admin credentials
|
|
||||||
const [result] = await pool.query(
|
|
||||||
`SELECT value FROM app_settings WHERE name = "first-startup";`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result[0]?.value === "true") {
|
|
||||||
const insertResult = await insertFirstData();
|
|
||||||
|
|
||||||
if (insertResult.affectedRows > 0) {
|
|
||||||
// print out admin credentials
|
|
||||||
console.log("Successfully created admin user!");
|
|
||||||
console.log("Username: admin");
|
|
||||||
console.log("Password: admin");
|
|
||||||
|
|
||||||
// Set startup variable to true if scheme insert was successfull
|
|
||||||
const [scndResponse] = await pool.query(
|
|
||||||
`UPDATE app_settings SET value = "false" WHERE name = "first-startup";`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (scndResponse.affectedRows > 0) {
|
|
||||||
console.log("Database settet up successfully!");
|
|
||||||
} else {
|
|
||||||
console.error("There was an error while setting up the database!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("Error while creating admin user.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Everything is settet up successfully!");
|
|
||||||
console.log(`Server is running on http://localhost:${port}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertFirstData = async () => {
|
|
||||||
const [insertResult] = await pool.query(
|
|
||||||
`INSERT INTO users (username, first_name, last_name, email, password, is_admin) VALUES ("admin", "admin", "admin", "admin@example.com", "admin", 1)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
`INSERT INTO storage_locations (name, description) VALUES (?, ?);`,
|
|
||||||
["Default Storage", "Initial storage location"],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [storageRows] = await pool.query(
|
|
||||||
`SELECT uuid FROM storage_locations WHERE name = ? LIMIT 1;`,
|
|
||||||
["Default Storage"],
|
|
||||||
);
|
|
||||||
|
|
||||||
const storageUuid = storageRows[0]?.uuid ?? null;
|
|
||||||
if (storageUuid) {
|
|
||||||
await pool.query(
|
|
||||||
`INSERT INTO products (name, description, price, amount, storage_location, picture) VALUES (?, ?, ?, ?, ?, ?);`,
|
|
||||||
[
|
|
||||||
"Welcome Product",
|
|
||||||
"Your first item in Stockhome",
|
|
||||||
"0.00",
|
|
||||||
1,
|
|
||||||
storageUuid,
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Welcome product is ready...")
|
|
||||||
return insertResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
// error handling code
|
|
||||||
app.use((err, req, res, next) => {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.status(500).send("Something broke!");
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import mysql, { type ResultSetHeader, type RowDataPacket } from "mysql2";
|
||||||
|
import { readFile } from "fs/promises"; // frontend routes
|
||||||
|
import userRouter from "./routes/app/users.route";
|
||||||
|
import productRouter from "./routes/app/products.route";
|
||||||
|
import storageRouter from "./routes/app/storage.route";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const app = express();
|
||||||
|
app.set("view engine", "ejs");
|
||||||
|
|
||||||
|
const port = 8004;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
const pool = mysql
|
||||||
|
.createPool({
|
||||||
|
host: process.env.DB_HOST!,
|
||||||
|
user: process.env.DB_USER!,
|
||||||
|
password: process.env.DB_PASSWORD!,
|
||||||
|
database: process.env.DB_NAME!,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
app.use("/users", userRouter);
|
||||||
|
|
||||||
|
app.use("/products", productRouter);
|
||||||
|
|
||||||
|
app.use("/storage", storageRouter);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
runStartup(port);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Startup code
|
||||||
|
const runStartup = async (port: number) => {
|
||||||
|
let firstStartupValue: string | null = null;
|
||||||
|
try {
|
||||||
|
const [firstResponse] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT value
|
||||||
|
FROM app_settings
|
||||||
|
WHERE name = "first-startup";`,
|
||||||
|
);
|
||||||
|
firstStartupValue = firstResponse[0]?.value ?? null;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (
|
||||||
|
err instanceof Error &&
|
||||||
|
(err as NodeJS.ErrnoException & { code?: string }).code !==
|
||||||
|
"ER_NO_SUCH_TABLE"
|
||||||
|
) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstStartupValue !== "false") {
|
||||||
|
const schemaPath = path.join(__dirname, "database.scheme.sql");
|
||||||
|
const schemaSql = await readFile(schemaPath, "utf8");
|
||||||
|
const statements = schemaSql
|
||||||
|
.split(";")
|
||||||
|
.map((statement: string) => statement.trim())
|
||||||
|
.filter((statement: string) => statement.length > 0);
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
await pool.query(statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT value
|
||||||
|
FROM app_settings
|
||||||
|
WHERE name = "first-startup";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result[0]?.value === "true") {
|
||||||
|
const insertResult = await insertFirstData();
|
||||||
|
|
||||||
|
if (insertResult.affectedRows > 0) {
|
||||||
|
console.log("Successfully created admin user!");
|
||||||
|
console.log("Username: admin");
|
||||||
|
console.log("Password: admin");
|
||||||
|
|
||||||
|
const [scndResponse] = await pool.query<ResultSetHeader>(
|
||||||
|
`UPDATE app_settings
|
||||||
|
SET value = "false"
|
||||||
|
WHERE name = "first-startup";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scndResponse.affectedRows > 0) {
|
||||||
|
console.log("Database settet up successfully!");
|
||||||
|
} else {
|
||||||
|
console.error("There was an error while setting up the database!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Error while creating admin user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Everything is settet up successfully!");
|
||||||
|
console.log(`Backend is running on http://localhost:${port}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertFirstData = async (): Promise<ResultSetHeader> => {
|
||||||
|
const [insertResult] = await pool.query<ResultSetHeader>(
|
||||||
|
`INSERT INTO users (username, first_name, last_name, email, password, is_admin)
|
||||||
|
VALUES ("admin", "admin", "admin", "admin@example.com", "admin", 1)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO storage_locations (name, description)
|
||||||
|
VALUES (?, ?);`,
|
||||||
|
["Default Storage", "Initial storage location"],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [storageRows] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT uuid
|
||||||
|
FROM storage_locations
|
||||||
|
WHERE name = ? LIMIT 1;`,
|
||||||
|
["Default Storage"],
|
||||||
|
);
|
||||||
|
|
||||||
|
const storageUuid: string | null = storageRows[0]?.uuid ?? null;
|
||||||
|
if (storageUuid) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO products (name, description, price, amount, storage_location, picture)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?);`,
|
||||||
|
[
|
||||||
|
"Welcome Product",
|
||||||
|
"Your first item in Stockhome",
|
||||||
|
"0.00",
|
||||||
|
1,
|
||||||
|
storageUuid,
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Welcome product is ready...");
|
||||||
|
return insertResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
// error handling code
|
||||||
|
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).send("Something broke!");
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ErrorCode } from "../misc/types.ts";
|
||||||
|
|
||||||
|
export const returnErrorCode = (errorKey: ErrorCode) => {
|
||||||
|
if (!Array.isArray(errorKey) || errorKey.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
code: errorKey[0],
|
||||||
|
data: null,
|
||||||
|
message: errorKey[1],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import bodyParser from "body-parser";
|
|
||||||
import { SignJWT, jwtVerify } from "jose";
|
|
||||||
import env from "dotenv";
|
|
||||||
env.config();
|
|
||||||
const secret = new TextEncoder().encode(process.env.SECRET_KEY);
|
|
||||||
|
|
||||||
export async function generateToken(payload) {
|
|
||||||
const newToken = await new SignJWT(payload)
|
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
|
||||||
.setIssuedAt()
|
|
||||||
.setExpirationTime("24h") // Token valid for 24 hours
|
|
||||||
.sign(secret);
|
|
||||||
return newToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function authenticate(req, res, next) {
|
|
||||||
const authHeader = req.headers["authorization"];
|
|
||||||
const token = authHeader && authHeader.split(" ")[1]; // Bearer <token>
|
|
||||||
|
|
||||||
if (token == null) return res.sendStatus(401); // No token present
|
|
||||||
|
|
||||||
const { payload } = await jwtVerify(token, secret);
|
|
||||||
req.user = payload;
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
import { type JWTPayload, jwtVerify, SignJWT } from "jose";
|
||||||
|
import env from "dotenv";
|
||||||
|
|
||||||
|
env.config();
|
||||||
|
const secret = new TextEncoder().encode(process.env.SECRET_KEY);
|
||||||
|
|
||||||
|
export type AuthTokenPayload = JWTPayload & {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module "express-serve-static-core" {
|
||||||
|
interface Request {
|
||||||
|
user?: AuthTokenPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateToken(payload: AuthTokenPayload) {
|
||||||
|
return await new SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime("24h")
|
||||||
|
.sign(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authenticate(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) {
|
||||||
|
const authHeader = req.headers["authorization"];
|
||||||
|
const token = authHeader && authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
if (token == null) return res.sendStatus(401);
|
||||||
|
|
||||||
|
const { payload } = await jwtVerify<AuthTokenPayload>(token, secret);
|
||||||
|
req.user = payload;
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES2022",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
|
"types": ["node"],
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["./**/*.ts"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"**/vite.config.ts",
|
||||||
|
"**/vite.config.*",
|
||||||
|
"**/*.config.ts",
|
||||||
|
"**/*.config.*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../shared"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import path from "path";
|
||||||
|
import { dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: "src",
|
||||||
|
build: {
|
||||||
|
outDir: "../dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: true,
|
||||||
|
minify: "esbuild",
|
||||||
|
assetsDir: "assets",
|
||||||
|
rollupOptions: {
|
||||||
|
input: path.resolve(__dirname, "src/index.html"),
|
||||||
|
output: {
|
||||||
|
assetFileNames: "assets/[name]-[hash][extname]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
+2
-21
@@ -3,7 +3,7 @@ services:
|
|||||||
container_name: stockhome-mysql
|
container_name: stockhome-mysql
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
ports:
|
ports:
|
||||||
- "3312:3306"
|
- "3306:3306"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
@@ -16,23 +16,4 @@ services:
|
|||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 20
|
retries: 10
|
||||||
|
|
||||||
backend:
|
|
||||||
container_name: stockhome-backend
|
|
||||||
ports:
|
|
||||||
- "8004:8004"
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
environment:
|
|
||||||
DB_HOST: stockhome-mysql
|
|
||||||
DB_USER: root
|
|
||||||
DB_NAME: stockhome
|
|
||||||
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
|
||||||
SECRET_KEY: ${AUTH_SIGNATURE}
|
|
||||||
NODE_ENV: production
|
|
||||||
depends_on:
|
|
||||||
database:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
+6
-5
@@ -11,7 +11,7 @@ services:
|
|||||||
- ./.docker/volumes/stockhome_mysql:/var/lib/mysql
|
- ./.docker/volumes/stockhome_mysql:/var/lib/mysql
|
||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 20
|
retries: 20
|
||||||
@@ -19,8 +19,8 @@ services:
|
|||||||
backend:
|
backend:
|
||||||
container_name: stockhome-backend
|
container_name: stockhome-backend
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8004:8004"
|
- "8004:8004"
|
||||||
environment:
|
environment:
|
||||||
@@ -39,10 +39,11 @@ services:
|
|||||||
container_name: stockhome-frontend
|
container_name: stockhome-frontend
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
build: ./frontend
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: frontend/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
BACKEND_HOST: ${BACKEND_HOST}
|
BACKEND_HOST: ${BACKEND_HOST}
|
||||||
VITE_BACKEND_URL: /backend
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
+12
-7
@@ -3,20 +3,25 @@ FROM node:22-alpine AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
COPY backend/package.json backend/
|
||||||
|
COPY frontend/package.json frontend/
|
||||||
|
COPY shared/package.json shared/
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY shared/ shared/
|
||||||
RUN npm run build
|
COPY frontend/ frontend/
|
||||||
|
|
||||||
|
# Build shared first, then frontend
|
||||||
|
RUN npm run build --workspace=shared
|
||||||
|
RUN npm run build --workspace=frontend
|
||||||
|
|
||||||
FROM nginx:alpine AS runner
|
FROM nginx:alpine AS runner
|
||||||
|
|
||||||
WORKDIR /usr/share/nginx/html
|
WORKDIR /usr/share/nginx/html
|
||||||
COPY --from=builder /app/dist .
|
COPY --from=builder /app/frontend/dist .
|
||||||
|
|
||||||
COPY docker-entrypoint.d/10-runtime-env.sh /docker-entrypoint.d/10-runtime-env.sh
|
COPY frontend/nginx.conf /etc/nginx/templates/default.conf.template
|
||||||
RUN chmod +x /docker-entrypoint.d/10-runtime-env.sh
|
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/templates/default.conf.template
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
+11
-5
@@ -4,12 +4,15 @@ This template provides a minimal setup to get React working in Vite with HMR and
|
|||||||
|
|
||||||
Currently, two official plugins are available:
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react)
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
uses [Oxc](https://oxc.rs)
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc)
|
||||||
|
uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
## React Compiler
|
## React Compiler
|
||||||
|
|
||||||
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
|
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for
|
||||||
|
more information.
|
||||||
|
|
||||||
Note: This will impact Vite dev & build performances.
|
Note: This will impact Vite dev & build performances.
|
||||||
|
|
||||||
@@ -45,10 +48,13 @@ export default defineConfig([
|
|||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
You can also
|
||||||
|
install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x)
|
||||||
|
and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom)
|
||||||
|
for React-specific lint rules:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// eslint.config.js
|
// eslint.config.mjs
|
||||||
import reactX from 'eslint-plugin-react-x'
|
import reactX from 'eslint-plugin-react-x'
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
VITE_BACKEND_URL_VALUE="${VITE_BACKEND_URL:-/backend}"
|
|
||||||
|
|
||||||
cat > /usr/share/nginx/html/env.js <<EOF
|
|
||||||
window.__ENV = {
|
|
||||||
VITE_BACKEND_URL: "${VITE_BACKEND_URL_VALUE}"
|
|
||||||
};
|
|
||||||
EOF
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="/env.js"></script>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0-dev",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-i18next": "^17.0.8",
|
"react-i18next": "^17.0.8",
|
||||||
"react-toastify": "^11.1.0",
|
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"validator": "^13.15.35",
|
"validator": "^13.15.35",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|||||||
+6
-16
@@ -1,9 +1,11 @@
|
|||||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { ToastContainer } from "react-toastify";
|
|
||||||
import { routeTree } from "./routeTree.gen";
|
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 });
|
const router = createRouter({ routeTree, defaultNotFoundComponent: NotFound });
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
@@ -13,21 +15,9 @@ declare module "@tanstack/react-router" {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<CssVarsProvider theme={theme} defaultMode={"system"}>
|
||||||
<ToastContainer
|
|
||||||
position="top-right"
|
|
||||||
autoClose={5000}
|
|
||||||
hideProgressBar={false}
|
|
||||||
newestOnTop={false}
|
|
||||||
closeOnClick={false}
|
|
||||||
rtl={false}
|
|
||||||
pauseOnFocusLoss
|
|
||||||
draggable
|
|
||||||
pauseOnHover
|
|
||||||
theme="colored"
|
|
||||||
/>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</>
|
</CssVarsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { Input, Button, Alert } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { signInUser } from "../utils/api/auth";
|
import { signInUser } from "../utils/api/auth";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { AlertInterface } from "../misc/interfaces";
|
import type { AlertInterface } from "../misc/interfaces";
|
||||||
|
import { MyAlert } from "./MyAlert.tsx";
|
||||||
|
|
||||||
export const LoginCard = () => {
|
export const LoginCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const search: { loggedOut?: boolean } = useSearch({ from: "/login" });
|
||||||
const [alert, setAlert] = useState<AlertInterface>({
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
isAlert: false,
|
isAlert: false,
|
||||||
type: "neutral",
|
type: "neutral",
|
||||||
@@ -17,6 +19,18 @@ export const LoginCard = () => {
|
|||||||
text: "",
|
text: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (search.loggedOut) {
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "primary",
|
||||||
|
header: t("success"),
|
||||||
|
text: t("logout-success-text"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void navigate({ to: "/login" });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
@@ -43,7 +57,7 @@ export const LoginCard = () => {
|
|||||||
header: "",
|
header: "",
|
||||||
text: "",
|
text: "",
|
||||||
});
|
});
|
||||||
navigate({ to: "/app/inventory" });
|
void navigate({ to: "/app/inventory" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
@@ -58,14 +72,34 @@ export const LoginCard = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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="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">
|
<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
|
Stockhome
|
||||||
</p>
|
</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")}
|
{t("login")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,15 +111,11 @@ export const LoginCard = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert
|
||||||
variant="soft"
|
type={alert.type}
|
||||||
color={alert.type}
|
header={alert.header}
|
||||||
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
text={alert.text}
|
||||||
>
|
/>
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
<form.Field name="username">
|
<form.Field name="username">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
@@ -95,7 +125,11 @@ export const LoginCard = () => {
|
|||||||
placeholder={t("username")}
|
placeholder={t("username")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
@@ -108,7 +142,11 @@ export const LoginCard = () => {
|
|||||||
placeholder={t("password")}
|
placeholder={t("password")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
@@ -116,7 +154,13 @@ export const LoginCard = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
size="lg"
|
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")}
|
{t("login")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Alert } from "@mui/joy";
|
||||||
|
|
||||||
|
interface MyAlertProps {
|
||||||
|
type: "success" | "warning" | "danger" | "neutral" | "primary";
|
||||||
|
header: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MyAlert = (props: MyAlertProps) => {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
variant="soft"
|
||||||
|
color={props.type}
|
||||||
|
className="rounded-2xl drop-shadow-sm"
|
||||||
|
>
|
||||||
|
{props.header}
|
||||||
|
<br />
|
||||||
|
{props.text}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Typography } from "@mui/joy";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const NotFound = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"flex flex-col items-center gap-2"}>
|
||||||
|
<Typography color={"primary"} level={"h1"}>
|
||||||
|
{t("not-found-header")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||||
|
level={"body-lg"}
|
||||||
|
>
|
||||||
|
{t("not-found-body")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,42 +7,59 @@ import StorageIcon from "@mui/icons-material/Storage";
|
|||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
|
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
|
||||||
import TranslateIcon from "@mui/icons-material/Translate";
|
import TranslateIcon from "@mui/icons-material/Translate";
|
||||||
|
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useNavigate, useMatchRoute } from "@tanstack/react-router";
|
import { useMatchRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { changeTranslation } from "../utils/uxFncs";
|
import { changeTranslation } from "../utils/uxFncs";
|
||||||
|
import { useColorScheme } from "@mui/joy/styles";
|
||||||
|
import { useLogout } from "../hooks/useLogout.ts";
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { logout } = useLogout();
|
||||||
const matchRoute = useMatchRoute();
|
const matchRoute = useMatchRoute();
|
||||||
|
const { mode, setMode } = useColorScheme();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const btnClass =
|
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) =>
|
const variant = (to: string) =>
|
||||||
!!matchRoute({ to, fuzzy: false }) ? "soft" : "plain";
|
!!matchRoute({ to, fuzzy: false }) ? "soft" : "plain";
|
||||||
|
|
||||||
const handleNavigate = (to: string) => {
|
const handleNavigate = (to: string) => {
|
||||||
navigate({ to });
|
void navigate({ to });
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="flex items-center justify-between gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Typography
|
<Typography
|
||||||
level="h2"
|
level="h2"
|
||||||
className="text-[22px] font-semibold text-[#0b6bcb]"
|
className="text-[22px] font-semibold"
|
||||||
|
sx={{ color: "var(--joy-palette-primary-solidBg)" }}
|
||||||
>
|
>
|
||||||
{t("app-title")}
|
{t("app-title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level="body-lg"
|
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") : ""}
|
{Cookies.get("app-name") ? Cookies.get("app-name") : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -50,7 +67,14 @@ export const Sidebar = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="lg:hidden 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)}
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
startDecorator={
|
startDecorator={
|
||||||
<span
|
<span
|
||||||
@@ -61,7 +85,6 @@ export const Sidebar = () => {
|
|||||||
{isOpen ? <CloseIcon /> : <MenuIcon />}
|
{isOpen ? <CloseIcon /> : <MenuIcon />}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
sx={{ display: { lg: "none" } }}
|
|
||||||
>
|
>
|
||||||
{isOpen ? t("close") : t("menu")}
|
{isOpen ? t("close") : t("menu")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -80,6 +103,13 @@ export const Sidebar = () => {
|
|||||||
variant={variant("/app/inventory")}
|
variant={variant("/app/inventory")}
|
||||||
startDecorator={<InventoryIcon />}
|
startDecorator={<InventoryIcon />}
|
||||||
className={btnClass}
|
className={btnClass}
|
||||||
|
sx={{
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
color: "var(--joy-palette-primary-solidBg)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("inventory")}
|
{t("inventory")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -88,6 +118,13 @@ export const Sidebar = () => {
|
|||||||
variant={variant("/app/add-product")}
|
variant={variant("/app/add-product")}
|
||||||
startDecorator={<AddBoxIcon />}
|
startDecorator={<AddBoxIcon />}
|
||||||
className={btnClass}
|
className={btnClass}
|
||||||
|
sx={{
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
color: "var(--joy-palette-primary-solidBg)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -96,6 +133,13 @@ export const Sidebar = () => {
|
|||||||
variant={variant("/app/storages")}
|
variant={variant("/app/storages")}
|
||||||
startDecorator={<StorageIcon />}
|
startDecorator={<StorageIcon />}
|
||||||
className={btnClass}
|
className={btnClass}
|
||||||
|
sx={{
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
color: "var(--joy-palette-primary-solidBg)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("storages")}
|
{t("storages")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -104,14 +148,18 @@ export const Sidebar = () => {
|
|||||||
variant={variant("/app/app-settings")}
|
variant={variant("/app/app-settings")}
|
||||||
startDecorator={<SettingsIcon />}
|
startDecorator={<SettingsIcon />}
|
||||||
className={btnClass}
|
className={btnClass}
|
||||||
|
sx={{
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
color: "var(--joy-palette-primary-solidBg)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("settings")}
|
{t("settings")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={logout}
|
||||||
Cookies.remove("token");
|
|
||||||
handleNavigate("/login");
|
|
||||||
}}
|
|
||||||
color="danger"
|
color="danger"
|
||||||
startDecorator={<ExitToAppIcon />}
|
startDecorator={<ExitToAppIcon />}
|
||||||
className={btnClass}
|
className={btnClass}
|
||||||
@@ -129,9 +177,25 @@ export const Sidebar = () => {
|
|||||||
>
|
>
|
||||||
{t("change-translation")}
|
{t("change-translation")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setMode(mode === "dark" ? "light" : "dark")}
|
||||||
|
color="neutral"
|
||||||
|
startDecorator={<Brightness4Icon />}
|
||||||
|
className={btnClass}
|
||||||
|
>
|
||||||
|
{mode === "dark" ? "Light Mode" : "Dark Mode"}
|
||||||
|
</Button>
|
||||||
</div>
|
</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
|
<img
|
||||||
src="/favicon.png"
|
src="/favicon.png"
|
||||||
alt="Stockhome"
|
alt="Stockhome"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQueryClient, useMutation } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { deleteStorage, updateStorage } from "../utils/api/storages";
|
import { deleteStorage, updateStorage } from "../utils/api/storages";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useStore } from "@tanstack/react-store";
|
||||||
import { Input, Button } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
import type { Storage } from "../misc/interfaces";
|
import type { Storage } from "../misc/interfaces";
|
||||||
import { formatDate } from "../utils/uxFncs";
|
import { formatDate } from "../utils/uxFncs";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -50,7 +50,7 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
|
|||||||
(values.description ?? "") !== (storage.description ?? "");
|
(values.description ?? "") !== (storage.description ?? "");
|
||||||
|
|
||||||
return (
|
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">
|
<td className="px-6 py-5">
|
||||||
<form.Field name="name">
|
<form.Field name="name">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
@@ -60,7 +60,12 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
@@ -74,15 +79,26 @@ export const StorageRow = ({ storage, onError }: StorageRowProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</td>
|
</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)}
|
{formatDate(storage.created_at)}
|
||||||
</td>
|
</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)}
|
{formatDate(storage.updated_at)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5 text-right">
|
<td className="px-6 py-5 text-right">
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import {
|
import { Button, DialogContent, DialogTitle, Input, Modal, ModalDialog, Stack, } from "@mui/joy";
|
||||||
Modal,
|
|
||||||
ModalDialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
Stack,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { NewStorage, AlertInterface } from "../../misc/interfaces";
|
import type { AlertInterface, NewStorage } from "../../misc/interfaces";
|
||||||
import { mutateNewStorage } from "../../utils/api/storages";
|
import { mutateNewStorage } from "../../utils/api/storages";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { MyAlert } from "../MyAlert.tsx";
|
||||||
|
|
||||||
interface AddStorageModalProps {
|
interface AddStorageModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -59,11 +51,20 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
|
<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">
|
<ModalDialog
|
||||||
<DialogTitle className="text-slate-900">
|
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")}
|
{t("new-storage-title")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent className="text-slate-500">
|
<DialogContent sx={{ color: "var(--joy-palette-text-tertiary)" }}>
|
||||||
{t("new-storage-content")}
|
{t("new-storage-content")}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<form
|
<form
|
||||||
@@ -81,7 +82,11 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
|
|||||||
placeholder={t("storage-name")}
|
placeholder={t("storage-name")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
@@ -93,29 +98,35 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
|
|||||||
placeholder={t("description")}
|
placeholder={t("description")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
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")}
|
{t("submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert
|
||||||
variant="soft"
|
type={alert.type}
|
||||||
color={alert.type}
|
header={alert.header}
|
||||||
className="mt-4 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
text={alert.text}
|
||||||
>
|
/>
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
DialogTitle,
|
||||||
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
DialogTitle,
|
|
||||||
Stack,
|
Stack,
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { ChangePasswordIntf, AlertInterface } from "../../misc/interfaces";
|
import type { AlertInterface, ChangePasswordIntf } from "../../misc/interfaces";
|
||||||
import { mutatePassword } from "../../utils/api/auth";
|
import { mutatePassword } from "../../utils/api/auth";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { USER_ERROR_CODE } from "@stockhome/shared";
|
||||||
|
import { MyAlert } from "../MyAlert.tsx";
|
||||||
|
|
||||||
interface ChangePasswordProps {
|
interface ChangePasswordProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setOpen: (value: boolean) => void;
|
setOpen: (value: boolean) => void;
|
||||||
|
alert: (alert: AlertInterface) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
||||||
@@ -35,7 +37,19 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
|||||||
newPasswordRep: "",
|
newPasswordRep: "",
|
||||||
},
|
},
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
|
if (
|
||||||
|
value.newPassword === value.newPasswordRep &&
|
||||||
|
value.newPassword !== ""
|
||||||
|
) {
|
||||||
mutate(value);
|
mutate(value);
|
||||||
|
} else {
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: t(USER_ERROR_CODE.PASSWORDS_NOT_MATCHED[0]),
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,6 +64,19 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
props.alert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "success",
|
||||||
|
header: t("success"),
|
||||||
|
text: t("SU005"),
|
||||||
|
});
|
||||||
|
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "success",
|
||||||
|
header: t("success"),
|
||||||
|
text: t("SU005"),
|
||||||
|
});
|
||||||
props.setOpen(false);
|
props.setOpen(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -57,8 +84,17 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
|
<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">
|
<ModalDialog
|
||||||
<DialogTitle className="text-slate-900">
|
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")}
|
{t("new-password-title")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<form
|
<form
|
||||||
@@ -77,7 +113,11 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
|||||||
placeholder={t("current-password")}
|
placeholder={t("current-password")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
@@ -90,7 +130,11 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
|||||||
placeholder={t("new-password")}
|
placeholder={t("new-password")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
@@ -103,29 +147,35 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
|||||||
placeholder={t("new-password-rep")}
|
placeholder={t("new-password-rep")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="lg"
|
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>
|
</form.Field>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
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")}
|
{t("change")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert
|
||||||
variant="soft"
|
type={alert.type}
|
||||||
color={alert.type}
|
header={alert.header}
|
||||||
className="mt-4 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
text={alert.text}
|
||||||
>
|
/>
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
type WindowWithEnv = Window & {
|
|
||||||
__ENV?: {
|
|
||||||
VITE_BACKEND_URL?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const runtimeEnv = (globalThis as unknown as WindowWithEnv).__ENV;
|
|
||||||
|
|
||||||
export const API_BASE =
|
export const API_BASE =
|
||||||
runtimeEnv?.VITE_BACKEND_URL ||
|
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||||
import.meta.env.VITE_BACKEND_URL ||
|
import.meta.env.VITE_BACKEND_URL ||
|
||||||
"http://localhost:8004";
|
"/backend";
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Alert,
|
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
Chip,
|
||||||
@@ -16,16 +15,16 @@ import { useForm } from "@tanstack/react-form";
|
|||||||
import { createProduct } from "../utils/api/products";
|
import { createProduct } from "../utils/api/products";
|
||||||
import { getStorages } from "../utils/api/storages";
|
import { getStorages } from "../utils/api/storages";
|
||||||
import type {
|
import type {
|
||||||
ProductFormValues,
|
|
||||||
AlertInterface,
|
AlertInterface,
|
||||||
|
ProductFormValues,
|
||||||
Storage,
|
Storage,
|
||||||
} from "../misc/interfaces";
|
} from "../misc/interfaces";
|
||||||
import type { ApiError } from "../utils/api/apiError";
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { MyAlert } from "../components/MyAlert.tsx";
|
||||||
|
|
||||||
export const AddProduct = () => {
|
export const AddProduct = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [success, setSuccess] = useState(false);
|
|
||||||
const [alert, setAlert] = useState<AlertInterface>({
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
isAlert: false,
|
isAlert: false,
|
||||||
type: "neutral",
|
type: "neutral",
|
||||||
@@ -69,7 +68,6 @@ export const AddProduct = () => {
|
|||||||
storage_location_uuid: "",
|
storage_location_uuid: "",
|
||||||
},
|
},
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
setSuccess(false);
|
|
||||||
mutate(value);
|
mutate(value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -77,7 +75,12 @@ export const AddProduct = () => {
|
|||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (values: ProductFormValues) => createProduct(values),
|
mutationFn: (values: ProductFormValues) => createProduct(values),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSuccess(true);
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "success",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: showError,
|
onError: showError,
|
||||||
});
|
});
|
||||||
@@ -87,10 +90,16 @@ export const AddProduct = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("add-product")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level="body-lg" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-lg"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{t("add-product-subtitle")}
|
{t("add-product-subtitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +112,16 @@ export const AddProduct = () => {
|
|||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -114,7 +132,10 @@ export const AddProduct = () => {
|
|||||||
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("product-name")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="name">
|
<form.Field name="name">
|
||||||
@@ -127,13 +148,20 @@ export const AddProduct = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="description">
|
<form.Field name="description">
|
||||||
@@ -145,14 +173,21 @@ export const AddProduct = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("expiry-date")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="expiry_date">
|
<form.Field name="expiry_date">
|
||||||
@@ -164,13 +199,19 @@ export const AddProduct = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("bottling-date")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="bottling_date">
|
<form.Field name="bottling_date">
|
||||||
@@ -182,7 +223,10 @@ export const AddProduct = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -190,14 +234,28 @@ export const AddProduct = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<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)]">
|
<div
|
||||||
<Typography level="title-md" className="text-slate-900">
|
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")}
|
{t("inventory")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider className="my-3" />
|
<Divider className="my-3" />
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("amount")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="amount">
|
<form.Field name="amount">
|
||||||
@@ -217,13 +275,19 @@ export const AddProduct = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
className="rounded-2xl bg-white/80"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("price")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="price">
|
<form.Field name="price">
|
||||||
@@ -235,16 +299,25 @@ export const AddProduct = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{Cookies.get("currency")}
|
{Cookies.get("currency")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("storage-place")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="storage_location_uuid">
|
<form.Field name="storage_location_uuid">
|
||||||
@@ -257,7 +330,10 @@ export const AddProduct = () => {
|
|||||||
}
|
}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{storages?.map((storage) => (
|
{storages?.map((storage) => (
|
||||||
<Option key={storage.uuid} value={storage.uuid}>
|
<Option key={storage.uuid} value={storage.uuid}>
|
||||||
@@ -272,42 +348,35 @@ export const AddProduct = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<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")}
|
{t("product-details")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<div className="grow"></div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
size="lg"
|
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")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert
|
||||||
color={alert.type}
|
type={alert.type}
|
||||||
variant="soft"
|
header={alert.header}
|
||||||
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
text={alert.text}
|
||||||
>
|
/>
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<Alert
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
className="rounded-2xl border border-emerald-200/70 bg-emerald-50/80 text-emerald-700 shadow-[0_14px_30px_rgba(16,185,129,0.18)]"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<Typography level="body-sm" className="text-emerald-700">
|
|
||||||
{t("success")}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { ChangeEvent, MouseEvent, useEffect, useState } from "react";
|
||||||
import {
|
import { Avatar, Button, Checkbox, Chip, CircularProgress, Sheet, Table, Typography, } from "@mui/joy";
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
Sheet,
|
|
||||||
Table,
|
|
||||||
Avatar,
|
|
||||||
Chip,
|
|
||||||
Checkbox,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
@@ -17,9 +7,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { deleteSelectedProducts, getProducts } from "../utils/api/products";
|
import { deleteSelectedProducts, getProducts } from "../utils/api/products";
|
||||||
import { formatDate } from "../utils/uxFncs";
|
import { formatDate } from "../utils/uxFncs";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import type { ProductRow } from "../misc/interfaces";
|
import type { AlertInterface, ProductRow } from "../misc/interfaces";
|
||||||
import type { AlertInterface } from "../misc/interfaces";
|
|
||||||
import type { ApiError } from "../utils/api/apiError";
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
|
import CategoryIcon from "@mui/icons-material/Category";
|
||||||
|
import { MyAlert } from "../components/MyAlert.tsx";
|
||||||
|
|
||||||
export const InventoryPage = () => {
|
export const InventoryPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -82,12 +73,12 @@ export const InventoryPage = () => {
|
|||||||
mutationFn: (values: string[]) => deleteSelectedProducts(values),
|
mutationFn: (values: string[]) => deleteSelectedProducts(values),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
queryClient.invalidateQueries({ queryKey: ["products"] });
|
void queryClient.invalidateQueries({ queryKey: ["products"] });
|
||||||
},
|
},
|
||||||
onError: showError,
|
onError: showError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSelectAllClick = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
setSelected(rows.map((row) => row.id));
|
setSelected(rows.map((row) => row.id));
|
||||||
return;
|
return;
|
||||||
@@ -95,7 +86,7 @@ export const InventoryPage = () => {
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (_event: React.MouseEvent<unknown>, id: string) => {
|
const handleClick = (_event: MouseEvent<unknown>, id: string) => {
|
||||||
const selectedIndex = selected.indexOf(id);
|
const selectedIndex = selected.indexOf(id);
|
||||||
let newSelected: readonly string[] = [];
|
let newSelected: readonly string[] = [];
|
||||||
if (selectedIndex === -1) {
|
if (selectedIndex === -1) {
|
||||||
@@ -128,22 +119,25 @@ export const InventoryPage = () => {
|
|||||||
{productsIsLoading && <CircularProgress size="sm" />}
|
{productsIsLoading && <CircularProgress size="sm" />}
|
||||||
</div>
|
</div>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert type={alert.type} header={alert.header} text={alert.text} />
|
||||||
variant="soft"
|
|
||||||
color={alert.type}
|
|
||||||
className="mt-4 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
|
||||||
>
|
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="mt-6 flex min-h-0 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">
|
<Typography level="body-lg" fontWeight="bold">
|
||||||
{t("inventory")}
|
{t("inventory")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -164,36 +158,67 @@ export const InventoryPage = () => {
|
|||||||
stripe="odd"
|
stripe="odd"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
hoverRow
|
hoverRow
|
||||||
className="min-w-240 text-slate-700"
|
className="w-full"
|
||||||
sx={{
|
sx={{
|
||||||
|
tableLayout: "fixed",
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
"--TableCell-headBackground":
|
"--TableCell-headBackground":
|
||||||
"var(--joy-palette-background-surface)",
|
"var(--joy-palette-background-level1)",
|
||||||
"& thead": {
|
"& thead": {
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
backgroundColor: "rgb(248 250 252)",
|
backgroundColor: "var(--joy-palette-background-level1)",
|
||||||
},
|
},
|
||||||
"& thead tr": {
|
"& thead tr": {
|
||||||
backgroundColor: "rgb(248 250 252)",
|
backgroundColor: "var(--joy-palette-background-level1)",
|
||||||
},
|
|
||||||
"& thead th:nth-child(2)": {
|
|
||||||
width: "40%",
|
|
||||||
},
|
|
||||||
"& thead th:nth-child(3)": {
|
|
||||||
width: "14%",
|
|
||||||
},
|
},
|
||||||
"& thead th": {
|
"& thead th": {
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
backgroundColor: "rgb(248 250 252)",
|
backgroundColor: "var(--joy-palette-background-level1)",
|
||||||
backgroundImage: "none",
|
backgroundImage: "none",
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(1)": {
|
||||||
|
width: "44px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(2)": {
|
||||||
|
width: "22%",
|
||||||
|
minWidth: "160px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(3)": {
|
||||||
|
width: "9%",
|
||||||
|
minWidth: "70px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(4)": {
|
||||||
|
width: "12%",
|
||||||
|
minWidth: "90px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(5)": {
|
||||||
|
width: "17%",
|
||||||
|
minWidth: "120px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(6)": {
|
||||||
|
width: "13%",
|
||||||
|
minWidth: "100px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(7)": {
|
||||||
|
width: "13%",
|
||||||
|
minWidth: "100px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(8)": {
|
||||||
|
width: "14%",
|
||||||
|
minWidth: "100px",
|
||||||
},
|
},
|
||||||
"& tr > *:nth-child(n+4)": { textAlign: "left" },
|
"& tr > *:nth-child(n+4)": { textAlign: "left" },
|
||||||
|
"& tbody tr": {
|
||||||
|
borderTop: "1px solid var(--joy-palette-divider)",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-slate-600">
|
<tr>
|
||||||
<th className="px-4 py-4">
|
<th className="px-2 py-4">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={rows.length > 0 && selected.length === rows.length}
|
checked={rows.length > 0 && selected.length === rows.length}
|
||||||
indeterminate={
|
indeterminate={
|
||||||
@@ -204,13 +229,13 @@ export const InventoryPage = () => {
|
|||||||
sx={{ verticalAlign: "sub" }}
|
sx={{ verticalAlign: "sub" }}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-4">{t("product-name")}</th>
|
<th className="px-3 py-4">{t("product-name")}</th>
|
||||||
<th className="px-6 py-4">{t("price")}</th>
|
<th className="px-3 py-4">{t("price")}</th>
|
||||||
<th className="px-6 py-4">{t("stock")}</th>
|
<th className="px-3 py-4">{t("stock")}</th>
|
||||||
<th className="px-6 py-4">{t("storage-place")}</th>
|
<th className="px-3 py-4">{t("storage-place")}</th>
|
||||||
<th className="px-6 py-4">{t("expiry-date")}</th>
|
<th className="px-3 py-4">{t("expiry-date")}</th>
|
||||||
<th className="px-6 py-4">{t("bottling-date")}</th>
|
<th className="px-3 py-4">{t("bottling-date")}</th>
|
||||||
<th className="px-6 py-4 text-right">{t("actions")}</th>
|
<th className="px-3 py-4 text-right">{t("actions")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -220,67 +245,90 @@ export const InventoryPage = () => {
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="border-t border-slate-200"
|
|
||||||
onClick={(event) => handleClick(event, row.id)}
|
onClick={(event) => handleClick(event, row.id)}
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
aria-checked={isItemSelected}
|
aria-checked={isItemSelected}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<th scope="row" className="px-4 py-5">
|
<th scope="row" className="px-2 py-5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isItemSelected}
|
checked={isItemSelected}
|
||||||
slotProps={{ input: { "aria-labelledby": labelId } }}
|
slotProps={{ input: { "aria-labelledby": labelId } }}
|
||||||
sx={{ verticalAlign: "top" }}
|
sx={{ verticalAlign: "top" }}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th id={labelId} scope="row" className="px-6 py-5">
|
<th
|
||||||
<div className="flex items-center gap-4">
|
id={labelId}
|
||||||
<Avatar size="lg" variant="soft" src={row.imageUrl} />
|
scope="row"
|
||||||
|
className="px-3 py-5 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<Avatar size="lg" variant="soft" className="shrink-0">
|
||||||
|
<CategoryIcon />
|
||||||
|
</Avatar>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<Typography
|
<Typography
|
||||||
level="title-md"
|
level="title-md"
|
||||||
className="text-slate-900"
|
className="truncate"
|
||||||
|
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||||
>
|
>
|
||||||
{row.name}
|
{row.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level="body-sm"
|
level="body-sm"
|
||||||
className="text-slate-500"
|
className="truncate"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
>
|
>
|
||||||
{row.description}
|
{row.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<td className="px-6 py-5">
|
<td className="px-3 py-5 overflow-hidden">
|
||||||
<Typography level="title-md">{row.price}</Typography>
|
<Typography level="title-md" className="truncate">
|
||||||
<Typography level="body-sm" className="text-slate-400">
|
{row.price}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
className="truncate"
|
||||||
|
sx={{ color: "var(--joy-palette-neutral-400)" }}
|
||||||
|
>
|
||||||
{Cookies.get("currency")}
|
{Cookies.get("currency")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-3 py-5 overflow-hidden">
|
||||||
<Chip
|
<Chip
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="px-3"
|
className="max-w-full px-3"
|
||||||
>
|
>
|
||||||
{row.stock}
|
<span className="truncate">{row.stock}</span>
|
||||||
</Chip>
|
</Chip>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-3 py-5 overflow-hidden">
|
||||||
<Typography level="title-md">{row.location}</Typography>
|
<Typography level="title-md" className="truncate">
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
{row.location}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
className="truncate"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{row.locationDetail}
|
{row.locationDetail}
|
||||||
</Typography>
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-3 py-5 overflow-hidden">
|
||||||
<Typography level="title-md">{row.expiryDate}</Typography>
|
<Typography level="title-md" className="truncate">
|
||||||
|
{row.expiryDate}
|
||||||
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-3 py-5 overflow-hidden">
|
||||||
<Typography level="title-md">{row.refillDate}</Typography>
|
<Typography level="title-md" className="truncate">
|
||||||
|
{row.refillDate}
|
||||||
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5 text-right">
|
<td className="px-3 py-5 text-right">
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate({
|
navigate({
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { getStorages } from "../utils/api/storages.ts";
|
import { getStorages } from "../utils/api/storages.ts";
|
||||||
import {
|
import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
|
||||||
CircularProgress,
|
|
||||||
Typography,
|
|
||||||
Select,
|
|
||||||
Option,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
Divider,
|
|
||||||
Box,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { mutateProduct, getProductDetails } from "../utils/api/products.ts";
|
import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
|
||||||
import { toInputDate } from "../utils/uxFncs";
|
import { toInputDate } from "../utils/uxFncs";
|
||||||
import type {
|
import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
|
||||||
ProductFormValues,
|
|
||||||
productDetailsInterface,
|
|
||||||
AlertInterface,
|
|
||||||
Storage,
|
|
||||||
} from "../misc/interfaces";
|
|
||||||
import type { ApiError } from "../utils/api/apiError";
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
|
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
|
||||||
|
import { MyAlert } from "../components/MyAlert.tsx";
|
||||||
|
|
||||||
export const ProductQuickView = () => {
|
export const ProductQuickView = () => {
|
||||||
const uuid = window.location.search.split("uuid=")[1];
|
const uuid = window.location.search.split("uuid=")[1];
|
||||||
@@ -140,7 +125,11 @@ export const ProductQuickView = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="space-y-1">
|
<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" />
|
<ElectricBoltIcon color="primary" />
|
||||||
{t("product-details-quick")}
|
{t("product-details-quick")}
|
||||||
<ElectricBoltIcon color="primary" />
|
<ElectricBoltIcon color="primary" />
|
||||||
@@ -157,7 +146,16 @@ export const ProductQuickView = () => {
|
|||||||
{productDetailsLoading && <CircularProgress size="sm" />}
|
{productDetailsLoading && <CircularProgress size="sm" />}
|
||||||
</div>
|
</div>
|
||||||
{isSuccess && (
|
{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
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -165,10 +163,13 @@ export const ProductQuickView = () => {
|
|||||||
form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
<div className="gap-5 lg:grid-cols-[1.2fr_1fr]">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("product-name")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="name">
|
<form.Field name="name">
|
||||||
@@ -180,13 +181,20 @@ export const ProductQuickView = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="description">
|
<form.Field name="description">
|
||||||
@@ -198,14 +206,21 @@ export const ProductQuickView = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("expiry-date")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="expiry_date">
|
<form.Field name="expiry_date">
|
||||||
@@ -217,13 +232,19 @@ export const ProductQuickView = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("bottling-date")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="bottling_date">
|
<form.Field name="bottling_date">
|
||||||
@@ -235,7 +256,10 @@ export const ProductQuickView = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -243,14 +267,28 @@ export const ProductQuickView = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<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)]">
|
<div
|
||||||
<Typography level="title-lg" className="text-slate-900">
|
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")}
|
{t("inventory")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider className="my-3" />
|
<Divider className="my-3" />
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("amount")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="amount">
|
<form.Field name="amount">
|
||||||
@@ -270,13 +308,19 @@ export const ProductQuickView = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
className="rounded-2xl bg-white/80"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("price")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="price">
|
<form.Field name="price">
|
||||||
@@ -288,16 +332,25 @@ export const ProductQuickView = () => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{Cookies.get("currency")}
|
{Cookies.get("currency")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("storage-place")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="storage_location_uuid">
|
<form.Field name="storage_location_uuid">
|
||||||
@@ -309,7 +362,10 @@ export const ProductQuickView = () => {
|
|||||||
}
|
}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{storages?.map((storage) => (
|
{storages?.map((storage) => (
|
||||||
<Option key={storage.uuid} value={storage.uuid}>
|
<Option key={storage.uuid} value={storage.uuid}>
|
||||||
@@ -325,41 +381,33 @@ export const ProductQuickView = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<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")}
|
{t("product-details")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
size="lg"
|
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")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert
|
||||||
color={alert.type}
|
type={alert.type}
|
||||||
variant="soft"
|
header={alert.header}
|
||||||
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
text={alert.text}
|
||||||
>
|
/>
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<Alert
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
className="rounded-2xl border border-emerald-200/70 bg-emerald-50/80 text-emerald-700 shadow-[0_14px_30px_rgba(16,185,129,0.18)]"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<Typography level="body-sm" className="text-emerald-700">
|
|
||||||
{t("success")}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import {
|
import { Button, Chip, CircularProgress, Divider, Input, Sheet, Typography, } from "@mui/joy";
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
Typography,
|
|
||||||
Sheet,
|
|
||||||
Chip,
|
|
||||||
Divider,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { SettingsIntf } from "../misc/interfaces";
|
import type { AlertInterface, SettingsIntf } from "../misc/interfaces";
|
||||||
import type { AlertInterface } from "../misc/interfaces";
|
|
||||||
import type { ApiError } from "../utils/api/apiError";
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import { mutateSettings, fetchSettings } from "../utils/api/settings";
|
import { fetchSettings, mutateSettings } from "../utils/api/settings";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ChangePasswordModal } from "../components/modals/ChangePasswordModal";
|
import { ChangePasswordModal } from "../components/modals/ChangePasswordModal";
|
||||||
|
import { MyAlert } from "../components/MyAlert.tsx";
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -83,6 +74,12 @@ export const Settings = () => {
|
|||||||
const { mutate } = useMutation({
|
const { mutate } = useMutation({
|
||||||
mutationFn: (values: SettingsIntf) => mutateSettings(values),
|
mutationFn: (values: SettingsIntf) => mutateSettings(values),
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "success",
|
||||||
|
header: t("success"),
|
||||||
|
text: t("SE001"),
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||||
},
|
},
|
||||||
onError: showError,
|
onError: showError,
|
||||||
@@ -90,14 +87,20 @@ export const Settings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChangePasswordModal isOpen={modal} setOpen={setModal} />
|
<ChangePasswordModal alert={setAlert} isOpen={modal} setOpen={setModal} />
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Typography level="h2" className="text-slate-900">
|
<Typography
|
||||||
|
level="h2"
|
||||||
|
sx={{ color: "var(--joy-palette-text-primary)" }}
|
||||||
|
>
|
||||||
{t("settings")}
|
{t("settings")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level="body-lg" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-lg"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{t("settings-sub")}
|
{t("settings-sub")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,17 +114,18 @@ export const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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
|
||||||
{alert.isAlert && (
|
className="mt-6 rounded-3xl p-6 backdrop-blur"
|
||||||
<Alert
|
sx={{
|
||||||
variant="soft"
|
border: "1px solid",
|
||||||
color={alert.type}
|
borderColor: "divider",
|
||||||
className="mb-6 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
bgcolor: "background.surface",
|
||||||
|
boxShadow:
|
||||||
|
"0 24px 60px rgba(var(--joy-palette-primary-900, 12 38 78) / 0.12)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{alert.header}
|
{alert.isAlert && (
|
||||||
<br />
|
<MyAlert type={alert.type} header={alert.header} text={alert.text} />
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
{settingsPending ? (
|
{settingsPending ? (
|
||||||
<div className="flex items-center justify-center py-16">
|
<div className="flex items-center justify-center py-16">
|
||||||
@@ -135,13 +139,19 @@ export const Settings = () => {
|
|||||||
form.handleSubmit();
|
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-5">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("app-name")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{t("app-name-sub")}
|
{t("app-name-sub")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="app-name">
|
<form.Field name="app-name">
|
||||||
@@ -152,16 +162,26 @@ export const Settings = () => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="Stockhome"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("currency")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{t("currency-sub")}
|
{t("currency-sub")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="currency">
|
<form.Field name="currency">
|
||||||
@@ -172,37 +192,63 @@ export const Settings = () => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="EUR"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
</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)]">
|
<div
|
||||||
<Typography level="title-lg" className="text-slate-900">
|
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")}
|
{t("quick-tips")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider className="my-3" />
|
<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-1")}</p>
|
||||||
<p>{t("quick-tips-2")}</p>
|
<p>{t("quick-tips-2")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
color="primary"
|
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")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setModal(true)}
|
onClick={() => setModal(true)}
|
||||||
size="lg"
|
size="lg"
|
||||||
color="warning"
|
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("change-password")}
|
{t("change-password")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getStorages } from "../utils/api/storages";
|
import { getStorages } from "../utils/api/storages";
|
||||||
import {
|
import { Button, CircularProgress, Sheet, Table, Typography } from "@mui/joy";
|
||||||
Sheet,
|
|
||||||
Table,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
Typography,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Storage } from "../misc/interfaces";
|
import type { AlertInterface, Storage } from "../misc/interfaces";
|
||||||
import type { AlertInterface } from "../misc/interfaces";
|
|
||||||
import type { ApiError } from "../utils/api/apiError";
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import { StorageRow } from "../components/StorageRow";
|
import { StorageRow } from "../components/StorageRow";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AddStorageModal } from "../components/modals/AddStorageModal";
|
import { AddStorageModal } from "../components/modals/AddStorageModal";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { MyAlert } from "../components/MyAlert.tsx";
|
||||||
|
|
||||||
export const Storages = () => {
|
export const Storages = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -57,10 +50,16 @@ export const Storages = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="min-w-65 space-y-2">
|
<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")}
|
{t("storages")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level="body-lg" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-lg"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{t("storage-delete-info")}
|
{t("storage-delete-info")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,21 +74,18 @@ export const Storages = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{alert.isAlert && (
|
{alert.isAlert && (
|
||||||
<Alert
|
<MyAlert type={alert.type} header={alert.header} text={alert.text} />
|
||||||
variant="soft"
|
|
||||||
color={alert.type}
|
|
||||||
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
|
||||||
>
|
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="mt-6 flex min-h-0 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} />
|
<AddStorageModal isOpen={modal} setOpen={setModal} />
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -98,7 +94,13 @@ export const Storages = () => {
|
|||||||
</div>
|
</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">
|
<Typography level="body-lg" fontWeight="bold">
|
||||||
{t("storages")}
|
{t("storages")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -109,40 +111,60 @@ export const Storages = () => {
|
|||||||
stripe="odd"
|
stripe="odd"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
hoverRow
|
hoverRow
|
||||||
className="min-w-240 text-slate-700"
|
className="w-full"
|
||||||
sx={{
|
sx={{
|
||||||
|
tableLayout: "fixed",
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
"--TableCell-headBackground":
|
"--TableCell-headBackground":
|
||||||
"var(--joy-palette-background-surface)",
|
"var(--joy-palette-background-level1)",
|
||||||
"& thead": {
|
"& thead": {
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
backgroundColor: "rgb(248 250 252)",
|
backgroundColor: "var(--joy-palette-background-level1)",
|
||||||
},
|
},
|
||||||
"& thead tr": {
|
"& thead tr": {
|
||||||
backgroundColor: "rgb(248 250 252)",
|
backgroundColor: "var(--joy-palette-background-level1)",
|
||||||
},
|
|
||||||
"& thead th:nth-child(2)": {
|
|
||||||
width: "40%",
|
|
||||||
},
|
|
||||||
"& thead th:nth-child(3)": {
|
|
||||||
width: "14%",
|
|
||||||
},
|
},
|
||||||
"& thead th": {
|
"& thead th": {
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
backgroundColor: "rgb(248 250 252)",
|
backgroundColor: "var(--joy-palette-background-level1)",
|
||||||
backgroundImage: "none",
|
backgroundImage: "none",
|
||||||
|
color: "var(--joy-palette-text-secondary)",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(1)": {
|
||||||
|
width: "26%",
|
||||||
|
minWidth: "140px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(2)": {
|
||||||
|
width: "34%",
|
||||||
|
minWidth: "160px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(3)": {
|
||||||
|
width: "16%",
|
||||||
|
minWidth: "110px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(4)": {
|
||||||
|
width: "16%",
|
||||||
|
minWidth: "110px",
|
||||||
|
},
|
||||||
|
"& thead th:nth-child(5)": {
|
||||||
|
width: "8%",
|
||||||
|
minWidth: "80px",
|
||||||
|
},
|
||||||
|
"& tr > *:nth-child(n+3)": { textAlign: "left" },
|
||||||
|
"& tbody tr": {
|
||||||
|
borderTop: "1px solid var(--joy-palette-divider)",
|
||||||
},
|
},
|
||||||
"& tr > *:nth-child(n+4)": { textAlign: "left" },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-slate-600">
|
<tr>
|
||||||
<th className="px-6 py-4">{t("storage-name")}</th>
|
<th className="px-3 py-4">{t("storage-name")}</th>
|
||||||
<th className="px-6 py-4">{t("description")}</th>
|
<th className="px-3 py-4">{t("description")}</th>
|
||||||
<th className="px-6 py-4">{t("created-at")}</th>
|
<th className="px-3 py-4">{t("created-at")}</th>
|
||||||
<th className="px-6 py-4">{t("updated-at")}</th>
|
<th className="px-3 py-4">{t("updated-at")}</th>
|
||||||
<th className="px-6 py-4 text-right"></th>
|
<th className="px-3 py-4 text-right"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -1,32 +1,17 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { getStorages } from "../utils/api/storages.ts";
|
import { getStorages } from "../utils/api/storages.ts";
|
||||||
import {
|
import { Box, Button, Chip, CircularProgress, Divider, Input, Option, Select, Typography, } from "@mui/joy";
|
||||||
CircularProgress,
|
|
||||||
Typography,
|
|
||||||
Select,
|
|
||||||
Option,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
Divider,
|
|
||||||
Box,
|
|
||||||
Alert,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { mutateProduct, getProductDetails } from "../utils/api/products.ts";
|
import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
|
||||||
import { toInputDate } from "../utils/uxFncs";
|
import { toInputDate } from "../utils/uxFncs";
|
||||||
import type {
|
import type { AlertInterface, productDetailsInterface, ProductFormValues, Storage, } from "../misc/interfaces";
|
||||||
ProductFormValues,
|
|
||||||
productDetailsInterface,
|
|
||||||
AlertInterface,
|
|
||||||
Storage,
|
|
||||||
} from "../misc/interfaces";
|
|
||||||
import type { ApiError } from "../utils/api/apiError";
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import QrCodeIcon from "@mui/icons-material/QrCode";
|
import QrCodeIcon from "@mui/icons-material/QrCode";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
|
import { MyAlert } from "../components/MyAlert.tsx";
|
||||||
|
|
||||||
interface ViewProductProps {
|
interface ViewProductProps {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -160,7 +145,10 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("product-details")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,7 +163,16 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
{productDetailsLoading && <CircularProgress size="sm" />}
|
{productDetailsLoading && <CircularProgress size="sm" />}
|
||||||
</div>
|
</div>
|
||||||
{isSuccess && (
|
{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
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -186,7 +183,10 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("product-name")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="name">
|
<form.Field name="name">
|
||||||
@@ -198,13 +198,20 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="description">
|
<form.Field name="description">
|
||||||
@@ -216,14 +223,21 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
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>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("expiry-date")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="expiry_date">
|
<form.Field name="expiry_date">
|
||||||
@@ -235,13 +249,19 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("bottling-date")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="bottling_date">
|
<form.Field name="bottling_date">
|
||||||
@@ -253,7 +273,10 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -261,14 +284,28 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<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)]">
|
<div
|
||||||
<Typography level="title-lg" className="text-slate-900">
|
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")}
|
{t("inventory")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider className="my-3" />
|
<Divider className="my-3" />
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("amount")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="amount">
|
<form.Field name="amount">
|
||||||
@@ -288,13 +325,19 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
className="rounded-2xl bg-white/80"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("price")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="price">
|
<form.Field name="price">
|
||||||
@@ -306,16 +349,25 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
onBlur={field.handleBlur}
|
onBlur={field.handleBlur}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{Cookies.get("currency")}
|
{Cookies.get("currency")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<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")}
|
{t("storage-place")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<form.Field name="storage_location_uuid">
|
<form.Field name="storage_location_uuid">
|
||||||
@@ -327,7 +379,10 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
}
|
}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "var(--joy-palette-background-surface)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{storages?.map((storage) => (
|
{storages?.map((storage) => (
|
||||||
<Option key={storage.uuid} value={storage.uuid}>
|
<Option key={storage.uuid} value={storage.uuid}>
|
||||||
@@ -342,16 +397,27 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div>
|
||||||
<Typography level="body-sm" className="text-slate-500">
|
<div className="flex gap-3 items-center">
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "var(--joy-palette-text-tertiary)" }}
|
||||||
|
>
|
||||||
{t("product-details")}
|
{t("product-details")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<div className="grow"></div>
|
||||||
<Button
|
<Button
|
||||||
startDecorator={<QrCodeIcon />}
|
startDecorator={<QrCodeIcon />}
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
onClick={() => downloadQRcode()}
|
onClick={() => downloadQRcode()}
|
||||||
size="lg"
|
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")}
|
{t("download-qr-code")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -359,34 +425,25 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
size="lg"
|
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")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{alert.isAlert && (
|
|
||||||
<Alert
|
|
||||||
color={alert.type}
|
|
||||||
variant="soft"
|
|
||||||
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
|
||||||
>
|
|
||||||
{alert.header}
|
|
||||||
<br />
|
|
||||||
{alert.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<Alert
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
className="rounded-2xl border border-emerald-200/70 bg-emerald-50/80 text-emerald-700 shadow-[0_14px_30px_rgba(16,185,129,0.18)]"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<Typography level="body-sm" className="text-emerald-700">
|
|
||||||
{t("success")}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
|
||||||
|
{alert.isAlert && (
|
||||||
|
<MyAlert
|
||||||
|
type={alert.type}
|
||||||
|
header={alert.header}
|
||||||
|
text={alert.text}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
+125
-97
@@ -12,12 +12,13 @@ import { Route as rootRouteImport } from './routes/__root'
|
|||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout'
|
import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout'
|
||||||
import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product'
|
import { Route as AppHiddenLayoutAuthedRouteImport } from './routes/app/_hiddenLayout/_authed'
|
||||||
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 AppHiddenLayoutQuickViewProductRouteImport } from './routes/app/_hiddenLayout/quick-view/product'
|
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({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
@@ -34,62 +35,67 @@ const AppHiddenLayoutRoute = AppHiddenLayoutRouteImport.update({
|
|||||||
path: '/app',
|
path: '/app',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AppHiddenLayoutViewProductRoute =
|
const AppHiddenLayoutAuthedRoute = AppHiddenLayoutAuthedRouteImport.update({
|
||||||
AppHiddenLayoutViewProductRouteImport.update({
|
id: '/_authed',
|
||||||
id: '/view-product',
|
|
||||||
path: '/view-product',
|
|
||||||
getParentRoute: () => AppHiddenLayoutRoute,
|
|
||||||
} as any)
|
|
||||||
const AppHiddenLayoutStoragesRoute = AppHiddenLayoutStoragesRouteImport.update({
|
|
||||||
id: '/storages',
|
|
||||||
path: '/storages',
|
|
||||||
getParentRoute: () => AppHiddenLayoutRoute,
|
getParentRoute: () => AppHiddenLayoutRoute,
|
||||||
} as any)
|
} 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 =
|
const AppHiddenLayoutQuickViewProductRoute =
|
||||||
AppHiddenLayoutQuickViewProductRouteImport.update({
|
AppHiddenLayoutQuickViewProductRouteImport.update({
|
||||||
id: '/quick-view/product',
|
id: '/quick-view/product',
|
||||||
path: '/quick-view/product',
|
path: '/quick-view/product',
|
||||||
getParentRoute: () => AppHiddenLayoutRoute,
|
getParentRoute: () => AppHiddenLayoutRoute,
|
||||||
} as any)
|
} 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 {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/app': typeof AppHiddenLayoutRouteWithChildren
|
'/app': typeof AppHiddenLayoutAuthedRouteWithChildren
|
||||||
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
|
'/app/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
|
||||||
'/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute
|
'/app/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
|
||||||
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
|
'/app/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
|
||||||
'/app/storages': typeof AppHiddenLayoutStoragesRoute
|
'/app/storages': typeof AppHiddenLayoutAuthedStoragesRoute
|
||||||
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
|
'/app/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
|
||||||
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
|
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/app': typeof AppHiddenLayoutRouteWithChildren
|
'/app': typeof AppHiddenLayoutAuthedRouteWithChildren
|
||||||
'/app/add-product': typeof AppHiddenLayoutAddProductRoute
|
'/app/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
|
||||||
'/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute
|
'/app/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
|
||||||
'/app/inventory': typeof AppHiddenLayoutInventoryRoute
|
'/app/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
|
||||||
'/app/storages': typeof AppHiddenLayoutStoragesRoute
|
'/app/storages': typeof AppHiddenLayoutAuthedStoragesRoute
|
||||||
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
|
'/app/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
|
||||||
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
|
'/app/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -97,11 +103,12 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren
|
'/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren
|
||||||
'/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute
|
'/app/_hiddenLayout/_authed': typeof AppHiddenLayoutAuthedRouteWithChildren
|
||||||
'/app/_hiddenLayout/app-settings': typeof AppHiddenLayoutAppSettingsRoute
|
'/app/_hiddenLayout/_authed/add-product': typeof AppHiddenLayoutAuthedAddProductRoute
|
||||||
'/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute
|
'/app/_hiddenLayout/_authed/app-settings': typeof AppHiddenLayoutAuthedAppSettingsRoute
|
||||||
'/app/_hiddenLayout/storages': typeof AppHiddenLayoutStoragesRoute
|
'/app/_hiddenLayout/_authed/inventory': typeof AppHiddenLayoutAuthedInventoryRoute
|
||||||
'/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute
|
'/app/_hiddenLayout/_authed/storages': typeof AppHiddenLayoutAuthedStoragesRoute
|
||||||
|
'/app/_hiddenLayout/_authed/view-product': typeof AppHiddenLayoutAuthedViewProductRoute
|
||||||
'/app/_hiddenLayout/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
|
'/app/_hiddenLayout/quick-view/product': typeof AppHiddenLayoutQuickViewProductRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@@ -132,11 +139,12 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/app/_hiddenLayout'
|
| '/app/_hiddenLayout'
|
||||||
| '/app/_hiddenLayout/add-product'
|
| '/app/_hiddenLayout/_authed'
|
||||||
| '/app/_hiddenLayout/app-settings'
|
| '/app/_hiddenLayout/_authed/add-product'
|
||||||
| '/app/_hiddenLayout/inventory'
|
| '/app/_hiddenLayout/_authed/app-settings'
|
||||||
| '/app/_hiddenLayout/storages'
|
| '/app/_hiddenLayout/_authed/inventory'
|
||||||
| '/app/_hiddenLayout/view-product'
|
| '/app/_hiddenLayout/_authed/storages'
|
||||||
|
| '/app/_hiddenLayout/_authed/view-product'
|
||||||
| '/app/_hiddenLayout/quick-view/product'
|
| '/app/_hiddenLayout/quick-view/product'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
@@ -169,39 +177,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppHiddenLayoutRouteImport
|
preLoaderRoute: typeof AppHiddenLayoutRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/app/_hiddenLayout/view-product': {
|
'/app/_hiddenLayout/_authed': {
|
||||||
id: '/app/_hiddenLayout/view-product'
|
id: '/app/_hiddenLayout/_authed'
|
||||||
path: '/view-product'
|
path: ''
|
||||||
fullPath: '/app/view-product'
|
fullPath: '/app'
|
||||||
preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport
|
preLoaderRoute: typeof AppHiddenLayoutAuthedRouteImport
|
||||||
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
|
|
||||||
parentRoute: typeof AppHiddenLayoutRoute
|
parentRoute: typeof AppHiddenLayoutRoute
|
||||||
}
|
}
|
||||||
'/app/_hiddenLayout/quick-view/product': {
|
'/app/_hiddenLayout/quick-view/product': {
|
||||||
@@ -211,24 +191,72 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppHiddenLayoutQuickViewProductRouteImport
|
preLoaderRoute: typeof AppHiddenLayoutQuickViewProductRouteImport
|
||||||
parentRoute: typeof AppHiddenLayoutRoute
|
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 {
|
interface AppHiddenLayoutRouteChildren {
|
||||||
AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute
|
AppHiddenLayoutAuthedRoute: typeof AppHiddenLayoutAuthedRouteWithChildren
|
||||||
AppHiddenLayoutAppSettingsRoute: typeof AppHiddenLayoutAppSettingsRoute
|
|
||||||
AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute
|
|
||||||
AppHiddenLayoutStoragesRoute: typeof AppHiddenLayoutStoragesRoute
|
|
||||||
AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute
|
|
||||||
AppHiddenLayoutQuickViewProductRoute: typeof AppHiddenLayoutQuickViewProductRoute
|
AppHiddenLayoutQuickViewProductRoute: typeof AppHiddenLayoutQuickViewProductRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = {
|
const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = {
|
||||||
AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute,
|
AppHiddenLayoutAuthedRoute: AppHiddenLayoutAuthedRouteWithChildren,
|
||||||
AppHiddenLayoutAppSettingsRoute: AppHiddenLayoutAppSettingsRoute,
|
|
||||||
AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute,
|
|
||||||
AppHiddenLayoutStoragesRoute: AppHiddenLayoutStoragesRoute,
|
|
||||||
AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute,
|
|
||||||
AppHiddenLayoutQuickViewProductRoute: AppHiddenLayoutQuickViewProductRoute,
|
AppHiddenLayoutQuickViewProductRoute: AppHiddenLayoutQuickViewProductRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||||
import { Sidebar } from "../../components/Sidebar";
|
import { Sidebar } from "../../components/Sidebar";
|
||||||
|
|
||||||
export const Route = createFileRoute("/app/_hiddenLayout")({
|
export const Route = createFileRoute("/app/_hiddenLayout")({
|
||||||
@@ -7,7 +7,10 @@ export const Route = createFileRoute("/app/_hiddenLayout")({
|
|||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
return (
|
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 />
|
<Sidebar />
|
||||||
<main className="flex-1 px-4 py-5 sm:px-6 lg:px-8 lg:py-6">
|
<main className="flex-1 px-4 py-5 sm:px-6 lg:px-8 lg:py-6">
|
||||||
<Outlet />
|
<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} />;
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ export const Route = createFileRoute("/")({
|
|||||||
if (!(await isAuthenticated())) {
|
if (!(await isAuthenticated())) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/login",
|
to: "/login",
|
||||||
|
search: { loggedOut: true },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { LoginCard } from "../components/LoginCard";
|
import { LoginCard } from "../components/LoginCard";
|
||||||
|
import { checkLogin } from "../utils/api/auth.ts";
|
||||||
|
|
||||||
export const Route = createFileRoute("/login")({
|
export const Route = createFileRoute("/login")({
|
||||||
|
beforeLoad: async () => {
|
||||||
|
await checkLogin();
|
||||||
|
},
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,8 +4,9 @@ import type { TFunction } from "i18next";
|
|||||||
import { fetchSettings } from "./settings";
|
import { fetchSettings } from "./settings";
|
||||||
import type { ChangePasswordIntf } from "../../misc/interfaces";
|
import type { ChangePasswordIntf } from "../../misc/interfaces";
|
||||||
import { createApiError } from "./apiError";
|
import { createApiError } from "./apiError";
|
||||||
|
import { redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
export async function isAuthenticated() {
|
export const isAuthenticated = async () => {
|
||||||
if (Cookies.get("token")) {
|
if (Cookies.get("token")) {
|
||||||
const result = await fetch(`${API_BASE}/users/verify-token`, {
|
const result = await fetch(`${API_BASE}/users/verify-token`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -23,13 +24,13 @@ export async function isAuthenticated() {
|
|||||||
|
|
||||||
Cookies.remove("token");
|
Cookies.remove("token");
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function signInUser(
|
export const signInUser = async (
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
) {
|
) => {
|
||||||
const result = await fetch(`${API_BASE}/users/login`, {
|
const result = await fetch(`${API_BASE}/users/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -54,12 +55,7 @@ export async function signInUser(
|
|||||||
|
|
||||||
Cookies.remove("token");
|
Cookies.remove("token");
|
||||||
throw createApiError(response.code, t(response.code || "unknown-error"));
|
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) => {
|
export const mutatePassword = async (payload: ChangePasswordIntf) => {
|
||||||
const result = await fetch(`${API_BASE}/users/change-password`, {
|
const result = await fetch(`${API_BASE}/users/change-password`, {
|
||||||
@@ -77,9 +73,26 @@ export const mutatePassword = async (payload: ChangePasswordIntf) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "su005") {
|
if (response.code === "SU005") {
|
||||||
return { code: response.code };
|
return { code: response.code };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createApiError(response.code, "Change password failed");
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE } from "../../config/api.config";
|
import {API_BASE} from "../../config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import type { ProductFormValues } from "../../misc/interfaces";
|
import type {ProductFormValues} from "../../misc/interfaces";
|
||||||
import { createApiError } from "./apiError";
|
import {createApiError} from "./apiError";
|
||||||
|
|
||||||
export const getProducts = async () => {
|
export const getProducts = async () => {
|
||||||
const result = await fetch(`${API_BASE}/products/all-products`, {
|
const result = await fetch(`${API_BASE}/products/all-products`, {
|
||||||
@@ -13,7 +13,7 @@ export const getProducts = async () => {
|
|||||||
});
|
});
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "sp002") {
|
if (response.code === "SP002") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export const getProductDetails = async (uuid: string) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "sp003") {
|
if (response.code === "SP003") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export const mutateProduct = async (
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "sp005") {
|
if (response.code === "SP005") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ export const deleteSelectedProducts = async (uuids: string[]) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "sp006") {
|
if (response.code === "SP006") {
|
||||||
return { success: true };
|
return {success: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createApiError(response.code, "Delete products failed");
|
throw createApiError(response.code, "Delete products failed");
|
||||||
@@ -113,7 +113,7 @@ export const createProduct = async (values: ProductFormValues) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "sp001") {
|
if (response.code === "SP001") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE } from "../../config/api.config";
|
import {API_BASE} from "../../config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import type { SettingsIntf } from "../../misc/interfaces";
|
import type {SettingsIntf} from "../../misc/interfaces";
|
||||||
import { createApiError } from "./apiError";
|
import {createApiError} from "./apiError";
|
||||||
|
|
||||||
export const fetchSettings = async () => {
|
export const fetchSettings = async () => {
|
||||||
const result = await fetch(`${API_BASE}/users/settings`, {
|
const result = await fetch(`${API_BASE}/users/settings`, {
|
||||||
@@ -15,8 +15,8 @@ export const fetchSettings = async () => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "su004") {
|
if (response.code === "SU004") {
|
||||||
return { success: true, data: response.data, code: response.code };
|
return {success: true, data: response.data, code: response.code};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createApiError(response.code, "Fetch settings failed");
|
throw createApiError(response.code, "Fetch settings failed");
|
||||||
@@ -35,8 +35,8 @@ export const mutateSettings = async (payload: SettingsIntf) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "su003") {
|
if (response.code === "SU003") {
|
||||||
return { success: true, code: response.code };
|
return {success: true, code: response.code};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createApiError(response.code, "Update settings failed");
|
throw createApiError(response.code, "Update settings failed");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { API_BASE } from "../../config/api.config";
|
import {API_BASE} from "../../config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import type { NewStorage, Storage } from "../../misc/interfaces";
|
import type {NewStorage, Storage} from "../../misc/interfaces";
|
||||||
import { createApiError } from "./apiError";
|
import {createApiError} from "./apiError";
|
||||||
|
|
||||||
export const getStorages = async () => {
|
export const getStorages = async () => {
|
||||||
const result = await fetch(`${API_BASE}/storage/all-storages`, {
|
const result = await fetch(`${API_BASE}/storage/all-storages`, {
|
||||||
@@ -14,7 +14,7 @@ export const getStorages = async () => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ss001") {
|
if (response.code === "SS001") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export const mutateNewStorage = async (values: NewStorage) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ss002") {
|
if (response.code === "SS002") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ export const deleteStorage = async (uuid: string) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ss004") {
|
if (response.code === "SS004") {
|
||||||
return { success: true, code: response.code };
|
return {success: true, code: response.code};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createApiError(response.code, "Delete storage failed");
|
throw createApiError(response.code, "Delete storage failed");
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"eu001": "Falscher Benutzername oder Passwort!",
|
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"product-name": "Produktname",
|
"product-name": "Produktname",
|
||||||
"price": "Preis",
|
"price": "Preis",
|
||||||
@@ -57,5 +56,35 @@
|
|||||||
"change": "Ändern",
|
"change": "Ändern",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
"submit": "Absenden"
|
"submit": "Absenden",
|
||||||
|
"logout-success-text": "Logout erfolgreich!",
|
||||||
|
"not-found-header": "404 - Nicht gefunden",
|
||||||
|
"not-found-body": "Die Seite nach der du gesucht hast, gibt es nicht.",
|
||||||
|
"SU005": "Das Passwort wurde erfolgreich geändert!",
|
||||||
|
"SE001": "Die Einstellungen wurden erfolgreich geändert!",
|
||||||
|
"EG001": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||||
|
"EU001": "Falscher Benutzername oder Passwort!",
|
||||||
|
"EU002": "Ihr Konto wurde deaktiviert. Bitte wenden Sie sich an einen Administrator.",
|
||||||
|
"EU003": "Bitte geben Sie Benutzername und Passwort ein.",
|
||||||
|
"EU004": "Einige erforderliche Angaben fehlen. Bitte überprüfen Sie Ihre Eingabe.",
|
||||||
|
"EU005": "Anmeldung aufgrund eines technischen Problems fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||||
|
"EU006": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||||
|
"EU007": "Passwort konnte nicht geändert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EU008": "Ihre Einstellungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EU009": "Ihre Einstellungen konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EU010": "Sie müssen Ihr neues Passwort zweimal korrekt eingeben.",
|
||||||
|
"ES001": "Bitte füllen Sie alle erforderlichen Felder für den Lagerort aus.",
|
||||||
|
"ES002": "Einige erforderliche Lagerinformationen fehlen. Bitte überprüfen Sie Ihre Eingabe.",
|
||||||
|
"ES003": "Der Lagerort konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"ES004": "Der Lagerort konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"ES005": "Keine Lagerorte gefunden.",
|
||||||
|
"ES006": "Der Lagerort konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EP001": "Das Produkt konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EP002": "Keine Produkte gefunden.",
|
||||||
|
"EP003": "Produkt nicht gefunden.",
|
||||||
|
"EP004": "Die Produktmenge konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EP005": "Das Produkt konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EP006": "Das Produkt konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"EP007": "Bitte geben Sie mindestens einen Produktnamen ein.",
|
||||||
|
"EP008": "Einige erforderliche Produktinformationen fehlen. Bitte überprüfen Sie Ihre Eingabe."
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"eu001": "Wrong username or password!",
|
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"product-name": "Product name",
|
"product-name": "Product name",
|
||||||
"price": "Price",
|
"price": "Price",
|
||||||
@@ -57,5 +56,35 @@
|
|||||||
"change": "Change",
|
"change": "Change",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"submit": "Submit"
|
"submit": "Submit",
|
||||||
|
"logout-success-text": "Logout successful!",
|
||||||
|
"not-found-header": "404 - Not found",
|
||||||
|
"not-found-body": "The page you were looking for, does not exist.",
|
||||||
|
"SU005": "Your password is updated successfully!",
|
||||||
|
"SE001": "The settings are updated successfully!",
|
||||||
|
"EG001": "An unexpected error occurred. Please try again later.",
|
||||||
|
"EU001": "Wrong username or password!",
|
||||||
|
"EU002": "Your account has been deactivated. Please contact an administrator.",
|
||||||
|
"EU003": "Please enter both username and password.",
|
||||||
|
"EU004": "Some required information is missing. Please check your input.",
|
||||||
|
"EU005": "Login failed due to a technical issue. Please try again.",
|
||||||
|
"EU006": "Login failed. Please try again.",
|
||||||
|
"EU007": "Failed to change your password. Please try again.",
|
||||||
|
"EU008": "Your settings could not be saved. Please try again.",
|
||||||
|
"EU009": "Your settings could not be loaded. Please try again.",
|
||||||
|
"EU010": "You must enter your new password twice correctly.",
|
||||||
|
"ES001": "Please fill in all required fields for the storage location.",
|
||||||
|
"ES002": "Some required storage information is missing. Please check your input.",
|
||||||
|
"ES003": "The storage location could not be updated. Please try again.",
|
||||||
|
"ES004": "The storage location could not be deleted. Please try again.",
|
||||||
|
"ES005": "No storage locations found.",
|
||||||
|
"ES006": "The storage location could not be created. Please try again.",
|
||||||
|
"EP001": "The product could not be created. Please try again.",
|
||||||
|
"EP002": "No products found.",
|
||||||
|
"EP003": "Product not found.",
|
||||||
|
"EP004": "The product amount could not be updated. Please try again.",
|
||||||
|
"EP005": "The product could not be updated. Please try again.",
|
||||||
|
"EP006": "The product could not be deleted. Please try again.",
|
||||||
|
"EP007": "Please enter at least a product name.",
|
||||||
|
"EP008": "Some required product information is missing. Please check your input."
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,10 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-4
@@ -1,7 +1,26 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"compilerOptions": {
|
||||||
"references": [
|
"target": "ES2022",
|
||||||
{ "path": "./tsconfig.app.json" },
|
"useDefineForClassFields": true,
|
||||||
{ "path": "./tsconfig.node.json" }
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,14 @@ import { defineConfig } from "vite";
|
|||||||
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
||||||
import babel from "@rolldown/plugin-babel";
|
import babel from "@rolldown/plugin-babel";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
|
import { tanstackRouter } from "@tanstack/router-vite-plugin";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
TanStackRouterVite(),
|
tanstackRouter(),
|
||||||
babel({ presets: [reactCompilerPreset()] }),
|
babel({ presets: [reactCompilerPreset()] }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
+2920
-607
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "stockhome",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"backend",
|
||||||
|
"frontend",
|
||||||
|
"shared"
|
||||||
|
],
|
||||||
|
"version": "0.1.0-dev",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"build:shared": "npm run build --workspace=@stockhome/shared",
|
||||||
|
"build:backend": "npm run build --workspace=backend",
|
||||||
|
"build:frontend": "npm run build --workspace=frontend",
|
||||||
|
"dev:frontend": "npm run dev --workspace=frontend",
|
||||||
|
"dev:backend": "npm run build:shared && npm run dev --workspace=backend",
|
||||||
|
"dev:docker": "docker compose -f docker-compose.dev.yml up -d --wait",
|
||||||
|
"dev": "npm run dev:docker && concurrently --kill-others \"npm:dev:frontend\" \"npm:dev:backend\"",
|
||||||
|
"dev:stop": "docker compose -f docker-compose.dev.yml down",
|
||||||
|
"buildProd": "npm run build:shared && npm run build:backend && npm run build:frontend"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.the1s.de/theis.gaedigk/stockhome.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"overrides": {
|
||||||
|
"@babel/core": "^7.26.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"concurrently": "^10.0.3",
|
||||||
|
"globals": "^17.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+948
@@ -0,0 +1,948 @@
|
|||||||
|
{
|
||||||
|
"name": "shared",
|
||||||
|
"version": "0.0.0-dev",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "shared",
|
||||||
|
"version": "0.0.0-dev",
|
||||||
|
"devDependencies": {
|
||||||
|
"barrelsby": "^2.8.1",
|
||||||
|
"vite-plugin-dts": "^5.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
|
"version": "0.3.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/remapping": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/yargs": {
|
||||||
|
"version": "17.0.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
|
"integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/yargs-parser": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/yargs-parser": {
|
||||||
|
"version": "21.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
|
||||||
|
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@volar/language-core": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/source-map": "2.4.28"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@volar/source-map": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@volar/typescript": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.28",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"vscode-uri": "^3.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||||
|
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/barrelsby": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/barrelsby/-/barrelsby-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-barN2MVKqUVwmjRy3JLSMYufrBDcdWUc2pjlR0V9P8S3aMvvJ4StFz1GJMzEi5GBoQlnBIWOcCxBDzI2xfaaGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/yargs": "^17.0.10",
|
||||||
|
"signale": "^1.4.0",
|
||||||
|
"yargs": "^17.4.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"barrelsby": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chalk": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^3.2.1",
|
||||||
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
"supports-color": "^5.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compare-versions": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/confbox": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/error-ex": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/exsolve": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/figures": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-arrayish": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-parse-better-errors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/kolorist": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/load-json-file": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"parse-json": "^4.0.0",
|
||||||
|
"pify": "^3.0.0",
|
||||||
|
"strip-bom": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/local-pkg": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mlly": "^1.7.4",
|
||||||
|
"pkg-types": "^2.3.0",
|
||||||
|
"quansync": "^0.2.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^2.0.0",
|
||||||
|
"path-exists": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mlly": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.16.0",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"pkg-types": "^1.3.1",
|
||||||
|
"ufo": "^1.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mlly/node_modules/confbox": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||||
|
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mlly/node_modules/pkg-types": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"confbox": "^0.1.8",
|
||||||
|
"mlly": "^1.7.4",
|
||||||
|
"pathe": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse-json": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"error-ex": "^1.3.1",
|
||||||
|
"json-parse-better-errors": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-browserify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pify": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-conf": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"find-up": "^2.0.0",
|
||||||
|
"load-json-file": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-types": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"confbox": "^0.2.4",
|
||||||
|
"exsolve": "^1.0.8",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quansync": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/sxzz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/signale": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^2.3.2",
|
||||||
|
"figures": "^2.0.0",
|
||||||
|
"pkg-conf": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-bom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ufo": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/unplugin": {
|
||||||
|
"version": "2.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"acorn": "^8.15.0",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"webpack-virtual-modules": "^0.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-dts": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-/GR887wfG4r1cWyt1UZsLRuMIjsmEbGkS9yJrz+0dsToHAYUD5CTyP3JMGVLv25j9K0mJcwAVvZno/aTuSUvNg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.1.4",
|
||||||
|
"@volar/typescript": "^2.4.26",
|
||||||
|
"compare-versions": "^6.1.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"local-pkg": "^1.1.1",
|
||||||
|
"magic-string": "^0.30.17",
|
||||||
|
"unplugin": "^2.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@microsoft/api-extractor": ">=7",
|
||||||
|
"@rspack/core": "^1",
|
||||||
|
"@vue/language-core": "^3.1.5",
|
||||||
|
"esbuild": "*",
|
||||||
|
"rolldown": "*",
|
||||||
|
"rollup": ">=3",
|
||||||
|
"typescript": ">=4",
|
||||||
|
"vite": ">=3",
|
||||||
|
"webpack": "^4 || ^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@microsoft/api-extractor": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@rspack/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vue/language-core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rolldown": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-dts": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-gIth6NdCEHWPiiRMCK3N6C8WjvdsrtEQrmsiG8h6Ov+lFP+b07Y+wcs9H0H7n146l0PDTYK4cQN1vgeG1pMdRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"unplugin-dts": "1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@microsoft/api-extractor": ">=7",
|
||||||
|
"rollup": ">=3",
|
||||||
|
"vite": ">=3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@microsoft/api-extractor": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/webpack-virtual-modules": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
|
||||||
|
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@stockhome/shared",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "barrelsby --directory src --delete --single",
|
||||||
|
"build:esm": "tsc -p tsconfig.esm.json && node scripts/postbuild-esm.cjs",
|
||||||
|
"build:cjs": "tsc -p tsconfig.cjs.json && find dist/cjs -name \"*.js\" -type f -exec sh -c 'mv \"$1\" \"${1%.js}.cjs\"' _ {} \\; && node scripts/postbuild.cjs",
|
||||||
|
"build": "rm -rf dist && npm run prebuild && npm run build:esm && npm run build:cjs",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"import": "./dist/esm/index.js",
|
||||||
|
"require": "./dist/cjs/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"devDependencies": {
|
||||||
|
"barrelsby": "^2.8.1",
|
||||||
|
"typescript": "~6.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Recursively process all .js files in esm directory
|
||||||
|
function processDirectory(dir) {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
processDirectory(filePath);
|
||||||
|
} else if (file.endsWith('.js')) {
|
||||||
|
// Read the file
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Replace import/export statements to add .js extension
|
||||||
|
// Replace from "./path" with from "./path.js" (for relative imports only)
|
||||||
|
content = content.replace(/from ["'](\.[^"']*?)(?<!\.js)["']/g, 'from "$1.js"');
|
||||||
|
|
||||||
|
// Write back
|
||||||
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
console.log(`✓ Updated ${filePath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processDirectory('./dist/esm');
|
||||||
|
console.log('✓ ESM post-build processing complete');
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Recursively process all .cjs files
|
||||||
|
function processDirectory(dir) {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
processDirectory(filePath);
|
||||||
|
} else if (file.endsWith('.cjs')) {
|
||||||
|
// Read the file
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Replace require statements to add .cjs extension
|
||||||
|
content = content.replace(/require\("([^"]*?)(?<!\.cjs)"\)/g, 'require("$1.cjs")');
|
||||||
|
|
||||||
|
// Write back
|
||||||
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
console.log(`✓ Updated ${filePath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processDirectory('./dist/cjs');
|
||||||
|
console.log('✓ Post-build processing complete');
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
export const GENERAL_ERROR_CODE = {
|
||||||
|
UNEXPECTED_SERVER_ERROR: ["EG001", "Unexpected server error"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const USER_ERROR_CODE = {
|
||||||
|
WRONG_USERNAME_PASSWORD: ["EU001", "Username or password is wrong"],
|
||||||
|
USER_IS_DEACTIVATED: ["EU002", "User is deactivated"],
|
||||||
|
INVALID_REQUEST_BODY: [
|
||||||
|
"EU003",
|
||||||
|
"Invalid request body. Username and password are required.",
|
||||||
|
],
|
||||||
|
INVALID_PARAMETERS: ["EU004", "Invalid or missing parameters."],
|
||||||
|
TOKEN_GENERATION_FAILED: ["EU005", "Failed to generate user token."],
|
||||||
|
LOGIN_FAILED: ["EU006", "Failed to log in user."],
|
||||||
|
PASSWORD_CHANGE_FAILED: ["EU007", "Failed to change password."],
|
||||||
|
SETTINGS_NOT_UPDATED: ["EU008", "Settings could not be updated."],
|
||||||
|
SETTINGS_NOT_FOUND: ["EU009", "Settings could not be retrieved."],
|
||||||
|
PASSWORDS_NOT_MATCHED: ["EU010", "The two passwords are not matching."],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const STORAGE_ERROR_CODE = {
|
||||||
|
INVALID_REQUEST_BODY: ["ES001", "Invalid request body"],
|
||||||
|
INVALID_PARAMETERS: [
|
||||||
|
"ES002",
|
||||||
|
"Parameter storageUUID or/and parameter values are/is missing.",
|
||||||
|
],
|
||||||
|
STORAGE_NOT_UPDATED: ["ES003", "Storage is not updated."],
|
||||||
|
STORAGE_NOT_DELETED: ["ES004", "Storage is not deleted."],
|
||||||
|
NO_STORAGE_LOCATIONS_FOUND: ["ES005", "No storage locations found"],
|
||||||
|
STORAGE_NOT_CREATED: ["ES006", "Storage is not created."],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PRODUCT_ERROR_CODE = {
|
||||||
|
PRODUCT_NOT_CREATED: ["EP001", "Error while creating product"],
|
||||||
|
NO_PRODUCTS_FOUND: ["EP002", "No products found"],
|
||||||
|
PRODUCT_NOT_FOUND: ["EP003", "Product not found"],
|
||||||
|
PRODUCT_AMOUNT_NOT_UPDATED: ["EP004", "Error while updating product amount"],
|
||||||
|
PRODUCT_NOT_UPDATED: ["EP005", "Error while updating product"],
|
||||||
|
PRODUCT_NOT_DELETED: ["EP006", "Error while deleting product"],
|
||||||
|
INVALID_REQUEST_BODY: ["EP007", "Invalid request body. Name is required."],
|
||||||
|
INVALID_PARAMETERS: ["EP008", "Invalid or missing parameters."],
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"target": "ES2022",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/cjs",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist/esm",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationDir": "./dist/types",
|
||||||
|
"declarationMap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist/refs",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./shared"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./backend"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user