basic AI front epta
This commit is contained in:
@@ -106,38 +106,48 @@ pub async fn create_app(db: DatabaseConnection) -> Result<Router, DbErr> {
|
|||||||
let backend = auth::AuthBackend { db: db.clone() };
|
let backend = auth::AuthBackend { db: db.clone() };
|
||||||
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
||||||
|
|
||||||
let protected_routes = Router::new()
|
let admin_family_routes = Router::new()
|
||||||
.route("/families", post(routes::family::create_family))
|
.route("/families", post(routes::family::create_family))
|
||||||
.route_layer(axum_middleware::from_fn(middleware::require_admin));
|
.route("/families/:id", delete(routes::family::delete_family))
|
||||||
|
.layer(auth_layer.clone())
|
||||||
|
.route_layer(axum_middleware::from_fn(middleware::require_admin))
|
||||||
|
.with_state(db.clone());
|
||||||
|
|
||||||
let api_routes = Router::new()
|
let auth_routes = Router::new()
|
||||||
.route("/login", post(routes::auth::login))
|
.route("/login", post(routes::auth::login))
|
||||||
.route("/logout", post(routes::auth::logout))
|
.route("/logout", post(routes::auth::logout))
|
||||||
.merge(protected_routes)
|
|
||||||
.route("/families", get(routes::family::get_all_families))
|
|
||||||
.route("/families/{id}", get(routes::family::get_family))
|
|
||||||
.route("/families/{id}", put(routes::family::update_family))
|
|
||||||
.route("/families/{id}", delete(routes::family::delete_family))
|
|
||||||
.route("/families/{family_id}/categories", post(routes::category::create_category))
|
|
||||||
.route("/families/{family_id}/categories", get(routes::category::get_categories_by_family))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}", get(routes::category::get_category))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}", put(routes::category::update_category))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}", delete(routes::category::delete_category))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}/expenses", post(routes::expense::create_expense))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}/expenses", get(routes::expense::get_expenses_by_category))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}/expenses/{expense_id}", get(routes::expense::get_expense))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}/expenses/{expense_id}", put(routes::expense::update_expense))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}/expenses/{expense_id}", delete(routes::expense::delete_expense))
|
|
||||||
.route("/families/{family_id}/categories/{category_id}/remaining", get(routes::expense::get_remaining_limit))
|
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
|
.with_state(db.clone());
|
||||||
|
|
||||||
|
let public_routes = Router::new()
|
||||||
|
.route("/families", get(routes::family::get_all_families))
|
||||||
|
.route("/families/:id", get(routes::family::get_family))
|
||||||
|
.route("/families/:id", put(routes::family::update_family))
|
||||||
|
.route("/families/:family_id/categories", post(routes::category::create_category))
|
||||||
|
.route("/families/:family_id/categories", get(routes::category::get_categories_by_family))
|
||||||
|
.route("/families/:family_id/categories/:category_id", get(routes::category::get_category))
|
||||||
|
.route("/families/:family_id/categories/:category_id", put(routes::category::update_category))
|
||||||
|
.route("/families/:family_id/categories/:category_id", delete(routes::category::delete_category))
|
||||||
|
.route("/families/:family_id/categories/:category_id/expenses", post(routes::expense::create_expense))
|
||||||
|
.route("/families/:family_id/categories/:category_id/expenses", get(routes::expense::get_expenses_by_category))
|
||||||
|
.route("/families/:family_id/categories/:category_id/expenses/:expense_id", get(routes::expense::get_expense))
|
||||||
|
.route("/families/:family_id/categories/:category_id/expenses/:expense_id", put(routes::expense::update_expense))
|
||||||
|
.route("/families/:family_id/categories/:category_id/expenses/:expense_id", delete(routes::expense::delete_expense))
|
||||||
|
.route("/families/:family_id/categories/:category_id/remaining", get(routes::expense::get_remaining_limit))
|
||||||
.with_state(db);
|
.with_state(db);
|
||||||
|
|
||||||
|
let api_routes = Router::new()
|
||||||
|
.merge(admin_family_routes)
|
||||||
|
.merge(auth_routes)
|
||||||
|
.merge(public_routes);
|
||||||
|
|
||||||
let swagger_ui = SwaggerUi::new("/swagger-ui")
|
let swagger_ui = SwaggerUi::new("/swagger-ui")
|
||||||
.url("/api-docs/openapi.json", ApiDoc::openapi());
|
.url("/api-docs/openapi.json", ApiDoc::openapi());
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_origin([
|
.allow_origin([
|
||||||
"http://localhost:3000".parse::<HeaderValue>().unwrap(),
|
"http://localhost:3000".parse::<HeaderValue>().unwrap(),
|
||||||
|
"http://localhost:5173".parse::<HeaderValue>().unwrap(),
|
||||||
"http://localhost:8080".parse::<HeaderValue>().unwrap(),
|
"http://localhost:8080".parse::<HeaderValue>().unwrap(),
|
||||||
])
|
])
|
||||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
|
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
|
||||||
|
|||||||
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
94
frontend/FRONTEND_README.md
Normal file
94
frontend/FRONTEND_README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Family Budget - Frontend
|
||||||
|
|
||||||
|
Фронтенд приложение для управления семейным бюджетом.
|
||||||
|
|
||||||
|
## Технологии
|
||||||
|
|
||||||
|
- React 19
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- React Router DOM
|
||||||
|
- Zustand (state management)
|
||||||
|
- Axios (HTTP client)
|
||||||
|
- Tailwind CSS
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── api/ # API клиент для общения с бэкендом
|
||||||
|
│ ├── pages/ # Страницы приложения
|
||||||
|
│ ├── store/ # Zustand store
|
||||||
|
│ ├── types/ # TypeScript типы
|
||||||
|
│ ├── App.tsx # Главный компонент с роутингом
|
||||||
|
│ └── main.tsx # Точка входа
|
||||||
|
├── .env # Переменные окружения
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
1. Установите зависимости:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Настройте переменные окружения в `.env`:
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Запустите dev сервер:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Соберите production версию:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Страницы
|
||||||
|
|
||||||
|
### Главная страница (`/`)
|
||||||
|
- Выбор семьи из списка
|
||||||
|
- Кнопка для перехода в админ панель
|
||||||
|
|
||||||
|
### Страница семьи (`/family/:familyId`)
|
||||||
|
- Отображение всех категорий семьи
|
||||||
|
- Показ оставшегося баланса для каждой категории
|
||||||
|
- Добавление расходов (вычет из остатка)
|
||||||
|
- Добавление новых категорий
|
||||||
|
- Удаление категорий
|
||||||
|
- Сброс лимита категории
|
||||||
|
|
||||||
|
### Админ панель (`/adminpanel`)
|
||||||
|
- Авторизация администратора
|
||||||
|
- Создание новых семей
|
||||||
|
- Удаление семей
|
||||||
|
- Выход из админки
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Все запросы отправляются на бэкенд через Axios клиент (`src/api/client.ts`):
|
||||||
|
|
||||||
|
- **Auth**: `POST /login`, `POST /logout`
|
||||||
|
- **Families**: `GET /families`, `POST /families`, `DELETE /families/:id`
|
||||||
|
- **Categories**: `GET /families/:id/categories`, `POST /families/:id/categories`, etc.
|
||||||
|
- **Expenses**: `GET /families/:id/categories/:id/expenses`, `POST /families/:id/categories/:id/expenses`, etc.
|
||||||
|
|
||||||
|
## Учетные данные по умолчанию
|
||||||
|
|
||||||
|
Согласно бэкенду, дефолтный админ:
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `2123`
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
Используется Zustand для управления глобальным состоянием:
|
||||||
|
- `isAdmin` - флаг авторизации администратора
|
||||||
|
- `selectedFamily` - выбранная семья
|
||||||
|
- `families` - список всех семей
|
||||||
|
- `categories` - категории текущей семьи
|
||||||
593
frontend/package-lock.json
generated
593
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -32,6 +33,18 @@
|
|||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -969,7 +982,6 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -980,7 +992,6 @@
|
|||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
@@ -991,7 +1002,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -1001,14 +1011,12 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -1330,6 +1338,262 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
|
"enhanced-resolve": "^5.18.3",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"lightningcss": "1.30.2",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"source-map-js": "^1.2.1",
|
||||||
|
"tailwindcss": "4.1.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@napi-rs/wasm-runtime",
|
||||||
|
"@emnapi/core",
|
||||||
|
"@emnapi/runtime",
|
||||||
|
"@tybys/wasm-util",
|
||||||
|
"@emnapi/wasi-threads",
|
||||||
|
"tslib"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1",
|
||||||
|
"@emnapi/wasi-threads": "^1.1.0",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.0",
|
||||||
|
"@tybys/wasm-util": "^0.10.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/postcss": {
|
||||||
|
"version": "4.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
|
||||||
|
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
|
"@tailwindcss/node": "4.1.18",
|
||||||
|
"@tailwindcss/oxide": "4.1.18",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "4.1.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -2065,6 +2329,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -2086,6 +2359,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/enhanced-resolve": {
|
||||||
|
"version": "5.18.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||||
|
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.4",
|
||||||
|
"tapable": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -2629,6 +2915,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -2762,6 +3054,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/jiti": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -2853,6 +3154,255 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-android-arm64": "1.30.2",
|
||||||
|
"lightningcss-darwin-arm64": "1.30.2",
|
||||||
|
"lightningcss-darwin-x64": "1.30.2",
|
||||||
|
"lightningcss-freebsd-x64": "1.30.2",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||||
|
"lightningcss-linux-x64-musl": "1.30.2",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-android-arm64": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -2886,6 +3436,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -2940,7 +3499,6 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3066,7 +3624,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
@@ -3086,7 +3643,6 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3314,7 +3870,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -3347,12 +3902,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tapable": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,18 @@
|
|||||||
import { useState } from 'react'
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import reactLogo from './assets/react.svg'
|
import Home from './pages/Home';
|
||||||
import viteLogo from '/vite.svg'
|
import FamilyView from './pages/FamilyView';
|
||||||
import './App.css'
|
import AdminPanel from './pages/AdminPanel';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BrowserRouter>
|
||||||
<div>
|
<Routes>
|
||||||
<a href="https://vite.dev" target="_blank">
|
<Route path="/" element={<Home />} />
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
<Route path="/family/:familyId" element={<FamilyView />} />
|
||||||
</a>
|
<Route path="/adminpanel" element={<AdminPanel />} />
|
||||||
<a href="https://react.dev" target="_blank">
|
</Routes>
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
</BrowserRouter>
|
||||||
</a>
|
);
|
||||||
</div>
|
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
84
frontend/src/api/client.ts
Normal file
84
frontend/src/api/client.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type {
|
||||||
|
Family,
|
||||||
|
Category,
|
||||||
|
Expense,
|
||||||
|
RemainingLimit,
|
||||||
|
LoginRequest,
|
||||||
|
LoginResponse,
|
||||||
|
CreateFamilyRequest,
|
||||||
|
CreateCategoryRequest,
|
||||||
|
CreateExpenseRequest,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
login: (data: LoginRequest) =>
|
||||||
|
apiClient.post<LoginResponse>('/login', data),
|
||||||
|
|
||||||
|
logout: () =>
|
||||||
|
apiClient.post('/logout'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const familyApi = {
|
||||||
|
getAll: () =>
|
||||||
|
apiClient.get<Family[]>('/families'),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
apiClient.get<Family>(`/families/${id}`),
|
||||||
|
|
||||||
|
create: (data: CreateFamilyRequest) =>
|
||||||
|
apiClient.post<Family>('/families', data),
|
||||||
|
|
||||||
|
update: (id: number, data: { name: string }) =>
|
||||||
|
apiClient.put<Family>(`/families/${id}`, data),
|
||||||
|
|
||||||
|
delete: (id: number) =>
|
||||||
|
apiClient.delete(`/families/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categoryApi = {
|
||||||
|
getAllByFamily: (familyId: number) =>
|
||||||
|
apiClient.get<Category[]>(`/families/${familyId}/categories`),
|
||||||
|
|
||||||
|
getById: (familyId: number, categoryId: number) =>
|
||||||
|
apiClient.get<Category>(`/families/${familyId}/categories/${categoryId}`),
|
||||||
|
|
||||||
|
create: (familyId: number, data: CreateCategoryRequest) =>
|
||||||
|
apiClient.post<Category>(`/families/${familyId}/categories`, data),
|
||||||
|
|
||||||
|
update: (familyId: number, categoryId: number, data: Partial<CreateCategoryRequest>) =>
|
||||||
|
apiClient.put<Category>(`/families/${familyId}/categories/${categoryId}`, data),
|
||||||
|
|
||||||
|
delete: (familyId: number, categoryId: number) =>
|
||||||
|
apiClient.delete(`/families/${familyId}/categories/${categoryId}`),
|
||||||
|
|
||||||
|
resetLimit: (familyId: number, categoryId: number, newLimit: number) =>
|
||||||
|
apiClient.put<Category>(`/families/${familyId}/categories/${categoryId}`, { limit_amount: newLimit }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expenseApi = {
|
||||||
|
getAllByCategory: (familyId: number, categoryId: number) =>
|
||||||
|
apiClient.get<Expense[]>(`/families/${familyId}/categories/${categoryId}/expenses`),
|
||||||
|
|
||||||
|
getById: (familyId: number, categoryId: number, expenseId: number) =>
|
||||||
|
apiClient.get<Expense>(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`),
|
||||||
|
|
||||||
|
create: (familyId: number, categoryId: number, data: CreateExpenseRequest) =>
|
||||||
|
apiClient.post<Expense>(`/families/${familyId}/categories/${categoryId}/expenses`, data),
|
||||||
|
|
||||||
|
update: (familyId: number, categoryId: number, expenseId: number, data: Partial<CreateExpenseRequest>) =>
|
||||||
|
apiClient.put<Expense>(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`, data),
|
||||||
|
|
||||||
|
delete: (familyId: number, categoryId: number, expenseId: number) =>
|
||||||
|
apiClient.delete(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`),
|
||||||
|
|
||||||
|
getRemainingLimit: (familyId: number, categoryId: number) =>
|
||||||
|
apiClient.get<RemainingLimit>(`/families/${familyId}/categories/${categoryId}/remaining`),
|
||||||
|
};
|
||||||
228
frontend/src/pages/AdminPanel.tsx
Normal file
228
frontend/src/pages/AdminPanel.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { authApi, familyApi } from '../api/client';
|
||||||
|
import { useStore } from '../store/useStore';
|
||||||
|
|
||||||
|
export default function AdminPanel() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isAdmin, setIsAdmin, logout: storeLogout } = useStore();
|
||||||
|
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loginError, setLoginError] = useState('');
|
||||||
|
|
||||||
|
const [newFamilyName, setNewFamilyName] = useState('');
|
||||||
|
const [families, setFamilies] = useState<Array<{ id: number; name: string }>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAdmin) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
loadFamilies();
|
||||||
|
}
|
||||||
|
}, [isAdmin]);
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoginError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authApi.login({ username, password });
|
||||||
|
if (response.data.success && response.data.is_admin) {
|
||||||
|
setIsAdmin(true);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
loadFamilies();
|
||||||
|
} else {
|
||||||
|
setLoginError('Доступ запрещен. Требуются права администратора.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setLoginError('Неверные учетные данные');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await authApi.logout();
|
||||||
|
storeLogout();
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFamilies = async () => {
|
||||||
|
try {
|
||||||
|
const response = await familyApi.getAll();
|
||||||
|
setFamilies(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading families:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateFamily = async () => {
|
||||||
|
if (!newFamilyName.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await familyApi.create({ name: newFamilyName });
|
||||||
|
setNewFamilyName('');
|
||||||
|
loadFamilies();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Ошибка создания семьи');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFamily = async (id: number) => {
|
||||||
|
if (!confirm('Удалить семью?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await familyApi.delete(id);
|
||||||
|
loadFamilies();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Ошибка удаления семьи');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-6 text-center">
|
||||||
|
Вход в админ панель
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{loginError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||||
|
{loginError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Логин
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Пароль
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Войти
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="w-full mt-4 px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition"
|
||||||
|
>
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-12 px-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900">
|
||||||
|
Админ панель
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
Выход
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||||
|
Создать новую семью
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Название семьи"
|
||||||
|
value={newFamilyName}
|
||||||
|
onChange={(e) => setNewFamilyName(e.target.value)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleCreateFamily}
|
||||||
|
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||||
|
Список семей
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{families.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-4">
|
||||||
|
Семьи не найдены
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{families.map((family) => (
|
||||||
|
<div
|
||||||
|
key={family.id}
|
||||||
|
className="flex justify-between items-center p-4 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-medium text-gray-900">
|
||||||
|
{family.name}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteFamily(family.id)}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition"
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="mt-6 px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition"
|
||||||
|
>
|
||||||
|
На главную
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
frontend/src/pages/FamilyView.tsx
Normal file
299
frontend/src/pages/FamilyView.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { categoryApi, expenseApi } from '../api/client';
|
||||||
|
import { useStore } from '../store/useStore';
|
||||||
|
import type { Category } from '../types';
|
||||||
|
|
||||||
|
export default function FamilyView() {
|
||||||
|
const { familyId } = useParams<{ familyId: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { selectedFamily } = useStore();
|
||||||
|
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [remainingLimits, setRemainingLimits] = useState<Map<number, number>>(new Map());
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const [showAddCategory, setShowAddCategory] = useState(false);
|
||||||
|
const [newCategoryName, setNewCategoryName] = useState('');
|
||||||
|
const [newCategoryLimit, setNewCategoryLimit] = useState('');
|
||||||
|
|
||||||
|
const [showAddExpense, setShowAddExpense] = useState<number | null>(null);
|
||||||
|
const [expenseAmount, setExpenseAmount] = useState('');
|
||||||
|
const [expenseDescription, setExpenseDescription] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!familyId) {
|
||||||
|
navigate('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadCategories();
|
||||||
|
}, [familyId]);
|
||||||
|
|
||||||
|
const loadCategories = async () => {
|
||||||
|
if (!familyId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
console.log('Loading categories for family:', familyId);
|
||||||
|
const response = await categoryApi.getAllByFamily(parseInt(familyId));
|
||||||
|
console.log('Categories loaded:', response.data);
|
||||||
|
setCategories(response.data);
|
||||||
|
|
||||||
|
const limits = new Map<number, number>();
|
||||||
|
for (const category of response.data) {
|
||||||
|
const limitResponse = await expenseApi.getRemainingLimit(
|
||||||
|
parseInt(familyId),
|
||||||
|
category.id
|
||||||
|
);
|
||||||
|
const limitValue = typeof limitResponse.data.remaining_limit === 'string'
|
||||||
|
? parseFloat(limitResponse.data.remaining_limit)
|
||||||
|
: limitResponse.data.remaining_limit;
|
||||||
|
limits.set(category.id, limitValue);
|
||||||
|
}
|
||||||
|
setRemainingLimits(limits);
|
||||||
|
console.log('All data loaded successfully');
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = err.response?.data?.message || err.message || 'Ошибка загрузки категорий';
|
||||||
|
setError(errorMsg);
|
||||||
|
console.error('Error loading categories:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCategory = async () => {
|
||||||
|
if (!familyId || !newCategoryName || !newCategoryLimit) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await categoryApi.create(parseInt(familyId), {
|
||||||
|
name: newCategoryName,
|
||||||
|
limit_amount: parseFloat(newCategoryLimit),
|
||||||
|
});
|
||||||
|
setNewCategoryName('');
|
||||||
|
setNewCategoryLimit('');
|
||||||
|
setShowAddCategory(false);
|
||||||
|
loadCategories();
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = err.response?.data?.message || err.response?.statusText || err.message || 'Ошибка создания категории';
|
||||||
|
alert(`Ошибка создания категории: ${errorMsg} (Статус: ${err.response?.status})`);
|
||||||
|
console.error('Full error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCategory = async (categoryId: number) => {
|
||||||
|
if (!familyId) return;
|
||||||
|
if (!confirm('Удалить категорию?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await categoryApi.delete(parseInt(familyId), categoryId);
|
||||||
|
loadCategories();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Ошибка удаления категории');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetLimit = async (categoryId: number) => {
|
||||||
|
if (!familyId) return;
|
||||||
|
const newLimit = prompt('Введите новый лимит:');
|
||||||
|
if (!newLimit) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await categoryApi.resetLimit(
|
||||||
|
parseInt(familyId),
|
||||||
|
categoryId,
|
||||||
|
parseFloat(newLimit)
|
||||||
|
);
|
||||||
|
loadCategories();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Ошибка сброса лимита');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddExpense = async (categoryId: number) => {
|
||||||
|
if (!familyId || !expenseAmount) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expenseApi.create(parseInt(familyId), categoryId, {
|
||||||
|
amount: parseFloat(expenseAmount),
|
||||||
|
description: expenseDescription || undefined,
|
||||||
|
});
|
||||||
|
setExpenseAmount('');
|
||||||
|
setExpenseDescription('');
|
||||||
|
setShowAddExpense(null);
|
||||||
|
loadCategories();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Ошибка добавления расхода');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="text-xl text-gray-600">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-12 px-4">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="text-blue-600 hover:text-blue-700 mb-2"
|
||||||
|
>
|
||||||
|
← Назад к списку семей
|
||||||
|
</button>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900">
|
||||||
|
{selectedFamily?.name || 'Семья'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.id}
|
||||||
|
className="bg-white rounded-lg shadow-md p-6 flex items-center gap-6"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-2">
|
||||||
|
{category.name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-700">
|
||||||
|
Остаток: <span className="font-bold text-green-600">
|
||||||
|
{remainingLimits.get(category.id)?.toFixed(2) || '0.00'} ₽
|
||||||
|
</span>
|
||||||
|
{' / '}
|
||||||
|
{category.limit_amount.toString()} ₽
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{showAddExpense === category.id ? (
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Сумма"
|
||||||
|
value={expenseAmount}
|
||||||
|
onChange={(e) => setExpenseAmount(e.target.value)}
|
||||||
|
className="w-full mb-2 px-3 py-2 border border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Описание (опционально)"
|
||||||
|
value={expenseDescription}
|
||||||
|
onChange={(e) => setExpenseDescription(e.target.value)}
|
||||||
|
className="w-full mb-2 px-3 py-2 border border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddExpense(category.id)}
|
||||||
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddExpense(null)}
|
||||||
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddExpense(category.id)}
|
||||||
|
className="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
Вычесть из остатка
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||||
|
Управление категориями
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{showAddCategory ? (
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Название категории"
|
||||||
|
value={newCategoryName}
|
||||||
|
onChange={(e) => setNewCategoryName(e.target.value)}
|
||||||
|
className="w-full mb-2 px-4 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Лимит"
|
||||||
|
value={newCategoryLimit}
|
||||||
|
onChange={(e) => setNewCategoryLimit(e.target.value)}
|
||||||
|
className="w-full mb-2 px-4 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleAddCategory}
|
||||||
|
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddCategory(false)}
|
||||||
|
className="px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddCategory(true)}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mb-4"
|
||||||
|
>
|
||||||
|
Добавить категорию
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div key={category.id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="font-medium">{category.name}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleResetLimit(category.id)}
|
||||||
|
className="px-4 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Сбросить лимит
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteCategory(category.id)}
|
||||||
|
className="px-4 py-1 bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
frontend/src/pages/Home.tsx
Normal file
96
frontend/src/pages/Home.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { familyApi } from '../api/client';
|
||||||
|
import { useStore } from '../store/useStore';
|
||||||
|
import type { Family } from '../types';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { families, setFamilies, setSelectedFamily } = useStore();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFamilies();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadFamilies = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await familyApi.getAll();
|
||||||
|
setFamilies(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Ошибка загрузки списка семей');
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFamily = (family: Family) => {
|
||||||
|
setSelectedFamily(family);
|
||||||
|
navigate(`/family/${family.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoToAdmin = () => {
|
||||||
|
navigate('/adminpanel');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="text-xl text-gray-600">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-12 px-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900">
|
||||||
|
Семейный бюджет
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
onClick={handleGoToAdmin}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Админка
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-800 mb-6">
|
||||||
|
Выберите семью
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{families.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-8">
|
||||||
|
Семьи не найдены. Создайте семью в админ панели.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{families.map((family) => (
|
||||||
|
<button
|
||||||
|
key={family.id}
|
||||||
|
onClick={() => handleSelectFamily(family)}
|
||||||
|
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900">
|
||||||
|
{family.name}
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
frontend/src/store/useStore.ts
Normal file
37
frontend/src/store/useStore.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import type { Family, Category } from '../types';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
isAdmin: boolean;
|
||||||
|
selectedFamily: Family | null;
|
||||||
|
families: Family[];
|
||||||
|
categories: Category[];
|
||||||
|
|
||||||
|
setIsAdmin: (isAdmin: boolean) => void;
|
||||||
|
setSelectedFamily: (family: Family | null) => void;
|
||||||
|
setFamilies: (families: Family[]) => void;
|
||||||
|
setCategories: (categories: Category[]) => void;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = create<AppState>((set) => ({
|
||||||
|
isAdmin: false,
|
||||||
|
selectedFamily: null,
|
||||||
|
families: [],
|
||||||
|
categories: [],
|
||||||
|
|
||||||
|
setIsAdmin: (isAdmin) => set({ isAdmin }),
|
||||||
|
|
||||||
|
setSelectedFamily: (family) => set({ selectedFamily: family }),
|
||||||
|
|
||||||
|
setFamilies: (families) => set({ families }),
|
||||||
|
|
||||||
|
setCategories: (categories) => set({ categories }),
|
||||||
|
|
||||||
|
logout: () => set({
|
||||||
|
isAdmin: false,
|
||||||
|
selectedFamily: null,
|
||||||
|
families: [],
|
||||||
|
categories: []
|
||||||
|
}),
|
||||||
|
}));
|
||||||
49
frontend/src/types/index.ts
Normal file
49
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export interface Family {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
id: number;
|
||||||
|
family_id: number;
|
||||||
|
name: string;
|
||||||
|
limit_amount: number | string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Expense {
|
||||||
|
id: number;
|
||||||
|
category_id: number;
|
||||||
|
amount: number;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemainingLimit {
|
||||||
|
category_id: number;
|
||||||
|
remaining_limit: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
is_admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFamilyRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCategoryRequest {
|
||||||
|
name: string;
|
||||||
|
limit_amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateExpenseRequest {
|
||||||
|
amount: number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user