Răsfoiți Sursa

Dockerized solution

svalavuo 2 zile în urmă
părinte
comite
2932879887
100 a modificat fișierele cu 9834 adăugiri și 67 ștergeri
  1. 16 0
      .devcontainer/devcontainer.json
  2. 19 0
      .dockerignore
  3. 36 0
      .env.example
  4. 240 0
      DOCKER_README.md
  5. 41 0
      Dockerfile
  6. 2 2
      README.md
  7. 48 0
      backend/add_sample_accounting_entries.sql
  8. 2 2
      backend/api/account_transactions.php
  9. 159 0
      backend/api/accounting_categories.php
  10. 184 0
      backend/api/accounting_entries.php
  11. 282 0
      backend/api/alv_laskenta.php
  12. 2 2
      backend/api/attachments.php
  13. 39 2
      backend/api/auth.php
  14. 6 2
      backend/api/chart_of_accounts.php
  15. 29 2
      backend/api/clients.php
  16. 30 0
      backend/api/company.php
  17. 10 2
      backend/api/contact_persons.php
  18. 3 3
      backend/api/invoices.php
  19. 92 6
      backend/api/items.php
  20. 3 3
      backend/api/journal_entries.php
  21. 2 2
      backend/api/payments.php
  22. 9 8
      backend/api/projects.php
  23. 2 2
      backend/api/rental_prices.php
  24. 110 0
      backend/api/session_helper.php
  25. 2 2
      backend/api/subprojects.php
  26. 192 0
      backend/api/tasks.php
  27. 315 0
      backend/api/tuloslaskelma.php
  28. BIN
      backend/api/uploads/69eb26fee9287.jpg
  29. 199 0
      backend/api/users.php
  30. 196 0
      backend/api/work_hours.php
  31. 47 0
      backend/config/company.php
  32. 15 6
      backend/config/database.php
  33. 35 0
      backend/create_category_groups.php
  34. 7 0
      backend/create_default_user.sql
  35. 22 0
      backend/create_groups_table.php
  36. 54 0
      backend/create_groups_table_fixed.php
  37. 36 0
      backend/create_tasks_table.php
  38. 41 0
      backend/database/accounting_category_groups_schema.sql
  39. 9 0
      backend/database/add_date_of_purchase.sql
  40. 19 0
      backend/database/add_vat_column.sql
  41. 19 0
      backend/database/contact_persons_schema.sql
  42. 143 0
      backend/database/excel_accounting_schema.sql
  43. 59 0
      backend/database/finnish_chart_of_accounts.sql
  44. 65 0
      backend/database/finnish_chart_of_accounts_fixed.sql
  45. 60 0
      backend/database/finnish_chart_simple.sql
  46. 20 0
      backend/database/tasks_schema.sql
  47. 22 0
      backend/database/work_hours_schema.sql
  48. 245 0
      backend/models/AccountingEntry.php
  49. 30 12
      backend/models/ChartOfAccounts.php
  50. 8 2
      backend/models/Client.php
  51. 14 2
      backend/models/ContactPerson.php
  52. 8 2
      backend/models/Item.php
  53. 37 3
      backend/models/User.php
  54. 232 0
      backend/models/WorkHour.php
  55. 78 0
      backend/router.php
  56. 8 0
      backend/start_server.sh
  57. 36 0
      backend/test_accounting_api.php
  58. 92 0
      backend/test_item_creation.php
  59. 159 0
      database/init.sql
  60. 116 0
      docker-compose.prod.yml
  61. 99 0
      docker-compose.yml
  62. 30 0
      docker/apache.conf
  63. 18 0
      docker/dockerignore
  64. 38 0
      docker/mysql/my.cnf
  65. 63 0
      docker/redis/redis.conf
  66. 74 0
      docker/startup.sh
  67. 4 0
      frontend/.env.production
  68. 32 0
      frontend/Dockerfile
  69. 60 0
      frontend/nginx.conf
  70. 1 0
      frontend/node_modules/.bin/esbuild
  71. 1 0
      frontend/node_modules/.bin/nanoid
  72. 1 0
      frontend/node_modules/.bin/parser
  73. 1 0
      frontend/node_modules/.bin/rollup
  74. 1 0
      frontend/node_modules/.bin/vite
  75. 648 0
      frontend/node_modules/.package-lock.json
  76. 23 0
      frontend/node_modules/.vite/deps/_metadata.json
  77. 2855 0
      frontend/node_modules/.vite/deps/axios.js
  78. 2 0
      frontend/node_modules/.vite/deps/axios.js.map
  79. 10 0
      frontend/node_modules/.vite/deps/chunk-SSYGV25P.js
  80. 7 0
      frontend/node_modules/.vite/deps/chunk-SSYGV25P.js.map
  81. 3 0
      frontend/node_modules/.vite/deps/package.json
  82. 203 0
      frontend/node_modules/.vite/deps/vue.js
  83. 3 0
      frontend/node_modules/.vite/deps/vue.js.map
  84. 22 0
      frontend/node_modules/@babel/helper-string-parser/LICENSE
  85. 19 0
      frontend/node_modules/@babel/helper-string-parser/README.md
  86. 295 0
      frontend/node_modules/@babel/helper-string-parser/lib/index.js
  87. 0 0
      frontend/node_modules/@babel/helper-string-parser/lib/index.js.map
  88. 31 0
      frontend/node_modules/@babel/helper-string-parser/package.json
  89. 22 0
      frontend/node_modules/@babel/helper-validator-identifier/LICENSE
  90. 19 0
      frontend/node_modules/@babel/helper-validator-identifier/README.md
  91. 8 0
      frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js
  92. 0 0
      frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js.map
  93. 57 0
      frontend/node_modules/@babel/helper-validator-identifier/lib/index.js
  94. 1 0
      frontend/node_modules/@babel/helper-validator-identifier/lib/index.js.map
  95. 35 0
      frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js
  96. 0 0
      frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js.map
  97. 31 0
      frontend/node_modules/@babel/helper-validator-identifier/package.json
  98. 1073 0
      frontend/node_modules/@babel/parser/CHANGELOG.md
  99. 19 0
      frontend/node_modules/@babel/parser/LICENSE
  100. 19 0
      frontend/node_modules/@babel/parser/README.md

+ 16 - 0
.devcontainer/devcontainer.json

@@ -0,0 +1,16 @@
+{
+  "name": "Wavium",
+  "image": "mcr.microsoft.com/devcontainers/javascript-node:22-bookworm",
+  "features": {
+    "ghcr.io/devcontainers/features/git:1": {}
+  },
+  "forwardPorts": [3000],
+  "postCreateCommand": "npm install",
+  "customizations": {
+    "windsurf": {
+      "extensions": [
+        "esbenp.prettier-vscode"
+      ]
+    }
+  }
+}

+ 19 - 0
.dockerignore

@@ -0,0 +1,19 @@
+# Docker ignore file
+.git
+.gitignore
+README.md
+DOCKER_README.md
+.env
+.env.local
+.env.*.local
+node_modules
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.DS_Store
+.vscode
+.idea
+*.log
+uploads/*
+!uploads/.gitkeep
+docker-compose.override.yml

+ 36 - 0
.env.example

@@ -0,0 +1,36 @@
+# Database Configuration
+DB_HOST=mysql
+DB_PORT=3306
+DB_NAME=inventory_db
+DB_USER=inventory_user
+DB_PASS=inventory_password
+MYSQL_ROOT_PASSWORD=root_password
+
+# Company Information
+COMPANY_NAME=Your Company Name
+COMPANY_ADDRESS=123 Business Street
+COMPANY_CITY=Helsinki
+COMPANY_POSTAL_CODE=00100
+COMPANY_COUNTRY=Finland
+COMPANY_PHONE=+358 123 456 789
+COMPANY_EMAIL=info@yourcompany.com
+COMPANY_Y_TUNNUS=1234567-8
+
+# File Upload Configuration
+UPLOAD_MAX_SIZE=10M
+ALLOWED_FILE_TYPES=pdf,doc,docx,xls,xlsx,jpg,jpeg,png,gif
+UPLOADS_PATH=./uploads
+
+# Frontend Configuration
+VUE_APP_API_URL=http://localhost:8080
+
+# Optional: Redis Configuration
+REDIS_HOST=redis
+REDIS_PORT=6379
+
+# Optional: Email Configuration (for future use)
+MAIL_HOST=smtp.gmail.com
+MAIL_PORT=587
+MAIL_USERNAME=your-email@gmail.com
+MAIL_PASSWORD=your-app-password
+MAIL_ENCRYPTION=tls

+ 240 - 0
DOCKER_README.md

@@ -0,0 +1,240 @@
+# Docker Containerization Guide
+
+## Overview
+This inventory management system has been containerized with Docker for easy deployment and scaling. The setup includes:
+- Backend PHP service with Apache
+- Frontend Vue.js service with Nginx
+- MySQL database
+- Redis cache (optional)
+- External volume mounts for uploads
+
+## Quick Start
+
+### Prerequisites
+- Docker and Docker Compose installed
+- At least 2GB RAM available
+- Sufficient disk space for uploads
+
+### 1. Environment Configuration
+Copy the example environment file and customize it:
+
+```bash
+cp .env.example .env
+```
+
+Edit the `.env` file with your specific configuration:
+
+```bash
+# Database Configuration
+DB_HOST=mysql
+DB_PORT=3306
+DB_NAME=inventory_db
+DB_USER=inventory_user
+DB_PASS=your_secure_password
+MYSQL_ROOT_PASSWORD=your_root_password
+
+# Company Information
+COMPANY_NAME=Your Company Name
+COMPANY_ADDRESS=123 Business Street
+COMPANY_CITY=Helsinki
+COMPANY_POSTAL_CODE=00100
+COMPANY_COUNTRY=Finland
+COMPANY_PHONE=+358 123 456 789
+COMPANY_EMAIL=info@yourcompany.com
+COMPANY_Y_TUNNUS=1234567-8
+
+# File Upload Configuration
+UPLOAD_MAX_SIZE=10M
+ALLOWED_FILE_TYPES=pdf,doc,docx,xls,xlsx,jpg,jpeg,png,gif
+UPLOADS_PATH=./uploads
+
+# Frontend Configuration
+VUE_APP_API_URL=http://localhost:8080
+```
+
+### 2. Build and Start Containers
+```bash
+docker-compose up -d --build
+```
+
+### 3. Access the Application
+- Frontend: http://localhost:3000
+- Backend API: http://localhost:8080
+- Database: localhost:3306 (with your configured credentials)
+
+## Configuration Details
+
+### Environment Variables
+
+#### Database Configuration
+- `DB_HOST`: Database server hostname (default: mysql)
+- `DB_PORT`: Database port (default: 3306)
+- `DB_NAME`: Database name (default: inventory_db)
+- `DB_USER`: Database username (default: inventory_user)
+- `DB_PASS`: Database password (required)
+- `MYSQL_ROOT_PASSWORD`: MySQL root password (required)
+
+#### Company Information
+- `COMPANY_NAME`: Your company name
+- `COMPANY_ADDRESS`: Company address
+- `COMPANY_CITY`: Company city
+- `COMPANY_POSTAL_CODE`: Postal code
+- `COMPANY_COUNTRY`: Country
+- `COMPANY_PHONE`: Phone number
+- `COMPANY_EMAIL`: Email address
+- `COMPANY_Y_TUNNUS`: Finnish business ID
+
+#### File Upload Configuration
+- `UPLOAD_MAX_SIZE`: Maximum file upload size (default: 10M)
+- `ALLOWED_FILE_TYPES`: Allowed file extensions
+- `UPLOADS_PATH`: External path for uploads volume mount
+
+#### Frontend Configuration
+- `VUE_APP_API_URL`: Backend API URL for frontend
+
+### Volume Mounts
+
+#### Uploads Directory
+The uploads directory is mounted as an external volume to persist file uploads across container restarts:
+
+```yaml
+volumes:
+  uploads_data:
+    driver: local
+    driver_opts:
+      type: none
+      o: bind
+      device: ${UPLOADS_PATH:-./uploads}
+```
+
+#### Database Data
+MySQL data is persisted in a Docker volume:
+
+```yaml
+volumes:
+  mysql_data:
+    driver: local
+```
+
+### Network Configuration
+All services communicate through a dedicated Docker network:
+
+```yaml
+networks:
+  inventory-network:
+    driver: bridge
+```
+
+## Production Deployment
+
+### Security Considerations
+1. Change all default passwords
+2. Use HTTPS in production (configure SSL certificates)
+3. Restrict database access to internal network only
+4. Regularly update containers and dependencies
+5. Implement proper backup strategy for database and uploads
+
+### Scaling
+The architecture supports horizontal scaling:
+- Frontend can be scaled by adding more Nginx containers behind a load balancer
+- Backend can be scaled by adding more PHP containers
+- Database can be moved to external managed service
+
+### Monitoring
+Consider adding:
+- Health checks for all services
+- Log aggregation
+- Performance monitoring
+- Backup automation
+
+## Development
+
+### Local Development
+For development, you can run services individually:
+
+```bash
+# Start only database
+docker-compose up -d mysql
+
+# Start backend with live reload
+docker-compose up -d --build backend
+
+# Start frontend with live reload
+cd frontend && npm run serve
+```
+
+### Debugging
+View logs for specific services:
+
+```bash
+# Backend logs
+docker-compose logs -f backend
+
+# Frontend logs
+docker-compose logs -f frontend
+
+# Database logs
+docker-compose logs -f mysql
+```
+
+### Database Management
+Connect to the database:
+
+```bash
+docker-compose exec mysql mysql -u inventory_user -p inventory_db
+```
+
+## Troubleshooting
+
+### Common Issues
+
+#### Database Connection Failed
+1. Check if MySQL container is running: `docker-compose ps`
+2. Verify database credentials in `.env` file
+3. Check database logs: `docker-compose logs mysql`
+
+#### Upload Directory Not Working
+1. Ensure uploads directory exists on host
+2. Check permissions: `ls -la uploads/`
+3. Verify volume mount in docker-compose.yml
+
+#### Frontend Not Loading
+1. Check if backend is accessible: `curl http://localhost:8080/api/company.php`
+2. Verify API URL configuration
+3. Check frontend logs: `docker-compose logs frontend`
+
+### Performance Optimization
+1. Enable Redis caching for frequently accessed data
+2. Configure PHP OPcache for better performance
+3. Use CDN for static assets
+4. Implement database connection pooling
+
+## Maintenance
+
+### Updates
+To update the application:
+
+```bash
+# Pull latest changes
+git pull
+
+# Rebuild and restart containers
+docker-compose down
+docker-compose up -d --build
+```
+
+### Backups
+Regular backups should include:
+1. Database dump: `docker-compose exec mysql mysqldump -u root -p inventory_db`
+2. Uploads directory: `tar -czf uploads_backup.tar.gz uploads/`
+
+### Cleanup
+Remove unused containers and images:
+
+```bash
+docker system prune -a
+docker volume prune
+```
+
+## Support
+For issues and questions, refer to the application logs or contact your system administrator.

+ 41 - 0
Dockerfile

@@ -0,0 +1,41 @@
+# Backend Dockerfile
+FROM php:8.1-apache
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+    libzip-dev \
+    libpng-dev \
+    libjpeg-dev \
+    libfreetype6-dev \
+    zip \
+    unzip \
+    curl \
+    && docker-php-ext-configure gd --with-freetype --with-jpeg \
+    && docker-php-ext-install gd zip pdo_mysql \
+    && a2enmod rewrite
+
+# Install Composer
+COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
+
+# Set working directory
+WORKDIR /var/www/html
+
+# Copy backend files
+COPY backend/ .
+
+# Install PHP dependencies
+RUN composer install --no-dev --optimize-autoloader
+
+# Create uploads directory and set permissions
+RUN mkdir -p uploads && \
+    chown -R www-data:www-data /var/www/html/uploads && \
+    chmod -R 755 /var/www/html/uploads
+
+# Copy Apache configuration
+COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf
+
+# Expose port
+EXPOSE 80
+
+# Start Apache
+CMD ["apache2-foreground"]

+ 2 - 2
README.md

@@ -109,7 +109,7 @@ The system uses a normalized database structure with proper relationships:
 3. **Backend Server:**
    ```bash
    cd backend
-   php -S localhost:8000
+   php -S localhost:8080
    ```
 
 4. **Frontend Setup:**
@@ -120,7 +120,7 @@ The system uses a normalized database structure with proper relationships:
    ```
 
 5. **Access Application:**
-   - Backend: `http://localhost:8000` (API)
+   - Backend: `http://localhost:8080` (API)
    - Frontend: `http://localhost:3000` (Web Interface)
 
 #### **Option 2: Existing Database Upgrade**

+ 48 - 0
backend/add_sample_accounting_entries.sql

@@ -0,0 +1,48 @@
+-- Sample accounting entries for testing Tuloslaskelma
+-- These entries will be used to populate the profit and loss statement
+
+-- Insert sample accounting entries for current year
+INSERT INTO accounting_entries (entry_date, description, entry_type, category, tax_free_amount, vat_percentage, vat_25_5, vat_14, vat_10, total_amount, net_amount, vat_amount, reference_number) VALUES
+-- Sales Revenue (Tulo)
+('2024-01-15', 'Tuotemyynti - Asiakas A', 'Tulo', '303', 10000.00, 25.50, 2550.00, 0.00, 0.00, 12550.00, 10000.00, 2550.00, 'REF001'),
+('2024-01-20', 'Palvelumyynti - Asiakas B', 'Tulo', '303', 5000.00, 25.50, 1275.00, 0.00, 0.00, 6275.00, 5000.00, 1275.00, 'REF002'),
+('2024-02-10', 'Tuotemyynti - Asiakas C', 'Tulo', '303', 15000.00, 25.50, 3825.00, 0.00, 0.00, 18825.00, 15000.00, 3825.00, 'REF003'),
+('2024-02-25', 'Myynti - EU 0%', 'Tulo', '300', 8000.00, 0.00, 0.00, 0.00, 0.00, 8000.00, 8000.00, 0.00, 'REF004'),
+('2024-03-05', 'Korkotulot', 'Tulo', '310', 500.00, 0.00, 0.00, 0.00, 0.00, 500.00, 500.00, 0.00, 'REF005'),
+
+-- Cost of Goods Sold (Kulu)
+('2024-01-18', 'Raaka-aineet', 'Kulu', '333', 3000.00, 24.00, 720.00, 0.00, 0.00, 3720.00, 3000.00, 720.00, 'REF006'),
+('2024-01-22', 'Ulkopuoliset palvelut', 'Kulu', '333', 1500.00, 24.00, 360.00, 0.00, 0.00, 1860.00, 1500.00, 360.00, 'REF007'),
+('2024-02-12', 'Raaka-aineet', 'Kulu', '333', 4500.00, 24.00, 1080.00, 0.00, 0.00, 5580.00, 4500.00, 1080.00, 'REF008'),
+
+-- Personnel Expenses (Kulu)
+('2024-01-25', 'Palkat - Tammikuu', 'Kulu', '334', 8000.00, 0.00, 0.00, 0.00, 0.00, 8000.00, 8000.00, 0.00, 'REF009'),
+('2024-02-25', 'Palkat - Helmikuu', 'Kulu', '334', 8000.00, 0.00, 0.00, 0.00, 0.00, 8000.00, 8000.00, 0.00, 'REF010'),
+('2024-03-25', 'Palkat - Maaliskuu', 'Kulu', '334', 8500.00, 0.00, 0.00, 0.00, 0.00, 8500.00, 8500.00, 0.00, 'REF011'),
+
+-- Other Expenses (Kulu)
+('2024-01-10', 'Toimitilavuokra', 'Kulu', '335', 2000.00, 24.00, 480.00, 0.00, 0.00, 2480.00, 2000.00, 480.00, 'REF012'),
+('2024-01-15', 'Markkinointikulut', 'Kulu', '336', 800.00, 24.00, 192.00, 0.00, 0.00, 992.00, 800.00, 192.00, 'REF013'),
+('2024-02-10', 'Toimitilavuokra', 'Kulu', '335', 2000.00, 24.00, 480.00, 0.00, 0.00, 2480.00, 2000.00, 480.00, 'REF014'),
+('2024-02-20', 'Vakuutukset', 'Kulu', '365', 500.00, 24.00, 120.00, 0.00, 0.00, 620.00, 500.00, 120.00, 'REF015'),
+
+-- Depreciation (Kulu)
+('2024-01-31', 'Tavarapoistot - Q1', 'Kulu', '349', 1000.00, 0.00, 0.00, 0.00, 0.00, 1000.00, 1000.00, 0.00, 'REF016'),
+('2024-02-28', 'Tavarapoistot - Q1', 'Kulu', '349', 1000.00, 0.00, 0.00, 0.00, 0.00, 1000.00, 1000.00, 0.00, 'REF017'),
+('2024-03-31', 'Tavarapoistot - Q1', 'Kulu', '349', 1000.00, 0.00, 0.00, 0.00, 0.00, 1000.00, 1000.00, 0.00, 'REF018'),
+
+-- Tax Expenses (Kulu)
+('2024-03-20', 'Ennakonpidätys', 'Kulu', '366', 3000.00, 0.00, 0.00, 0.00, 0.00, 3000.00, 3000.00, 0.00, 'REF019');
+
+-- Previous year sample data for comparison
+INSERT INTO accounting_entries (entry_date, description, entry_type, category, tax_free_amount, vat_percentage, vat_25_5, vat_14, vat_10, total_amount, net_amount, vat_amount, reference_number) VALUES
+-- Previous year sales
+('2023-01-15', 'Tuotemyynti - Asiakas A', 'Tulo', '303', 9000.00, 25.50, 2295.00, 0.00, 0.00, 11295.00, 9000.00, 2295.00, 'REF023'),
+('2023-02-10', 'Tuotemyynti - Asiakas C', 'Tulo', '303', 13000.00, 25.50, 3315.00, 0.00, 0.00, 16315.00, 13000.00, 3315.00, 'REF024'),
+('2023-03-05', 'Korkotulot', 'Tulo', '310', 400.00, 0.00, 0.00, 0.00, 0.00, 400.00, 400.00, 0.00, 'REF025'),
+
+-- Previous year expenses
+('2023-01-18', 'Raaka-aineet', 'Kulu', '333', 2800.00, 24.00, 672.00, 0.00, 0.00, 3472.00, 2800.00, 672.00, 'REF026'),
+('2023-01-25', 'Palkat - Tammikuu', 'Kulu', '334', 7500.00, 0.00, 0.00, 0.00, 0.00, 7500.00, 7500.00, 0.00, 'REF027'),
+('2023-01-10', 'Toimitilavuokra', 'Kulu', '335', 1800.00, 24.00, 432.00, 0.00, 0.00, 2232.00, 1800.00, 432.00, 'REF028'),
+('2023-01-31', 'Tavarapoistot - Q1', 'Kulu', '349', 900.00, 0.00, 0.00, 0.00, 0.00, 900.00, 900.00, 0.00, 'REF029');

+ 2 - 2
backend/api/account_transactions.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/AccountTransaction.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/AccountTransaction.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 159 - 0
backend/api/accounting_categories.php

@@ -0,0 +1,159 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once __DIR__ . '/../config/database.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['type'])) {
+            // Get categories by type (Tulo/Kulu)
+            $type = $_GET['type'];
+            $query = "SELECT category_code, category_name, category_type, vat_percentage 
+                      FROM accounting_categories 
+                      WHERE category_type = ? AND is_active = 1 
+                      ORDER BY category_code";
+            
+            $stmt = $db->prepare($query);
+            $stmt->bindParam(1, $type);
+            $stmt->execute();
+            
+            $categories = [];
+            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                $categories[] = [
+                    'code' => $row['category_code'],
+                    'name' => $row['category_name'],
+                    'type' => $row['category_type'],
+                    'vat_percentage' => $row['vat_percentage']
+                ];
+            }
+            
+            http_response_code(200);
+            echo json_encode($categories);
+        } else {
+            // Get all categories
+            $query = "SELECT category_code, category_name, category_type, vat_percentage 
+                      FROM accounting_categories 
+                      WHERE is_active = 1 
+                      ORDER BY category_type, category_code";
+            
+            $stmt = $db->prepare($query);
+            $stmt->execute();
+            
+            $categories = [];
+            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                $categories[] = [
+                    'code' => $row['category_code'],
+                    'name' => $row['category_name'],
+                    'type' => $row['category_type'],
+                    'vat_percentage' => $row['vat_percentage']
+                ];
+            }
+            
+            http_response_code(200);
+            echo json_encode($categories);
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->category_code) && !empty($data->category_name) && !empty($data->category_type)) {
+            $query = "INSERT INTO accounting_categories 
+                      SET category_code=:category_code, 
+                          category_name=:category_name, 
+                          category_type=:category_type, 
+                          vat_percentage=:vat_percentage, 
+                          is_active=:is_active";
+            
+            $stmt = $db->prepare($query);
+            
+            $stmt->bindParam(":category_code", $data->category_code);
+            $stmt->bindParam(":category_name", $data->category_name);
+            $stmt->bindParam(":category_type", $data->category_type);
+            $stmt->bindParam(":vat_percentage", $data->vat_percentage);
+            $stmt->bindParam(":is_active", $data->is_active ?? 1);
+            
+            if($stmt->execute()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Category was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create category."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create category. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->category_code) && !empty($data->category_name) && !empty($data->category_type)) {
+            $query = "UPDATE accounting_categories 
+                      SET category_name=:category_name, 
+                          category_type=:category_type, 
+                          vat_percentage=:vat_percentage, 
+                          is_active=:is_active 
+                      WHERE category_code=:category_code";
+            
+            $stmt = $db->prepare($query);
+            
+            $stmt->bindParam(":category_code", $data->category_code);
+            $stmt->bindParam(":category_name", $data->category_name);
+            $stmt->bindParam(":category_type", $data->category_type);
+            $stmt->bindParam(":vat_percentage", $data->vat_percentage);
+            $stmt->bindParam(":is_active", $data->is_active ?? 1);
+            
+            if($stmt->execute()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Category was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update category."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update category. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['code'])) {
+            $code = $_GET['code'];
+            $query = "DELETE FROM accounting_categories WHERE category_code = ?";
+            
+            $stmt = $db->prepare($query);
+            $stmt->bindParam(1, $code);
+            
+            if($stmt->execute()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Category was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete category."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete category. Category code is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 184 - 0
backend/api/accounting_entries.php

@@ -0,0 +1,184 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/AccountingEntry.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$entry = new AccountingEntry($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $entry->id = $_GET['id'];
+            $entry->readOne();
+            
+            if($entry->description != null) {
+                $entry_arr = array(
+                    "id" => $entry->id,
+                    "entry_date" => $entry->entry_date,
+                    "description" => $entry->description,
+                    "entry_type" => $entry->entry_type,
+                    "category" => $entry->category,
+                    "tax_free_amount" => $entry->tax_free_amount,
+                    "vat_percentage" => $entry->vat_percentage,
+                    "vat_25_5" => $entry->vat_25_5,
+                    "vat_14" => $entry->vat_14,
+                    "vat_10" => $entry->vat_10,
+                    "total_amount" => $entry->total_amount,
+                    "net_amount" => $entry->net_amount,
+                    "vat_amount" => $entry->vat_amount,
+                    "reference_number" => $entry->reference_number,
+                    "entry_type_badge" => $entry->getEntryTypeBadge(),
+                    "entry_type_name" => $entry->getEntryTypeName()
+                );
+                
+                http_response_code(200);
+                echo json_encode($entry_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "Entry not found."));
+            }
+        } else {
+            // Handle date range filtering
+            if(isset($_GET['start_date']) && isset($_GET['end_date'])) {
+                $stmt = $entry->getByDateRange($_GET['start_date'], $_GET['end_date']);
+            } else {
+                $stmt = $entry->read();
+            }
+            
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $entries_arr = array();
+                $entries_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    $entry_item = array(
+                        "id" => $row['id'],
+                        "entry_date" => $row['entry_date'],
+                        "description" => $row['description'],
+                        "entry_type" => $row['entry_type'],
+                        "category" => $row['category'],
+                        "tax_free_amount" => $row['tax_free_amount'],
+                        "vat_percentage" => $row['vat_percentage'],
+                        "vat_25_5" => $row['vat_25_5'],
+                        "vat_14" => $row['vat_14'],
+                        "vat_10" => $row['vat_10'],
+                        "total_amount" => $row['total_amount'],
+                        "net_amount" => $row['net_amount'],
+                        "vat_amount" => $row['vat_amount'],
+                        "reference_number" => $row['reference_number'],
+                        "entry_type_badge" => $entry->getEntryTypeBadge($row['entry_type']),
+                        "entry_type_name" => $entry->getEntryTypeName($row['entry_type'])
+                    );
+                    
+                    array_push($entries_arr["records"], $entry_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($entries_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->description) && !empty($data->entry_type)) {
+            $entry->entry_date = $data->entry_date ?? date('Y-m-d');
+            $entry->description = $data->description;
+            $entry->entry_type = $data->entry_type;
+            $entry->category = $data->category ?? '';
+            $entry->tax_free_amount = $data->tax_free_amount ?? 0;
+            $entry->vat_percentage = $data->vat_percentage ?? 0;
+            $entry->vat_25_5 = $data->vat_25_5 ?? 0;
+            $entry->vat_14 = $data->vat_14 ?? 0;
+            $entry->vat_10 = $data->vat_10 ?? 0;
+            $entry->total_amount = $data->total_amount ?? 0;
+            $entry->net_amount = $data->net_amount ?? 0;
+            $entry->vat_amount = $data->vat_amount ?? 0;
+            $entry->reference_number = $data->reference_number ?? '';
+            
+            if($entry->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "Entry was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create entry."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create entry. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->description) && !empty($data->entry_type)) {
+            $entry->id = $data->id;
+            $entry->entry_date = $data->entry_date ?? date('Y-m-d');
+            $entry->description = $data->description;
+            $entry->entry_type = $data->entry_type;
+            $entry->category = $data->category ?? '';
+            $entry->tax_free_amount = $data->tax_free_amount ?? 0;
+            $entry->vat_percentage = $data->vat_percentage ?? 0;
+            $entry->vat_25_5 = $data->vat_25_5 ?? 0;
+            $entry->vat_14 = $data->vat_14 ?? 0;
+            $entry->vat_10 = $data->vat_10 ?? 0;
+            $entry->total_amount = $data->total_amount ?? 0;
+            $entry->net_amount = $data->net_amount ?? 0;
+            $entry->vat_amount = $data->vat_amount ?? 0;
+            $entry->reference_number = $data->reference_number ?? '';
+            
+            if($entry->update()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Entry was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update entry."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update entry. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $entry->id = $_GET['id'];
+            
+            if($entry->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "Entry was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete entry."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete entry. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+?>

+ 282 - 0
backend/api/alv_laskenta.php

@@ -0,0 +1,282 @@
+<?php
+header('Content-Type: application/json');
+header('Access-Control-Allow-Origin: *');
+header('Access-Control-Allow-Methods: GET, OPTIONS');
+header('Access-Control-Allow-Headers: Content-Type, Authorization');
+
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/AccountingEntry.php';
+
+// Handle preflight requests
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+    exit(0);
+}
+
+try {
+    $database = new Database();
+    $conn = $database->getConnection();
+    
+    if (!$conn) {
+        throw new Exception('Database connection failed');
+    }
+
+    // Get query parameters
+    $period = $_GET['period'] ?? 'current';
+    $month = $_GET['month'] ?? null;
+    $year = $_GET['year'] ?? null;
+    
+    // Determine date range based on period
+    $dateConditions = getDateConditions($period, $month, $year);
+    
+    // Fetch accounting entries for VAT calculation
+    $sql = "SELECT 
+                ae.*,
+                ac.category_name,
+                ac.category_type,
+                ac.vat_percentage as category_vat
+            FROM accounting_entries ae
+            LEFT JOIN accounting_categories ac ON ae.category = ac.category_code
+            WHERE ae.entry_date BETWEEN :start_date AND :end_date
+            ORDER BY ae.entry_date";
+    
+    $stmt = $conn->prepare($sql);
+    $stmt->bindParam(':start_date', $dateConditions['start_date']);
+    $stmt->bindParam(':end_date', $dateConditions['end_date']);
+    $stmt->execute();
+    
+    $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    
+    // Calculate VAT breakdown
+    $vatBreakdown = calculateVATBreakdown($entries);
+    
+    // Get previous period data for comparison
+    $previousDateConditions = getPreviousDateConditions($period, $month, $year);
+    $previousData = getPreviousPeriodVATData($conn, $previousDateConditions);
+    
+    // Combine current and previous data
+    $result = [
+        'period' => $period,
+        'date_range' => $dateConditions,
+        'vat_breakdown' => $vatBreakdown,
+        'previous_period' => $previousData,
+        'totals' => calculateVATTotals($vatBreakdown)
+    ];
+    
+    echo json_encode([
+        'success' => true,
+        'data' => $result
+    ]);
+    
+} catch (Exception $e) {
+    http_response_code(500);
+    echo json_encode([
+        'success' => false,
+        'message' => $e->getMessage()
+    ]);
+}
+
+function getDateConditions($period, $month, $year) {
+    $currentYear = $year ?: date('Y');
+    $currentMonth = $month ? date('m', strtotime($month)) : date('m');
+    
+    switch ($period) {
+        case 'month':
+            return [
+                'start_date' => "$currentYear-$currentMonth-01",
+                'end_date' => date('Y-m-t', strtotime("$currentYear-$currentMonth-01"))
+            ];
+        case 'quarter':
+            $quarter = ceil($currentMonth / 3);
+            $startMonth = ($quarter - 1) * 3 + 1;
+            $endMonth = $quarter * 3;
+            return [
+                'start_date' => "$currentYear-" . str_pad($startMonth, 2, '0', STR_PAD_LEFT) . "-01",
+                'end_date' => "$currentYear-" . str_pad($endMonth, 2, '0', STR_PAD_LEFT) . "-" . date('t', strtotime("$currentYear-$endMonth-01"))
+            ];
+        case 'year':
+            return [
+                'start_date' => "$currentYear-01-01",
+                'end_date' => "$currentYear-12-31"
+            ];
+        case 'current':
+        default:
+            return [
+                'start_date' => date('Y-01-01'),
+                'end_date' => date('Y-m-d')
+            ];
+    }
+}
+
+function getPreviousDateConditions($period, $month, $year) {
+    $currentYear = $year ?: date('Y');
+    $currentMonth = $month ? date('m', strtotime($month)) : date('m');
+    
+    switch ($period) {
+        case 'month':
+            $previousDate = date('Y-m', strtotime("$currentYear-$currentMonth-01 -1 month"));
+            $prevYear = date('Y', strtotime($previousDate));
+            $prevMonth = date('m', strtotime($previousDate));
+            return [
+                'start_date' => "$prevYear-$prevMonth-01",
+                'end_date' => "$prevYear-$prevMonth-" . date('t', strtotime("$prevYear-$prevMonth-01"))
+            ];
+        case 'quarter':
+            $quarter = ceil($currentMonth / 3);
+            if ($quarter == 1) {
+                $prevYear = $currentYear - 1;
+                $prevQuarter = 4;
+            } else {
+                $prevYear = $currentYear;
+                $prevQuarter = $quarter - 1;
+            }
+            $startMonth = ($prevQuarter - 1) * 3 + 1;
+            $endMonth = $prevQuarter * 3;
+            return [
+                'start_date' => "$prevYear-" . str_pad($startMonth, 2, '0', STR_PAD_LEFT) . "-01",
+                'end_date' => "$prevYear-" . str_pad($endMonth, 2, '0', STR_PAD_LEFT) . "-" . date('t', strtotime("$prevYear-$endMonth-01"))
+            ];
+        case 'year':
+            return [
+                'start_date' => ($currentYear - 1) . "-01-01",
+                'end_date' => ($currentYear - 1) . "-12-31"
+            ];
+        case 'current':
+        default:
+            return [
+                'start_date' => (date('Y') - 1) . "-01-01",
+                'end_date' => (date('Y') - 1) . "-12-31"
+            ];
+    }
+}
+
+function calculateVATBreakdown($entries) {
+    $vatData = [
+        '0' => [
+            'taxable_sales' => 0,
+            'taxable_purchases' => 0,
+            'payable_vat' => 0,
+            'deductible_vat' => 0,
+            'net_vat' => 0
+        ],
+        '10' => [
+            'taxable_sales' => 0,
+            'taxable_purchases' => 0,
+            'payable_vat' => 0,
+            'deductible_vat' => 0,
+            'net_vat' => 0
+        ],
+        '14' => [
+            'taxable_sales' => 0,
+            'taxable_purchases' => 0,
+            'payable_vat' => 0,
+            'deductible_vat' => 0,
+            'net_vat' => 0
+        ],
+        '24' => [
+            'taxable_sales' => 0,
+            'taxable_purchases' => 0,
+            'payable_vat' => 0,
+            'deductible_vat' => 0,
+            'net_vat' => 0
+        ],
+        '25.5' => [
+            'taxable_sales' => 0,
+            'taxable_purchases' => 0,
+            'payable_vat' => 0,
+            'deductible_vat' => 0,
+            'net_vat' => 0
+        ]
+    ];
+    
+    foreach ($entries as $entry) {
+        $vatRate = floatval($entry['vat_percentage']);
+        $vatKey = normalizeVatRate($vatRate);
+        
+        if (!isset($vatData[$vatKey])) {
+            $vatData[$vatKey] = [
+                'taxable_sales' => 0,
+                'taxable_purchases' => 0,
+                'payable_vat' => 0,
+                'deductible_vat' => 0,
+                'net_vat' => 0
+            ];
+        }
+        
+        $taxFreeAmount = floatval($entry['tax_free_amount']);
+        $vatAmount = floatval($entry['vat_amount']);
+        
+        if ($entry['entry_type'] === 'Tulo') {
+            // Sales - VAT is payable
+            $vatData[$vatKey]['taxable_sales'] += $taxFreeAmount;
+            $vatData[$vatKey]['payable_vat'] += $vatAmount;
+        } else {
+            // Purchases - VAT is deductible
+            $vatData[$vatKey]['taxable_purchases'] += $taxFreeAmount;
+            $vatData[$vatKey]['deductible_vat'] += $vatAmount;
+        }
+    }
+    
+    // Calculate net VAT for each rate
+    foreach ($vatData as $rate => &$data) {
+        $data['net_vat'] = $data['payable_vat'] - $data['deductible_vat'];
+    }
+    
+    return $vatData;
+}
+
+function normalizeVatRate($rate) {
+    // Normalize VAT rates to standard keys
+    if ($rate == 0) return '0';
+    if ($rate == 10) return '10';
+    if ($rate == 14) return '14';
+    if ($rate == 24) return '24';
+    if ($rate == 25.5) return '25.5';
+    
+    // Handle edge cases
+    if ($rate > 24 && $rate <= 26) return '25.5';
+    if ($rate > 14 && $rate <= 15) return '14';
+    if ($rate > 9 && $rate <= 11) return '10';
+    
+    return '0'; // Default to 0% if unknown
+}
+
+function calculateVATTotals($vatBreakdown) {
+    $totals = [
+        'taxable_sales' => 0,
+        'taxable_purchases' => 0,
+        'payable_vat' => 0,
+        'deductible_vat' => 0,
+        'net_vat' => 0
+    ];
+    
+    foreach ($vatBreakdown as $data) {
+        $totals['taxable_sales'] += $data['taxable_sales'];
+        $totals['taxable_purchases'] += $data['taxable_purchases'];
+        $totals['payable_vat'] += $data['payable_vat'];
+        $totals['deductible_vat'] += $data['deductible_vat'];
+        $totals['net_vat'] += $data['net_vat'];
+    }
+    
+    return $totals;
+}
+
+function getPreviousPeriodVATData($conn, $dateConditions) {
+    $sql = "SELECT 
+                ae.*,
+                ac.category_name,
+                ac.category_type,
+                ac.vat_percentage as category_vat
+            FROM accounting_entries ae
+            LEFT JOIN accounting_categories ac ON ae.category = ac.category_code
+            WHERE ae.entry_date BETWEEN :start_date AND :end_date
+            ORDER BY ae.entry_date";
+    
+    $stmt = $conn->prepare($sql);
+    $stmt->bindParam(':start_date', $dateConditions['start_date']);
+    $stmt->bindParam(':end_date', $dateConditions['end_date']);
+    $stmt->execute();
+    
+    $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    return calculateVATBreakdown($entries);
+}
+?>

+ 2 - 2
backend/api/attachments.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Attachment.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Attachment.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 39 - 2
backend/api/auth.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/User.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/User.php';
 
 $database = new Database();
 $db = $database->getConnection();
@@ -22,6 +22,43 @@ session_start();
 $request_method = $_SERVER['REQUEST_METHOD'];
 
 switch($request_method) {
+    case 'GET':
+        if(isset($_GET['action']) && $_GET['action'] === 'status') {
+            // Check if user is logged in
+            if(isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
+                // Validate session by checking if user still exists and is active
+                $user->id = $_SESSION['user_id'];
+                $user->readOne();
+                
+                if($user->username && $user->is_active) {
+                    http_response_code(200);
+                    echo json_encode(array(
+                        "message" => "User is authenticated",
+                        "user" => array(
+                            "id" => $user->id,
+                            "username" => $user->username,
+                            "email" => $user->email,
+                            "first_name" => $user->first_name,
+                            "last_name" => $user->last_name,
+                            "role" => $user->role
+                        )
+                    ));
+                } else {
+                    // User no longer exists or is inactive, destroy session
+                    session_destroy();
+                    http_response_code(401);
+                    echo json_encode(array("message" => "Session invalid - user not found or inactive"));
+                }
+            } else {
+                http_response_code(401);
+                echo json_encode(array("message" => "No active session"));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Invalid action"));
+        }
+        break;
+        
     case 'POST':
         $data = json_decode(file_get_contents("php://input"));
         

+ 6 - 2
backend/api/chart_of_accounts.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/ChartOfAccounts.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/ChartOfAccounts.php';
 
 $database = new Database();
 $db = $database->getConnection();
@@ -34,6 +34,7 @@ switch($request_method) {
                     "description" => $account->description,
                     "opening_balance" => $account->opening_balance,
                     "current_balance" => $account->current_balance,
+                    "vat_percentage" => $account->vat_percentage,
                     "is_active" => $account->is_active,
                     "created_at" => $account->created_at,
                     "updated_at" => $account->updated_at,
@@ -66,6 +67,7 @@ switch($request_method) {
                         "description" => $description,
                         "opening_balance" => $opening_balance,
                         "current_balance" => $current_balance,
+                        "vat_percentage" => $vat_percentage,
                         "is_active" => $is_active,
                         "created_at" => $created_at,
                         "updated_at" => $updated_at,
@@ -95,6 +97,7 @@ switch($request_method) {
             $account->description = $data->description ?? '';
             $account->opening_balance = $data->opening_balance ?? 0;
             $account->current_balance = $data->current_balance ?? 0;
+            $account->vat_percentage = $data->vat_percentage ?? 0;
             $account->is_active = $data->is_active ?? true;
             
             if($account->create()) {
@@ -122,6 +125,7 @@ switch($request_method) {
             $account->description = $data->description ?? '';
             $account->opening_balance = $data->opening_balance ?? 0;
             $account->current_balance = $data->current_balance ?? 0;
+            $account->vat_percentage = $data->vat_percentage ?? 0;
             $account->is_active = $data->is_active ?? true;
             
             if($account->update()) {

+ 29 - 2
backend/api/clients.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Client.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Client.php';
 
 $database = new Database();
 $db = $database->getConnection();
@@ -39,6 +39,7 @@ switch($request_method) {
                     "postal_code" => $client->postal_code,
                     "country" => $client->country,
                     "notes" => $client->notes,
+                    "hour_price" => $client->hour_price,
                     "created_at" => $client->created_at,
                     "updated_at" => $client->updated_at
                 );
@@ -98,9 +99,31 @@ switch($request_method) {
                 while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                     extract($row);
                     
+                    // Fetch contact persons for this client
+                    require_once __DIR__ . '/../models/ContactPerson.php';
+                    $contactPerson = new ContactPerson($db);
+                    $contactPerson->client_id = $id;
+                    $contact_stmt = $contactPerson->read();
+                    
+                    $contact_persons = array();
+                    while ($contact_row = $contact_stmt->fetch(PDO::FETCH_ASSOC)) {
+                        $contact_persons[] = array(
+                            "id" => $contact_row['id'],
+                            "first_name" => $contact_row['first_name'],
+                            "last_name" => $contact_row['last_name'],
+                            "email" => $contact_row['email'],
+                            "phone" => $contact_row['phone'],
+                            "position" => $contact_row['position'],
+                            "department" => $contact_row['department'],
+                            "is_primary" => $contact_row['is_primary'],
+                            "notes" => $contact_row['notes']
+                        );
+                    }
+                    
                     $client_item = array(
                         "id" => $id,
                         "company_name" => $company_name,
+                        "y_tunnus" => $y_tunnus,
                         "first_name" => $first_name,
                         "last_name" => $last_name,
                         "email" => $email,
@@ -111,6 +134,8 @@ switch($request_method) {
                         "postal_code" => $postal_code,
                         "country" => $country,
                         "notes" => $notes,
+                        "hour_price" => $hour_price,
+                        "contact_persons" => $contact_persons,
                         "created_at" => $created_at,
                         "updated_at" => $updated_at
                     );
@@ -143,6 +168,7 @@ switch($request_method) {
             $client->postal_code = $data->postal_code ?? '';
             $client->country = $data->country ?? '';
             $client->notes = $data->notes ?? '';
+            $client->hour_price = $data->hour_price ?? 0;
             
             if($client->create()) {
                 http_response_code(201);
@@ -174,6 +200,7 @@ switch($request_method) {
             $client->postal_code = $data->postal_code ?? '';
             $client->country = $data->country ?? '';
             $client->notes = $data->notes ?? '';
+            $client->hour_price = $data->hour_price ?? 0;
             
             if($client->update()) {
                 http_response_code(200);

+ 30 - 0
backend/api/company.php

@@ -0,0 +1,30 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once __DIR__ . '/../config/company.php';
+
+$company = new Company();
+
+try {
+    $company_details = $company->getDetails();
+    
+    http_response_code(200);
+    echo json_encode([
+        'success' => true,
+        'data' => $company_details
+    ]);
+} catch (Exception $e) {
+    http_response_code(500);
+    echo json_encode([
+        'success' => false,
+        'message' => 'Error retrieving company information'
+    ]);
+}
+?>

+ 10 - 2
backend/api/contact_persons.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/ContactPerson.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/ContactPerson.php';
 
 $database = new Database();
 $db = $database->getConnection();
@@ -33,7 +33,9 @@ switch($request_method) {
                     "email" => $contactPerson->email,
                     "phone" => $contactPerson->phone,
                     "position" => $contactPerson->position,
+                    "department" => $contactPerson->department,
                     "is_primary" => $contactPerson->is_primary,
+                    "notes" => $contactPerson->notes,
                     "created_at" => $contactPerson->created_at,
                     "updated_at" => $contactPerson->updated_at
                 );
@@ -64,7 +66,9 @@ switch($request_method) {
                         "email" => $email,
                         "phone" => $phone,
                         "position" => $position,
+                        "department" => $department,
                         "is_primary" => $is_primary,
+                        "notes" => $notes,
                         "created_at" => $created_at,
                         "updated_at" => $updated_at
                     );
@@ -94,6 +98,8 @@ switch($request_method) {
             $contactPerson->email = $data->email ?? '';
             $contactPerson->phone = $data->phone ?? '';
             $contactPerson->position = $data->position ?? '';
+            $contactPerson->department = $data->department ?? '';
+            $contactPerson->notes = $data->notes ?? '';
             $contactPerson->is_primary = $data->is_primary ?? false;
             
             if($contactPerson->create()) {
@@ -120,6 +126,8 @@ switch($request_method) {
             $contactPerson->email = $data->email ?? '';
             $contactPerson->phone = $data->phone ?? '';
             $contactPerson->position = $data->position ?? '';
+            $contactPerson->department = $data->department ?? '';
+            $contactPerson->notes = $data->notes ?? '';
             $contactPerson->is_primary = $data->is_primary ?? false;
             
             if($contactPerson->update()) {

+ 3 - 3
backend/api/invoices.php

@@ -8,9 +8,9 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Invoice.php';
-require_once '../models/InvoiceItem.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Invoice.php';
+require_once __DIR__ . '/../models/InvoiceItem.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 92 - 6
backend/api/items.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Item.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Item.php';
 
 $database = new Database();
 $db = $database->getConnection();
@@ -33,6 +33,7 @@ switch($request_method) {
                     "picture" => $item->picture,
                     "quantity" => $item->quantity,
                     "price" => $item->price,
+                    "date_of_purchase" => $item->date_of_purchase,
                     "created_at" => $item->created_at,
                     "updated_at" => $item->updated_at
                 );
@@ -62,6 +63,7 @@ switch($request_method) {
                         "picture" => $picture,
                         "quantity" => $quantity,
                         "price" => $price,
+                        "date_of_purchase" => $date_of_purchase,
                         "created_at" => $created_at,
                         "updated_at" => $updated_at
                     );
@@ -88,10 +90,43 @@ switch($request_method) {
             $item->picture = $data->picture ?? '';
             $item->quantity = $data->quantity;
             $item->price = $data->price;
+            $item->date_of_purchase = $data->date_of_purchase ?? null;
             
             if($item->create()) {
-                http_response_code(201);
-                echo json_encode(array("message" => "Item was created."));
+                // Create corresponding accounting entry
+                require_once __DIR__ . '/../models/AccountingEntry.php';
+                $accounting_entry = new AccountingEntry($db);
+                
+                // Calculate accounting entry fields
+                $total_amount = floatval($data->price) * intval($data->quantity);
+                $vat_percentage = 25.50;
+                $net_amount = $total_amount / (1 + ($vat_percentage / 100));
+                $vat_amount = $total_amount - $net_amount;
+                $tax_free_amount = $net_amount;
+                
+                // Set accounting entry properties
+                $accounting_entry->entry_date = $data->date_of_purchase ?? date('Y-m-d');
+                $accounting_entry->description = $data->name;
+                $accounting_entry->entry_type = 'Kulu';
+                $accounting_entry->category = '333';
+                $accounting_entry->tax_free_amount = $tax_free_amount;
+                $accounting_entry->vat_percentage = $vat_percentage;
+                $accounting_entry->vat_25_5 = $vat_amount;
+                $accounting_entry->vat_14 = 0;
+                $accounting_entry->vat_10 = 0;
+                $accounting_entry->total_amount = $total_amount;
+                $accounting_entry->net_amount = $net_amount;
+                $accounting_entry->vat_amount = $vat_amount;
+                $accounting_entry->reference_number = '';
+                
+                // Create accounting entry
+                if($accounting_entry->create()) {
+                    http_response_code(201);
+                    echo json_encode(array("message" => "Item and accounting entry were created."));
+                } else {
+                    http_response_code(201);
+                    echo json_encode(array("message" => "Item was created but accounting entry failed."));
+                }
             } else {
                 http_response_code(503);
                 echo json_encode(array("message" => "Unable to create item."));
@@ -113,10 +148,61 @@ switch($request_method) {
             $item->picture = $data->picture ?? '';
             $item->quantity = $data->quantity;
             $item->price = $data->price;
+            $item->date_of_purchase = $data->date_of_purchase ?? null;
             
             if($item->update()) {
-                http_response_code(200);
-                echo json_encode(array("message" => "Item was updated."));
+                // Update or create corresponding accounting entry
+                require_once __DIR__ . '/../models/AccountingEntry.php';
+                $accounting_entry = new AccountingEntry($db);
+                
+                // Calculate accounting entry fields
+                $total_amount = floatval($data->price) * intval($data->quantity);
+                $vat_percentage = 25.50;
+                $net_amount = $total_amount / (1 + ($vat_percentage / 100));
+                $vat_amount = $total_amount - $net_amount;
+                $tax_free_amount = $net_amount;
+                
+                // Set accounting entry properties
+                $accounting_entry->entry_date = $data->date_of_purchase ?? date('Y-m-d');
+                $accounting_entry->description = $data->name;
+                $accounting_entry->entry_type = 'Kulu';
+                $accounting_entry->category = '333';
+                $accounting_entry->tax_free_amount = $tax_free_amount;
+                $accounting_entry->vat_percentage = $vat_percentage;
+                $accounting_entry->vat_25_5 = $vat_amount;
+                $accounting_entry->vat_14 = 0;
+                $accounting_entry->vat_10 = 0;
+                $accounting_entry->total_amount = $total_amount;
+                $accounting_entry->net_amount = $net_amount;
+                $accounting_entry->vat_amount = $vat_amount;
+                $accounting_entry->reference_number = '';
+                
+                // Try to find existing accounting entry for this item
+                $existing_entry_query = "SELECT id FROM accounting_entries WHERE description = ? AND entry_type = 'Kulu' AND category = '333' ORDER BY entry_date DESC LIMIT 1";
+                $stmt = $db->prepare($existing_entry_query);
+                $stmt->execute([$data->name]);
+                $existing_entry = $stmt->fetch(PDO::FETCH_ASSOC);
+                
+                if ($existing_entry) {
+                    // Update existing entry
+                    $accounting_entry->id = $existing_entry['id'];
+                    if($accounting_entry->update()) {
+                        http_response_code(200);
+                        echo json_encode(array("message" => "Item and accounting entry were updated."));
+                    } else {
+                        http_response_code(200);
+                        echo json_encode(array("message" => "Item was updated but accounting entry update failed."));
+                    }
+                } else {
+                    // Create new entry
+                    if($accounting_entry->create()) {
+                        http_response_code(200);
+                        echo json_encode(array("message" => "Item was updated and new accounting entry was created."));
+                    } else {
+                        http_response_code(200);
+                        echo json_encode(array("message" => "Item was updated but accounting entry creation failed."));
+                    }
+                }
             } else {
                 http_response_code(503);
                 echo json_encode(array("message" => "Unable to update item."));

+ 3 - 3
backend/api/journal_entries.php

@@ -8,9 +8,9 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/JournalEntry.php';
-require_once '../models/AccountTransaction.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/JournalEntry.php';
+require_once __DIR__ . '/../models/AccountTransaction.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 2 - 2
backend/api/payments.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Payment.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Payment.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 9 - 8
backend/api/projects.php

@@ -8,9 +8,9 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Project.php';
-require_once '../models/Subproject.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Project.php';
+require_once __DIR__ . '/../models/Subproject.php';
 
 $database = new Database();
 $db = $database->getConnection();
@@ -38,7 +38,7 @@ switch($request_method) {
                     "budget" => $project->budget,
                     "created_at" => $project->created_at,
                     "updated_at" => $project->updated_at,
-                    "customer_name" => $project->getCustomerName(),
+                    "customer_name" => $company_name ? $company_name : ($first_name . ' ' . $last_name),
                     "status_badge" => $project->getStatusBadge(),
                     "progress" => $project->getProgress()
                 );
@@ -71,7 +71,7 @@ switch($request_method) {
                         "budget" => $budget,
                         "created_at" => $created_at,
                         "updated_at" => $updated_at,
-                        "customer_name" => $project->getCustomerName(),
+                        "customer_name" => $company_name ? $company_name : ($first_name . ' ' . $last_name),
                         "status_badge" => $project->getStatusBadge(),
                         "progress" => $project->getProgress()
                     );
@@ -107,7 +107,7 @@ switch($request_method) {
                         "budget" => $budget,
                         "created_at" => $created_at,
                         "updated_at" => $updated_at,
-                        "customer_name" => $project->getCustomerName(),
+                        "customer_name" => $company_name ? $company_name : ($first_name . ' ' . $last_name),
                         "status_badge" => $project->getStatusBadge(),
                         "progress" => $project->getProgress()
                     );
@@ -151,9 +151,10 @@ switch($request_method) {
         
     case 'PUT':
         $data = json_decode(file_get_contents("php://input"));
+        $project_id = isset($_GET['id']) ? $_GET['id'] : '';
         
-        if(!empty($data->id) && !empty($data->customer_id) && !empty($data->project_name)) {
-            $project->id = $data->id;
+        if(!empty($project_id) && !empty($data->customer_id) && !empty($data->project_name)) {
+            $project->id = $project_id;
             $project->customer_id = $data->customer_id;
             $project->project_name = $data->project_name;
             $project->description = $data->description ?? '';

+ 2 - 2
backend/api/rental_prices.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/RentalPrice.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/RentalPrice.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 110 - 0
backend/api/session_helper.php

@@ -0,0 +1,110 @@
+<?php
+/**
+ * Session Helper for API Authentication
+ * Provides session validation functions for API endpoints
+ */
+
+// Start session if not already started
+if (session_status() == PHP_SESSION_NONE) {
+    session_start();
+}
+
+/**
+ * Validate user session and return user data
+ * @param PDO $db Database connection
+ * @return array|null User data if valid, null if invalid
+ */
+function validateSession($db) {
+    // Check if session exists
+    if (!isset($_SESSION['user_id']) || empty($_SESSION['user_id'])) {
+        return null;
+    }
+    
+    try {
+        // Validate user still exists and is active
+        $query = "SELECT id, username, email, first_name, last_name, role, is_active 
+                  FROM users 
+                  WHERE id = ? AND is_active = 1";
+        
+        $stmt = $db->prepare($query);
+        $stmt->bindParam(1, $_SESSION['user_id']);
+        $stmt->execute();
+        
+        $user = $stmt->fetch(PDO::FETCH_ASSOC);
+        
+        if ($user) {
+            return $user;
+        } else {
+            // User no longer exists or is inactive, destroy session
+            session_destroy();
+            return null;
+        }
+    } catch (PDOException $e) {
+        // Database error, invalidate session
+        session_destroy();
+        return null;
+    }
+}
+
+/**
+ * Require authentication for API endpoints
+ * @param PDO $db Database connection
+ * @return array User data
+ */
+function requireAuth($db) {
+    $user = validateSession($db);
+    
+    if (!$user) {
+        http_response_code(401);
+        echo json_encode(array(
+            "message" => "Authentication required",
+            "error" => "UNAUTHORIZED"
+        ));
+        exit;
+    }
+    
+    return $user;
+}
+
+/**
+ * Send JSON response with proper headers
+ * @param mixed $data Response data
+ * @param int $statusCode HTTP status code
+ * @param string $message Response message
+ */
+function sendJsonResponse($data, $statusCode = 200, $message = '') {
+    http_response_code($statusCode);
+    
+    $response = array();
+    
+    if (!empty($message)) {
+        $response['message'] = $message;
+    }
+    
+    if ($data !== null) {
+        if (is_array($data) && isset($data['message'])) {
+            $response = array_merge($response, $data);
+        } else {
+            $response['data'] = $data;
+        }
+    }
+    
+    header('Content-Type: application/json');
+    echo json_encode($response);
+    exit;
+}
+
+/**
+ * Handle CORS preflight requests
+ */
+function handleCors() {
+    header("Access-Control-Allow-Origin: *");
+    header("Content-Type: application/json; charset=UTF-8");
+    header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+    header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+    
+    if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+        exit(0);
+    }
+}
+?>

+ 2 - 2
backend/api/subprojects.php

@@ -8,8 +8,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
     exit(0);
 }
 
-require_once '../config/database.php';
-require_once '../models/Subproject.php';
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/Subproject.php';
 
 $database = new Database();
 $db = $database->getConnection();

+ 192 - 0
backend/api/tasks.php

@@ -0,0 +1,192 @@
+<?php
+header('Content-Type: application/json');
+header('Access-Control-Allow-Origin: *');
+header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
+header('Access-Control-Allow-Headers: Content-Type, Authorization');
+
+require_once __DIR__ . '/../config/database.php';
+
+class Task {
+    private $conn;
+    private $table = 'tasks';
+    
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+    
+    public function create($data) {
+        $sql = "INSERT INTO {$this->table} (title, description, status, priority, project_id, due_date, created_at, updated_at) 
+                  VALUES (:title, :description, :status, :priority, :project_id, :due_date, NOW(), NOW())";
+        
+        $stmt = $this->conn->prepare($sql);
+        $stmt->bindParam(':title', $data['title']);
+        $stmt->bindParam(':description', $data['description']);
+        $stmt->bindParam(':status', $data['status']);
+        $stmt->bindParam(':priority', $data['priority']);
+        $stmt->bindParam(':project_id', $data['project_id']);
+        $stmt->bindParam(':due_date', $data['due_date']);
+        
+        if ($stmt->execute()) {
+            return [
+                'success' => true,
+                'message' => 'Task created successfully',
+                'id' => $this->conn->lastInsertId()
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Failed to create task'
+        ];
+    }
+    
+    public function getAll($projectId = null) {
+        $sql = "SELECT t.*, p.project_name 
+                  FROM {$this->table} t 
+                  LEFT JOIN projects p ON t.project_id = p.id 
+                  WHERE 1=1";
+        
+        if ($projectId) {
+            $sql .= " AND t.project_id = :project_id";
+        }
+        
+        $sql .= " ORDER BY t.created_at DESC";
+        
+        $stmt = $this->conn->prepare($sql);
+        
+        if ($projectId) {
+            $stmt->bindParam(':project_id', $projectId);
+        }
+        
+        $stmt->execute();
+        
+        $tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
+        
+        return [
+            'success' => true,
+            'data' => $tasks
+        ];
+    }
+    
+    public function getById($id) {
+        $sql = "SELECT t.*, p.project_name 
+                  FROM {$this->table} t 
+                  LEFT JOIN projects p ON t.project_id = p.id 
+                  WHERE t.id = :id";
+        
+        $stmt = $this->conn->prepare($sql);
+        $stmt->bindParam(':id', $id);
+        $stmt->execute();
+        
+        $task = $stmt->fetch(PDO::FETCH_ASSOC);
+        
+        if ($task) {
+            return [
+                'success' => true,
+                'data' => $task
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Task not found'
+        ];
+    }
+    
+    public function update($id, $data) {
+        $sql = "UPDATE {$this->table} 
+                  SET title = :title, description = :description, status = :status, 
+                      priority = :priority, project_id = :project_id, due_date = :due_date, updated_at = NOW() 
+                  WHERE id = :id";
+        
+        $stmt = $this->conn->prepare($sql);
+        $stmt->bindParam(':title', $data['title']);
+        $stmt->bindParam(':description', $data['description']);
+        $stmt->bindParam(':status', $data['status']);
+        $stmt->bindParam(':priority', $data['priority']);
+        $stmt->bindParam(':project_id', $data['project_id']);
+        $stmt->bindParam(':due_date', $data['due_date']);
+        $stmt->bindParam(':id', $id);
+        
+        if ($stmt->execute()) {
+            return [
+                'success' => true,
+                'message' => 'Task updated successfully'
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Failed to update task'
+        ];
+    }
+    
+    public function delete($id) {
+        $sql = "DELETE FROM {$this->table} WHERE id = :id";
+        
+        $stmt = $this->conn->prepare($sql);
+        $stmt->bindParam(':id', $id);
+        
+        if ($stmt->execute()) {
+            return [
+                'success' => true,
+                'message' => 'Task deleted successfully'
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Failed to delete task'
+        ];
+    }
+}
+
+// Handle API requests
+$method = $_SERVER['REQUEST_METHOD'];
+
+try {
+    $database = new Database();
+    $conn = $database->getConnection();
+    $task = new Task($conn);
+    
+    switch ($method) {
+        case 'GET':
+            $projectId = $_GET['project_id'] ?? null;
+            $result = $task->getAll($projectId);
+            echo json_encode($result);
+            break;
+            
+        case 'POST':
+            $data = json_decode(file_get_contents('php://input'), true);
+            $result = $task->create($data);
+            echo json_encode($result);
+            break;
+            
+        case 'PUT':
+            $data = json_decode(file_get_contents('php://input'), true);
+            $id = $data['id'];
+            $result = $task->update($id, $data);
+            echo json_encode($result);
+            break;
+            
+        case 'DELETE':
+            $id = $_GET['id'];
+            $result = $task->delete($id);
+            echo json_encode($result);
+            break;
+            
+        default:
+            echo json_encode([
+                'success' => false,
+                'message' => 'Invalid request method'
+            ]);
+            break;
+    }
+    
+} catch (Exception $e) {
+    echo json_encode([
+        'success' => false,
+        'message' => 'Database error: ' . $e->getMessage()
+    ]);
+}
+?>

+ 315 - 0
backend/api/tuloslaskelma.php

@@ -0,0 +1,315 @@
+<?php
+header('Content-Type: application/json');
+header('Access-Control-Allow-Origin: *');
+header('Access-Control-Allow-Methods: GET, OPTIONS');
+header('Access-Control-Allow-Headers: Content-Type, Authorization');
+
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/AccountingEntry.php';
+
+// Handle preflight requests
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+    exit(0);
+}
+
+try {
+    $database = new Database();
+    $conn = $database->getConnection();
+    
+    if (!$conn) {
+        throw new Exception('Database connection failed');
+    }
+
+    // Get query parameters
+    $period = $_GET['period'] ?? 'current';
+    $month = $_GET['month'] ?? null;
+    $year = $_GET['year'] ?? null;
+    
+    // Determine date range based on period
+    $dateConditions = getDateConditions($period, $month, $year);
+    
+    // Use AccountingEntry model to get data with category information
+    $sql = "SELECT 
+                ae.*,
+                ac.category_name,
+                ac.category_type,
+                ac.vat_percentage as category_vat,
+                acgn.category_group_name
+            FROM accounting_entries ae
+            LEFT JOIN accounting_categories ac ON ae.category = ac.category_code
+            LEFT JOIN accounting_category_group_names acgn ON SUBSTRING(ae.category, 1, 3) = acgn.category_group_code
+            WHERE ae.entry_date BETWEEN :start_date AND :end_date
+            ORDER BY ae.entry_type, ac.category_code";
+    
+    $stmt = $conn->prepare($sql);
+    $stmt->bindParam(':start_date', $dateConditions['start_date']);
+    $stmt->bindParam(':end_date', $dateConditions['end_date']);
+    $stmt->execute();
+    
+    $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    
+    // Group entries by category for profit and loss statement
+    $tuloslaskelmaData = groupEntriesForTuloslaskelma($entries);
+    
+    // Calculate totals
+    $totals = calculateTotals($tuloslaskelmaData);
+    
+    // Get previous period data for comparison
+    $previousDateConditions = getPreviousDateConditions($period, $month, $year);
+    $previousData = getPreviousPeriodData($conn, $previousDateConditions);
+    
+    // Combine current and previous data
+    $result = [
+        'period' => $period,
+        'date_range' => $dateConditions,
+        'current_period' => array_merge($tuloslaskelmaData, $totals),
+        'previous_period' => $previousData,
+        'comparison' => calculateComparison($tuloslaskelmaData, $previousData)
+    ];
+    
+    echo json_encode([
+        'success' => true,
+        'data' => $result
+    ]);
+    
+} catch (Exception $e) {
+    http_response_code(500);
+    echo json_encode([
+        'success' => false,
+        'message' => $e->getMessage()
+    ]);
+}
+
+function getDateConditions($period, $month, $year) {
+    $currentYear = $year ?: date('Y');
+    $currentMonth = $month ? date('m', strtotime($month)) : date('m');
+    
+    switch ($period) {
+        case 'month':
+            return [
+                'start_date' => "$currentYear-$currentMonth-01",
+                'end_date' => date('Y-m-t', strtotime("$currentYear-$currentMonth-01"))
+            ];
+        case 'quarter':
+            $quarter = ceil($currentMonth / 3);
+            $startMonth = ($quarter - 1) * 3 + 1;
+            $endMonth = $quarter * 3;
+            return [
+                'start_date' => "$currentYear-" . str_pad($startMonth, 2, '0', STR_PAD_LEFT) . "-01",
+                'end_date' => "$currentYear-" . str_pad($endMonth, 2, '0', STR_PAD_LEFT) . "-" . date('t', strtotime("$currentYear-$endMonth-01"))
+            ];
+        case 'year':
+            return [
+                'start_date' => "$currentYear-01-01",
+                'end_date' => "$currentYear-12-31"
+            ];
+        case 'current':
+        default:
+            return [
+                'start_date' => date('Y-01-01'),
+                'end_date' => date('Y-m-d')
+            ];
+    }
+}
+
+function getPreviousDateConditions($period, $month, $year) {
+    $currentYear = $year ?: date('Y');
+    $currentMonth = $month ? date('m', strtotime($month)) : date('m');
+    
+    switch ($period) {
+        case 'month':
+            $previousDate = date('Y-m', strtotime("$currentYear-$currentMonth-01 -1 month"));
+            $prevYear = date('Y', strtotime($previousDate));
+            $prevMonth = date('m', strtotime($previousDate));
+            return [
+                'start_date' => "$prevYear-$prevMonth-01",
+                'end_date' => date('Y-m-t', strtotime("$prevYear-$prevMonth-01"))
+            ];
+        case 'quarter':
+            $quarter = ceil($currentMonth / 3);
+            if ($quarter == 1) {
+                $prevYear = $currentYear - 1;
+                $prevQuarter = 4;
+            } else {
+                $prevYear = $currentYear;
+                $prevQuarter = $quarter - 1;
+            }
+            $startMonth = ($prevQuarter - 1) * 3 + 1;
+            $endMonth = $prevQuarter * 3;
+            return [
+                'start_date' => "$prevYear-" . str_pad($startMonth, 2, '0', STR_PAD_LEFT) . "-01",
+                'end_date' => "$prevYear-" . str_pad($endMonth, 2, '0', STR_PAD_LEFT) . "-" . date('t', strtotime("$prevYear-$endMonth-01"))
+            ];
+        case 'year':
+            return [
+                'start_date' => ($currentYear - 1) . "-01-01",
+                'end_date' => ($currentYear - 1) . "-12-31"
+            ];
+        case 'current':
+        default:
+            return [
+                'start_date' => (date('Y') - 1) . "-01-01",
+                'end_date' => (date('Y') - 1) . "-12-31"
+            ];
+    }
+}
+
+function groupEntriesForTuloslaskelma($entries) {
+    $grouped = [];
+    
+    // First, group by account (first three digits)
+    $accountGroups = [];
+    foreach ($entries as $entry) {
+        $accountCode = substr($entry['category'], 0, 3);
+        if (!isset($accountGroups[$accountCode])) {
+            $accountGroups[$accountCode] = [
+                'account_code' => $accountCode,
+                'account_group_name' => $entry['category_group_name'] ?: 'Unknown Group',
+                'entries' => []
+            ];
+        }
+        $accountGroups[$accountCode]['entries'][] = $entry;
+    }
+    
+    // Now, group account groups by Finnish accounting categories
+    foreach ($accountGroups as $accountCode => $accountGroup) {
+        $totalAmount = 0;
+        foreach ($accountGroup['entries'] as $entry) {
+            $totalAmount += floatval($entry['tax_free_amount']);
+        }
+        
+        $category = [
+            'code' => $accountCode,
+            'name' => $accountGroup['account_group_name'],
+            'currentAmount' => $totalAmount
+        ];
+        
+        // Group by Finnish accounting categories
+        if ($accountGroup['entries'][0]['entry_type'] === 'Tulo') {
+            if (strpos($accountCode, '30') === 0) {
+                $grouped['sales_revenue'][] = $category;
+            } else {
+                $grouped['other_revenue'][] = $category;
+            }
+        } else { // Kulu (Expense)
+            switch ($accountCode) {
+                case '333':
+                    $grouped['cost_of_goods'][] = $category;
+                    break;
+                case '335':
+                    $grouped['personnel_expenses'][] = $category;
+                    break;
+                case '336':
+                    $grouped['personnel_expenses'][] = $category;
+                    break;
+                case '337':
+                    $grouped['depreciation'][] = $category;
+                    break;
+                case '365':
+                    $grouped['tax_expenses'][] = $category;
+                    break;
+                default:
+                    $grouped['other_expenses'][] = $category;
+                    break;
+            }
+        }
+    }
+    
+    return $grouped;
+}
+
+function calculateTotals($data) {
+    $totals = [];
+    
+    foreach ($data as $category => $items) {
+        $total = 0;
+        foreach ($items as $item) {
+            $total += $item['currentAmount'];
+        }
+        $totals[$category . '_total'] = [
+            'currentAmount' => $total
+        ];
+    }
+    
+    // Calculate derived totals
+    $salesRevenueTotal = $totals['sales_revenue_total']['currentAmount'] ?? 0;
+    $otherRevenueTotal = $totals['other_revenue_total']['currentAmount'] ?? 0;
+    $totalRevenue = $salesRevenueTotal + $otherRevenueTotal;
+    
+    $costOfGoodsTotal = $totals['cost_of_goods_total']['currentAmount'] ?? 0;
+    $personnelExpensesTotal = $totals['personnel_expenses_total']['currentAmount'] ?? 0;
+    $otherExpensesTotal = $totals['other_expenses_total']['currentAmount'] ?? 0;
+    $depreciationTotal = $totals['depreciation_total']['currentAmount'] ?? 0;
+    $taxExpensesTotal = $totals['tax_expenses_total']['currentAmount'] ?? 0;
+    
+    $totalExpenses = $costOfGoodsTotal + $personnelExpensesTotal + $otherExpensesTotal + $depreciationTotal;
+    $grossProfit = $salesRevenueTotal - $costOfGoodsTotal;
+    $profitBeforeTax = $totalRevenue - $totalExpenses;
+    $netProfit = $profitBeforeTax - $taxExpensesTotal;
+    
+    $totals['total_revenue'] = ['currentAmount' => $totalRevenue];
+    $totals['total_expenses'] = ['currentAmount' => $totalExpenses];
+    $totals['gross_profit'] = ['currentAmount' => $grossProfit];
+    $totals['profit_before_tax'] = ['currentAmount' => $profitBeforeTax];
+    $totals['net_profit'] = ['currentAmount' => $netProfit];
+    
+    return $totals;
+}
+
+function getPreviousPeriodData($conn, $dateConditions) {
+    $sql = "SELECT 
+                ae.*,
+                ac.category_name,
+                ac.category_type,
+                ac.vat_percentage as category_vat,
+                acgn.category_group_name
+            FROM accounting_entries ae
+            LEFT JOIN accounting_categories ac ON ae.category = ac.category_code
+            LEFT JOIN accounting_category_group_names acgn ON SUBSTRING(ae.category, 1, 3) = acgn.category_group_code
+            WHERE ae.entry_date BETWEEN :start_date AND :end_date
+            ORDER BY ae.entry_type, ac.category_code";
+    
+    $stmt = $conn->prepare($sql);
+    $stmt->bindParam(':start_date', $dateConditions['start_date']);
+    $stmt->bindParam(':end_date', $dateConditions['end_date']);
+    $stmt->execute();
+    
+    $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    $grouped = groupEntriesForTuloslaskelma($entries);
+    
+    // Add previous amounts to categories
+    foreach ($grouped as $category => $items) {
+        foreach ($items as $index => $item) {
+            $grouped[$category][$index]['previousAmount'] = $item['currentAmount'];
+            // Also add isGroup flag for frontend
+            $grouped[$category][$index]['isGroup'] = true;
+        }
+    }
+    
+    return $grouped;
+}
+
+function calculateComparison($current, $previous) {
+    $comparison = [];
+    
+    foreach ($current as $category => $items) {
+        $comparison[$category] = [];
+        foreach ($items as $index => $item) {
+            $prevAmount = 0;
+            if (isset($previous[$category][$index])) {
+                $prevAmount = $previous[$category][$index]['currentAmount'] ?? 0;
+            }
+            
+            $change = $item['currentAmount'] - $prevAmount;
+            $changePercent = $prevAmount != 0 ? ($change / $prevAmount) * 100 : 0;
+            
+            $comparison[$category][$index] = [
+                'change' => $change,
+                'changePercent' => $changePercent
+            ];
+        }
+    }
+    
+    return $comparison;
+}
+?>

BIN
backend/api/uploads/69eb26fee9287.jpg


+ 199 - 0
backend/api/users.php

@@ -0,0 +1,199 @@
+<?php
+header("Access-Control-Allow-Origin: *");
+header("Content-Type: application/json; charset=UTF-8");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+    exit(0);
+}
+
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/User.php';
+
+$database = new Database();
+$db = $database->getConnection();
+
+$user = new User($db);
+
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+switch($request_method) {
+    case 'GET':
+        if(isset($_GET['id'])) {
+            $user->id = $_GET['id'];
+            $user->readOne();
+            
+            if($user->username != null) {
+                $user_arr = array(
+                    "id" => $user->id,
+                    "username" => $user->username,
+                    "email" => $user->email,
+                    "first_name" => $user->first_name,
+                    "last_name" => $user->last_name,
+                    "role" => $user->role,
+                    "is_active" => $user->is_active,
+                    "last_login" => $user->last_login,
+                    "created_at" => $user->created_at,
+                    "updated_at" => $user->updated_at,
+                    "role_badge" => $user->getRoleBadge(),
+                    "status_badge" => $user->getStatusBadge()
+                );
+                
+                http_response_code(200);
+                echo json_encode($user_arr);
+            } else {
+                http_response_code(404);
+                echo json_encode(array("message" => "User not found."));
+            }
+        } else {
+            $stmt = $user->read();
+            $num = $stmt->rowCount();
+            
+            if($num > 0) {
+                $users_arr = array();
+                $users_arr["records"] = array();
+                
+                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+                    extract($row);
+                    
+                    $user_item = array(
+                        "id" => $id,
+                        "username" => $username,
+                        "email" => $email,
+                        "first_name" => $first_name,
+                        "last_name" => $last_name,
+                        "role" => $role,
+                        "is_active" => $is_active,
+                        "last_login" => $last_login,
+                        "created_at" => $created_at,
+                        "updated_at" => $updated_at,
+                        "role_badge" => getRoleBadge($role),
+                        "status_badge" => getStatusBadge($is_active)
+                    );
+                    
+                    array_push($users_arr["records"], $user_item);
+                }
+                
+                http_response_code(200);
+                echo json_encode($users_arr);
+            } else {
+                http_response_code(200);
+                echo json_encode(array("records" => array()));
+            }
+        }
+        break;
+        
+    case 'POST':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->username) && !empty($data->email) && !empty($data->first_name) && !empty($data->last_name) && !empty($data->password)) {
+            
+            // Check if username or email already exists
+            $existing_user = $user->findByEmail($data->email);
+            if($existing_user) {
+                http_response_code(409);
+                echo json_encode(array("message" => "Email already exists"));
+                break;
+            }
+            
+            $existing_user = $user->findByUsername($data->username);
+            if($existing_user) {
+                http_response_code(409);
+                echo json_encode(array("message" => "Username already exists"));
+                break;
+            }
+            
+            $user->username = $data->username;
+            $user->email = $data->email;
+            $user->password_hash = $data->password;
+            $user->first_name = $data->first_name;
+            $user->last_name = $data->last_name;
+            $user->role = $data->role ?? 'user';
+            $user->is_active = $data->is_active ?? true;
+            
+            if($user->create()) {
+                http_response_code(201);
+                echo json_encode(array("message" => "User was created."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to create user."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to create user. Data is incomplete."));
+        }
+        break;
+        
+    case 'PUT':
+        $data = json_decode(file_get_contents("php://input"));
+        
+        if(!empty($data->id) && !empty($data->username) && !empty($data->email) && !empty($data->first_name) && !empty($data->last_name)) {
+            $user->id = $data->id;
+            $user->username = $data->username;
+            $user->email = $data->email;
+            $user->first_name = $data->first_name;
+            $user->last_name = $data->last_name;
+            $user->role = $data->role ?? 'user';
+            $user->is_active = $data->is_active ?? true;
+            
+            // Update password if provided
+            $update_password = !empty($data->password);
+            if($update_password) {
+                $user->password_hash = $data->password;
+            }
+            
+            if($user->update($update_password)) {
+                http_response_code(200);
+                echo json_encode(array("message" => "User was updated."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to update user."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to update user. Data is incomplete."));
+        }
+        break;
+        
+    case 'DELETE':
+        if(isset($_GET['id'])) {
+            $user->id = $_GET['id'];
+            
+            if($user->delete()) {
+                http_response_code(200);
+                echo json_encode(array("message" => "User was deleted."));
+            } else {
+                http_response_code(503);
+                echo json_encode(array("message" => "Unable to delete user."));
+            }
+        } else {
+            http_response_code(400);
+            echo json_encode(array("message" => "Unable to delete user. ID is missing."));
+        }
+        break;
+        
+    default:
+        http_response_code(405);
+        echo json_encode(array("message" => "Method not allowed."));
+        break;
+}
+
+// Helper functions
+function getRoleBadge($role) {
+    $badges = array(
+        'admin' => '<span style="background-color: #dc3545; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">Admin</span>',
+        'manager' => '<span style="background-color: #28a745; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">Manager</span>',
+        'user' => '<span style="background-color: #6c757d; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">User</span>'
+    );
+    return $badges[$role] ?? $badges['user'];
+}
+
+function getStatusBadge($is_active) {
+    if($is_active) {
+        return '<span style="background-color: #28a745; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">Active</span>';
+    } else {
+        return '<span style="background-color: #6c757d; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500;">Inactive</span>';
+    }
+}
+?>

+ 196 - 0
backend/api/work_hours.php

@@ -0,0 +1,196 @@
+<?php
+header('Content-Type: application/json');
+header('Access-Control-Allow-Origin: *');
+header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
+header('Access-Control-Allow-Headers: Content-Type, Authorization');
+
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../models/WorkHour.php';
+
+class WorkHourAPI {
+    private $conn;
+    private $workHour;
+    
+    public function __construct() {
+        $database = new Database();
+        $this->conn = $database->getConnection();
+        $this->workHour = new WorkHour($this->conn);
+    }
+    
+    public function create($data) {
+        $this->workHour->task_id = $data['task_id'];
+        $this->workHour->user_id = $data['user_id'];
+        $this->workHour->date = $data['date'];
+        $this->workHour->hours = $data['hours'];
+        $this->workHour->description = $data['description'] ?? '';
+        $this->workHour->rate = $data['rate'] ?? null;
+        
+        // Calculate total amount if rate is provided
+        if ($this->workHour->rate) {
+            $this->workHour->total_amount = $this->workHour->hours * $this->workHour->rate;
+        }
+        
+        if ($this->workHour->create()) {
+            return [
+                'success' => true,
+                'message' => 'Work hour entry created successfully',
+                'id' => $this->conn->lastInsertId()
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Failed to create work hour entry'
+        ];
+    }
+    
+    public function getAll($task_id = null, $user_id = null) {
+        if ($task_id) {
+            $stmt = $this->workHour->readByTask($task_id);
+        } elseif ($user_id) {
+            // For user-specific queries, we'd need to add a readByUser method
+            $stmt = $this->workHour->read();
+        } else {
+            $stmt = $this->workHour->read();
+        }
+        
+        $workHours = [];
+        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+            $workHours[] = $row;
+        }
+        
+        return [
+            'success' => true,
+            'data' => $workHours
+        ];
+    }
+    
+    public function getById($id) {
+        $this->workHour->id = $id;
+        $this->workHour->readOne();
+        
+        if ($this->workHour->id) {
+            return [
+                'success' => true,
+                'data' => [
+                    'id' => $this->workHour->id,
+                    'task_id' => $this->workHour->task_id,
+                    'user_id' => $this->workHour->user_id,
+                    'date' => $this->workHour->date,
+                    'hours' => $this->workHour->hours,
+                    'description' => $this->workHour->description,
+                    'rate' => $this->workHour->rate,
+                    'total_amount' => $this->workHour->total_amount,
+                    'created_at' => $this->workHour->created_at,
+                    'updated_at' => $this->workHour->updated_at
+                ]
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Work hour entry not found'
+        ];
+    }
+    
+    public function update($id, $data) {
+        $this->workHour->id = $id;
+        $this->workHour->task_id = $data['task_id'];
+        $this->workHour->user_id = $data['user_id'];
+        $this->workHour->date = $data['date'];
+        $this->workHour->hours = $data['hours'];
+        $this->workHour->description = $data['description'] ?? '';
+        $this->workHour->rate = $data['rate'] ?? null;
+        
+        // Recalculate total amount if rate is provided
+        if ($this->workHour->rate) {
+            $this->workHour->total_amount = $this->workHour->hours * $this->workHour->rate;
+        }
+        
+        if ($this->workHour->update()) {
+            return [
+                'success' => true,
+                'message' => 'Work hour entry updated successfully'
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Failed to update work hour entry'
+        ];
+    }
+    
+    public function delete($id) {
+        $this->workHour->id = $id;
+        
+        if ($this->workHour->delete()) {
+            return [
+                'success' => true,
+                'message' => 'Work hour entry deleted successfully'
+            ];
+        }
+        
+        return [
+            'success' => false,
+            'message' => 'Failed to delete work hour entry'
+        ];
+    }
+    
+    public function getTaskSummary($task_id) {
+        $summary = $this->workHour->getTotalHoursByTask($task_id);
+        
+        return [
+            'success' => true,
+            'data' => $summary
+        ];
+    }
+}
+
+// Handle API requests
+$method = $_SERVER['REQUEST_METHOD'];
+
+try {
+    $api = new WorkHourAPI();
+    
+    switch ($method) {
+        case 'GET':
+            $task_id = $_GET['task_id'] ?? null;
+            $user_id = $_GET['user_id'] ?? null;
+            $result = $api->getAll($task_id, $user_id);
+            echo json_encode($result);
+            break;
+            
+        case 'POST':
+            $data = json_decode(file_get_contents('php://input'), true);
+            $result = $api->create($data);
+            echo json_encode($result);
+            break;
+            
+        case 'PUT':
+            $data = json_decode(file_get_contents('php://input'), true);
+            $id = $data['id'];
+            $result = $api->update($id, $data);
+            echo json_encode($result);
+            break;
+            
+        case 'DELETE':
+            $id = $_GET['id'];
+            $result = $api->delete($id);
+            echo json_encode($result);
+            break;
+            
+        default:
+            echo json_encode([
+                'success' => false,
+                'message' => 'Invalid request method'
+            ]);
+            break;
+    }
+    
+} catch (Exception $e) {
+    echo json_encode([
+        'success' => false,
+        'message' => 'Database error: ' . $e->getMessage()
+    ]);
+}
+?>

+ 47 - 0
backend/config/company.php

@@ -0,0 +1,47 @@
+<?php
+class Company {
+    private $name;
+    private $address;
+    private $city;
+    private $postal_code;
+    private $country;
+    private $phone;
+    private $email;
+    private $y_tunnus;
+
+    public function __construct() {
+        $this->name = getenv('COMPANY_NAME') ?: 'Inventory Management System';
+        $this->address = getenv('COMPANY_ADDRESS') ?: '123 Business Street';
+        $this->city = getenv('COMPANY_CITY') ?: 'Helsinki';
+        $this->postal_code = getenv('COMPANY_POSTAL_CODE') ?: '00100';
+        $this->country = getenv('COMPANY_COUNTRY') ?: 'Finland';
+        $this->phone = getenv('COMPANY_PHONE') ?: '+358 123 456 789';
+        $this->email = getenv('COMPANY_EMAIL') ?: 'info@company.com';
+        $this->y_tunnus = getenv('COMPANY_Y_TUNNUS') ?: '1234567-8';
+    }
+
+    public function getDetails() {
+        return [
+            'name' => $this->name,
+            'address' => $this->address,
+            'city' => $this->city,
+            'postal_code' => $this->postal_code,
+            'country' => $this->country,
+            'phone' => $this->phone,
+            'email' => $this->email,
+            'y_tunnus' => $this->y_tunnus,
+            'full_address' => $this->address . ', ' . $this->postal_code . ' ' . $this->city . ', ' . $this->country,
+            'contact_info' => $this->phone . ' | ' . $this->email
+        ];
+    }
+
+    public function getName() { return $this->name; }
+    public function getAddress() { return $this->address; }
+    public function getCity() { return $this->city; }
+    public function getPostalCode() { return $this->postal_code; }
+    public function getCountry() { return $this->country; }
+    public function getPhone() { return $this->phone; }
+    public function getEmail() { return $this->email; }
+    public function getYTunnus() { return $this->y_tunnus; }
+}
+?>

+ 15 - 6
backend/config/database.php

@@ -1,20 +1,29 @@
 <?php
 class Database {
-    private $host = '10.15.10.8';
-    private $db_name = 'inventory_db';
-    private $username = 'inventory_db';
-    private $password = 'fNk@6P[!cTK)wgkO';
+    private $host;
+    private $db_name;
+    private $username;
+    private $password;
     public $conn;
 
+    public function __construct() {
+        $this->host = getenv('DB_HOST') ?: 'localhost';
+        $this->db_name = getenv('DB_NAME') ?: 'inventory_db';
+        $this->username = getenv('DB_USER') ?: 'root';
+        $this->password = getenv('DB_PASS') ?: '';
+    }
+
     public function getConnection() {
         $this->conn = null;
 
         try {
-            $this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
+            $dsn = "mysql:host=" . $this->host . ";dbname=" . $this->db_name . ";charset=utf8mb4";
+            $this->conn = new PDO($dsn, $this->username, $this->password);
             $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
             $this->conn->exec("set names utf8");
         } catch(PDOException $exception) {
-            echo "Connection error: " . $exception->getMessage();
+            error_log("Database connection error: " . $exception->getMessage());
+            throw new Exception("Database connection failed");
         }
 
         return $this->conn;

+ 35 - 0
backend/create_category_groups.php

@@ -0,0 +1,35 @@
+<?php
+require_once __DIR__ . '/config/database.php';
+
+try {
+    $database = new Database();
+    $conn = $database->getConnection();
+    
+    if (!$conn) {
+        throw new Exception('Database connection failed');
+    }
+    
+    // Read and execute the schema file
+    $schemaFile = __DIR__ . '/database/accounting_category_groups_schema.sql';
+    $schema = file_get_contents($schemaFile);
+    
+    // Split by semicolons and execute each statement
+    $statements = array_filter(array_map('trim', explode(';', $schema)));
+    
+    foreach ($statements as $statement) {
+        if (!empty($statement)) {
+            try {
+                $conn->exec($statement);
+                echo "Executed: " . $statement . "\n";
+            } catch (Exception $e) {
+                echo "Error: " . $e->getMessage() . "\n";
+            }
+        }
+    }
+    
+    echo "Category groups table created successfully!\n";
+    
+} catch (Exception $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}
+?>

+ 7 - 0
backend/create_default_user.sql

@@ -0,0 +1,7 @@
+-- Insert default admin user
+INSERT INTO users (username, email, password_hash, first_name, last_name, role, is_active) VALUES 
+('admin', 'admin@inventory.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin', 'User', 'admin', TRUE);
+
+-- Password for this user is: password
+-- The hash is for the password "password"
+-- You should change this password after first login

+ 22 - 0
backend/create_groups_table.php

@@ -0,0 +1,22 @@
+<?php
+try {
+    // Create the table using the existing database connection
+    $pdo = new PDO('mysql:host=localhost;dbname=inventory_db', 'root', '');
+    
+    $sql = "CREATE TABLE IF NOT EXISTS accounting_category_group_names (
+        id INT AUTO_INCREMENT PRIMARY KEY,
+        category_group_code VARCHAR(3) NOT NULL,
+        category_group_name VARCHAR(200) NOT NULL,
+        description TEXT NULL,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        UNIQUE KEY unique_category_group_code (category_group_code)
+    )";
+    
+    $pdo->exec($sql);
+    echo "Table created successfully!\n";
+    
+} catch (PDOException $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}
+?>

+ 54 - 0
backend/create_groups_table_fixed.php

@@ -0,0 +1,54 @@
+<?php
+try {
+    // Create table using correct database connection
+    $pdo = new PDO('mysql:host=10.8.10.31;dbname=inventory_db', 'inventory_db', 'fNk@6P[!cTK)wgkO');
+    
+    $sql = "CREATE TABLE IF NOT EXISTS accounting_category_group_names (
+        id INT AUTO_INCREMENT PRIMARY KEY,
+        category_group_code VARCHAR(3) NOT NULL,
+        category_group_name VARCHAR(200) NOT NULL,
+        description TEXT NULL,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        UNIQUE KEY unique_category_group_code (category_group_code)
+    )";
+    
+    $pdo->exec($sql);
+    echo "Table created successfully!\n";
+    
+    // Insert default category groups
+    $insertSql = "INSERT IGNORE INTO accounting_category_group_names (category_group_code, category_group_name) VALUES
+        ('300', 'Tuotot ammatista'),
+        ('301', 'Muut tuotot'),
+        ('312', 'Varausten vähennys'),
+        ('313', 'Auton yksityiskäyttö'),
+        ('314', 'Tavaroiden yksityiskäyttö'),
+        ('315', 'Muut yksityiskäyttö'),
+        ('317', 'Tuloslaskelman verovapaat tuotot'),
+        ('318', 'Saadut avustukset ja tuet'),
+        ('319', 'Saadut osingot'),
+        ('323', 'Korkotuotot ja muut rahoitustuotot'),
+        ('324', 'Muut veronalaiset tuotot'),
+        ('333', 'Ostot ja varastojen muutokset'),
+        ('334', 'Ulkopuoliset palvelut'),
+        ('335', 'Palkat ja palkkiot'),
+        ('336', 'Eläke- ja henkilösivukulut'),
+        ('337', 'Poistot'),
+        ('341', 'Edustuskulut'),
+        ('343', 'Vuokrat'),
+        ('344', 'Matkakulut'),
+        ('368', 'Julkinen liikenne'),
+        ('349', 'Muut rahoituskulut'),
+        ('353', 'Varausten lisäykset'),
+        ('354', 'Kirjanpidon ulkopuoliset vähennyskelpoiset kulut'),
+        ('365', 'Välittömät verot'),
+        ('366', 'Sakot ja muut rangaistusmaksut'),
+        ('367', 'Muut vähennyskelvottomat kulut')";
+    
+    $pdo->exec($insertSql);
+    echo "Default groups inserted successfully!\n";
+    
+} catch (PDOException $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}
+?>

+ 36 - 0
backend/create_tasks_table.php

@@ -0,0 +1,36 @@
+<?php
+require_once __DIR__ . '/../config/database.php';
+
+try {
+    $database = new Database();
+    $conn = $database->getConnection();
+    
+    // Read and execute the schema file
+    $schemaFile = __DIR__ . '/database/tasks_schema.sql';
+    $schema = file_get_contents($schemaFile);
+    
+    if ($conn) {
+        // Split by semicolons and execute each statement
+        $statements = array_filter(array_map('trim', explode(';', $schema)));
+        
+        foreach ($statements as $statement) {
+            if (!empty(trim($statement))) {
+                echo "Executing: $statement\n";
+                if ($conn->exec($statement)) {
+                    echo "Success: $statement\n";
+                } else {
+                    echo "Error: $statement\n";
+                }
+            }
+        }
+        
+        echo "Tasks table creation completed!\n";
+        
+    } else {
+        echo "Database connection failed: " . $conn->errorInfo()[2] . "\n";
+    }
+    
+} catch (Exception $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}
+?>

+ 41 - 0
backend/database/accounting_category_groups_schema.sql

@@ -0,0 +1,41 @@
+-- Accounting category groups schema
+-- Groups accounting categories by first three digits of category code
+
+CREATE TABLE IF NOT EXISTS accounting_category_group_names (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    category_group_code VARCHAR(3) NOT NULL,
+    category_group_name VARCHAR(200) NOT NULL,
+    description TEXT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY unique_category_group_code (category_group_code)
+);
+
+-- Insert default category groups based on Finnish accounting standards
+INSERT IGNORE INTO accounting_category_group_names (category_group_code, category_group_name, description) VALUES
+('300', 'Myyntituotot', 'Sales revenue'),
+('301', 'Myyntituotot', 'Sales revenue'),
+('302', 'Myyntituotot', 'Sales revenue'),
+('303', 'Myyntituotot', 'Sales revenue'),
+('308', 'Myyntituotot', 'Sales revenue'),
+('309', 'Myyntituotot', 'Sales revenue'),
+('310', 'Myyntituotot', 'Sales revenue'),
+('311', 'Myyntituotot', 'Sales revenue'),
+('312', 'Myyntituotot', 'Sales revenue'),
+('313', 'Myyntituotot', 'Sales revenue'),
+('314', 'Myyntituotot', 'Sales revenue'),
+('333', 'Ainekset ja palvelut', 'Cost of goods and services'),
+('334', 'Henkilöstökulut', 'Personnel expenses'),
+('335', 'Poistot ja alaskennaukset', 'Depreciation and write-downs'),
+('336', 'Muut kulut', 'Other expenses'),
+('337', 'Rahoituskulut', 'Financing costs'),
+('341', 'Varausten muutokset', 'Asset changes'),
+('343', 'Varausten myynnit', 'Asset sales'),
+('344', 'Matkakulut', 'Travel expenses'),
+('346', 'Muut kulut', 'Other expenses'),
+('349', 'Poistot', 'Depreciation'),
+('353', 'Varainsiirrot', 'Asset transfers'),
+('354', 'Vähennyskelvottomat kulut', 'Non-deductible expenses'),
+('365', 'Vakuutukset', 'Insurance'),
+('366', 'Verot ja maksut', 'Taxes and fees'),
+('367', 'Yhtiölainat ja muut rahoituskulut', 'Company loans and other financing costs');

+ 9 - 0
backend/database/add_date_of_purchase.sql

@@ -0,0 +1,9 @@
+-- Add date_of_purchase column to items table
+ALTER TABLE items 
+ADD COLUMN date_of_purchase DATE NULL 
+AFTER price;
+
+-- Set default date for existing items (current date)
+UPDATE items 
+SET date_of_purchase = CURDATE() 
+WHERE date_of_purchase IS NULL;

+ 19 - 0
backend/database/add_vat_column.sql

@@ -0,0 +1,19 @@
+-- Add VAT percentage column to chart_of_accounts table
+ALTER TABLE chart_of_accounts 
+ADD COLUMN vat_percentage DECIMAL(5,2) DEFAULT 0.00 
+AFTER current_balance;
+
+-- Update existing revenue accounts to have default VAT rates (24% for Finland)
+UPDATE chart_of_accounts 
+SET vat_percentage = 24.00 
+WHERE account_type = 'revenue';
+
+-- Update existing expense accounts that typically have VAT
+UPDATE chart_of_accounts 
+SET vat_percentage = 24.00 
+WHERE account_type = 'expense' AND (
+    account_name LIKE '%rent%' OR 
+    account_name LIKE '%utilities%' OR 
+    account_name LIKE '%supplies%' OR
+    account_name LIKE '%services%'
+);

+ 19 - 0
backend/database/contact_persons_schema.sql

@@ -0,0 +1,19 @@
+-- Contact Persons Table
+CREATE TABLE IF NOT EXISTS contact_persons (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    client_id INT NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255),
+    phone VARCHAR(50),
+    position VARCHAR(100),
+    department VARCHAR(100),
+    is_primary BOOLEAN DEFAULT FALSE,
+    notes TEXT,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
+    INDEX idx_client_id (client_id),
+    INDEX idx_name (first_name, last_name),
+    INDEX idx_email (email)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

+ 143 - 0
backend/database/excel_accounting_schema.sql

@@ -0,0 +1,143 @@
+-- Database schema for Excel-based Finnish accounting system
+-- Based on Excelpohja.xlsx structure
+
+-- Monthly accounting entries table
+CREATE TABLE IF NOT EXISTS accounting_entries (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    entry_date DATE NOT NULL,
+    description TEXT NOT NULL,
+    entry_type ENUM('Tulo', 'Kulu') NOT NULL,
+    category VARCHAR(100),
+    tax_free_amount DECIMAL(12,2) DEFAULT 0.00,
+    vat_percentage DECIMAL(5,2) DEFAULT 0.00,
+    vat_25_5 DECIMAL(12,2) DEFAULT 0.00,
+    vat_14 DECIMAL(12,2) DEFAULT 0.00,
+    vat_10 DECIMAL(12,2) DEFAULT 0.00,
+    total_amount DECIMAL(12,2) DEFAULT 0.00,
+    net_amount DECIMAL(12,2) DEFAULT 0.00,
+    vat_amount DECIMAL(12,2) DEFAULT 0.00,
+    reference_number VARCHAR(50),
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_entry_date (entry_date),
+    INDEX idx_entry_type (entry_type),
+    INDEX idx_category (category)
+);
+
+-- Accounting categories configuration
+CREATE TABLE IF NOT EXISTS accounting_categories (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    category_code VARCHAR(20) NOT NULL,
+    category_name VARCHAR(200) NOT NULL,
+    category_type ENUM('Tulo', 'Kulu') NOT NULL,
+    vat_percentage DECIMAL(5,2) DEFAULT 0.00,
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY unique_category_code (category_code)
+);
+
+-- Monthly summaries
+CREATE TABLE IF NOT EXISTS monthly_summaries (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    year INT NOT NULL,
+    month INT NOT NULL,
+    total_income DECIMAL(12,2) DEFAULT 0.00,
+    total_expenses DECIMAL(12,2) DEFAULT 0.00,
+    net_result DECIMAL(12,2) DEFAULT 0.00,
+    vat_payable DECIMAL(12,2) DEFAULT 0.00,
+    vat_deductible DECIMAL(12,2) DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY unique_year_month (year, month)
+);
+
+-- Expense categories (from Excel sheet structure)
+CREATE TABLE IF NOT EXISTS expense_categories (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    category_code VARCHAR(50) NOT NULL,
+    category_name VARCHAR(200) NOT NULL,
+    parent_id INT NULL,
+    description TEXT,
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (parent_id) REFERENCES expense_categories(id) ON DELETE SET NULL,
+    UNIQUE KEY unique_category_code (category_code)
+);
+
+-- Income categories (from Excel sheet structure)
+CREATE TABLE IF NOT EXISTS income_categories (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    category_code VARCHAR(50) NOT NULL,
+    category_name VARCHAR(200) NOT NULL,
+    parent_id INT NULL,
+    description TEXT,
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (parent_id) REFERENCES income_categories(id) ON DELETE SET NULL,
+    UNIQUE KEY unique_category_code (category_code)
+);
+
+-- Insert default expense categories
+INSERT IGNORE INTO expense_categories (category_code, category_name, description) VALUES
+('333', 'Ostot ja ulkopuoliset palvelut', 'Purchases and external services'),
+('334', 'Henkilöstökulut', 'Personnel costs'),
+('335', 'Poistot, edustuskulut ja vuokrat', 'Depreciation, representation and rental costs'),
+('336', 'Muut vähennyskelpoiset kulut', 'Other deductible expenses'),
+('337', 'Rahoituskulut', 'Financing costs'),
+('341', 'Varausten muutokset', 'Asset changes'),
+('343', 'Varausten myynnit', 'Asset sales'),
+('344', 'Matkakulut', 'Travel expenses'),
+('344 Matka-auto', 'Matkakulut - Car', 'Car travel expenses'),
+('344 Ajoneuvo', 'Matkakulut - Vehicle', 'Vehicle travel expenses'),
+('344 Muu', 'Matkakulut - Other', 'Other travel expenses'),
+('344 Kokous', 'Matkakulut - Meeting', 'Meeting travel expenses'),
+('346', 'Muut kulut', 'Other expenses'),
+('349', 'Poistot', 'Depreciation'),
+('353', 'Varainsiirrot', 'Asset transfers'),
+('354', 'Vähennyskelvottomat kulut', 'Non-deductible expenses'),
+('365', 'Vakuutukset', 'Insurance'),
+('366', 'Verot ja maksut', 'Taxes and fees'),
+('367', 'Yhtiölainat ja muut rahoituskulut', 'Company loans and other financing costs');
+
+-- Insert default income categories
+INSERT IGNORE INTO income_categories (category_code, category_name, description) VALUES
+('300', 'Myynti - EU 0 %', 'Sales - EU 0% VAT'),
+('301', 'Myynti - Muut maat', 'Sales - Other countries'),
+('302', 'Myynti - Käänteinen vero', 'Sales - Reverse VAT'),
+('303', 'Myynti - Suomi 0 %', 'Sales - Finland 0% VAT'),
+('308', 'Myyntitavarat', 'Sales of fixed assets'),
+('309', 'Osuuskorvaukset', 'Share compensation'),
+('310', 'Korkotulot ja muut rahoitustuotot', 'Interest income and other financial income'),
+('311', 'Voitonjako', 'Profit distribution'),
+('312', 'Sijoitustoiminnan tuotot', 'Investment income'),
+('313', 'Apurahat ja muut avustukset', 'Grants and other subsidies'),
+('314', 'Muu tuotto', 'Other income');
+
+-- Insert accounting categories
+INSERT IGNORE INTO accounting_categories (category_code, category_name, category_type, vat_percentage) VALUES
+('333', 'Ostot ja ulkopuoliset palvelut', 'Kulu', 24.00),
+('334', 'Henkilöstökulut', 'Kulu', 0.00),
+('335', 'Poistot, edustuskulut ja vuokrat', 'Kulu', 24.00),
+('336', 'Muut vähennyskelpoiset kulut', 'Kulu', 24.00),
+('337', 'Rahoituskulut', 'Kulu', 0.00),
+('341', 'Varausten muutokset', 'Kulu', 0.00),
+('343', 'Varausten myynnit', 'Kulu', 0.00),
+('344', 'Matkakulut', 'Kulu', 24.00),
+('346', 'Muut kulut', 'Kulu', 24.00),
+('349', 'Poistot', 'Kulu', 0.00),
+('353', 'Varainsiirrot', 'Kulu', 0.00),
+('354', 'Vähennyskelvottomat kulut', 'Kulu', 0.00),
+('365', 'Vakuutukset', 'Kulu', 24.00),
+('366', 'Verot ja maksut', 'Kulu', 0.00),
+('367', 'Yhtiölainat ja muut rahoituskulut', 'Kulu', 0.00),
+('300', 'Myynti - EU 0 %', 'Tulo', 0.00),
+('301', 'Myynti - Muut maat', 'Tulo', 0.00),
+('302', 'Myynti - Käänteinen vero', 'Tulo', 0.00),
+('303', 'Myynti - Suomi 0 %', 'Tulo', 0.00),
+('308', 'Myyntitavarat', 'Tulo', 0.00),
+('309', 'Osuuskorvaukset', 'Tulo', 0.00),
+('310', 'Korkotulot ja muut rahoitustuotot', 'Tulo', 0.00),
+('311', 'Voitonjako', 'Tulo', 0.00),
+('312', 'Sijoitustoiminnan tuotot', 'Tulo', 0.00),
+('313', 'Apurahat ja muut avustukset', 'Tulo', 0.00),
+('314', 'Muu tuotto', 'Tulo', 0.00);

+ 59 - 0
backend/database/finnish_chart_of_accounts.sql

@@ -0,0 +1,59 @@
+-- Finnish Chart of Accounts Structure
+-- Following Finnish accounting standards and numbering system
+
+-- Clear existing data and reset structure
+TRUNCATE TABLE chart_of_accounts;
+
+-- ASSETS (VASTA-OMAISUUS) - 1000-1999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('1000', 'Käyttöpääoma', 'asset', 'Pääomaisuus', 0.00, 0.00, 0.00, 1),
+('1100', 'Saamiset ja vaatimukset', 'asset', 'Tase-erät ja poistot', 0.00, 0.00, 0.00, 1),
+('1200', 'Aineelliset vaihto-omaisuudet', 'asset', 'Varasto ja raaka-aineet', 0.00, 0.00, 24.00, 1),
+('1300', 'Pitkän aikaiset sijoitukset', 'asset', 'Pitkän aikaiset sijoitukset', 0.00, 0.00, 0.00, 1),
+('1400', 'Kiinteistöt', 'asset', 'Kiinteistöt ja rakennukset', 0.00, 0.00, 0.00, 1),
+('1500', 'Koneet ja kalusto', 'asset', 'Koneet, kalusto ja laitteet', 0.00, 0.00, 24.00, 1),
+('1600', 'Intangible assets', 'asset', 'Aineettomat varat', 0.00, 0.00, 0.00, 1),
+('1700', 'Sijoitukset rahoituslaitoksissa', 'asset', 'Rahoituslaitokset', 0.00, 0.00, 0.00, 1),
+('1800', 'Erityiset vaihto-omaisuudet', 'asset', 'Erityiset vaihto-omaisuudet', 0.00, 0.00, 0.00, 1),
+('1900', 'Vaihto-omaisuudet luovutukseen', 'asset', 'Myyntikohteiset varat', 0.00, 0.00, 0.00, 1),
+
+-- LIABILITIES (VELAT) - 2000-2999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('2000', 'Oma pääoma', 'liability', 'Oma pääoma', 0.00, 0.00, 0.00, 1),
+('2100', 'Pitkän aikaiset velat', 'liability', 'Pitkän aikaiset velat', 0.00, 0.00, 0.00, 1),
+('2200', 'Verovelat', 'liability', 'Verovelat ja vakuudet', 0.00, 0.00, 0.00, 1),
+('2300', 'Saamiset', 'liability', 'Ostovelat ja saamiset', 0.00, 0.00, 0.00, 1),
+('2400', 'Maksuvelat', 'liability', 'Maksuvelat ja saadut ennakot', 0.00, 0.00, 0.00, 1),
+('2500', 'Verovelat konserneille', 'liability', 'Verovelat konserneille', 0.00, 0.00, 0.00, 1),
+('2600', 'Muut velat', 'liability', 'Muut velat', 0.00, 0.00, 0.00, 1),
+('2700', 'Siirrot konserninointiin', 'liability', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1),
+('2800', 'Tilikaudin tuloverot', 'liability', 'Tilikaudin tuloverot', 0.00, 0.00, 0.00, 1),
+
+-- EQUITY (OMA PÄÄOMA) - 3000-3999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('3000', 'Osakepääoma', 'equity', 'Osakepääoma', 0.00, 0.00, 0.00, 1),
+('3100', 'Sijoitetun pääoman rahastot', 'equity', 'Sijoitetun pääoman rahastot', 0.00, 0.00, 0.00, 1),
+('3200', 'Muut pääomat rahastot', 'equity', 'Muut pääomat rahastot', 0.00, 0.00, 0.00, 1),
+('3300', 'Tilikaudin voitto', 'equity', 'Tilikaudin voitto/tappio', 0.00, 0.00, 0.00, 1),
+
+-- REVENUE (TUOTOT) - 4000-4999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('4000', 'Myyntituotot', 'revenue', 'Myyntituotot', 0.00, 0.00, 24.00, 1),
+('4100', 'Palvelutuotot', 'revenue', 'Palvelutuotot', 0.00, 0.00, 24.00, 1),
+('4200', 'Vuokratuotot', 'revenue', 'Vuokratuotot', 0.00, 0.00, 24.00, 1),
+('4300', 'Korko- ja valuuttatuotot', 'revenue', 'Korko- ja valuuttatuotot', 0.00, 0.00, 0.00, 1),
+('4400', 'Muu tuotto', 'revenue', 'Muu tuotto', 0.00, 0.00, 0.00, 1),
+('4500', 'Siirrot konserninointiin', 'revenue', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1),
+
+-- EXPENSES (KULUT) - 5000-5999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('5000', 'Aineelliset kulut', 'expense', 'Aineelliset kulut ja palvelut', 0.00, 0.00, 24.00, 1),
+('5100', 'Henkilöstökulut', 'expense', 'Henkilöstökulut', 0.00, 0.00, 0.00, 1),
+('5200', 'Poistot ja alennukset', 'expense', 'Poistot ja alennukset', 0.00, 0.00, 0.00, 1),
+('5300', 'Vuokrat', 'expense', 'Vuokrat ja vuokrakulut', 0.00, 0.00, 0.00, 1),
+('5400', 'Korko- ja rahoituskulut', 'expense', 'Korko- ja rahoituskulut', 0.00, 0.00, 0.00, 1),
+('5500', 'Poistot', 'expense', 'Tase-erät ja poistot', 0.00, 0.00, 0.00, 1),
+('5600', 'Arvonmäärityksen alennukset', 'expense', 'Arvonmäärityksen alennukset', 0.00, 0.00, 0.00, 1),
+('5700', 'Verotukset', 'expense', 'Verotukset ja maksut', 0.00, 0.00, 0.00, 1),
+('5800', 'Muut kulut', 'expense', 'Muut kulut', 0.00, 0.00, 0.00, 1),
+('5900', 'Siirrot konserninointiin', 'expense', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1);

+ 65 - 0
backend/database/finnish_chart_of_accounts_fixed.sql

@@ -0,0 +1,65 @@
+-- Finnish Chart of Accounts Structure
+-- Following Finnish accounting standards and numbering system
+
+-- Disable foreign key checks temporarily
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- Clear existing data
+TRUNCATE TABLE chart_of_accounts;
+
+-- Re-enable foreign key checks
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- ASSETS (VASTA-OMAISUUS) - 1000-1999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('1000', 'Käyttöpääoma', 'asset', 'Pääomaisuus', 0.00, 0.00, 0.00, 1),
+('1100', 'Saamiset ja vaatimukset', 'asset', 'Tase-erät ja poistot', 0.00, 0.00, 0.00, 1),
+('1200', 'Aineelliset vaihto-omaisuudet', 'asset', 'Varasto ja raaka-aineet', 0.00, 0.00, 24.00, 1),
+('1300', 'Pitkän aikaiset sijoitukset', 'asset', 'Pitkän aikaiset sijoitukset', 0.00, 0.00, 0.00, 1),
+('1400', 'Kiinteistöt', 'asset', 'Kiinteistöt ja rakennukset', 0.00, 0.00, 0.00, 1),
+('1500', 'Koneet ja kalusto', 'asset', 'Koneet, kalusto ja laitteet', 0.00, 0.00, 24.00, 1),
+('1600', 'Intangible assets', 'asset', 'Aineettomat varat', 0.00, 0.00, 0.00, 1),
+('1700', 'Sijoitukset rahoituslaitoksissa', 'asset', 'Rahoituslaitokset', 0.00, 0.00, 0.00, 1),
+('1800', 'Erityiset vaihto-omaisuudet', 'asset', 'Erityiset vaihto-omaisuudet', 0.00, 0.00, 0.00, 1),
+('1900', 'Vaihto-omaisuudet luovutukseen', 'asset', 'Myyntikohteiset varat', 0.00, 0.00, 0.00, 1),
+
+-- LIABILITIES (VELAT) - 2000-2999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('2000', 'Oma pääoma', 'liability', 'Oma pääoma', 0.00, 0.00, 0.00, 1),
+('2100', 'Pitkän aikaiset velat', 'liability', 'Pitkän aikaiset velat', 0.00, 0.00, 0.00, 1),
+('2200', 'Verovelat', 'liability', 'Verovelat ja vakuudet', 0.00, 0.00, 0.00, 1),
+('2300', 'Saamiset', 'liability', 'Ostovelat ja saamiset', 0.00, 0.00, 0.00, 1),
+('2400', 'Maksuvelat', 'liability', 'Maksuvelat ja saadut ennakot', 0.00, 0.00, 0.00, 1),
+('2500', 'Verovelat konserneille', 'liability', 'Verovelat konserneille', 0.00, 0.00, 0.00, 1),
+('2600', 'Muut velat', 'liability', 'Muut velat', 0.00, 0.00, 0.00, 1),
+('2700', 'Siirrot konserninointiin', 'liability', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1),
+('2800', 'Tilikaudin tuloverot', 'liability', 'Tilikaudin tuloverot', 0.00, 0.00, 0.00, 1),
+
+-- EQUITY (OMA PÄÄOMA) - 3000-3999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('3000', 'Osakepääoma', 'equity', 'Osakepääoma', 0.00, 0.00, 0.00, 1),
+('3100', 'Sijoitetun pääoman rahastot', 'equity', 'Sijoitetun pääoman rahastot', 0.00, 0.00, 0.00, 1),
+('3200', 'Muut pääomat rahastot', 'equity', 'Muut pääomat rahastot', 0.00, 0.00, 0.00, 1),
+('3300', 'Tilikaudin voitto', 'equity', 'Tilikaudin voitto/tappio', 0.00, 0.00, 0.00, 1),
+
+-- REVENUE (TUOTOT) - 4000-4999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('4000', 'Myyntituotot', 'revenue', 'Myyntituotot', 0.00, 0.00, 24.00, 1),
+('4100', 'Palvelutuotot', 'revenue', 'Palvelutuotot', 0.00, 0.00, 24.00, 1),
+('4200', 'Vuokratuotot', 'revenue', 'Vuokratuotot', 0.00, 0.00, 24.00, 1),
+('4300', 'Korko- ja valuuttatuotot', 'revenue', 'Korko- ja valuuttatuotot', 0.00, 0.00, 0.00, 1),
+('4400', 'Muu tuotto', 'revenue', 'Muu tuotto', 0.00, 0.00, 0.00, 1),
+('4500', 'Siirrot konserninointiin', 'revenue', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1),
+
+-- EXPENSES (KULUT) - 5000-5999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES
+('5000', 'Aineelliset kulut', 'expense', 'Aineelliset kulut ja palvelut', 0.00, 0.00, 24.00, 1),
+('5100', 'Henkilöstökulut', 'expense', 'Henkilöstökulut', 0.00, 0.00, 0.00, 1),
+('5200', 'Poistot ja alennukset', 'expense', 'Poistot ja alennukset', 0.00, 0.00, 0.00, 1),
+('5300', 'Vuokrat', 'expense', 'Vuokrat ja vuokrakulut', 0.00, 0.00, 0.00, 1),
+('5400', 'Korko- ja rahoituskulut', 'expense', 'Korko- ja rahoituskulut', 0.00, 0.00, 0.00, 1),
+('5500', 'Poistot', 'expense', 'Tase-erät ja poistot', 0.00, 0.00, 0.00, 1),
+('5600', 'Arvonmäärityksen alennukset', 'expense', 'Arvonmäärityksen alennukset', 0.00, 0.00, 0.00, 1),
+('5700', 'Verotukset', 'expense', 'Verotukset ja maksut', 0.00, 0.00, 0.00, 1),
+('5800', 'Muut kulut', 'expense', 'Muut kulut', 0.00, 0.00, 0.00, 1),
+('5900', 'Siirrot konserninointiin', 'expense', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1);

+ 60 - 0
backend/database/finnish_chart_simple.sql

@@ -0,0 +1,60 @@
+-- Finnish Chart of Accounts Structure
+-- Following Finnish accounting standards and numbering system
+
+-- Disable foreign key checks temporarily
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- Clear existing data
+TRUNCATE TABLE chart_of_accounts;
+
+-- Re-enable foreign key checks
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- ASSETS (VASTA-OMAISUUS) - 1000-1999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1000', 'Käyttöpääoma', 'asset', 'Pääomaisuus', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1100', 'Saamiset ja vaatimukset', 'asset', 'Tase-erät ja poistot', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1200', 'Aineelliset vaihto-omaisuudet', 'asset', 'Varasto ja raaka-aineet', 0.00, 0.00, 24.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1300', 'Pitkän aikaiset sijoitukset', 'asset', 'Pitkän aikaiset sijoitukset', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1400', 'Kiinteistöt', 'asset', 'Kiinteistöt ja rakennukset', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1500', 'Koneet ja kalusto', 'asset', 'Koneet, kalusto ja laitteet', 0.00, 0.00, 24.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1600', 'Aineettomat varat', 'asset', 'Aineettomat varat', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1700', 'Sijoitukset rahoituslaitoksissa', 'asset', 'Rahoituslaitokset', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1800', 'Erityiset vaihto-omaisuudet', 'asset', 'Erityiset vaihto-omaisuudet', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('1900', 'Vaihto-omaisuudet luovutukseen', 'asset', 'Myyntikohteiset varat', 0.00, 0.00, 0.00, 1);
+
+-- LIABILITIES (VELAT) - 2000-2999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2000', 'Oma pääoma', 'liability', 'Oma pääoma', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2100', 'Pitkän aikaiset velat', 'liability', 'Pitkän aikaiset velat', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2200', 'Verovelat', 'liability', 'Verovelat ja vakuudet', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2300', 'Ostovelat ja saamiset', 'liability', 'Ostovelat ja saamiset', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2400', 'Maksuvelat ja ennakot', 'liability', 'Maksuvelat ja saadut ennakot', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2500', 'Verovelat konserneille', 'liability', 'Verovelat konserneille', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2600', 'Muut velat', 'liability', 'Muut velat', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2700', 'Siirrot konserninointiin', 'liability', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('2800', 'Tilikaudin tuloverot', 'liability', 'Tilikaudin tuloverot', 0.00, 0.00, 0.00, 1);
+
+-- EQUITY (OMA PÄÄOMA) - 3000-3999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('3000', 'Osakepääoma', 'equity', 'Osakepääoma', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('3100', 'Sijoitetun pääoman rahastot', 'equity', 'Sijoitetun pääoman rahastot', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('3200', 'Muut pääomat rahastot', 'equity', 'Muut pääomat rahastot', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('3300', 'Tilikauden voitto', 'equity', 'Tilikauden voitto/tappio', 0.00, 0.00, 0.00, 1);
+
+-- REVENUE (TUOTOT) - 4000-4999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('4000', 'Myyntituotot', 'revenue', 'Myyntituotot', 0.00, 0.00, 24.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('4100', 'Palvelutuotot', 'revenue', 'Palvelutuotot', 0.00, 0.00, 24.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('4200', 'Vuokratuotot', 'revenue', 'Vuokratuotot', 0.00, 0.00, 24.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('4300', 'Korko- ja valuuttatuotot', 'revenue', 'Korko- ja valuuttatuotot', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('4400', 'Muu tuotto', 'revenue', 'Muu tuotto', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('4500', 'Siirrot konserninointiin', 'revenue', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1);
+
+-- EXPENSES (KULUT) - 5000-5999
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5000', 'Aineelliset kulut', 'expense', 'Aineelliset kulut ja palvelut', 0.00, 0.00, 24.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5100', 'Henkilöstökulut', 'expense', 'Henkilöstökulut', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5200', 'Poistot ja alennukset', 'expense', 'Poistot ja alennukset', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5300', 'Vuokrat ja vuokrakulut', 'expense', 'Vuokrat ja vuokrakulut', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5400', 'Korko- ja rahoituskulut', 'expense', 'Korko- ja rahoituskulut', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5500', 'Tase-erät ja poistot', 'expense', 'Tase-erät ja poistot', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5600', 'Arvonmäärityksen alennukset', 'expense', 'Arvonmäärityksen alennukset', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5700', 'Verotukset ja maksut', 'expense', 'Verotukset ja maksut', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5800', 'Muut kulut', 'expense', 'Muut kulut', 0.00, 0.00, 0.00, 1);
+INSERT INTO chart_of_accounts (account_number, account_name, account_type, description, opening_balance, current_balance, vat_percentage, is_active) VALUES ('5900', 'Siirrot konserninointiin', 'expense', 'Siirrot konserninointiin', 0.00, 0.00, 0.00, 1);

+ 20 - 0
backend/database/tasks_schema.sql

@@ -0,0 +1,20 @@
+-- Tasks database schema
+-- For managing project tasks and subtasks
+
+CREATE TABLE IF NOT EXISTS tasks (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    title VARCHAR(255) NOT NULL,
+    description TEXT NULL,
+    status ENUM('pending', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'pending',
+    priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
+    project_id INT NULL,
+    due_date DATE NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    
+    INDEX idx_project_id (project_id),
+    INDEX idx_status (status),
+    INDEX idx_priority (priority),
+    INDEX idx_created_at (created_at),
+    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
+);

+ 22 - 0
backend/database/work_hours_schema.sql

@@ -0,0 +1,22 @@
+-- Work Hours database schema
+-- For tracking time spent on tasks
+
+CREATE TABLE IF NOT EXISTS work_hours (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_id INT NOT NULL,
+    user_id INT NOT NULL,
+    date DATE NOT NULL,
+    hours DECIMAL(5,2) NOT NULL,
+    description TEXT NULL,
+    rate DECIMAL(10,2) NULL,
+    total_amount DECIMAL(10,2) NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    
+    INDEX idx_task_id (task_id),
+    INDEX idx_user_id (user_id),
+    INDEX idx_date (date),
+    INDEX idx_created_at (created_at),
+    FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+);

+ 245 - 0
backend/models/AccountingEntry.php

@@ -0,0 +1,245 @@
+<?php
+class AccountingEntry {
+    private $conn;
+    private $table_name = "accounting_entries";
+
+    public $id;
+    public $entry_date;
+    public $description;
+    public $entry_type;
+    public $category;
+    public $tax_free_amount;
+    public $vat_percentage;
+    public $vat_25_5;
+    public $vat_14;
+    public $vat_10;
+    public $total_amount;
+    public $net_amount;
+    public $vat_amount;
+    public $reference_number;
+
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+
+    public function create() {
+        $query = "INSERT INTO " . $this->table_name . " SET 
+                  entry_date=:entry_date, 
+                  description=:description, 
+                  entry_type=:entry_type, 
+                  category=:category, 
+                  tax_free_amount=:tax_free_amount, 
+                  vat_percentage=:vat_percentage, 
+                  vat_25_5=:vat_25_5, 
+                  vat_14=:vat_14, 
+                  vat_10=:vat_10, 
+                  total_amount=:total_amount, 
+                  net_amount=:net_amount, 
+                  vat_amount=:vat_amount, 
+                  reference_number=:reference_number";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->entry_date = htmlspecialchars(strip_tags($this->entry_date));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->entry_type = htmlspecialchars(strip_tags($this->entry_type));
+        $this->category = htmlspecialchars(strip_tags($this->category));
+        $this->tax_free_amount = htmlspecialchars(strip_tags($this->tax_free_amount));
+        $this->vat_percentage = htmlspecialchars(strip_tags($this->vat_percentage));
+        $this->vat_25_5 = htmlspecialchars(strip_tags($this->vat_25_5));
+        $this->vat_14 = htmlspecialchars(strip_tags($this->vat_14));
+        $this->vat_10 = htmlspecialchars(strip_tags($this->vat_10));
+        $this->total_amount = htmlspecialchars(strip_tags($this->total_amount));
+        $this->net_amount = htmlspecialchars(strip_tags($this->net_amount));
+        $this->vat_amount = htmlspecialchars(strip_tags($this->vat_amount));
+        $this->reference_number = htmlspecialchars(strip_tags($this->reference_number));
+
+        $stmt->bindParam(":entry_date", $this->entry_date);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":entry_type", $this->entry_type);
+        $stmt->bindParam(":category", $this->category);
+        $stmt->bindParam(":tax_free_amount", $this->tax_free_amount);
+        $stmt->bindParam(":vat_percentage", $this->vat_percentage);
+        $stmt->bindParam(":vat_25_5", $this->vat_25_5);
+        $stmt->bindParam(":vat_14", $this->vat_14);
+        $stmt->bindParam(":vat_10", $this->vat_10);
+        $stmt->bindParam(":total_amount", $this->total_amount);
+        $stmt->bindParam(":net_amount", $this->net_amount);
+        $stmt->bindParam(":vat_amount", $this->vat_amount);
+        $stmt->bindParam(":reference_number", $this->reference_number);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function read() {
+        $query = "SELECT * FROM " . $this->table_name . " ORDER BY entry_date DESC, id DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function readOne() {
+        $query = "SELECT * FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        $this->entry_date = $row['entry_date'];
+        $this->description = $row['description'];
+        $this->entry_type = $row['entry_type'];
+        $this->category = $row['category'];
+        $this->tax_free_amount = $row['tax_free_amount'];
+        $this->vat_percentage = $row['vat_percentage'];
+        $this->vat_25_5 = $row['vat_25_5'];
+        $this->vat_14 = $row['vat_14'];
+        $this->vat_10 = $row['vat_10'];
+        $this->total_amount = $row['total_amount'];
+        $this->net_amount = $row['net_amount'];
+        $this->vat_amount = $row['vat_amount'];
+        $this->reference_number = $row['reference_number'];
+    }
+
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " SET 
+                  entry_date=:entry_date, 
+                  description=:description, 
+                  entry_type=:entry_type, 
+                  category=:category, 
+                  tax_free_amount=:tax_free_amount, 
+                  vat_percentage=:vat_percentage, 
+                  vat_25_5=:vat_25_5, 
+                  vat_14=:vat_14, 
+                  vat_10=:vat_10, 
+                  total_amount=:total_amount, 
+                  net_amount=:net_amount, 
+                  vat_amount=:vat_amount, 
+                  reference_number=:reference_number 
+                  WHERE id=:id";
+
+        $stmt = $this->conn->prepare($query);
+
+        $this->entry_date = htmlspecialchars(strip_tags($this->entry_date));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->entry_type = htmlspecialchars(strip_tags($this->entry_type));
+        $this->category = htmlspecialchars(strip_tags($this->category));
+        $this->tax_free_amount = htmlspecialchars(strip_tags($this->tax_free_amount));
+        $this->vat_percentage = htmlspecialchars(strip_tags($this->vat_percentage));
+        $this->vat_25_5 = htmlspecialchars(strip_tags($this->vat_25_5));
+        $this->vat_14 = htmlspecialchars(strip_tags($this->vat_14));
+        $this->vat_10 = htmlspecialchars(strip_tags($this->vat_10));
+        $this->total_amount = htmlspecialchars(strip_tags($this->total_amount));
+        $this->net_amount = htmlspecialchars(strip_tags($this->net_amount));
+        $this->vat_amount = htmlspecialchars(strip_tags($this->vat_amount));
+        $this->reference_number = htmlspecialchars(strip_tags($this->reference_number));
+
+        $stmt->bindParam(":entry_date", $this->entry_date);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":entry_type", $this->entry_type);
+        $stmt->bindParam(":category", $this->category);
+        $stmt->bindParam(":tax_free_amount", $this->tax_free_amount);
+        $stmt->bindParam(":vat_percentage", $this->vat_percentage);
+        $stmt->bindParam(":vat_25_5", $this->vat_25_5);
+        $stmt->bindParam(":vat_14", $this->vat_14);
+        $stmt->bindParam(":vat_10", $this->vat_10);
+        $stmt->bindParam(":total_amount", $this->total_amount);
+        $stmt->bindParam(":net_amount", $this->net_amount);
+        $stmt->bindParam(":vat_amount", $this->vat_amount);
+        $stmt->bindParam(":id", $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+
+        if($stmt->execute()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public function search($search_term) {
+        $query = "SELECT * FROM " . $this->table_name . " 
+                  WHERE description LIKE ? OR 
+                        category LIKE ? OR 
+                        reference_number LIKE ? 
+                  ORDER BY entry_date DESC, id DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $search_term = "%{$search_term}%";
+        $stmt->bindParam(1, $search_term);
+        $stmt->bindParam(2, $search_term);
+        $stmt->bindParam(3, $search_term);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getByDateRange($start_date, $end_date) {
+        $query = "SELECT * FROM " . $this->table_name . " 
+                  WHERE entry_date BETWEEN ? AND ? 
+                  ORDER BY entry_date DESC, id DESC";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $start_date);
+        $stmt->bindParam(2, $end_date);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getMonthlySummary($year, $month) {
+        $query = "SELECT 
+                    entry_type,
+                    SUM(total_amount) as total,
+                    SUM(vat_amount) as vat_total,
+                    SUM(net_amount) as net_total,
+                    COUNT(*) as count
+                  FROM " . $this->table_name . " 
+                  WHERE YEAR(entry_date) = ? AND MONTH(entry_date) = ?
+                  GROUP BY entry_type";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $year);
+        $stmt->bindParam(2, $month);
+        $stmt->execute();
+
+        return $stmt;
+    }
+
+    public function getEntryTypeBadge() {
+        $badges = [
+            'Tulo' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Tulo</span>',
+            'Kulu' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Kulu</span>'
+        ];
+        
+        return $badges[$this->entry_type] ?? $this->entry_type;
+    }
+
+    public function getEntryTypeName() {
+        $types = [
+            'Tulo' => 'Tulo',
+            'Kulu' => 'Kulu'
+        ];
+        
+        return $types[$this->entry_type] ?? $this->entry_type;
+    }
+}
+?>

+ 30 - 12
backend/models/ChartOfAccounts.php

@@ -11,6 +11,7 @@ class ChartOfAccounts {
     public $description;
     public $opening_balance;
     public $current_balance;
+    public $vat_percentage;
     public $is_active;
     public $created_at;
     public $updated_at;
@@ -20,7 +21,7 @@ class ChartOfAccounts {
     }
 
     public function create() {
-        $query = "INSERT INTO " . $this->table_name . " SET account_number=:account_number, account_name=:account_name, account_type=:account_type, parent_id=:parent_id, description=:description, opening_balance=:opening_balance, current_balance=:current_balance, is_active=:is_active, created_at=:created_at, updated_at=:updated_at";
+        $query = "INSERT INTO " . $this->table_name . " SET account_number=:account_number, account_name=:account_name, account_type=:account_type, parent_id=:parent_id, description=:description, opening_balance=:opening_balance, current_balance=:current_balance, vat_percentage=:vat_percentage, is_active=:is_active, created_at=:created_at, updated_at=:updated_at";
 
         $stmt = $this->conn->prepare($query);
 
@@ -31,6 +32,7 @@ class ChartOfAccounts {
         $this->description = htmlspecialchars(strip_tags($this->description));
         $this->opening_balance = htmlspecialchars(strip_tags($this->opening_balance));
         $this->current_balance = htmlspecialchars(strip_tags($this->current_balance));
+        $this->vat_percentage = htmlspecialchars(strip_tags($this->vat_percentage));
         $this->is_active = $this->is_active ? 1 : 0;
         $this->created_at = date('Y-m-d H:i:s');
         $this->updated_at = date('Y-m-d H:i:s');
@@ -42,6 +44,7 @@ class ChartOfAccounts {
         $stmt->bindParam(":description", $this->description);
         $stmt->bindParam(":opening_balance", $this->opening_balance);
         $stmt->bindParam(":current_balance", $this->current_balance);
+        $stmt->bindParam(":vat_percentage", $this->vat_percentage);
         $stmt->bindParam(":is_active", $this->is_active);
         $stmt->bindParam(":created_at", $this->created_at);
         $stmt->bindParam(":updated_at", $this->updated_at);
@@ -78,13 +81,14 @@ class ChartOfAccounts {
         $this->description = $row['description'];
         $this->opening_balance = $row['opening_balance'];
         $this->current_balance = $row['current_balance'];
+        $this->vat_percentage = $row['vat_percentage'];
         $this->is_active = $row['is_active'];
         $this->created_at = $row['created_at'];
         $this->updated_at = $row['updated_at'];
     }
 
     public function update() {
-        $query = "UPDATE " . $this->table_name . " SET account_number=:account_number, account_name=:account_name, account_type=:account_type, parent_id=:parent_id, description=:description, opening_balance=:opening_balance, current_balance=:current_balance, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
+        $query = "UPDATE " . $this->table_name . " SET account_number=:account_number, account_name=:account_name, account_type=:account_type, parent_id=:parent_id, description=:description, opening_balance=:opening_balance, current_balance=:current_balance, vat_percentage=:vat_percentage, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
 
         $stmt = $this->conn->prepare($query);
 
@@ -95,6 +99,7 @@ class ChartOfAccounts {
         $this->description = htmlspecialchars(strip_tags($this->description));
         $this->opening_balance = htmlspecialchars(strip_tags($this->opening_balance));
         $this->current_balance = htmlspecialchars(strip_tags($this->current_balance));
+        $this->vat_percentage = htmlspecialchars(strip_tags($this->vat_percentage));
         $this->is_active = $this->is_active ? 1 : 0;
         $this->updated_at = date('Y-m-d H:i:s');
 
@@ -105,6 +110,7 @@ class ChartOfAccounts {
         $stmt->bindParam(":description", $this->description);
         $stmt->bindParam(":opening_balance", $this->opening_balance);
         $stmt->bindParam(":current_balance", $this->current_balance);
+        $stmt->bindParam(":vat_percentage", $this->vat_percentage);
         $stmt->bindParam(":is_active", $this->is_active);
         $stmt->bindParam(":updated_at", $this->updated_at);
         $stmt->bindParam(":id", $this->id);
@@ -148,11 +154,11 @@ class ChartOfAccounts {
 
     public function getAccountTypeBadge() {
         $badges = [
-            'asset' => '<span style="background-color: #007bff; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Asset</span>',
-            'liability' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Liability</span>',
-            'equity' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Equity</span>',
-            'revenue' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Revenue</span>',
-            'expense' => '<span style="background-color: #ffc107; color: black; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Expense</span>'
+            'asset' => '<span style="background-color: #007bff; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Vasta-omaisuus</span>',
+            'liability' => '<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Velat</span>',
+            'equity' => '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Oma pääoma</span>',
+            'revenue' => '<span style="background-color: #17a2b8; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Tuotot</span>',
+            'expense' => '<span style="background-color: #ffc107; color: black; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Kulut</span>'
         ];
         
         return $badges[$this->account_type] ?? $this->account_type;
@@ -160,14 +166,26 @@ class ChartOfAccounts {
 
     public function getAccountTypeName() {
         $types = [
-            'asset' => 'Asset',
-            'liability' => 'Liability',
-            'equity' => 'Equity',
-            'revenue' => 'Revenue',
-            'expense' => 'Expense'
+            'asset' => 'Vasta-omaisuus',
+            'liability' => 'Velat',
+            'equity' => 'Oma pääoma',
+            'revenue' => 'Tuotot',
+            'expense' => 'Kulut'
         ];
         
         return $types[$this->account_type] ?? $this->account_type;
     }
+
+    public function getAccountCategory() {
+        $categories = [
+            'asset' => '1000-1999',
+            'liability' => '2000-2999',
+            'equity' => '3000-3999',
+            'revenue' => '4000-4999',
+            'expense' => '5000-5999'
+        ];
+        
+        return $categories[$this->account_type] ?? $this->account_type;
+    }
 }
 ?>

+ 8 - 2
backend/models/Client.php

@@ -16,6 +16,7 @@ class Client {
     public $postal_code;
     public $country;
     public $notes;
+    public $hour_price;
     public $created_at;
     public $updated_at;
 
@@ -24,7 +25,7 @@ class Client {
     }
 
     public function create() {
-        $query = "INSERT INTO " . $this->table_name . " SET company_name=:company_name, y_tunnus=:y_tunnus, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, created_at=:created_at, updated_at=:updated_at";
+        $query = "INSERT INTO " . $this->table_name . " SET company_name=:company_name, y_tunnus=:y_tunnus, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, hour_price=:hour_price, created_at=:created_at, updated_at=:updated_at";
 
         $stmt = $this->conn->prepare($query);
 
@@ -40,6 +41,7 @@ class Client {
         $this->postal_code = htmlspecialchars(strip_tags($this->postal_code));
         $this->country = htmlspecialchars(strip_tags($this->country));
         $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->hour_price = htmlspecialchars(strip_tags($this->hour_price));
         $this->created_at = date('Y-m-d H:i:s');
         $this->updated_at = date('Y-m-d H:i:s');
 
@@ -55,6 +57,7 @@ class Client {
         $stmt->bindParam(":postal_code", $this->postal_code);
         $stmt->bindParam(":country", $this->country);
         $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":hour_price", $this->hour_price);
         $stmt->bindParam(":created_at", $this->created_at);
         $stmt->bindParam(":updated_at", $this->updated_at);
 
@@ -95,12 +98,13 @@ class Client {
         $this->postal_code = $row['postal_code'];
         $this->country = $row['country'];
         $this->notes = $row['notes'];
+        $this->hour_price = $row['hour_price'];
         $this->created_at = $row['created_at'];
         $this->updated_at = $row['updated_at'];
     }
 
     public function update() {
-        $query = "UPDATE " . $this->table_name . " SET company_name=:company_name, y_tunnus=:y_tunnus, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, updated_at=:updated_at WHERE id=:id";
+        $query = "UPDATE " . $this->table_name . " SET company_name=:company_name, y_tunnus=:y_tunnus, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, address=:address, city=:city, state=:state, postal_code=:postal_code, country=:country, notes=:notes, hour_price=:hour_price, updated_at=:updated_at WHERE id=:id";
 
         $stmt = $this->conn->prepare($query);
 
@@ -116,6 +120,7 @@ class Client {
         $this->postal_code = htmlspecialchars(strip_tags($this->postal_code));
         $this->country = htmlspecialchars(strip_tags($this->country));
         $this->notes = htmlspecialchars(strip_tags($this->notes));
+        $this->hour_price = htmlspecialchars(strip_tags($this->hour_price));
         $this->updated_at = date('Y-m-d H:i:s');
 
         $stmt->bindParam(":company_name", $this->company_name);
@@ -130,6 +135,7 @@ class Client {
         $stmt->bindParam(":postal_code", $this->postal_code);
         $stmt->bindParam(":country", $this->country);
         $stmt->bindParam(":notes", $this->notes);
+        $stmt->bindParam(":hour_price", $this->hour_price);
         $stmt->bindParam(":updated_at", $this->updated_at);
         $stmt->bindParam(":id", $this->id);
 

+ 14 - 2
backend/models/ContactPerson.php

@@ -10,7 +10,9 @@ class ContactPerson {
     public $email;
     public $phone;
     public $position;
+    public $department;
     public $is_primary;
+    public $notes;
     public $created_at;
     public $updated_at;
 
@@ -19,7 +21,7 @@ class ContactPerson {
     }
 
     public function create() {
-        $query = "INSERT INTO " . $this->table_name . " SET client_id=:client_id, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, position=:position, is_primary=:is_primary, created_at=:created_at, updated_at=:updated_at";
+        $query = "INSERT INTO " . $this->table_name . " SET client_id=:client_id, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, position=:position, department=:department, is_primary=:is_primary, notes=:notes, created_at=:created_at, updated_at=:updated_at";
 
         $stmt = $this->conn->prepare($query);
 
@@ -29,6 +31,8 @@ class ContactPerson {
         $this->email = htmlspecialchars(strip_tags($this->email));
         $this->phone = htmlspecialchars(strip_tags($this->phone));
         $this->position = htmlspecialchars(strip_tags($this->position));
+        $this->department = htmlspecialchars(strip_tags($this->department));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
         $this->is_primary = $this->is_primary ? 1 : 0;
         $this->created_at = date('Y-m-d H:i:s');
         $this->updated_at = date('Y-m-d H:i:s');
@@ -39,7 +43,9 @@ class ContactPerson {
         $stmt->bindParam(":email", $this->email);
         $stmt->bindParam(":phone", $this->phone);
         $stmt->bindParam(":position", $this->position);
+        $stmt->bindParam(":department", $this->department);
         $stmt->bindParam(":is_primary", $this->is_primary);
+        $stmt->bindParam(":notes", $this->notes);
         $stmt->bindParam(":created_at", $this->created_at);
         $stmt->bindParam(":updated_at", $this->updated_at);
 
@@ -75,13 +81,15 @@ class ContactPerson {
         $this->email = $row['email'];
         $this->phone = $row['phone'];
         $this->position = $row['position'];
+        $this->department = $row['department'];
         $this->is_primary = $row['is_primary'];
+        $this->notes = $row['notes'];
         $this->created_at = $row['created_at'];
         $this->updated_at = $row['updated_at'];
     }
 
     public function update() {
-        $query = "UPDATE " . $this->table_name . " SET client_id=:client_id, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, position=:position, is_primary=:is_primary, updated_at=:updated_at WHERE id=:id";
+        $query = "UPDATE " . $this->table_name . " SET client_id=:client_id, first_name=:first_name, last_name=:last_name, email=:email, phone=:phone, position=:position, department=:department, is_primary=:is_primary, notes=:notes, updated_at=:updated_at WHERE id=:id";
 
         $stmt = $this->conn->prepare($query);
 
@@ -91,6 +99,8 @@ class ContactPerson {
         $this->email = htmlspecialchars(strip_tags($this->email));
         $this->phone = htmlspecialchars(strip_tags($this->phone));
         $this->position = htmlspecialchars(strip_tags($this->position));
+        $this->department = htmlspecialchars(strip_tags($this->department));
+        $this->notes = htmlspecialchars(strip_tags($this->notes));
         $this->is_primary = $this->is_primary ? 1 : 0;
         $this->updated_at = date('Y-m-d H:i:s');
 
@@ -100,7 +110,9 @@ class ContactPerson {
         $stmt->bindParam(":email", $this->email);
         $stmt->bindParam(":phone", $this->phone);
         $stmt->bindParam(":position", $this->position);
+        $stmt->bindParam(":department", $this->department);
         $stmt->bindParam(":is_primary", $this->is_primary);
+        $stmt->bindParam(":notes", $this->notes);
         $stmt->bindParam(":updated_at", $this->updated_at);
         $stmt->bindParam(":id", $this->id);
 

+ 8 - 2
backend/models/Item.php

@@ -10,6 +10,7 @@ class Item {
     public $picture;
     public $quantity;
     public $price;
+    public $date_of_purchase;
     public $created_at;
     public $updated_at;
 
@@ -18,7 +19,7 @@ class Item {
     }
 
     public function create() {
-        $query = "INSERT INTO " . $this->table_name . " SET name=:name, description=:description, serial_number=:serial_number, picture=:picture, quantity=:quantity, price=:price, created_at=:created_at, updated_at=:updated_at";
+        $query = "INSERT INTO " . $this->table_name . " SET name=:name, description=:description, serial_number=:serial_number, picture=:picture, quantity=:quantity, price=:price, date_of_purchase=:date_of_purchase, created_at=:created_at, updated_at=:updated_at";
 
         $stmt = $this->conn->prepare($query);
 
@@ -28,6 +29,7 @@ class Item {
         $this->picture = htmlspecialchars(strip_tags($this->picture));
         $this->quantity = htmlspecialchars(strip_tags($this->quantity));
         $this->price = htmlspecialchars(strip_tags($this->price));
+        $this->date_of_purchase = htmlspecialchars(strip_tags($this->date_of_purchase));
         $this->created_at = date('Y-m-d H:i:s');
         $this->updated_at = date('Y-m-d H:i:s');
 
@@ -37,6 +39,7 @@ class Item {
         $stmt->bindParam(":picture", $this->picture);
         $stmt->bindParam(":quantity", $this->quantity);
         $stmt->bindParam(":price", $this->price);
+        $stmt->bindParam(":date_of_purchase", $this->date_of_purchase);
         $stmt->bindParam(":created_at", $this->created_at);
         $stmt->bindParam(":updated_at", $this->updated_at);
 
@@ -71,12 +74,13 @@ class Item {
         $this->picture = $row['picture'];
         $this->quantity = $row['quantity'];
         $this->price = $row['price'];
+        $this->date_of_purchase = $row['date_of_purchase'];
         $this->created_at = $row['created_at'];
         $this->updated_at = $row['updated_at'];
     }
 
     public function update() {
-        $query = "UPDATE " . $this->table_name . " SET name=:name, description=:description, serial_number=:serial_number, picture=:picture, quantity=:quantity, price=:price, updated_at=:updated_at WHERE id=:id";
+        $query = "UPDATE " . $this->table_name . " SET name=:name, description=:description, serial_number=:serial_number, picture=:picture, quantity=:quantity, price=:price, date_of_purchase=:date_of_purchase, updated_at=:updated_at WHERE id=:id";
 
         $stmt = $this->conn->prepare($query);
 
@@ -86,6 +90,7 @@ class Item {
         $this->picture = htmlspecialchars(strip_tags($this->picture));
         $this->quantity = htmlspecialchars(strip_tags($this->quantity));
         $this->price = htmlspecialchars(strip_tags($this->price));
+        $this->date_of_purchase = htmlspecialchars(strip_tags($this->date_of_purchase));
         $this->updated_at = date('Y-m-d H:i:s');
 
         $stmt->bindParam(":name", $this->name);
@@ -94,6 +99,7 @@ class Item {
         $stmt->bindParam(":picture", $this->picture);
         $stmt->bindParam(":quantity", $this->quantity);
         $stmt->bindParam(":price", $this->price);
+        $stmt->bindParam(":date_of_purchase", $this->date_of_purchase);
         $stmt->bindParam(":updated_at", $this->updated_at);
         $stmt->bindParam(":id", $this->id);
 

+ 37 - 3
backend/models/User.php

@@ -81,8 +81,12 @@ class User {
         $this->updated_at = $row['updated_at'];
     }
 
-    public function update() {
-        $query = "UPDATE " . $this->table_name . " SET username=:username, email=:email, first_name=:first_name, last_name=:last_name, role=:role, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
+    public function update($update_password = false) {
+        if($update_password) {
+            $query = "UPDATE " . $this->table_name . " SET username=:username, email=:email, password_hash=:password_hash, first_name=:first_name, last_name=:last_name, role=:role, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
+        } else {
+            $query = "UPDATE " . $this->table_name . " SET username=:username, email=:email, first_name=:first_name, last_name=:last_name, role=:role, is_active=:is_active, updated_at=:updated_at WHERE id=:id";
+        }
 
         $stmt = $this->conn->prepare($query);
 
@@ -103,6 +107,11 @@ class User {
         $stmt->bindParam(":updated_at", $this->updated_at);
         $stmt->bindParam(":id", $this->id);
 
+        if($update_password) {
+            $this->password_hash = password_hash($this->password_hash, PASSWORD_DEFAULT);
+            $stmt->bindParam(":password_hash", $this->password_hash);
+        }
+
         if($stmt->execute()) {
             return true;
         }
@@ -136,7 +145,8 @@ class User {
             // Update last login
             $update_query = "UPDATE " . $this->table_name . " SET last_login = ? WHERE id = ?";
             $update_stmt = $this->conn->prepare($update_query);
-            $update_stmt->bindParam(1, date('Y-m-d H:i:s'));
+            $current_time = date('Y-m-d H:i:s');
+            $update_stmt->bindParam(1, $current_time);
             $update_stmt->bindParam(2, $row['id']);
             $update_stmt->execute();
 
@@ -189,5 +199,29 @@ class User {
     public function isActive() {
         return $this->is_active;
     }
+
+    public function getStatusBadge() {
+        if($this->is_active) {
+            return '<span style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Active</span>';
+        } else {
+            return '<span style="background-color: #6c757d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Inactive</span>';
+        }
+    }
+
+    public function findByUsername($username) {
+        $query = "SELECT id, username, email, first_name, last_name, role, is_active, last_login, created_at, updated_at FROM " . $this->table_name . " WHERE username = ? LIMIT 0,1";
+
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $username);
+        $stmt->execute();
+
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+        if($row) {
+            return $row;
+        }
+
+        return false;
+    }
 }
 ?>

+ 232 - 0
backend/models/WorkHour.php

@@ -0,0 +1,232 @@
+<?php
+
+class WorkHour {
+    private $conn;
+    private $table_name = 'work_hours';
+    
+    public $id;
+    public $task_id;
+    public $user_id;
+    public $date;
+    public $hours;
+    public $description;
+    public $rate;
+    public $total_amount;
+    public $created_at;
+    public $updated_at;
+    
+    public function __construct($db) {
+        $this->conn = $db;
+    }
+    
+    public function create() {
+        // Auto-fetch client hour price if rate is not provided or empty
+        if (!$this->rate || $this->rate === '') {
+            $this->rate = $this->getClientHourPrice();
+        }
+        
+        // Calculate total amount if rate is provided and greater than 0
+        if ($this->rate && $this->rate > 0) {
+            $this->total_amount = $this->hours * $this->rate;
+        } else {
+            $this->rate = null;
+            $this->total_amount = null;
+        }
+        
+        $query = "INSERT INTO " . $this->table_name . " 
+                  SET task_id=:task_id, user_id=:user_id, date=:date, hours=:hours, 
+                      description=:description, rate=:rate, total_amount=:total_amount, 
+                      created_at=:created_at, updated_at=:updated_at";
+        
+        $stmt = $this->conn->prepare($query);
+        
+        $this->task_id = htmlspecialchars(strip_tags($this->task_id));
+        $this->user_id = htmlspecialchars(strip_tags($this->user_id));
+        $this->date = htmlspecialchars(strip_tags($this->date));
+        $this->hours = htmlspecialchars(strip_tags($this->hours));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->rate = $this->rate ? htmlspecialchars(strip_tags($this->rate)) : null;
+        $this->total_amount = $this->total_amount ? htmlspecialchars(strip_tags($this->total_amount)) : null;
+        $this->created_at = date('Y-m-d H:i:s');
+        $this->updated_at = date('Y-m-d H:i:s');
+        
+        $stmt->bindParam(":task_id", $this->task_id);
+        $stmt->bindParam(":user_id", $this->user_id);
+        $stmt->bindParam(":date", $this->date);
+        $stmt->bindParam(":hours", $this->hours);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":rate", $this->rate);
+        $stmt->bindParam(":total_amount", $this->total_amount);
+        $stmt->bindParam(":created_at", $this->created_at);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        
+        if($stmt->execute()) {
+            return true;
+        }
+        
+        return false;
+    }
+    
+    public function read() {
+        $query = "SELECT wh.*, t.title as task_title, u.first_name, u.last_name 
+                  FROM " . $this->table_name . " wh 
+                  LEFT JOIN tasks t ON wh.task_id = t.id 
+                  LEFT JOIN users u ON wh.user_id = u.id 
+                  ORDER BY wh.date DESC, wh.created_at DESC";
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->execute();
+        
+        return $stmt;
+    }
+    
+    public function readByTask($task_id) {
+        $query = "SELECT wh.*, u.first_name, u.last_name, c.hour_price as client_hour_price,
+                  c.company_name as client_name, c.first_name as client_first_name, c.last_name as client_last_name
+                  FROM " . $this->table_name . " wh 
+                  LEFT JOIN users u ON wh.user_id = u.id 
+                  LEFT JOIN tasks t ON wh.task_id = t.id 
+                  LEFT JOIN projects p ON t.project_id = p.id 
+                  LEFT JOIN clients c ON p.customer_id = c.id 
+                  WHERE wh.task_id = ? 
+                  ORDER BY wh.date DESC, wh.created_at DESC";
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $task_id);
+        $stmt->execute();
+        
+        return $stmt;
+    }
+    
+    public function readOne() {
+        $query = "SELECT wh.*, t.title as task_title, u.first_name, u.last_name 
+                  FROM " . $this->table_name . " wh 
+                  LEFT JOIN tasks t ON wh.task_id = t.id 
+                  LEFT JOIN users u ON wh.user_id = u.id 
+                  WHERE wh.id = ? LIMIT 0,1";
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        
+        $this->task_id = $row['task_id'];
+        $this->user_id = $row['user_id'];
+        $this->date = $row['date'];
+        $this->hours = $row['hours'];
+        $this->description = $row['description'];
+        $this->rate = $row['rate'];
+        $this->total_amount = $row['total_amount'];
+        $this->created_at = $row['created_at'];
+        $this->updated_at = $row['updated_at'];
+    }
+    
+    public function update() {
+        $query = "UPDATE " . $this->table_name . " 
+                  SET task_id=:task_id, user_id=:user_id, date=:date, hours=:hours, 
+                      description=:description, rate=:rate, total_amount=:total_amount, 
+                      updated_at=:updated_at 
+                  WHERE id=:id";
+        
+        $stmt = $this->conn->prepare($query);
+        
+        $this->task_id = htmlspecialchars(strip_tags($this->task_id));
+        $this->user_id = htmlspecialchars(strip_tags($this->user_id));
+        $this->date = htmlspecialchars(strip_tags($this->date));
+        $this->hours = htmlspecialchars(strip_tags($this->hours));
+        $this->description = htmlspecialchars(strip_tags($this->description));
+        $this->rate = htmlspecialchars(strip_tags($this->rate));
+        $this->total_amount = htmlspecialchars(strip_tags($this->total_amount));
+        $this->updated_at = date('Y-m-d H:i:s');
+        
+        $stmt->bindParam(":task_id", $this->task_id);
+        $stmt->bindParam(":user_id", $this->user_id);
+        $stmt->bindParam(":date", $this->date);
+        $stmt->bindParam(":hours", $this->hours);
+        $stmt->bindParam(":description", $this->description);
+        $stmt->bindParam(":rate", $this->rate);
+        $stmt->bindParam(":total_amount", $this->total_amount);
+        $stmt->bindParam(":updated_at", $this->updated_at);
+        $stmt->bindParam(":id", $this->id);
+        
+        if($stmt->execute()) {
+            return true;
+        }
+        
+        return false;
+    }
+    
+    public function delete() {
+        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->id);
+        
+        if($stmt->execute()) {
+            return true;
+        }
+        
+        return false;
+    }
+    
+    public function getTotalHoursByTask($task_id) {
+        $query = "SELECT SUM(hours) as total_hours, COUNT(*) as entries 
+                  FROM " . $this->table_name . " 
+                  WHERE task_id = ?";
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $task_id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        return [
+            'total_hours' => $row['total_hours'] || 0,
+            'entries' => $row['entries'] || 0
+        ];
+    }
+    
+    public function getTotalHoursByUser($user_id, $start_date = null, $end_date = null) {
+        $query = "SELECT SUM(wh.hours) as total_hours, COUNT(*) as entries 
+                  FROM " . $this->table_name . " wh 
+                  WHERE wh.user_id = ?";
+        
+        if ($start_date && $end_date) {
+            $query .= " AND wh.date BETWEEN ? AND ?";
+        }
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $user_id);
+        
+        if ($start_date && $end_date) {
+            $stmt->bindParam(2, $start_date);
+            $stmt->bindParam(3, $end_date);
+        }
+        
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        return [
+            'total_hours' => $row['total_hours'] || 0,
+            'entries' => $row['entries'] || 0
+        ];
+    }
+    
+    public function getClientHourPrice() {
+        $query = "SELECT c.hour_price 
+                  FROM " . $this->table_name . " wh 
+                  LEFT JOIN tasks t ON wh.task_id = t.id 
+                  LEFT JOIN projects p ON t.project_id = p.id 
+                  LEFT JOIN clients c ON p.customer_id = c.id 
+                  WHERE wh.task_id = ? AND c.hour_price IS NOT NULL AND c.hour_price > 0 
+                  LIMIT 1";
+        
+        $stmt = $this->conn->prepare($query);
+        $stmt->bindParam(1, $this->task_id);
+        $stmt->execute();
+        
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        return $row ? $row['hour_price'] : 0;
+    }
+}
+?>

+ 78 - 0
backend/router.php

@@ -0,0 +1,78 @@
+<?php
+// Router for PHP built-in server
+$request_uri = $_SERVER['REQUEST_URI'];
+$request_method = $_SERVER['REQUEST_METHOD'];
+
+// Remove query string from request URI
+$path = parse_url($request_uri, PHP_URL_PATH);
+
+// Set CORS headers
+header("Access-Control-Allow-Origin: *");
+header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
+header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
+
+// Handle OPTIONS requests
+if ($request_method == 'OPTIONS') {
+    exit(0);
+}
+
+// Route API requests
+if (strpos($path, '/api/') === 0) {
+    // Extract the API file path
+    $api_file = substr($path, 5); // Remove '/api/' prefix
+    
+    // Add .php extension if not present
+    if (!strpos($api_file, '.php')) {
+        $api_file .= '.php';
+    }
+    
+    // Construct full file path
+    $file_path = __DIR__ . '/api/' . $api_file;
+    
+    // Check if file exists
+    if (file_exists($file_path)) {
+        include $file_path;
+    } else {
+        // Return 404 for non-existent API endpoints
+        http_response_code(404);
+        echo json_encode(array("message" => "API endpoint not found"));
+    }
+} else {
+    // Serve static files or return 404
+    $file_path = __DIR__ . $path;
+    
+    if ($path === '/' || $path === '/index.html') {
+        // Serve a simple index page
+        http_response_code(200);
+        echo "<h1>Inventory API Server</h1>";
+        echo "<p>API is running on port 8080</p>";
+        echo "<p>Available endpoints:</p>";
+        echo "<ul>";
+        echo "<li><a href='/api/auth.php'>/api/auth.php</a></li>";
+        echo "<li><a href='/api/items.php'>/api/items.php</a></li>";
+        echo "<li><a href='/api/clients.php'>/api/clients.php</a></li>";
+        echo "<li><a href='/api/invoices.php'>/api/invoices.php</a></li>";
+        echo "</ul>";
+    } elseif (file_exists($file_path) && is_file($file_path)) {
+        // Serve static files
+        $mime_types = array(
+            'css' => 'text/css',
+            'js' => 'application/javascript',
+            'json' => 'application/json',
+            'html' => 'text/html',
+            'txt' => 'text/plain'
+        );
+        
+        $extension = pathinfo($file_path, PATHINFO_EXTENSION);
+        if (isset($mime_types[$extension])) {
+            header("Content-Type: " . $mime_types[$extension]);
+        }
+        
+        readfile($file_path);
+    } else {
+        // Return 404 for other requests
+        http_response_code(404);
+        echo json_encode(array("message" => "Not found"));
+    }
+}
+?>

+ 8 - 0
backend/start_server.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+# Start PHP built-in server on port 8080
+echo "Starting PHP server on port 8080..."
+cd "$(dirname "$0")"
+php -S 0.0.0.0:8080 -t . router.php
+
+echo "Server stopped."

+ 36 - 0
backend/test_accounting_api.php

@@ -0,0 +1,36 @@
+<?php
+// Test script to verify accounting_entries API is working
+header('Content-Type: application/json');
+
+require_once 'config/database.php';
+require_once 'models/AccountingEntry.php';
+
+try {
+    $database = new Database();
+    $conn = $database->getConnection();
+    
+    if (!$conn) {
+        echo json_encode(['success' => false, 'message' => 'Database connection failed']);
+        exit;
+    }
+
+    // Test 1: Check if accounting_entries table has data
+    $sql = "SELECT COUNT(*) as count FROM accounting_entries";
+    $stmt = $conn->prepare($sql);
+    $stmt->execute();
+    $count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
+    
+    echo json_encode([
+        'success' => true,
+        'database_connected' => true,
+        'entries_count' => $count,
+        'test_result' => $count > 0 ? 'Data exists in accounting_entries table' : 'No data in accounting_entries table'
+    ]);
+    
+} catch (Exception $e) {
+    echo json_encode([
+        'success' => false,
+        'message' => $e->getMessage()
+    ]);
+}
+?>

+ 92 - 0
backend/test_item_creation.php

@@ -0,0 +1,92 @@
+<?php
+// Test script to verify item creation creates accounting entry
+header('Content-Type: application/json');
+
+require_once 'config/database.php';
+require_once 'models/Item.php';
+require_once 'models/AccountingEntry.php';
+
+try {
+    $database = new Database();
+    $db = $database->getConnection();
+    
+    if (!$db) {
+        echo json_encode(['success' => false, 'message' => 'Database connection failed']);
+        exit;
+    }
+
+    // Test data for item creation
+    $test_item_data = [
+        'name' => 'Test Item for Accounting',
+        'description' => 'Test item to verify accounting entry creation',
+        'quantity' => 2,
+        'price' => 100.00,
+        'date_of_purchase' => '2024-06-15'
+    ];
+
+    // Create item
+    $item = new Item($db);
+    $item->name = $test_item_data['name'];
+    $item->description = $test_item_data['description'];
+    $item->quantity = $test_item_data['quantity'];
+    $item->price = $test_item_data['price'];
+    $item->date_of_purchase = $test_item_data['date_of_purchase'];
+
+    if ($item->create()) {
+        // Create corresponding accounting entry
+        $accounting_entry = new AccountingEntry($db);
+        
+        // Calculate accounting entry fields
+        $total_amount = floatval($test_item_data['price']) * intval($test_item_data['quantity']);
+        $vat_percentage = 25.50;
+        $net_amount = $total_amount / (1 + ($vat_percentage / 100));
+        $vat_amount = $total_amount - $net_amount;
+        $tax_free_amount = $net_amount;
+        
+        // Set accounting entry properties
+        $accounting_entry->entry_date = $test_item_data['date_of_purchase'];
+        $accounting_entry->description = $test_item_data['name'];
+        $accounting_entry->entry_type = 'Kulu';
+        $accounting_entry->category = '222';
+        $accounting_entry->tax_free_amount = $tax_free_amount;
+        $accounting_entry->vat_percentage = $vat_percentage;
+        $accounting_entry->vat_25_5 = $vat_amount;
+        $accounting_entry->vat_14 = 0;
+        $accounting_entry->vat_10 = 0;
+        $accounting_entry->total_amount = $total_amount;
+        $accounting_entry->net_amount = $net_amount;
+        $accounting_entry->vat_amount = $vat_amount;
+        $accounting_entry->reference_number = '';
+        
+        // Create accounting entry
+        if ($accounting_entry->create()) {
+            echo json_encode([
+                'success' => true,
+                'message' => 'Item and accounting entry created successfully',
+                'test_data' => [
+                    'total_amount' => $total_amount,
+                    'net_amount' => $net_amount,
+                    'vat_amount' => $vat_amount,
+                    'tax_free_amount' => $tax_free_amount
+                ]
+            ]);
+        } else {
+            echo json_encode([
+                'success' => false,
+                'message' => 'Item created but accounting entry creation failed'
+            ]);
+        }
+    } else {
+        echo json_encode([
+            'success' => false,
+            'message' => 'Item creation failed'
+        ]);
+    }
+    
+} catch (Exception $e) {
+    echo json_encode([
+        'success' => false,
+        'message' => $e->getMessage()
+    ]);
+}
+?>

+ 159 - 0
database/init.sql

@@ -0,0 +1,159 @@
+-- Database initialization script for Docker container
+-- This script will be executed when the MySQL container starts for the first time
+
+-- Create database if it doesn't exist
+CREATE DATABASE IF NOT EXISTS inventory_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- Use the database
+USE inventory_db;
+
+-- Create users table
+CREATE TABLE IF NOT EXISTS users (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    phone VARCHAR(20),
+    password VARCHAR(255) NOT NULL,
+    role ENUM('admin', 'user') DEFAULT 'user',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+-- Create clients table
+CREATE TABLE IF NOT EXISTS clients (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    y_tunnus VARCHAR(255),
+    company_name VARCHAR(255),
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255) NOT NULL UNIQUE,
+    phone VARCHAR(20),
+    address VARCHAR(255),
+    city VARCHAR(100),
+    state VARCHAR(100),
+    postal_code VARCHAR(20),
+    country VARCHAR(100),
+    notes TEXT,
+    hour_price DECIMAL(10,2) DEFAULT 0.00,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_last_name (last_name)
+);
+
+-- Create contact_persons table
+CREATE TABLE IF NOT EXISTS contact_persons (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    client_id INT NOT NULL,
+    first_name VARCHAR(100) NOT NULL,
+    last_name VARCHAR(100) NOT NULL,
+    email VARCHAR(255),
+    phone VARCHAR(20),
+    position VARCHAR(100),
+    department VARCHAR(100),
+    is_primary BOOLEAN DEFAULT FALSE,
+    notes TEXT,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE
+);
+
+-- Create projects table
+CREATE TABLE IF NOT EXISTS projects (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(255) NOT NULL,
+    description TEXT,
+    status ENUM('planning', 'active', 'completed', 'on_hold', 'cancelled') DEFAULT 'planning',
+    customer_id INT,
+    start_date DATE,
+    end_date DATE,
+    budget DECIMAL(12,2),
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (customer_id) REFERENCES clients(id) ON DELETE SET NULL
+);
+
+-- Create tasks table
+CREATE TABLE IF NOT EXISTS tasks (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    title VARCHAR(255) NOT NULL,
+    description TEXT,
+    status ENUM('pending', 'in_progress', 'completed', 'on_hold', 'cancelled') DEFAULT 'pending',
+    priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
+    project_id INT,
+    assigned_to INT,
+    due_date DATE,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+    FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL
+);
+
+-- Create work_hours table
+CREATE TABLE IF NOT EXISTS work_hours (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_id INT NOT NULL,
+    user_id INT NOT NULL,
+    date DATE NOT NULL,
+    hours DECIMAL(5,2) NOT NULL,
+    description TEXT,
+    rate DECIMAL(10,2),
+    total_amount DECIMAL(10,2),
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+    INDEX idx_task_date (task_id, date),
+    INDEX idx_user_date (user_id, date)
+);
+
+-- Create items table
+CREATE TABLE IF NOT EXISTS items (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(255) NOT NULL,
+    description TEXT,
+    sku VARCHAR(100) UNIQUE,
+    category VARCHAR(100),
+    unit_price DECIMAL(10,2),
+    quantity INT DEFAULT 0,
+    min_quantity INT DEFAULT 0,
+    location VARCHAR(100),
+    supplier VARCHAR(255),
+    purchase_price DECIMAL(10,2),
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+-- Create accounting_entries table
+CREATE TABLE IF NOT EXISTS accounting_entries (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    date DATE NOT NULL,
+    description TEXT NOT NULL,
+    account_number VARCHAR(20) NOT NULL,
+    account_name VARCHAR(255) NOT NULL,
+    category VARCHAR(100),
+    debit DECIMAL(12,2) DEFAULT 0.00,
+    credit DECIMAL(12,2) DEFAULT 0.00,
+    balance DECIMAL(12,2) DEFAULT 0.00,
+    entry_type ENUM('income', 'expense', 'opening_balance') DEFAULT 'expense',
+    reference_number VARCHAR(100),
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_date (date),
+    INDEX idx_account (account_number),
+    INDEX idx_category (category)
+);
+
+-- Insert default admin user (password: admin123)
+INSERT IGNORE INTO users (first_name, last_name, email, password, role) 
+VALUES ('Admin', 'User', 'admin@inventory.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin');
+
+-- Insert sample data for testing
+INSERT IGNORE INTO clients (first_name, last_name, email, company_name, y_tunnus, hour_price) 
+VALUES ('Weikka', 'Valavuo', 'weikka@wavium.fi', 'Wavium', '3464619-2', 10.00);
+
+INSERT IGNORE INTO projects (name, description, status, customer_id) 
+VALUES ('Inventory', 'Inventory Management System', 'active', 1);
+
+INSERT IGNORE INTO tasks (title, description, status, priority, project_id, assigned_to, due_date) 
+VALUES ('Working hours', 'Tuntien kirjaamismahdollisuus', 'in_progress', 'medium', 1, 1, '2026-04-26');

+ 116 - 0
docker-compose.prod.yml

@@ -0,0 +1,116 @@
+version: '3.8'
+
+services:
+  # Backend PHP Service
+  backend:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    container_name: inventory-backend-prod
+    ports:
+      - "8080:80"
+    environment:
+      - DB_HOST=${DB_HOST:-mysql}
+      - DB_PORT=${DB_PORT:-3306}
+      - DB_NAME=${DB_NAME:-inventory_db}
+      - DB_USER=${DB_USER:-inventory_user}
+      - DB_PASS=${DB_PASS}
+      - COMPANY_NAME=${COMPANY_NAME}
+      - COMPANY_ADDRESS=${COMPANY_ADDRESS}
+      - COMPANY_CITY=${COMPANY_CITY}
+      - COMPANY_POSTAL_CODE=${COMPANY_POSTAL_CODE}
+      - COMPANY_COUNTRY=${COMPANY_COUNTRY}
+      - COMPANY_PHONE=${COMPANY_PHONE}
+      - COMPANY_EMAIL=${COMPANY_EMAIL}
+      - COMPANY_Y_TUNNUS=${COMPANY_Y_TUNNUS}
+      - UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-10M}
+      - ALLOWED_FILE_TYPES=${ALLOWED_FILE_TYPES:-pdf,doc,docx,xls,xlsx,jpg,jpeg,png,gif}
+    volumes:
+      - uploads_data:/var/www/html/uploads
+    depends_on:
+      - mysql
+    networks:
+      - inventory-network
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost/api/company.php"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+
+  # Frontend Vue.js Service
+  frontend:
+    build:
+      context: ./frontend
+      dockerfile: Dockerfile
+      args:
+        - VUE_APP_API_URL=${VUE_APP_API_URL:-http://localhost:8080}
+    container_name: inventory-frontend-prod
+    ports:
+      - "3000:80"
+    depends_on:
+      - backend
+    networks:
+      - inventory-network
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost/"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+
+  # MySQL Database Service
+  mysql:
+    image: mysql:8.0
+    container_name: inventory-mysql-prod
+    environment:
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+      - MYSQL_DATABASE=${DB_NAME:-inventory_db}
+      - MYSQL_USER=${DB_USER:-inventory_user}
+      - MYSQL_PASSWORD=${DB_PASS}
+    volumes:
+      - mysql_data:/var/lib/mysql
+      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
+      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro
+    networks:
+      - inventory-network
+    restart: unless-stopped
+    command: --default-authentication-plugin=mysql_native_password --innodb-buffer-pool-size=256M --max-connections=100
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USER:-inventory_user}", "-p${DB_PASS}"]
+      interval: 30s
+      timeout: 10s
+      retries: 5
+
+  # Redis Cache Service
+  redis:
+    image: redis:7-alpine
+    container_name: inventory-redis-prod
+    volumes:
+      - redis_data:/data
+      - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
+    networks:
+      - inventory-network
+    restart: unless-stopped
+    command: redis-server /usr/local/etc/redis/redis.conf
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+
+volumes:
+  mysql_data:
+    driver: local
+  redis_data:
+    driver: local
+  uploads_data:
+    driver: local
+    driver_opts:
+      type: none
+      o: bind
+      device: ${UPLOADS_PATH:-/var/inventory/uploads}
+
+networks:
+  inventory-network:
+    driver: bridge

+ 99 - 0
docker-compose.yml

@@ -0,0 +1,99 @@
+version: '3.8'
+
+services:
+  # Backend PHP Service
+  backend:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    container_name: inventory-backend
+    ports:
+      - "8080:80"
+    environment:
+      - DB_HOST=${DB_HOST:-mysql}
+      - DB_PORT=${DB_PORT:-3306}
+      - DB_NAME=${DB_NAME:-inventory_db}
+      - DB_USER=${DB_USER:-inventory_user}
+      - DB_PASS=${DB_PASS:-inventory_password}
+      - COMPANY_NAME=${COMPANY_NAME:-Inventory Management}
+      - COMPANY_ADDRESS=${COMPANY_ADDRESS:-123 Business St}
+      - COMPANY_CITY=${COMPANY_CITY:-Helsinki}
+      - COMPANY_POSTAL_CODE=${COMPANY_POSTAL_CODE:-00100}
+      - COMPANY_COUNTRY=${COMPANY_COUNTRY:-Finland}
+      - COMPANY_PHONE=${COMPANY_PHONE:-+358 123 456 789}
+      - COMPANY_EMAIL=${COMPANY_EMAIL:-info@company.com}
+      - COMPANY_Y_TUNNUS=${COMPANY_Y_TUNNUS:-1234567-8}
+      - UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-10M}
+      - ALLOWED_FILE_TYPES=${ALLOWED_FILE_TYPES:-pdf,doc,docx,xls,xlsx,jpg,jpeg,png,gif}
+    volumes:
+      - uploads_data:/var/www/html/uploads
+      - ./backend:/var/www/html
+    depends_on:
+      - mysql
+    networks:
+      - inventory-network
+    restart: unless-stopped
+
+  # Frontend Vue.js Service
+  frontend:
+    build:
+      context: ./frontend
+      dockerfile: Dockerfile
+    container_name: inventory-frontend
+    ports:
+      - "3000:80"
+    environment:
+      - VUE_APP_API_URL=${VUE_APP_API_URL:-http://localhost:8080}
+    depends_on:
+      - backend
+    networks:
+      - inventory-network
+    restart: unless-stopped
+
+  # MySQL Database Service
+  mysql:
+    image: mysql:8.0
+    container_name: inventory-mysql
+    ports:
+      - "3306:3306"
+    environment:
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root_password}
+      - MYSQL_DATABASE=${DB_NAME:-inventory_db}
+      - MYSQL_USER=${DB_USER:-inventory_user}
+      - MYSQL_PASSWORD=${DB_PASS:-inventory_password}
+    volumes:
+      - mysql_data:/var/lib/mysql
+      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
+    networks:
+      - inventory-network
+    restart: unless-stopped
+    command: --default-authentication-plugin=mysql_native_password
+
+  # Redis Cache Service (Optional)
+  redis:
+    image: redis:7-alpine
+    container_name: inventory-redis
+    ports:
+      - "6379:6379"
+    volumes:
+      - redis_data:/data
+    networks:
+      - inventory-network
+    restart: unless-stopped
+    command: redis-server --appendonly yes
+
+volumes:
+  mysql_data:
+    driver: local
+  redis_data:
+    driver: local
+  uploads_data:
+    driver: local
+    driver_opts:
+      type: none
+      o: bind
+      device: ${UPLOADS_PATH:-./uploads}
+
+networks:
+  inventory-network:
+    driver: bridge

+ 30 - 0
docker/apache.conf

@@ -0,0 +1,30 @@
+<VirtualHost *:80>
+    ServerName localhost
+    DocumentRoot /var/www/html
+
+    <Directory /var/www/html>
+        AllowOverride All
+        Require all granted
+    </Directory>
+
+    # Enable CORS for API endpoints
+    <Directory "/var/www/html/api">
+        Header set Access-Control-Allow-Origin "*"
+        Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+        Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
+    </Directory>
+
+    # Handle OPTIONS requests for CORS
+    <FilesMatch "\.php$">
+        SetEnvIf Request_Method "OPTIONS" CORs
+    </FilesMatch>
+
+    <IfModule mod_headers.c>
+        Header always set Access-Control-Allow-Origin "*" env=CORs
+        Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" env=CORs
+        Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" env=CORs
+    </IfModule>
+
+    ErrorLog ${APACHE_LOG_DIR}/error.log
+    CustomLog ${APACHE_LOG_DIR}/access.log combined
+</VirtualHost>

+ 18 - 0
docker/dockerignore

@@ -0,0 +1,18 @@
+# Docker ignore file
+.git
+.gitignore
+README.md
+DOCKER_README.md
+.env
+.env.local
+.env.*.local
+node_modules
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.DS_Store
+.vscode
+.idea
+*.log
+uploads/*
+!uploads/.gitkeep

+ 38 - 0
docker/mysql/my.cnf

@@ -0,0 +1,38 @@
+[mysqld]
+# General Configuration
+default-storage-engine=InnoDB
+character-set-server=utf8mb4
+collation-server=utf8mb4_unicode_ci
+
+# Performance Settings
+innodb_buffer_pool_size=256M
+innodb_log_file_size=64M
+innodb_flush_log_at_trx_commit=2
+innodb_flush_method=O_DIRECT
+
+# Connection Settings
+max_connections=100
+max_allowed_packet=64M
+
+# Query Cache (disabled for performance in MySQL 8)
+query_cache_type=0
+query_cache_size=0
+
+# Slow Query Log
+slow_query_log=1
+slow_query_log_file=/var/log/mysql/slow.log
+long_query_time=2
+
+# Error Log
+log_error=/var/log/mysql/error.log
+
+# Binary Log (for replication if needed)
+log_bin=mysql-bin
+binlog_format=ROW
+expire_logs_days=7
+
+[mysql]
+default-character-set=utf8mb4
+
+[client]
+default-character-set=utf8mb4

+ 63 - 0
docker/redis/redis.conf

@@ -0,0 +1,63 @@
+# Redis Configuration for Production
+
+# Network
+bind 0.0.0.0
+port 6379
+timeout 300
+tcp-keepalive 60
+
+# General
+daemonize no
+supervised no
+pidfile /var/run/redis/redis-server.pid
+loglevel notice
+logfile ""
+
+# Snapshotting
+save 900 1
+save 300 10
+save 60 10000
+stop-writes-on-bgsave-error yes
+rdbcompression yes
+rdbchecksum yes
+dbfilename dump.rdb
+dir /data
+
+# Append Only File
+appendonly yes
+appendfilename "appendonly.aof"
+appendfsync everysec
+no-appendfsync-on-rewrite no
+auto-aof-rewrite-percentage 100
+auto-aof-rewrite-min-size 64mb
+aof-load-truncated yes
+
+# Memory Management
+maxmemory 256mb
+maxmemory-policy allkeys-lru
+
+# Security
+# requirepass your_redis_password_here
+
+# Clients
+maxclients 10000
+
+# Slow Log
+slowlog-log-slower-than 10000
+slowlog-max-len 128
+
+# Advanced Config
+hash-max-ziplist-entries 512
+hash-max-ziplist-value 64
+list-max-ziplist-size -2
+list-compress-depth 0
+set-max-intset-entries 512
+zset-max-ziplist-entries 128
+zset-max-ziplist-value 64
+hll-sparse-max-bytes 3000
+activerehashing yes
+client-output-buffer-limit normal 0 0 0
+client-output-buffer-limit slave 256mb 64mb 60
+client-output-buffer-limit pubsub 32mb 8mb 60
+hz 10
+aof-rewrite-incremental-fsync yes

+ 74 - 0
docker/startup.sh

@@ -0,0 +1,74 @@
+#!/bin/bash
+
+# Startup script for the inventory management container
+
+echo "Starting Inventory Management System..."
+
+# Check if database is available
+echo "Waiting for database connection..."
+while ! mysqladmin ping -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" --silent; do
+    echo "Database not ready, waiting..."
+    sleep 2
+done
+
+echo "Database is ready!"
+
+# Create uploads directory if it doesn't exist
+mkdir -p /var/www/html/uploads
+
+# Set proper permissions for uploads directory
+chown -R www-data:www-data /var/www/html/uploads
+chmod -R 755 /var/www/html/uploads
+
+# Create necessary directories for file uploads
+mkdir -p /var/www/html/uploads/documents
+mkdir -p /var/www/html/uploads/images
+mkdir -p /var/www/html/uploads/temp
+
+# Set permissions for subdirectories
+chown -R www-data:www-data /var/www/html/uploads/*
+chmod -R 755 /var/www/html/uploads/*
+
+# Check if .htaccess exists, create if not
+if [ ! -f /var/www/html/.htaccess ]; then
+    echo "Creating .htaccess file..."
+    cat > /var/www/html/.htaccess << EOF
+# Enable URL rewriting
+RewriteEngine On
+
+# API endpoints
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^api/(.*)$ api/$1.php [L]
+
+# Frontend routes (for SPA)
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(.*)$ index.html [L]
+
+# Security headers
+<IfModule mod_headers.c>
+    Header always set X-Frame-Options "SAMEORIGIN"
+    Header always set X-Content-Type-Options "nosniff"
+    Header always set X-XSS-Protection "1; mode=block"
+    Header always set Referrer-Policy "strict-origin-when-cross-origin"
+</IfModule>
+
+# PHP settings
+php_value upload_max_fileSize ${UPLOAD_MAX_SIZE:-10M}
+php_value post_max_size ${UPLOAD_MAX_SIZE:-10M}
+php_value max_execution_time 300
+EOF
+
+    chown www-data:www-data /var/www/html/.htaccess
+fi
+
+# Display company information
+echo "==============================================="
+echo "Company: $(getenv 'COMPANY_NAME' 'Inventory Management')"
+echo "Email: $(getenv 'COMPANY_EMAIL' 'info@company.com')"
+echo "Phone: $(getenv 'COMPANY_PHONE' '+358 123 456 789')"
+echo "==============================================="
+
+echo "Starting Apache..."
+exec apache2-foreground

+ 4 - 0
frontend/.env.production

@@ -0,0 +1,4 @@
+# Production environment variables for Vue.js frontend
+VUE_APP_API_URL=http://localhost:8080
+VUE_APP_TITLE=Inventory Management System
+VUE_APP_VERSION=1.0.0

+ 32 - 0
frontend/Dockerfile

@@ -0,0 +1,32 @@
+# Frontend Dockerfile
+FROM node:18-alpine as build
+
+# Set working directory
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci --only=production
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+
+# Copy built application
+COPY --from=build /app/dist /usr/share/nginx/html
+
+# Copy nginx configuration
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# Expose port
+EXPOSE 80
+
+# Start nginx
+CMD ["nginx", "-g", "daemon off;"]

+ 60 - 0
frontend/nginx.conf

@@ -0,0 +1,60 @@
+server {
+    listen 80;
+    server_name localhost;
+    root /usr/share/nginx/html;
+    index index.html;
+
+    # Enable gzip compression
+    gzip on;
+    gzip_vary on;
+    gzip_min_length 1024;
+    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
+
+    # Handle Vue.js SPA routing
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # API proxy to backend
+    location /api/ {
+        proxy_pass http://backend:80;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        
+        # Handle CORS headers
+        add_header Access-Control-Allow-Origin "*" always;
+        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
+        add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
+        
+        # Handle preflight requests
+        if ($request_method = 'OPTIONS') {
+            add_header Access-Control-Allow-Origin "*";
+            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
+            add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With";
+            add_header Content-Length 0;
+            add_header Content-Type text/plain;
+            return 204;
+        }
+    }
+
+    # Cache static assets
+    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
+        expires 1y;
+        add_header Cache-Control "public, immutable";
+    }
+
+    # Security headers
+    add_header X-Frame-Options "SAMEORIGIN" always;
+    add_header X-Content-Type-Options "nosniff" always;
+    add_header X-XSS-Protection "1; mode=block" always;
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+    # Error pages
+    error_page 404 /index.html;
+    error_page 500 502 503 504 /50x.html;
+    location = /50x.html {
+        root /usr/share/nginx/html;
+    }
+}

+ 1 - 0
frontend/node_modules/.bin/esbuild

@@ -0,0 +1 @@
+../esbuild/bin/esbuild

+ 1 - 0
frontend/node_modules/.bin/nanoid

@@ -0,0 +1 @@
+../nanoid/bin/nanoid.cjs

+ 1 - 0
frontend/node_modules/.bin/parser

@@ -0,0 +1 @@
+../@babel/parser/bin/babel-parser.js

+ 1 - 0
frontend/node_modules/.bin/rollup

@@ -0,0 +1 @@
+../rollup/dist/bin/rollup

+ 1 - 0
frontend/node_modules/.bin/vite

@@ -0,0 +1 @@
+../vite/bin/vite.js

+ 648 - 0
frontend/node_modules/.package-lock.json

@@ -0,0 +1,648 @@
+{
+  "name": "inventory-frontend",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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=="
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
+      "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+      "dev": true,
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.0.0 || ^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
+      "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/shared": "3.5.33",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
+      "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.33",
+        "@vue/shared": "3.5.33"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
+      "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/compiler-core": "3.5.33",
+        "@vue/compiler-dom": "3.5.33",
+        "@vue/compiler-ssr": "3.5.33",
+        "@vue/shared": "3.5.33",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.10",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
+      "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.33",
+        "@vue/shared": "3.5.33"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz",
+      "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
+      "dependencies": {
+        "@vue/shared": "3.5.33"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
+      "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.33",
+        "@vue/shared": "3.5.33"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
+      "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.33",
+        "@vue/runtime-core": "3.5.33",
+        "@vue/shared": "3.5.33",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
+      "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.33",
+        "@vue/shared": "3.5.33"
+      },
+      "peerDependencies": {
+        "vue": "3.5.33"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
+      "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ=="
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
+      "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.18.20",
+        "@esbuild/android-arm64": "0.18.20",
+        "@esbuild/android-x64": "0.18.20",
+        "@esbuild/darwin-arm64": "0.18.20",
+        "@esbuild/darwin-x64": "0.18.20",
+        "@esbuild/freebsd-arm64": "0.18.20",
+        "@esbuild/freebsd-x64": "0.18.20",
+        "@esbuild/linux-arm": "0.18.20",
+        "@esbuild/linux-arm64": "0.18.20",
+        "@esbuild/linux-ia32": "0.18.20",
+        "@esbuild/linux-loong64": "0.18.20",
+        "@esbuild/linux-mips64el": "0.18.20",
+        "@esbuild/linux-ppc64": "0.18.20",
+        "@esbuild/linux-riscv64": "0.18.20",
+        "@esbuild/linux-s390x": "0.18.20",
+        "@esbuild/linux-x64": "0.18.20",
+        "@esbuild/netbsd-x64": "0.18.20",
+        "@esbuild/openbsd-x64": "0.18.20",
+        "@esbuild/sunos-x64": "0.18.20",
+        "@esbuild/win32-arm64": "0.18.20",
+        "@esbuild/win32-ia32": "0.18.20",
+        "@esbuild/win32-x64": "0.18.20"
+      }
+    },
+    "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=="
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.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==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/postcss": {
+      "version": "8.5.10",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+      "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "3.30.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz",
+      "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==",
+      "dev": true,
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=14.18.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "4.5.14",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
+      "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.18.10",
+        "postcss": "^8.4.27",
+        "rollup": "^3.27.1"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 14",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.33",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz",
+      "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.33",
+        "@vue/compiler-sfc": "3.5.33",
+        "@vue/runtime-dom": "3.5.33",
+        "@vue/server-renderer": "3.5.33",
+        "@vue/shared": "3.5.33"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 23 - 0
frontend/node_modules/.vite/deps/_metadata.json

@@ -0,0 +1,23 @@
+{
+  "hash": "83d81001",
+  "browserHash": "4e519e74",
+  "optimized": {
+    "axios": {
+      "src": "../../axios/index.js",
+      "file": "axios.js",
+      "fileHash": "a12bbfea",
+      "needsInterop": false
+    },
+    "vue": {
+      "src": "../../vue/dist/vue.runtime.esm-bundler.js",
+      "file": "vue.js",
+      "fileHash": "ac1136bc",
+      "needsInterop": false
+    }
+  },
+  "chunks": {
+    "chunk-SSYGV25P": {
+      "file": "chunk-SSYGV25P.js"
+    }
+  }
+}

+ 2855 - 0
frontend/node_modules/.vite/deps/axios.js

@@ -0,0 +1,2855 @@
+import {
+  __export
+} from "./chunk-SSYGV25P.js";
+
+// node_modules/axios/lib/helpers/bind.js
+function bind(fn, thisArg) {
+  return function wrap() {
+    return fn.apply(thisArg, arguments);
+  };
+}
+
+// node_modules/axios/lib/utils.js
+var { toString } = Object.prototype;
+var { getPrototypeOf } = Object;
+var { iterator, toStringTag } = Symbol;
+var kindOf = ((cache) => (thing) => {
+  const str = toString.call(thing);
+  return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
+})(/* @__PURE__ */ Object.create(null));
+var kindOfTest = (type) => {
+  type = type.toLowerCase();
+  return (thing) => kindOf(thing) === type;
+};
+var typeOfTest = (type) => (thing) => typeof thing === type;
+var { isArray } = Array;
+var isUndefined = typeOfTest("undefined");
+function isBuffer(val) {
+  return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) && isFunction(val.constructor.isBuffer) && val.constructor.isBuffer(val);
+}
+var isArrayBuffer = kindOfTest("ArrayBuffer");
+function isArrayBufferView(val) {
+  let result;
+  if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView) {
+    result = ArrayBuffer.isView(val);
+  } else {
+    result = val && val.buffer && isArrayBuffer(val.buffer);
+  }
+  return result;
+}
+var isString = typeOfTest("string");
+var isFunction = typeOfTest("function");
+var isNumber = typeOfTest("number");
+var isObject = (thing) => thing !== null && typeof thing === "object";
+var isBoolean = (thing) => thing === true || thing === false;
+var isPlainObject = (val) => {
+  if (kindOf(val) !== "object") {
+    return false;
+  }
+  const prototype2 = getPrototypeOf(val);
+  return (prototype2 === null || prototype2 === Object.prototype || Object.getPrototypeOf(prototype2) === null) && !(toStringTag in val) && !(iterator in val);
+};
+var isEmptyObject = (val) => {
+  if (!isObject(val) || isBuffer(val)) {
+    return false;
+  }
+  try {
+    return Object.keys(val).length === 0 && Object.getPrototypeOf(val) === Object.prototype;
+  } catch (e) {
+    return false;
+  }
+};
+var isDate = kindOfTest("Date");
+var isFile = kindOfTest("File");
+var isReactNativeBlob = (value) => {
+  return !!(value && typeof value.uri !== "undefined");
+};
+var isReactNative = (formData) => formData && typeof formData.getParts !== "undefined";
+var isBlob = kindOfTest("Blob");
+var isFileList = kindOfTest("FileList");
+var isStream = (val) => isObject(val) && isFunction(val.pipe);
+function getGlobal() {
+  if (typeof globalThis !== "undefined")
+    return globalThis;
+  if (typeof self !== "undefined")
+    return self;
+  if (typeof window !== "undefined")
+    return window;
+  if (typeof global !== "undefined")
+    return global;
+  return {};
+}
+var G = getGlobal();
+var FormDataCtor = typeof G.FormData !== "undefined" ? G.FormData : void 0;
+var isFormData = (thing) => {
+  if (!thing)
+    return false;
+  if (FormDataCtor && thing instanceof FormDataCtor)
+    return true;
+  const proto = getPrototypeOf(thing);
+  if (!proto || proto === Object.prototype)
+    return false;
+  if (!isFunction(thing.append))
+    return false;
+  const kind = kindOf(thing);
+  return kind === "formdata" || // detect form-data instance
+  kind === "object" && isFunction(thing.toString) && thing.toString() === "[object FormData]";
+};
+var isURLSearchParams = kindOfTest("URLSearchParams");
+var [isReadableStream, isRequest, isResponse, isHeaders] = [
+  "ReadableStream",
+  "Request",
+  "Response",
+  "Headers"
+].map(kindOfTest);
+var trim = (str) => {
+  return str.trim ? str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "");
+};
+function forEach(obj, fn, { allOwnKeys = false } = {}) {
+  if (obj === null || typeof obj === "undefined") {
+    return;
+  }
+  let i;
+  let l;
+  if (typeof obj !== "object") {
+    obj = [obj];
+  }
+  if (isArray(obj)) {
+    for (i = 0, l = obj.length; i < l; i++) {
+      fn.call(null, obj[i], i, obj);
+    }
+  } else {
+    if (isBuffer(obj)) {
+      return;
+    }
+    const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
+    const len = keys.length;
+    let key;
+    for (i = 0; i < len; i++) {
+      key = keys[i];
+      fn.call(null, obj[key], key, obj);
+    }
+  }
+}
+function findKey(obj, key) {
+  if (isBuffer(obj)) {
+    return null;
+  }
+  key = key.toLowerCase();
+  const keys = Object.keys(obj);
+  let i = keys.length;
+  let _key;
+  while (i-- > 0) {
+    _key = keys[i];
+    if (key === _key.toLowerCase()) {
+      return _key;
+    }
+  }
+  return null;
+}
+var _global = (() => {
+  if (typeof globalThis !== "undefined")
+    return globalThis;
+  return typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : global;
+})();
+var isContextDefined = (context) => !isUndefined(context) && context !== _global;
+function merge() {
+  const { caseless, skipUndefined } = isContextDefined(this) && this || {};
+  const result = {};
+  const assignValue = (val, key) => {
+    if (key === "__proto__" || key === "constructor" || key === "prototype") {
+      return;
+    }
+    const targetKey = caseless && findKey(result, key) || key;
+    if (isPlainObject(result[targetKey]) && isPlainObject(val)) {
+      result[targetKey] = merge(result[targetKey], val);
+    } else if (isPlainObject(val)) {
+      result[targetKey] = merge({}, val);
+    } else if (isArray(val)) {
+      result[targetKey] = val.slice();
+    } else if (!skipUndefined || !isUndefined(val)) {
+      result[targetKey] = val;
+    }
+  };
+  for (let i = 0, l = arguments.length; i < l; i++) {
+    arguments[i] && forEach(arguments[i], assignValue);
+  }
+  return result;
+}
+var extend = (a, b, thisArg, { allOwnKeys } = {}) => {
+  forEach(
+    b,
+    (val, key) => {
+      if (thisArg && isFunction(val)) {
+        Object.defineProperty(a, key, {
+          value: bind(val, thisArg),
+          writable: true,
+          enumerable: true,
+          configurable: true
+        });
+      } else {
+        Object.defineProperty(a, key, {
+          value: val,
+          writable: true,
+          enumerable: true,
+          configurable: true
+        });
+      }
+    },
+    { allOwnKeys }
+  );
+  return a;
+};
+var stripBOM = (content) => {
+  if (content.charCodeAt(0) === 65279) {
+    content = content.slice(1);
+  }
+  return content;
+};
+var inherits = (constructor, superConstructor, props, descriptors) => {
+  constructor.prototype = Object.create(superConstructor.prototype, descriptors);
+  Object.defineProperty(constructor.prototype, "constructor", {
+    value: constructor,
+    writable: true,
+    enumerable: false,
+    configurable: true
+  });
+  Object.defineProperty(constructor, "super", {
+    value: superConstructor.prototype
+  });
+  props && Object.assign(constructor.prototype, props);
+};
+var toFlatObject = (sourceObj, destObj, filter2, propFilter) => {
+  let props;
+  let i;
+  let prop;
+  const merged = {};
+  destObj = destObj || {};
+  if (sourceObj == null)
+    return destObj;
+  do {
+    props = Object.getOwnPropertyNames(sourceObj);
+    i = props.length;
+    while (i-- > 0) {
+      prop = props[i];
+      if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop]) {
+        destObj[prop] = sourceObj[prop];
+        merged[prop] = true;
+      }
+    }
+    sourceObj = filter2 !== false && getPrototypeOf(sourceObj);
+  } while (sourceObj && (!filter2 || filter2(sourceObj, destObj)) && sourceObj !== Object.prototype);
+  return destObj;
+};
+var endsWith = (str, searchString, position) => {
+  str = String(str);
+  if (position === void 0 || position > str.length) {
+    position = str.length;
+  }
+  position -= searchString.length;
+  const lastIndex = str.indexOf(searchString, position);
+  return lastIndex !== -1 && lastIndex === position;
+};
+var toArray = (thing) => {
+  if (!thing)
+    return null;
+  if (isArray(thing))
+    return thing;
+  let i = thing.length;
+  if (!isNumber(i))
+    return null;
+  const arr = new Array(i);
+  while (i-- > 0) {
+    arr[i] = thing[i];
+  }
+  return arr;
+};
+var isTypedArray = ((TypedArray) => {
+  return (thing) => {
+    return TypedArray && thing instanceof TypedArray;
+  };
+})(typeof Uint8Array !== "undefined" && getPrototypeOf(Uint8Array));
+var forEachEntry = (obj, fn) => {
+  const generator = obj && obj[iterator];
+  const _iterator = generator.call(obj);
+  let result;
+  while ((result = _iterator.next()) && !result.done) {
+    const pair = result.value;
+    fn.call(obj, pair[0], pair[1]);
+  }
+};
+var matchAll = (regExp, str) => {
+  let matches;
+  const arr = [];
+  while ((matches = regExp.exec(str)) !== null) {
+    arr.push(matches);
+  }
+  return arr;
+};
+var isHTMLForm = kindOfTest("HTMLFormElement");
+var toCamelCase = (str) => {
+  return str.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g, function replacer(m, p1, p2) {
+    return p1.toUpperCase() + p2;
+  });
+};
+var hasOwnProperty = (({ hasOwnProperty: hasOwnProperty2 }) => (obj, prop) => hasOwnProperty2.call(obj, prop))(Object.prototype);
+var isRegExp = kindOfTest("RegExp");
+var reduceDescriptors = (obj, reducer) => {
+  const descriptors = Object.getOwnPropertyDescriptors(obj);
+  const reducedDescriptors = {};
+  forEach(descriptors, (descriptor, name) => {
+    let ret;
+    if ((ret = reducer(descriptor, name, obj)) !== false) {
+      reducedDescriptors[name] = ret || descriptor;
+    }
+  });
+  Object.defineProperties(obj, reducedDescriptors);
+};
+var freezeMethods = (obj) => {
+  reduceDescriptors(obj, (descriptor, name) => {
+    if (isFunction(obj) && ["arguments", "caller", "callee"].indexOf(name) !== -1) {
+      return false;
+    }
+    const value = obj[name];
+    if (!isFunction(value))
+      return;
+    descriptor.enumerable = false;
+    if ("writable" in descriptor) {
+      descriptor.writable = false;
+      return;
+    }
+    if (!descriptor.set) {
+      descriptor.set = () => {
+        throw Error("Can not rewrite read-only method '" + name + "'");
+      };
+    }
+  });
+};
+var toObjectSet = (arrayOrString, delimiter) => {
+  const obj = {};
+  const define = (arr) => {
+    arr.forEach((value) => {
+      obj[value] = true;
+    });
+  };
+  isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter));
+  return obj;
+};
+var noop = () => {
+};
+var toFiniteNumber = (value, defaultValue) => {
+  return value != null && Number.isFinite(value = +value) ? value : defaultValue;
+};
+function isSpecCompliantForm(thing) {
+  return !!(thing && isFunction(thing.append) && thing[toStringTag] === "FormData" && thing[iterator]);
+}
+var toJSONObject = (obj) => {
+  const stack = new Array(10);
+  const visit = (source, i) => {
+    if (isObject(source)) {
+      if (stack.indexOf(source) >= 0) {
+        return;
+      }
+      if (isBuffer(source)) {
+        return source;
+      }
+      if (!("toJSON" in source)) {
+        stack[i] = source;
+        const target = isArray(source) ? [] : {};
+        forEach(source, (value, key) => {
+          const reducedValue = visit(value, i + 1);
+          !isUndefined(reducedValue) && (target[key] = reducedValue);
+        });
+        stack[i] = void 0;
+        return target;
+      }
+    }
+    return source;
+  };
+  return visit(obj, 0);
+};
+var isAsyncFn = kindOfTest("AsyncFunction");
+var isThenable = (thing) => thing && (isObject(thing) || isFunction(thing)) && isFunction(thing.then) && isFunction(thing.catch);
+var _setImmediate = ((setImmediateSupported, postMessageSupported) => {
+  if (setImmediateSupported) {
+    return setImmediate;
+  }
+  return postMessageSupported ? ((token, callbacks) => {
+    _global.addEventListener(
+      "message",
+      ({ source, data }) => {
+        if (source === _global && data === token) {
+          callbacks.length && callbacks.shift()();
+        }
+      },
+      false
+    );
+    return (cb) => {
+      callbacks.push(cb);
+      _global.postMessage(token, "*");
+    };
+  })(`axios@${Math.random()}`, []) : (cb) => setTimeout(cb);
+})(typeof setImmediate === "function", isFunction(_global.postMessage));
+var asap = typeof queueMicrotask !== "undefined" ? queueMicrotask.bind(_global) : typeof process !== "undefined" && process.nextTick || _setImmediate;
+var isIterable = (thing) => thing != null && isFunction(thing[iterator]);
+var utils_default = {
+  isArray,
+  isArrayBuffer,
+  isBuffer,
+  isFormData,
+  isArrayBufferView,
+  isString,
+  isNumber,
+  isBoolean,
+  isObject,
+  isPlainObject,
+  isEmptyObject,
+  isReadableStream,
+  isRequest,
+  isResponse,
+  isHeaders,
+  isUndefined,
+  isDate,
+  isFile,
+  isReactNativeBlob,
+  isReactNative,
+  isBlob,
+  isRegExp,
+  isFunction,
+  isStream,
+  isURLSearchParams,
+  isTypedArray,
+  isFileList,
+  forEach,
+  merge,
+  extend,
+  trim,
+  stripBOM,
+  inherits,
+  toFlatObject,
+  kindOf,
+  kindOfTest,
+  endsWith,
+  toArray,
+  forEachEntry,
+  matchAll,
+  isHTMLForm,
+  hasOwnProperty,
+  hasOwnProp: hasOwnProperty,
+  // an alias to avoid ESLint no-prototype-builtins detection
+  reduceDescriptors,
+  freezeMethods,
+  toObjectSet,
+  toCamelCase,
+  noop,
+  toFiniteNumber,
+  findKey,
+  global: _global,
+  isContextDefined,
+  isSpecCompliantForm,
+  toJSONObject,
+  isAsyncFn,
+  isThenable,
+  setImmediate: _setImmediate,
+  asap,
+  isIterable
+};
+
+// node_modules/axios/lib/core/AxiosError.js
+var AxiosError = class _AxiosError extends Error {
+  static from(error, code, config, request, response, customProps) {
+    const axiosError = new _AxiosError(error.message, code || error.code, config, request, response);
+    axiosError.cause = error;
+    axiosError.name = error.name;
+    if (error.status != null && axiosError.status == null) {
+      axiosError.status = error.status;
+    }
+    customProps && Object.assign(axiosError, customProps);
+    return axiosError;
+  }
+  /**
+   * Create an Error with the specified message, config, error code, request and response.
+   *
+   * @param {string} message The error message.
+   * @param {string} [code] The error code (for example, 'ECONNABORTED').
+   * @param {Object} [config] The config.
+   * @param {Object} [request] The request.
+   * @param {Object} [response] The response.
+   *
+   * @returns {Error} The created error.
+   */
+  constructor(message, code, config, request, response) {
+    super(message);
+    Object.defineProperty(this, "message", {
+      value: message,
+      enumerable: true,
+      writable: true,
+      configurable: true
+    });
+    this.name = "AxiosError";
+    this.isAxiosError = true;
+    code && (this.code = code);
+    config && (this.config = config);
+    request && (this.request = request);
+    if (response) {
+      this.response = response;
+      this.status = response.status;
+    }
+  }
+  toJSON() {
+    return {
+      // Standard
+      message: this.message,
+      name: this.name,
+      // Microsoft
+      description: this.description,
+      number: this.number,
+      // Mozilla
+      fileName: this.fileName,
+      lineNumber: this.lineNumber,
+      columnNumber: this.columnNumber,
+      stack: this.stack,
+      // Axios
+      config: utils_default.toJSONObject(this.config),
+      code: this.code,
+      status: this.status
+    };
+  }
+};
+AxiosError.ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE";
+AxiosError.ERR_BAD_OPTION = "ERR_BAD_OPTION";
+AxiosError.ECONNABORTED = "ECONNABORTED";
+AxiosError.ETIMEDOUT = "ETIMEDOUT";
+AxiosError.ERR_NETWORK = "ERR_NETWORK";
+AxiosError.ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS";
+AxiosError.ERR_DEPRECATED = "ERR_DEPRECATED";
+AxiosError.ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
+AxiosError.ERR_BAD_REQUEST = "ERR_BAD_REQUEST";
+AxiosError.ERR_CANCELED = "ERR_CANCELED";
+AxiosError.ERR_NOT_SUPPORT = "ERR_NOT_SUPPORT";
+AxiosError.ERR_INVALID_URL = "ERR_INVALID_URL";
+AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED = "ERR_FORM_DATA_DEPTH_EXCEEDED";
+var AxiosError_default = AxiosError;
+
+// node_modules/axios/lib/helpers/null.js
+var null_default = null;
+
+// node_modules/axios/lib/helpers/toFormData.js
+function isVisitable(thing) {
+  return utils_default.isPlainObject(thing) || utils_default.isArray(thing);
+}
+function removeBrackets(key) {
+  return utils_default.endsWith(key, "[]") ? key.slice(0, -2) : key;
+}
+function renderKey(path, key, dots) {
+  if (!path)
+    return key;
+  return path.concat(key).map(function each(token, i) {
+    token = removeBrackets(token);
+    return !dots && i ? "[" + token + "]" : token;
+  }).join(dots ? "." : "");
+}
+function isFlatArray(arr) {
+  return utils_default.isArray(arr) && !arr.some(isVisitable);
+}
+var predicates = utils_default.toFlatObject(utils_default, {}, null, function filter(prop) {
+  return /^is[A-Z]/.test(prop);
+});
+function toFormData(obj, formData, options) {
+  if (!utils_default.isObject(obj)) {
+    throw new TypeError("target must be an object");
+  }
+  formData = formData || new (null_default || FormData)();
+  options = utils_default.toFlatObject(
+    options,
+    {
+      metaTokens: true,
+      dots: false,
+      indexes: false
+    },
+    false,
+    function defined(option, source) {
+      return !utils_default.isUndefined(source[option]);
+    }
+  );
+  const metaTokens = options.metaTokens;
+  const visitor = options.visitor || defaultVisitor;
+  const dots = options.dots;
+  const indexes = options.indexes;
+  const _Blob = options.Blob || typeof Blob !== "undefined" && Blob;
+  const maxDepth = options.maxDepth === void 0 ? 100 : options.maxDepth;
+  const useBlob = _Blob && utils_default.isSpecCompliantForm(formData);
+  if (!utils_default.isFunction(visitor)) {
+    throw new TypeError("visitor must be a function");
+  }
+  function convertValue(value) {
+    if (value === null)
+      return "";
+    if (utils_default.isDate(value)) {
+      return value.toISOString();
+    }
+    if (utils_default.isBoolean(value)) {
+      return value.toString();
+    }
+    if (!useBlob && utils_default.isBlob(value)) {
+      throw new AxiosError_default("Blob is not supported. Use a Buffer instead.");
+    }
+    if (utils_default.isArrayBuffer(value) || utils_default.isTypedArray(value)) {
+      return useBlob && typeof Blob === "function" ? new Blob([value]) : Buffer.from(value);
+    }
+    return value;
+  }
+  function defaultVisitor(value, key, path) {
+    let arr = value;
+    if (utils_default.isReactNative(formData) && utils_default.isReactNativeBlob(value)) {
+      formData.append(renderKey(path, key, dots), convertValue(value));
+      return false;
+    }
+    if (value && !path && typeof value === "object") {
+      if (utils_default.endsWith(key, "{}")) {
+        key = metaTokens ? key : key.slice(0, -2);
+        value = JSON.stringify(value);
+      } else if (utils_default.isArray(value) && isFlatArray(value) || (utils_default.isFileList(value) || utils_default.endsWith(key, "[]")) && (arr = utils_default.toArray(value))) {
+        key = removeBrackets(key);
+        arr.forEach(function each(el, index) {
+          !(utils_default.isUndefined(el) || el === null) && formData.append(
+            // eslint-disable-next-line no-nested-ternary
+            indexes === true ? renderKey([key], index, dots) : indexes === null ? key : key + "[]",
+            convertValue(el)
+          );
+        });
+        return false;
+      }
+    }
+    if (isVisitable(value)) {
+      return true;
+    }
+    formData.append(renderKey(path, key, dots), convertValue(value));
+    return false;
+  }
+  const stack = [];
+  const exposedHelpers = Object.assign(predicates, {
+    defaultVisitor,
+    convertValue,
+    isVisitable
+  });
+  function build(value, path, depth = 0) {
+    if (utils_default.isUndefined(value))
+      return;
+    if (depth > maxDepth) {
+      throw new AxiosError_default(
+        "Object is too deeply nested (" + depth + " levels). Max depth: " + maxDepth,
+        AxiosError_default.ERR_FORM_DATA_DEPTH_EXCEEDED
+      );
+    }
+    if (stack.indexOf(value) !== -1) {
+      throw Error("Circular reference detected in " + path.join("."));
+    }
+    stack.push(value);
+    utils_default.forEach(value, function each(el, key) {
+      const result = !(utils_default.isUndefined(el) || el === null) && visitor.call(formData, el, utils_default.isString(key) ? key.trim() : key, path, exposedHelpers);
+      if (result === true) {
+        build(el, path ? path.concat(key) : [key], depth + 1);
+      }
+    });
+    stack.pop();
+  }
+  if (!utils_default.isObject(obj)) {
+    throw new TypeError("data must be an object");
+  }
+  build(obj);
+  return formData;
+}
+var toFormData_default = toFormData;
+
+// node_modules/axios/lib/helpers/AxiosURLSearchParams.js
+function encode(str) {
+  const charMap = {
+    "!": "%21",
+    "'": "%27",
+    "(": "%28",
+    ")": "%29",
+    "~": "%7E",
+    "%20": "+"
+  };
+  return encodeURIComponent(str).replace(/[!'()~]|%20/g, function replacer(match) {
+    return charMap[match];
+  });
+}
+function AxiosURLSearchParams(params, options) {
+  this._pairs = [];
+  params && toFormData_default(params, this, options);
+}
+var prototype = AxiosURLSearchParams.prototype;
+prototype.append = function append(name, value) {
+  this._pairs.push([name, value]);
+};
+prototype.toString = function toString2(encoder) {
+  const _encode = encoder ? function(value) {
+    return encoder.call(this, value, encode);
+  } : encode;
+  return this._pairs.map(function each(pair) {
+    return _encode(pair[0]) + "=" + _encode(pair[1]);
+  }, "").join("&");
+};
+var AxiosURLSearchParams_default = AxiosURLSearchParams;
+
+// node_modules/axios/lib/helpers/buildURL.js
+function encode2(val) {
+  return encodeURIComponent(val).replace(/%3A/gi, ":").replace(/%24/g, "$").replace(/%2C/gi, ",").replace(/%20/g, "+");
+}
+function buildURL(url, params, options) {
+  if (!params) {
+    return url;
+  }
+  const _encode = options && options.encode || encode2;
+  const _options = utils_default.isFunction(options) ? {
+    serialize: options
+  } : options;
+  const serializeFn = _options && _options.serialize;
+  let serializedParams;
+  if (serializeFn) {
+    serializedParams = serializeFn(params, _options);
+  } else {
+    serializedParams = utils_default.isURLSearchParams(params) ? params.toString() : new AxiosURLSearchParams_default(params, _options).toString(_encode);
+  }
+  if (serializedParams) {
+    const hashmarkIndex = url.indexOf("#");
+    if (hashmarkIndex !== -1) {
+      url = url.slice(0, hashmarkIndex);
+    }
+    url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
+  }
+  return url;
+}
+
+// node_modules/axios/lib/core/InterceptorManager.js
+var InterceptorManager = class {
+  constructor() {
+    this.handlers = [];
+  }
+  /**
+   * Add a new interceptor to the stack
+   *
+   * @param {Function} fulfilled The function to handle `then` for a `Promise`
+   * @param {Function} rejected The function to handle `reject` for a `Promise`
+   * @param {Object} options The options for the interceptor, synchronous and runWhen
+   *
+   * @return {Number} An ID used to remove interceptor later
+   */
+  use(fulfilled, rejected, options) {
+    this.handlers.push({
+      fulfilled,
+      rejected,
+      synchronous: options ? options.synchronous : false,
+      runWhen: options ? options.runWhen : null
+    });
+    return this.handlers.length - 1;
+  }
+  /**
+   * Remove an interceptor from the stack
+   *
+   * @param {Number} id The ID that was returned by `use`
+   *
+   * @returns {void}
+   */
+  eject(id) {
+    if (this.handlers[id]) {
+      this.handlers[id] = null;
+    }
+  }
+  /**
+   * Clear all interceptors from the stack
+   *
+   * @returns {void}
+   */
+  clear() {
+    if (this.handlers) {
+      this.handlers = [];
+    }
+  }
+  /**
+   * Iterate over all the registered interceptors
+   *
+   * This method is particularly useful for skipping over any
+   * interceptors that may have become `null` calling `eject`.
+   *
+   * @param {Function} fn The function to call for each interceptor
+   *
+   * @returns {void}
+   */
+  forEach(fn) {
+    utils_default.forEach(this.handlers, function forEachHandler(h) {
+      if (h !== null) {
+        fn(h);
+      }
+    });
+  }
+};
+var InterceptorManager_default = InterceptorManager;
+
+// node_modules/axios/lib/defaults/transitional.js
+var transitional_default = {
+  silentJSONParsing: true,
+  forcedJSONParsing: true,
+  clarifyTimeoutError: false,
+  legacyInterceptorReqResOrdering: true
+};
+
+// node_modules/axios/lib/platform/browser/classes/URLSearchParams.js
+var URLSearchParams_default = typeof URLSearchParams !== "undefined" ? URLSearchParams : AxiosURLSearchParams_default;
+
+// node_modules/axios/lib/platform/browser/classes/FormData.js
+var FormData_default = typeof FormData !== "undefined" ? FormData : null;
+
+// node_modules/axios/lib/platform/browser/classes/Blob.js
+var Blob_default = typeof Blob !== "undefined" ? Blob : null;
+
+// node_modules/axios/lib/platform/browser/index.js
+var browser_default = {
+  isBrowser: true,
+  classes: {
+    URLSearchParams: URLSearchParams_default,
+    FormData: FormData_default,
+    Blob: Blob_default
+  },
+  protocols: ["http", "https", "file", "blob", "url", "data"]
+};
+
+// node_modules/axios/lib/platform/common/utils.js
+var utils_exports = {};
+__export(utils_exports, {
+  hasBrowserEnv: () => hasBrowserEnv,
+  hasStandardBrowserEnv: () => hasStandardBrowserEnv,
+  hasStandardBrowserWebWorkerEnv: () => hasStandardBrowserWebWorkerEnv,
+  navigator: () => _navigator,
+  origin: () => origin
+});
+var hasBrowserEnv = typeof window !== "undefined" && typeof document !== "undefined";
+var _navigator = typeof navigator === "object" && navigator || void 0;
+var hasStandardBrowserEnv = hasBrowserEnv && (!_navigator || ["ReactNative", "NativeScript", "NS"].indexOf(_navigator.product) < 0);
+var hasStandardBrowserWebWorkerEnv = (() => {
+  return typeof WorkerGlobalScope !== "undefined" && // eslint-disable-next-line no-undef
+  self instanceof WorkerGlobalScope && typeof self.importScripts === "function";
+})();
+var origin = hasBrowserEnv && window.location.href || "http://localhost";
+
+// node_modules/axios/lib/platform/index.js
+var platform_default = {
+  ...utils_exports,
+  ...browser_default
+};
+
+// node_modules/axios/lib/helpers/toURLEncodedForm.js
+function toURLEncodedForm(data, options) {
+  return toFormData_default(data, new platform_default.classes.URLSearchParams(), {
+    visitor: function(value, key, path, helpers) {
+      if (platform_default.isNode && utils_default.isBuffer(value)) {
+        this.append(key, value.toString("base64"));
+        return false;
+      }
+      return helpers.defaultVisitor.apply(this, arguments);
+    },
+    ...options
+  });
+}
+
+// node_modules/axios/lib/helpers/formDataToJSON.js
+function parsePropPath(name) {
+  return utils_default.matchAll(/\w+|\[(\w*)]/g, name).map((match) => {
+    return match[0] === "[]" ? "" : match[1] || match[0];
+  });
+}
+function arrayToObject(arr) {
+  const obj = {};
+  const keys = Object.keys(arr);
+  let i;
+  const len = keys.length;
+  let key;
+  for (i = 0; i < len; i++) {
+    key = keys[i];
+    obj[key] = arr[key];
+  }
+  return obj;
+}
+function formDataToJSON(formData) {
+  function buildPath(path, value, target, index) {
+    let name = path[index++];
+    if (name === "__proto__")
+      return true;
+    const isNumericKey = Number.isFinite(+name);
+    const isLast = index >= path.length;
+    name = !name && utils_default.isArray(target) ? target.length : name;
+    if (isLast) {
+      if (utils_default.hasOwnProp(target, name)) {
+        target[name] = utils_default.isArray(target[name]) ? target[name].concat(value) : [target[name], value];
+      } else {
+        target[name] = value;
+      }
+      return !isNumericKey;
+    }
+    if (!target[name] || !utils_default.isObject(target[name])) {
+      target[name] = [];
+    }
+    const result = buildPath(path, value, target[name], index);
+    if (result && utils_default.isArray(target[name])) {
+      target[name] = arrayToObject(target[name]);
+    }
+    return !isNumericKey;
+  }
+  if (utils_default.isFormData(formData) && utils_default.isFunction(formData.entries)) {
+    const obj = {};
+    utils_default.forEachEntry(formData, (name, value) => {
+      buildPath(parsePropPath(name), value, obj, 0);
+    });
+    return obj;
+  }
+  return null;
+}
+var formDataToJSON_default = formDataToJSON;
+
+// node_modules/axios/lib/defaults/index.js
+var own = (obj, key) => obj != null && utils_default.hasOwnProp(obj, key) ? obj[key] : void 0;
+function stringifySafely(rawValue, parser, encoder) {
+  if (utils_default.isString(rawValue)) {
+    try {
+      (parser || JSON.parse)(rawValue);
+      return utils_default.trim(rawValue);
+    } catch (e) {
+      if (e.name !== "SyntaxError") {
+        throw e;
+      }
+    }
+  }
+  return (encoder || JSON.stringify)(rawValue);
+}
+var defaults = {
+  transitional: transitional_default,
+  adapter: ["xhr", "http", "fetch"],
+  transformRequest: [
+    function transformRequest(data, headers) {
+      const contentType = headers.getContentType() || "";
+      const hasJSONContentType = contentType.indexOf("application/json") > -1;
+      const isObjectPayload = utils_default.isObject(data);
+      if (isObjectPayload && utils_default.isHTMLForm(data)) {
+        data = new FormData(data);
+      }
+      const isFormData2 = utils_default.isFormData(data);
+      if (isFormData2) {
+        return hasJSONContentType ? JSON.stringify(formDataToJSON_default(data)) : data;
+      }
+      if (utils_default.isArrayBuffer(data) || utils_default.isBuffer(data) || utils_default.isStream(data) || utils_default.isFile(data) || utils_default.isBlob(data) || utils_default.isReadableStream(data)) {
+        return data;
+      }
+      if (utils_default.isArrayBufferView(data)) {
+        return data.buffer;
+      }
+      if (utils_default.isURLSearchParams(data)) {
+        headers.setContentType("application/x-www-form-urlencoded;charset=utf-8", false);
+        return data.toString();
+      }
+      let isFileList2;
+      if (isObjectPayload) {
+        const formSerializer = own(this, "formSerializer");
+        if (contentType.indexOf("application/x-www-form-urlencoded") > -1) {
+          return toURLEncodedForm(data, formSerializer).toString();
+        }
+        if ((isFileList2 = utils_default.isFileList(data)) || contentType.indexOf("multipart/form-data") > -1) {
+          const env = own(this, "env");
+          const _FormData = env && env.FormData;
+          return toFormData_default(
+            isFileList2 ? { "files[]": data } : data,
+            _FormData && new _FormData(),
+            formSerializer
+          );
+        }
+      }
+      if (isObjectPayload || hasJSONContentType) {
+        headers.setContentType("application/json", false);
+        return stringifySafely(data);
+      }
+      return data;
+    }
+  ],
+  transformResponse: [
+    function transformResponse(data) {
+      const transitional2 = own(this, "transitional") || defaults.transitional;
+      const forcedJSONParsing = transitional2 && transitional2.forcedJSONParsing;
+      const responseType = own(this, "responseType");
+      const JSONRequested = responseType === "json";
+      if (utils_default.isResponse(data) || utils_default.isReadableStream(data)) {
+        return data;
+      }
+      if (data && utils_default.isString(data) && (forcedJSONParsing && !responseType || JSONRequested)) {
+        const silentJSONParsing = transitional2 && transitional2.silentJSONParsing;
+        const strictJSONParsing = !silentJSONParsing && JSONRequested;
+        try {
+          return JSON.parse(data, own(this, "parseReviver"));
+        } catch (e) {
+          if (strictJSONParsing) {
+            if (e.name === "SyntaxError") {
+              throw AxiosError_default.from(e, AxiosError_default.ERR_BAD_RESPONSE, this, null, own(this, "response"));
+            }
+            throw e;
+          }
+        }
+      }
+      return data;
+    }
+  ],
+  /**
+   * A timeout in milliseconds to abort a request. If set to 0 (default) a
+   * timeout is not created.
+   */
+  timeout: 0,
+  xsrfCookieName: "XSRF-TOKEN",
+  xsrfHeaderName: "X-XSRF-TOKEN",
+  maxContentLength: -1,
+  maxBodyLength: -1,
+  env: {
+    FormData: platform_default.classes.FormData,
+    Blob: platform_default.classes.Blob
+  },
+  validateStatus: function validateStatus(status) {
+    return status >= 200 && status < 300;
+  },
+  headers: {
+    common: {
+      Accept: "application/json, text/plain, */*",
+      "Content-Type": void 0
+    }
+  }
+};
+utils_default.forEach(["delete", "get", "head", "post", "put", "patch"], (method) => {
+  defaults.headers[method] = {};
+});
+var defaults_default = defaults;
+
+// node_modules/axios/lib/helpers/parseHeaders.js
+var ignoreDuplicateOf = utils_default.toObjectSet([
+  "age",
+  "authorization",
+  "content-length",
+  "content-type",
+  "etag",
+  "expires",
+  "from",
+  "host",
+  "if-modified-since",
+  "if-unmodified-since",
+  "last-modified",
+  "location",
+  "max-forwards",
+  "proxy-authorization",
+  "referer",
+  "retry-after",
+  "user-agent"
+]);
+var parseHeaders_default = (rawHeaders) => {
+  const parsed = {};
+  let key;
+  let val;
+  let i;
+  rawHeaders && rawHeaders.split("\n").forEach(function parser(line) {
+    i = line.indexOf(":");
+    key = line.substring(0, i).trim().toLowerCase();
+    val = line.substring(i + 1).trim();
+    if (!key || parsed[key] && ignoreDuplicateOf[key]) {
+      return;
+    }
+    if (key === "set-cookie") {
+      if (parsed[key]) {
+        parsed[key].push(val);
+      } else {
+        parsed[key] = [val];
+      }
+    } else {
+      parsed[key] = parsed[key] ? parsed[key] + ", " + val : val;
+    }
+  });
+  return parsed;
+};
+
+// node_modules/axios/lib/core/AxiosHeaders.js
+var $internals = Symbol("internals");
+var INVALID_HEADER_VALUE_CHARS_RE = /[^\x09\x20-\x7E\x80-\xFF]/g;
+function trimSPorHTAB(str) {
+  let start = 0;
+  let end = str.length;
+  while (start < end) {
+    const code = str.charCodeAt(start);
+    if (code !== 9 && code !== 32) {
+      break;
+    }
+    start += 1;
+  }
+  while (end > start) {
+    const code = str.charCodeAt(end - 1);
+    if (code !== 9 && code !== 32) {
+      break;
+    }
+    end -= 1;
+  }
+  return start === 0 && end === str.length ? str : str.slice(start, end);
+}
+function normalizeHeader(header) {
+  return header && String(header).trim().toLowerCase();
+}
+function sanitizeHeaderValue(str) {
+  return trimSPorHTAB(str.replace(INVALID_HEADER_VALUE_CHARS_RE, ""));
+}
+function normalizeValue(value) {
+  if (value === false || value == null) {
+    return value;
+  }
+  return utils_default.isArray(value) ? value.map(normalizeValue) : sanitizeHeaderValue(String(value));
+}
+function parseTokens(str) {
+  const tokens = /* @__PURE__ */ Object.create(null);
+  const tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;
+  let match;
+  while (match = tokensRE.exec(str)) {
+    tokens[match[1]] = match[2];
+  }
+  return tokens;
+}
+var isValidHeaderName = (str) => /^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(str.trim());
+function matchHeaderValue(context, value, header, filter2, isHeaderNameFilter) {
+  if (utils_default.isFunction(filter2)) {
+    return filter2.call(this, value, header);
+  }
+  if (isHeaderNameFilter) {
+    value = header;
+  }
+  if (!utils_default.isString(value))
+    return;
+  if (utils_default.isString(filter2)) {
+    return value.indexOf(filter2) !== -1;
+  }
+  if (utils_default.isRegExp(filter2)) {
+    return filter2.test(value);
+  }
+}
+function formatHeader(header) {
+  return header.trim().toLowerCase().replace(/([a-z\d])(\w*)/g, (w, char, str) => {
+    return char.toUpperCase() + str;
+  });
+}
+function buildAccessors(obj, header) {
+  const accessorName = utils_default.toCamelCase(" " + header);
+  ["get", "set", "has"].forEach((methodName) => {
+    Object.defineProperty(obj, methodName + accessorName, {
+      value: function(arg1, arg2, arg3) {
+        return this[methodName].call(this, header, arg1, arg2, arg3);
+      },
+      configurable: true
+    });
+  });
+}
+var AxiosHeaders = class {
+  constructor(headers) {
+    headers && this.set(headers);
+  }
+  set(header, valueOrRewrite, rewrite) {
+    const self2 = this;
+    function setHeader(_value, _header, _rewrite) {
+      const lHeader = normalizeHeader(_header);
+      if (!lHeader) {
+        throw new Error("header name must be a non-empty string");
+      }
+      const key = utils_default.findKey(self2, lHeader);
+      if (!key || self2[key] === void 0 || _rewrite === true || _rewrite === void 0 && self2[key] !== false) {
+        self2[key || _header] = normalizeValue(_value);
+      }
+    }
+    const setHeaders = (headers, _rewrite) => utils_default.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite));
+    if (utils_default.isPlainObject(header) || header instanceof this.constructor) {
+      setHeaders(header, valueOrRewrite);
+    } else if (utils_default.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
+      setHeaders(parseHeaders_default(header), valueOrRewrite);
+    } else if (utils_default.isObject(header) && utils_default.isIterable(header)) {
+      let obj = {}, dest, key;
+      for (const entry of header) {
+        if (!utils_default.isArray(entry)) {
+          throw TypeError("Object iterator must return a key-value pair");
+        }
+        obj[key = entry[0]] = (dest = obj[key]) ? utils_default.isArray(dest) ? [...dest, entry[1]] : [dest, entry[1]] : entry[1];
+      }
+      setHeaders(obj, valueOrRewrite);
+    } else {
+      header != null && setHeader(valueOrRewrite, header, rewrite);
+    }
+    return this;
+  }
+  get(header, parser) {
+    header = normalizeHeader(header);
+    if (header) {
+      const key = utils_default.findKey(this, header);
+      if (key) {
+        const value = this[key];
+        if (!parser) {
+          return value;
+        }
+        if (parser === true) {
+          return parseTokens(value);
+        }
+        if (utils_default.isFunction(parser)) {
+          return parser.call(this, value, key);
+        }
+        if (utils_default.isRegExp(parser)) {
+          return parser.exec(value);
+        }
+        throw new TypeError("parser must be boolean|regexp|function");
+      }
+    }
+  }
+  has(header, matcher) {
+    header = normalizeHeader(header);
+    if (header) {
+      const key = utils_default.findKey(this, header);
+      return !!(key && this[key] !== void 0 && (!matcher || matchHeaderValue(this, this[key], key, matcher)));
+    }
+    return false;
+  }
+  delete(header, matcher) {
+    const self2 = this;
+    let deleted = false;
+    function deleteHeader(_header) {
+      _header = normalizeHeader(_header);
+      if (_header) {
+        const key = utils_default.findKey(self2, _header);
+        if (key && (!matcher || matchHeaderValue(self2, self2[key], key, matcher))) {
+          delete self2[key];
+          deleted = true;
+        }
+      }
+    }
+    if (utils_default.isArray(header)) {
+      header.forEach(deleteHeader);
+    } else {
+      deleteHeader(header);
+    }
+    return deleted;
+  }
+  clear(matcher) {
+    const keys = Object.keys(this);
+    let i = keys.length;
+    let deleted = false;
+    while (i--) {
+      const key = keys[i];
+      if (!matcher || matchHeaderValue(this, this[key], key, matcher, true)) {
+        delete this[key];
+        deleted = true;
+      }
+    }
+    return deleted;
+  }
+  normalize(format) {
+    const self2 = this;
+    const headers = {};
+    utils_default.forEach(this, (value, header) => {
+      const key = utils_default.findKey(headers, header);
+      if (key) {
+        self2[key] = normalizeValue(value);
+        delete self2[header];
+        return;
+      }
+      const normalized = format ? formatHeader(header) : String(header).trim();
+      if (normalized !== header) {
+        delete self2[header];
+      }
+      self2[normalized] = normalizeValue(value);
+      headers[normalized] = true;
+    });
+    return this;
+  }
+  concat(...targets) {
+    return this.constructor.concat(this, ...targets);
+  }
+  toJSON(asStrings) {
+    const obj = /* @__PURE__ */ Object.create(null);
+    utils_default.forEach(this, (value, header) => {
+      value != null && value !== false && (obj[header] = asStrings && utils_default.isArray(value) ? value.join(", ") : value);
+    });
+    return obj;
+  }
+  [Symbol.iterator]() {
+    return Object.entries(this.toJSON())[Symbol.iterator]();
+  }
+  toString() {
+    return Object.entries(this.toJSON()).map(([header, value]) => header + ": " + value).join("\n");
+  }
+  getSetCookie() {
+    return this.get("set-cookie") || [];
+  }
+  get [Symbol.toStringTag]() {
+    return "AxiosHeaders";
+  }
+  static from(thing) {
+    return thing instanceof this ? thing : new this(thing);
+  }
+  static concat(first, ...targets) {
+    const computed = new this(first);
+    targets.forEach((target) => computed.set(target));
+    return computed;
+  }
+  static accessor(header) {
+    const internals = this[$internals] = this[$internals] = {
+      accessors: {}
+    };
+    const accessors = internals.accessors;
+    const prototype2 = this.prototype;
+    function defineAccessor(_header) {
+      const lHeader = normalizeHeader(_header);
+      if (!accessors[lHeader]) {
+        buildAccessors(prototype2, _header);
+        accessors[lHeader] = true;
+      }
+    }
+    utils_default.isArray(header) ? header.forEach(defineAccessor) : defineAccessor(header);
+    return this;
+  }
+};
+AxiosHeaders.accessor([
+  "Content-Type",
+  "Content-Length",
+  "Accept",
+  "Accept-Encoding",
+  "User-Agent",
+  "Authorization"
+]);
+utils_default.reduceDescriptors(AxiosHeaders.prototype, ({ value }, key) => {
+  let mapped = key[0].toUpperCase() + key.slice(1);
+  return {
+    get: () => value,
+    set(headerValue) {
+      this[mapped] = headerValue;
+    }
+  };
+});
+utils_default.freezeMethods(AxiosHeaders);
+var AxiosHeaders_default = AxiosHeaders;
+
+// node_modules/axios/lib/core/transformData.js
+function transformData(fns, response) {
+  const config = this || defaults_default;
+  const context = response || config;
+  const headers = AxiosHeaders_default.from(context.headers);
+  let data = context.data;
+  utils_default.forEach(fns, function transform(fn) {
+    data = fn.call(config, data, headers.normalize(), response ? response.status : void 0);
+  });
+  headers.normalize();
+  return data;
+}
+
+// node_modules/axios/lib/cancel/isCancel.js
+function isCancel(value) {
+  return !!(value && value.__CANCEL__);
+}
+
+// node_modules/axios/lib/cancel/CanceledError.js
+var CanceledError = class extends AxiosError_default {
+  /**
+   * A `CanceledError` is an object that is thrown when an operation is canceled.
+   *
+   * @param {string=} message The message.
+   * @param {Object=} config The config.
+   * @param {Object=} request The request.
+   *
+   * @returns {CanceledError} The created error.
+   */
+  constructor(message, config, request) {
+    super(message == null ? "canceled" : message, AxiosError_default.ERR_CANCELED, config, request);
+    this.name = "CanceledError";
+    this.__CANCEL__ = true;
+  }
+};
+var CanceledError_default = CanceledError;
+
+// node_modules/axios/lib/core/settle.js
+function settle(resolve, reject, response) {
+  const validateStatus2 = response.config.validateStatus;
+  if (!response.status || !validateStatus2 || validateStatus2(response.status)) {
+    resolve(response);
+  } else {
+    reject(
+      new AxiosError_default(
+        "Request failed with status code " + response.status,
+        [AxiosError_default.ERR_BAD_REQUEST, AxiosError_default.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
+        response.config,
+        response.request,
+        response
+      )
+    );
+  }
+}
+
+// node_modules/axios/lib/helpers/parseProtocol.js
+function parseProtocol(url) {
+  const match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url);
+  return match && match[1] || "";
+}
+
+// node_modules/axios/lib/helpers/speedometer.js
+function speedometer(samplesCount, min) {
+  samplesCount = samplesCount || 10;
+  const bytes = new Array(samplesCount);
+  const timestamps = new Array(samplesCount);
+  let head = 0;
+  let tail = 0;
+  let firstSampleTS;
+  min = min !== void 0 ? min : 1e3;
+  return function push(chunkLength) {
+    const now = Date.now();
+    const startedAt = timestamps[tail];
+    if (!firstSampleTS) {
+      firstSampleTS = now;
+    }
+    bytes[head] = chunkLength;
+    timestamps[head] = now;
+    let i = tail;
+    let bytesCount = 0;
+    while (i !== head) {
+      bytesCount += bytes[i++];
+      i = i % samplesCount;
+    }
+    head = (head + 1) % samplesCount;
+    if (head === tail) {
+      tail = (tail + 1) % samplesCount;
+    }
+    if (now - firstSampleTS < min) {
+      return;
+    }
+    const passed = startedAt && now - startedAt;
+    return passed ? Math.round(bytesCount * 1e3 / passed) : void 0;
+  };
+}
+var speedometer_default = speedometer;
+
+// node_modules/axios/lib/helpers/throttle.js
+function throttle(fn, freq) {
+  let timestamp = 0;
+  let threshold = 1e3 / freq;
+  let lastArgs;
+  let timer;
+  const invoke = (args, now = Date.now()) => {
+    timestamp = now;
+    lastArgs = null;
+    if (timer) {
+      clearTimeout(timer);
+      timer = null;
+    }
+    fn(...args);
+  };
+  const throttled = (...args) => {
+    const now = Date.now();
+    const passed = now - timestamp;
+    if (passed >= threshold) {
+      invoke(args, now);
+    } else {
+      lastArgs = args;
+      if (!timer) {
+        timer = setTimeout(() => {
+          timer = null;
+          invoke(lastArgs);
+        }, threshold - passed);
+      }
+    }
+  };
+  const flush = () => lastArgs && invoke(lastArgs);
+  return [throttled, flush];
+}
+var throttle_default = throttle;
+
+// node_modules/axios/lib/helpers/progressEventReducer.js
+var progressEventReducer = (listener, isDownloadStream, freq = 3) => {
+  let bytesNotified = 0;
+  const _speedometer = speedometer_default(50, 250);
+  return throttle_default((e) => {
+    const rawLoaded = e.loaded;
+    const total = e.lengthComputable ? e.total : void 0;
+    const loaded = total != null ? Math.min(rawLoaded, total) : rawLoaded;
+    const progressBytes = Math.max(0, loaded - bytesNotified);
+    const rate = _speedometer(progressBytes);
+    bytesNotified = Math.max(bytesNotified, loaded);
+    const data = {
+      loaded,
+      total,
+      progress: total ? loaded / total : void 0,
+      bytes: progressBytes,
+      rate: rate ? rate : void 0,
+      estimated: rate && total ? (total - loaded) / rate : void 0,
+      event: e,
+      lengthComputable: total != null,
+      [isDownloadStream ? "download" : "upload"]: true
+    };
+    listener(data);
+  }, freq);
+};
+var progressEventDecorator = (total, throttled) => {
+  const lengthComputable = total != null;
+  return [
+    (loaded) => throttled[0]({
+      lengthComputable,
+      total,
+      loaded
+    }),
+    throttled[1]
+  ];
+};
+var asyncDecorator = (fn) => (...args) => utils_default.asap(() => fn(...args));
+
+// node_modules/axios/lib/helpers/isURLSameOrigin.js
+var isURLSameOrigin_default = platform_default.hasStandardBrowserEnv ? ((origin2, isMSIE) => (url) => {
+  url = new URL(url, platform_default.origin);
+  return origin2.protocol === url.protocol && origin2.host === url.host && (isMSIE || origin2.port === url.port);
+})(
+  new URL(platform_default.origin),
+  platform_default.navigator && /(msie|trident)/i.test(platform_default.navigator.userAgent)
+) : () => true;
+
+// node_modules/axios/lib/helpers/cookies.js
+var cookies_default = platform_default.hasStandardBrowserEnv ? (
+  // Standard browser envs support document.cookie
+  {
+    write(name, value, expires, path, domain, secure, sameSite) {
+      if (typeof document === "undefined")
+        return;
+      const cookie = [`${name}=${encodeURIComponent(value)}`];
+      if (utils_default.isNumber(expires)) {
+        cookie.push(`expires=${new Date(expires).toUTCString()}`);
+      }
+      if (utils_default.isString(path)) {
+        cookie.push(`path=${path}`);
+      }
+      if (utils_default.isString(domain)) {
+        cookie.push(`domain=${domain}`);
+      }
+      if (secure === true) {
+        cookie.push("secure");
+      }
+      if (utils_default.isString(sameSite)) {
+        cookie.push(`SameSite=${sameSite}`);
+      }
+      document.cookie = cookie.join("; ");
+    },
+    read(name) {
+      if (typeof document === "undefined")
+        return null;
+      const match = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
+      return match ? decodeURIComponent(match[1]) : null;
+    },
+    remove(name) {
+      this.write(name, "", Date.now() - 864e5, "/");
+    }
+  }
+) : (
+  // Non-standard browser env (web workers, react-native) lack needed support.
+  {
+    write() {
+    },
+    read() {
+      return null;
+    },
+    remove() {
+    }
+  }
+);
+
+// node_modules/axios/lib/helpers/isAbsoluteURL.js
+function isAbsoluteURL(url) {
+  if (typeof url !== "string") {
+    return false;
+  }
+  return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
+}
+
+// node_modules/axios/lib/helpers/combineURLs.js
+function combineURLs(baseURL, relativeURL) {
+  return relativeURL ? baseURL.replace(/\/?\/$/, "") + "/" + relativeURL.replace(/^\/+/, "") : baseURL;
+}
+
+// node_modules/axios/lib/core/buildFullPath.js
+function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
+  let isRelativeUrl = !isAbsoluteURL(requestedURL);
+  if (baseURL && (isRelativeUrl || allowAbsoluteUrls === false)) {
+    return combineURLs(baseURL, requestedURL);
+  }
+  return requestedURL;
+}
+
+// node_modules/axios/lib/core/mergeConfig.js
+var headersToObject = (thing) => thing instanceof AxiosHeaders_default ? { ...thing } : thing;
+function mergeConfig(config1, config2) {
+  config2 = config2 || {};
+  const config = /* @__PURE__ */ Object.create(null);
+  Object.defineProperty(config, "hasOwnProperty", {
+    value: Object.prototype.hasOwnProperty,
+    enumerable: false,
+    writable: true,
+    configurable: true
+  });
+  function getMergedValue(target, source, prop, caseless) {
+    if (utils_default.isPlainObject(target) && utils_default.isPlainObject(source)) {
+      return utils_default.merge.call({ caseless }, target, source);
+    } else if (utils_default.isPlainObject(source)) {
+      return utils_default.merge({}, source);
+    } else if (utils_default.isArray(source)) {
+      return source.slice();
+    }
+    return source;
+  }
+  function mergeDeepProperties(a, b, prop, caseless) {
+    if (!utils_default.isUndefined(b)) {
+      return getMergedValue(a, b, prop, caseless);
+    } else if (!utils_default.isUndefined(a)) {
+      return getMergedValue(void 0, a, prop, caseless);
+    }
+  }
+  function valueFromConfig2(a, b) {
+    if (!utils_default.isUndefined(b)) {
+      return getMergedValue(void 0, b);
+    }
+  }
+  function defaultToConfig2(a, b) {
+    if (!utils_default.isUndefined(b)) {
+      return getMergedValue(void 0, b);
+    } else if (!utils_default.isUndefined(a)) {
+      return getMergedValue(void 0, a);
+    }
+  }
+  function mergeDirectKeys(a, b, prop) {
+    if (utils_default.hasOwnProp(config2, prop)) {
+      return getMergedValue(a, b);
+    } else if (utils_default.hasOwnProp(config1, prop)) {
+      return getMergedValue(void 0, a);
+    }
+  }
+  const mergeMap = {
+    url: valueFromConfig2,
+    method: valueFromConfig2,
+    data: valueFromConfig2,
+    baseURL: defaultToConfig2,
+    transformRequest: defaultToConfig2,
+    transformResponse: defaultToConfig2,
+    paramsSerializer: defaultToConfig2,
+    timeout: defaultToConfig2,
+    timeoutMessage: defaultToConfig2,
+    withCredentials: defaultToConfig2,
+    withXSRFToken: defaultToConfig2,
+    adapter: defaultToConfig2,
+    responseType: defaultToConfig2,
+    xsrfCookieName: defaultToConfig2,
+    xsrfHeaderName: defaultToConfig2,
+    onUploadProgress: defaultToConfig2,
+    onDownloadProgress: defaultToConfig2,
+    decompress: defaultToConfig2,
+    maxContentLength: defaultToConfig2,
+    maxBodyLength: defaultToConfig2,
+    beforeRedirect: defaultToConfig2,
+    transport: defaultToConfig2,
+    httpAgent: defaultToConfig2,
+    httpsAgent: defaultToConfig2,
+    cancelToken: defaultToConfig2,
+    socketPath: defaultToConfig2,
+    allowedSocketPaths: defaultToConfig2,
+    responseEncoding: defaultToConfig2,
+    validateStatus: mergeDirectKeys,
+    headers: (a, b, prop) => mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true)
+  };
+  utils_default.forEach(Object.keys({ ...config1, ...config2 }), function computeConfigValue(prop) {
+    if (prop === "__proto__" || prop === "constructor" || prop === "prototype")
+      return;
+    const merge2 = utils_default.hasOwnProp(mergeMap, prop) ? mergeMap[prop] : mergeDeepProperties;
+    const a = utils_default.hasOwnProp(config1, prop) ? config1[prop] : void 0;
+    const b = utils_default.hasOwnProp(config2, prop) ? config2[prop] : void 0;
+    const configValue = merge2(a, b, prop);
+    utils_default.isUndefined(configValue) && merge2 !== mergeDirectKeys || (config[prop] = configValue);
+  });
+  return config;
+}
+
+// node_modules/axios/lib/helpers/resolveConfig.js
+var resolveConfig_default = (config) => {
+  const newConfig = mergeConfig({}, config);
+  const own2 = (key) => utils_default.hasOwnProp(newConfig, key) ? newConfig[key] : void 0;
+  const data = own2("data");
+  let withXSRFToken = own2("withXSRFToken");
+  const xsrfHeaderName = own2("xsrfHeaderName");
+  const xsrfCookieName = own2("xsrfCookieName");
+  let headers = own2("headers");
+  const auth = own2("auth");
+  const baseURL = own2("baseURL");
+  const allowAbsoluteUrls = own2("allowAbsoluteUrls");
+  const url = own2("url");
+  newConfig.headers = headers = AxiosHeaders_default.from(headers);
+  newConfig.url = buildURL(
+    buildFullPath(baseURL, url, allowAbsoluteUrls),
+    config.params,
+    config.paramsSerializer
+  );
+  if (auth) {
+    headers.set(
+      "Authorization",
+      "Basic " + btoa(
+        (auth.username || "") + ":" + (auth.password ? unescape(encodeURIComponent(auth.password)) : "")
+      )
+    );
+  }
+  if (utils_default.isFormData(data)) {
+    if (platform_default.hasStandardBrowserEnv || platform_default.hasStandardBrowserWebWorkerEnv) {
+      headers.setContentType(void 0);
+    } else if (utils_default.isFunction(data.getHeaders)) {
+      const formHeaders = data.getHeaders();
+      const allowedHeaders = ["content-type", "content-length"];
+      Object.entries(formHeaders).forEach(([key, val]) => {
+        if (allowedHeaders.includes(key.toLowerCase())) {
+          headers.set(key, val);
+        }
+      });
+    }
+  }
+  if (platform_default.hasStandardBrowserEnv) {
+    if (utils_default.isFunction(withXSRFToken)) {
+      withXSRFToken = withXSRFToken(newConfig);
+    }
+    const shouldSendXSRF = withXSRFToken === true || withXSRFToken == null && isURLSameOrigin_default(newConfig.url);
+    if (shouldSendXSRF) {
+      const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies_default.read(xsrfCookieName);
+      if (xsrfValue) {
+        headers.set(xsrfHeaderName, xsrfValue);
+      }
+    }
+  }
+  return newConfig;
+};
+
+// node_modules/axios/lib/adapters/xhr.js
+var isXHRAdapterSupported = typeof XMLHttpRequest !== "undefined";
+var xhr_default = isXHRAdapterSupported && function(config) {
+  return new Promise(function dispatchXhrRequest(resolve, reject) {
+    const _config = resolveConfig_default(config);
+    let requestData = _config.data;
+    const requestHeaders = AxiosHeaders_default.from(_config.headers).normalize();
+    let { responseType, onUploadProgress, onDownloadProgress } = _config;
+    let onCanceled;
+    let uploadThrottled, downloadThrottled;
+    let flushUpload, flushDownload;
+    function done() {
+      flushUpload && flushUpload();
+      flushDownload && flushDownload();
+      _config.cancelToken && _config.cancelToken.unsubscribe(onCanceled);
+      _config.signal && _config.signal.removeEventListener("abort", onCanceled);
+    }
+    let request = new XMLHttpRequest();
+    request.open(_config.method.toUpperCase(), _config.url, true);
+    request.timeout = _config.timeout;
+    function onloadend() {
+      if (!request) {
+        return;
+      }
+      const responseHeaders = AxiosHeaders_default.from(
+        "getAllResponseHeaders" in request && request.getAllResponseHeaders()
+      );
+      const responseData = !responseType || responseType === "text" || responseType === "json" ? request.responseText : request.response;
+      const response = {
+        data: responseData,
+        status: request.status,
+        statusText: request.statusText,
+        headers: responseHeaders,
+        config,
+        request
+      };
+      settle(
+        function _resolve(value) {
+          resolve(value);
+          done();
+        },
+        function _reject(err) {
+          reject(err);
+          done();
+        },
+        response
+      );
+      request = null;
+    }
+    if ("onloadend" in request) {
+      request.onloadend = onloadend;
+    } else {
+      request.onreadystatechange = function handleLoad() {
+        if (!request || request.readyState !== 4) {
+          return;
+        }
+        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf("file:") === 0)) {
+          return;
+        }
+        setTimeout(onloadend);
+      };
+    }
+    request.onabort = function handleAbort() {
+      if (!request) {
+        return;
+      }
+      reject(new AxiosError_default("Request aborted", AxiosError_default.ECONNABORTED, config, request));
+      request = null;
+    };
+    request.onerror = function handleError(event) {
+      const msg = event && event.message ? event.message : "Network Error";
+      const err = new AxiosError_default(msg, AxiosError_default.ERR_NETWORK, config, request);
+      err.event = event || null;
+      reject(err);
+      request = null;
+    };
+    request.ontimeout = function handleTimeout() {
+      let timeoutErrorMessage = _config.timeout ? "timeout of " + _config.timeout + "ms exceeded" : "timeout exceeded";
+      const transitional2 = _config.transitional || transitional_default;
+      if (_config.timeoutErrorMessage) {
+        timeoutErrorMessage = _config.timeoutErrorMessage;
+      }
+      reject(
+        new AxiosError_default(
+          timeoutErrorMessage,
+          transitional2.clarifyTimeoutError ? AxiosError_default.ETIMEDOUT : AxiosError_default.ECONNABORTED,
+          config,
+          request
+        )
+      );
+      request = null;
+    };
+    requestData === void 0 && requestHeaders.setContentType(null);
+    if ("setRequestHeader" in request) {
+      utils_default.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
+        request.setRequestHeader(key, val);
+      });
+    }
+    if (!utils_default.isUndefined(_config.withCredentials)) {
+      request.withCredentials = !!_config.withCredentials;
+    }
+    if (responseType && responseType !== "json") {
+      request.responseType = _config.responseType;
+    }
+    if (onDownloadProgress) {
+      [downloadThrottled, flushDownload] = progressEventReducer(onDownloadProgress, true);
+      request.addEventListener("progress", downloadThrottled);
+    }
+    if (onUploadProgress && request.upload) {
+      [uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress);
+      request.upload.addEventListener("progress", uploadThrottled);
+      request.upload.addEventListener("loadend", flushUpload);
+    }
+    if (_config.cancelToken || _config.signal) {
+      onCanceled = (cancel) => {
+        if (!request) {
+          return;
+        }
+        reject(!cancel || cancel.type ? new CanceledError_default(null, config, request) : cancel);
+        request.abort();
+        request = null;
+      };
+      _config.cancelToken && _config.cancelToken.subscribe(onCanceled);
+      if (_config.signal) {
+        _config.signal.aborted ? onCanceled() : _config.signal.addEventListener("abort", onCanceled);
+      }
+    }
+    const protocol = parseProtocol(_config.url);
+    if (protocol && platform_default.protocols.indexOf(protocol) === -1) {
+      reject(
+        new AxiosError_default(
+          "Unsupported protocol " + protocol + ":",
+          AxiosError_default.ERR_BAD_REQUEST,
+          config
+        )
+      );
+      return;
+    }
+    request.send(requestData || null);
+  });
+};
+
+// node_modules/axios/lib/helpers/composeSignals.js
+var composeSignals = (signals, timeout) => {
+  const { length } = signals = signals ? signals.filter(Boolean) : [];
+  if (timeout || length) {
+    let controller = new AbortController();
+    let aborted;
+    const onabort = function(reason) {
+      if (!aborted) {
+        aborted = true;
+        unsubscribe();
+        const err = reason instanceof Error ? reason : this.reason;
+        controller.abort(
+          err instanceof AxiosError_default ? err : new CanceledError_default(err instanceof Error ? err.message : err)
+        );
+      }
+    };
+    let timer = timeout && setTimeout(() => {
+      timer = null;
+      onabort(new AxiosError_default(`timeout of ${timeout}ms exceeded`, AxiosError_default.ETIMEDOUT));
+    }, timeout);
+    const unsubscribe = () => {
+      if (signals) {
+        timer && clearTimeout(timer);
+        timer = null;
+        signals.forEach((signal2) => {
+          signal2.unsubscribe ? signal2.unsubscribe(onabort) : signal2.removeEventListener("abort", onabort);
+        });
+        signals = null;
+      }
+    };
+    signals.forEach((signal2) => signal2.addEventListener("abort", onabort));
+    const { signal } = controller;
+    signal.unsubscribe = () => utils_default.asap(unsubscribe);
+    return signal;
+  }
+};
+var composeSignals_default = composeSignals;
+
+// node_modules/axios/lib/helpers/trackStream.js
+var streamChunk = function* (chunk, chunkSize) {
+  let len = chunk.byteLength;
+  if (!chunkSize || len < chunkSize) {
+    yield chunk;
+    return;
+  }
+  let pos = 0;
+  let end;
+  while (pos < len) {
+    end = pos + chunkSize;
+    yield chunk.slice(pos, end);
+    pos = end;
+  }
+};
+var readBytes = async function* (iterable, chunkSize) {
+  for await (const chunk of readStream(iterable)) {
+    yield* streamChunk(chunk, chunkSize);
+  }
+};
+var readStream = async function* (stream) {
+  if (stream[Symbol.asyncIterator]) {
+    yield* stream;
+    return;
+  }
+  const reader = stream.getReader();
+  try {
+    for (; ; ) {
+      const { done, value } = await reader.read();
+      if (done) {
+        break;
+      }
+      yield value;
+    }
+  } finally {
+    await reader.cancel();
+  }
+};
+var trackStream = (stream, chunkSize, onProgress, onFinish) => {
+  const iterator2 = readBytes(stream, chunkSize);
+  let bytes = 0;
+  let done;
+  let _onFinish = (e) => {
+    if (!done) {
+      done = true;
+      onFinish && onFinish(e);
+    }
+  };
+  return new ReadableStream(
+    {
+      async pull(controller) {
+        try {
+          const { done: done2, value } = await iterator2.next();
+          if (done2) {
+            _onFinish();
+            controller.close();
+            return;
+          }
+          let len = value.byteLength;
+          if (onProgress) {
+            let loadedBytes = bytes += len;
+            onProgress(loadedBytes);
+          }
+          controller.enqueue(new Uint8Array(value));
+        } catch (err) {
+          _onFinish(err);
+          throw err;
+        }
+      },
+      cancel(reason) {
+        _onFinish(reason);
+        return iterator2.return();
+      }
+    },
+    {
+      highWaterMark: 2
+    }
+  );
+};
+
+// node_modules/axios/lib/adapters/fetch.js
+var DEFAULT_CHUNK_SIZE = 64 * 1024;
+var { isFunction: isFunction2 } = utils_default;
+var globalFetchAPI = (({ Request, Response }) => ({
+  Request,
+  Response
+}))(utils_default.global);
+var { ReadableStream: ReadableStream2, TextEncoder } = utils_default.global;
+var test = (fn, ...args) => {
+  try {
+    return !!fn(...args);
+  } catch (e) {
+    return false;
+  }
+};
+var factory = (env) => {
+  env = utils_default.merge.call(
+    {
+      skipUndefined: true
+    },
+    globalFetchAPI,
+    env
+  );
+  const { fetch: envFetch, Request, Response } = env;
+  const isFetchSupported = envFetch ? isFunction2(envFetch) : typeof fetch === "function";
+  const isRequestSupported = isFunction2(Request);
+  const isResponseSupported = isFunction2(Response);
+  if (!isFetchSupported) {
+    return false;
+  }
+  const isReadableStreamSupported = isFetchSupported && isFunction2(ReadableStream2);
+  const encodeText = isFetchSupported && (typeof TextEncoder === "function" ? ((encoder) => (str) => encoder.encode(str))(new TextEncoder()) : async (str) => new Uint8Array(await new Request(str).arrayBuffer()));
+  const supportsRequestStream = isRequestSupported && isReadableStreamSupported && test(() => {
+    let duplexAccessed = false;
+    const request = new Request(platform_default.origin, {
+      body: new ReadableStream2(),
+      method: "POST",
+      get duplex() {
+        duplexAccessed = true;
+        return "half";
+      }
+    });
+    const hasContentType = request.headers.has("Content-Type");
+    if (request.body != null) {
+      request.body.cancel();
+    }
+    return duplexAccessed && !hasContentType;
+  });
+  const supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(() => utils_default.isReadableStream(new Response("").body));
+  const resolvers = {
+    stream: supportsResponseStream && ((res) => res.body)
+  };
+  isFetchSupported && (() => {
+    ["text", "arrayBuffer", "blob", "formData", "stream"].forEach((type) => {
+      !resolvers[type] && (resolvers[type] = (res, config) => {
+        let method = res && res[type];
+        if (method) {
+          return method.call(res);
+        }
+        throw new AxiosError_default(
+          `Response type '${type}' is not supported`,
+          AxiosError_default.ERR_NOT_SUPPORT,
+          config
+        );
+      });
+    });
+  })();
+  const getBodyLength = async (body) => {
+    if (body == null) {
+      return 0;
+    }
+    if (utils_default.isBlob(body)) {
+      return body.size;
+    }
+    if (utils_default.isSpecCompliantForm(body)) {
+      const _request = new Request(platform_default.origin, {
+        method: "POST",
+        body
+      });
+      return (await _request.arrayBuffer()).byteLength;
+    }
+    if (utils_default.isArrayBufferView(body) || utils_default.isArrayBuffer(body)) {
+      return body.byteLength;
+    }
+    if (utils_default.isURLSearchParams(body)) {
+      body = body + "";
+    }
+    if (utils_default.isString(body)) {
+      return (await encodeText(body)).byteLength;
+    }
+  };
+  const resolveBodyLength = async (headers, body) => {
+    const length = utils_default.toFiniteNumber(headers.getContentLength());
+    return length == null ? getBodyLength(body) : length;
+  };
+  return async (config) => {
+    let {
+      url,
+      method,
+      data,
+      signal,
+      cancelToken,
+      timeout,
+      onDownloadProgress,
+      onUploadProgress,
+      responseType,
+      headers,
+      withCredentials = "same-origin",
+      fetchOptions
+    } = resolveConfig_default(config);
+    let _fetch = envFetch || fetch;
+    responseType = responseType ? (responseType + "").toLowerCase() : "text";
+    let composedSignal = composeSignals_default(
+      [signal, cancelToken && cancelToken.toAbortSignal()],
+      timeout
+    );
+    let request = null;
+    const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => {
+      composedSignal.unsubscribe();
+    });
+    let requestContentLength;
+    try {
+      if (onUploadProgress && supportsRequestStream && method !== "get" && method !== "head" && (requestContentLength = await resolveBodyLength(headers, data)) !== 0) {
+        let _request = new Request(url, {
+          method: "POST",
+          body: data,
+          duplex: "half"
+        });
+        let contentTypeHeader;
+        if (utils_default.isFormData(data) && (contentTypeHeader = _request.headers.get("content-type"))) {
+          headers.setContentType(contentTypeHeader);
+        }
+        if (_request.body) {
+          const [onProgress, flush] = progressEventDecorator(
+            requestContentLength,
+            progressEventReducer(asyncDecorator(onUploadProgress))
+          );
+          data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
+        }
+      }
+      if (!utils_default.isString(withCredentials)) {
+        withCredentials = withCredentials ? "include" : "omit";
+      }
+      const isCredentialsSupported = isRequestSupported && "credentials" in Request.prototype;
+      if (utils_default.isFormData(data)) {
+        const contentType = headers.getContentType();
+        if (contentType && /^multipart\/form-data/i.test(contentType) && !/boundary=/i.test(contentType)) {
+          headers.delete("content-type");
+        }
+      }
+      const resolvedOptions = {
+        ...fetchOptions,
+        signal: composedSignal,
+        method: method.toUpperCase(),
+        headers: headers.normalize().toJSON(),
+        body: data,
+        duplex: "half",
+        credentials: isCredentialsSupported ? withCredentials : void 0
+      };
+      request = isRequestSupported && new Request(url, resolvedOptions);
+      let response = await (isRequestSupported ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions));
+      const isStreamResponse = supportsResponseStream && (responseType === "stream" || responseType === "response");
+      if (supportsResponseStream && (onDownloadProgress || isStreamResponse && unsubscribe)) {
+        const options = {};
+        ["status", "statusText", "headers"].forEach((prop) => {
+          options[prop] = response[prop];
+        });
+        const responseContentLength = utils_default.toFiniteNumber(response.headers.get("content-length"));
+        const [onProgress, flush] = onDownloadProgress && progressEventDecorator(
+          responseContentLength,
+          progressEventReducer(asyncDecorator(onDownloadProgress), true)
+        ) || [];
+        response = new Response(
+          trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
+            flush && flush();
+            unsubscribe && unsubscribe();
+          }),
+          options
+        );
+      }
+      responseType = responseType || "text";
+      let responseData = await resolvers[utils_default.findKey(resolvers, responseType) || "text"](
+        response,
+        config
+      );
+      !isStreamResponse && unsubscribe && unsubscribe();
+      return await new Promise((resolve, reject) => {
+        settle(resolve, reject, {
+          data: responseData,
+          headers: AxiosHeaders_default.from(response.headers),
+          status: response.status,
+          statusText: response.statusText,
+          config,
+          request
+        });
+      });
+    } catch (err) {
+      unsubscribe && unsubscribe();
+      if (err && err.name === "TypeError" && /Load failed|fetch/i.test(err.message)) {
+        throw Object.assign(
+          new AxiosError_default(
+            "Network Error",
+            AxiosError_default.ERR_NETWORK,
+            config,
+            request,
+            err && err.response
+          ),
+          {
+            cause: err.cause || err
+          }
+        );
+      }
+      throw AxiosError_default.from(err, err && err.code, config, request, err && err.response);
+    }
+  };
+};
+var seedCache = /* @__PURE__ */ new Map();
+var getFetch = (config) => {
+  let env = config && config.env || {};
+  const { fetch: fetch2, Request, Response } = env;
+  const seeds = [Request, Response, fetch2];
+  let len = seeds.length, i = len, seed, target, map = seedCache;
+  while (i--) {
+    seed = seeds[i];
+    target = map.get(seed);
+    target === void 0 && map.set(seed, target = i ? /* @__PURE__ */ new Map() : factory(env));
+    map = target;
+  }
+  return target;
+};
+var adapter = getFetch();
+
+// node_modules/axios/lib/adapters/adapters.js
+var knownAdapters = {
+  http: null_default,
+  xhr: xhr_default,
+  fetch: {
+    get: getFetch
+  }
+};
+utils_default.forEach(knownAdapters, (fn, value) => {
+  if (fn) {
+    try {
+      Object.defineProperty(fn, "name", { value });
+    } catch (e) {
+    }
+    Object.defineProperty(fn, "adapterName", { value });
+  }
+});
+var renderReason = (reason) => `- ${reason}`;
+var isResolvedHandle = (adapter2) => utils_default.isFunction(adapter2) || adapter2 === null || adapter2 === false;
+function getAdapter(adapters, config) {
+  adapters = utils_default.isArray(adapters) ? adapters : [adapters];
+  const { length } = adapters;
+  let nameOrAdapter;
+  let adapter2;
+  const rejectedReasons = {};
+  for (let i = 0; i < length; i++) {
+    nameOrAdapter = adapters[i];
+    let id;
+    adapter2 = nameOrAdapter;
+    if (!isResolvedHandle(nameOrAdapter)) {
+      adapter2 = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
+      if (adapter2 === void 0) {
+        throw new AxiosError_default(`Unknown adapter '${id}'`);
+      }
+    }
+    if (adapter2 && (utils_default.isFunction(adapter2) || (adapter2 = adapter2.get(config)))) {
+      break;
+    }
+    rejectedReasons[id || "#" + i] = adapter2;
+  }
+  if (!adapter2) {
+    const reasons = Object.entries(rejectedReasons).map(
+      ([id, state]) => `adapter ${id} ` + (state === false ? "is not supported by the environment" : "is not available in the build")
+    );
+    let s = length ? reasons.length > 1 ? "since :\n" + reasons.map(renderReason).join("\n") : " " + renderReason(reasons[0]) : "as no adapter specified";
+    throw new AxiosError_default(
+      `There is no suitable adapter to dispatch the request ` + s,
+      "ERR_NOT_SUPPORT"
+    );
+  }
+  return adapter2;
+}
+var adapters_default = {
+  /**
+   * Resolve an adapter from a list of adapter names or functions.
+   * @type {Function}
+   */
+  getAdapter,
+  /**
+   * Exposes all known adapters
+   * @type {Object<string, Function|Object>}
+   */
+  adapters: knownAdapters
+};
+
+// node_modules/axios/lib/core/dispatchRequest.js
+function throwIfCancellationRequested(config) {
+  if (config.cancelToken) {
+    config.cancelToken.throwIfRequested();
+  }
+  if (config.signal && config.signal.aborted) {
+    throw new CanceledError_default(null, config);
+  }
+}
+function dispatchRequest(config) {
+  throwIfCancellationRequested(config);
+  config.headers = AxiosHeaders_default.from(config.headers);
+  config.data = transformData.call(config, config.transformRequest);
+  if (["post", "put", "patch"].indexOf(config.method) !== -1) {
+    config.headers.setContentType("application/x-www-form-urlencoded", false);
+  }
+  const adapter2 = adapters_default.getAdapter(config.adapter || defaults_default.adapter, config);
+  return adapter2(config).then(
+    function onAdapterResolution(response) {
+      throwIfCancellationRequested(config);
+      response.data = transformData.call(config, config.transformResponse, response);
+      response.headers = AxiosHeaders_default.from(response.headers);
+      return response;
+    },
+    function onAdapterRejection(reason) {
+      if (!isCancel(reason)) {
+        throwIfCancellationRequested(config);
+        if (reason && reason.response) {
+          reason.response.data = transformData.call(
+            config,
+            config.transformResponse,
+            reason.response
+          );
+          reason.response.headers = AxiosHeaders_default.from(reason.response.headers);
+        }
+      }
+      return Promise.reject(reason);
+    }
+  );
+}
+
+// node_modules/axios/lib/env/data.js
+var VERSION = "1.15.2";
+
+// node_modules/axios/lib/helpers/validator.js
+var validators = {};
+["object", "boolean", "number", "function", "string", "symbol"].forEach((type, i) => {
+  validators[type] = function validator(thing) {
+    return typeof thing === type || "a" + (i < 1 ? "n " : " ") + type;
+  };
+});
+var deprecatedWarnings = {};
+validators.transitional = function transitional(validator, version, message) {
+  function formatMessage(opt, desc) {
+    return "[Axios v" + VERSION + "] Transitional option '" + opt + "'" + desc + (message ? ". " + message : "");
+  }
+  return (value, opt, opts) => {
+    if (validator === false) {
+      throw new AxiosError_default(
+        formatMessage(opt, " has been removed" + (version ? " in " + version : "")),
+        AxiosError_default.ERR_DEPRECATED
+      );
+    }
+    if (version && !deprecatedWarnings[opt]) {
+      deprecatedWarnings[opt] = true;
+      console.warn(
+        formatMessage(
+          opt,
+          " has been deprecated since v" + version + " and will be removed in the near future"
+        )
+      );
+    }
+    return validator ? validator(value, opt, opts) : true;
+  };
+};
+validators.spelling = function spelling(correctSpelling) {
+  return (value, opt) => {
+    console.warn(`${opt} is likely a misspelling of ${correctSpelling}`);
+    return true;
+  };
+};
+function assertOptions(options, schema, allowUnknown) {
+  if (typeof options !== "object") {
+    throw new AxiosError_default("options must be an object", AxiosError_default.ERR_BAD_OPTION_VALUE);
+  }
+  const keys = Object.keys(options);
+  let i = keys.length;
+  while (i-- > 0) {
+    const opt = keys[i];
+    const validator = Object.prototype.hasOwnProperty.call(schema, opt) ? schema[opt] : void 0;
+    if (validator) {
+      const value = options[opt];
+      const result = value === void 0 || validator(value, opt, options);
+      if (result !== true) {
+        throw new AxiosError_default(
+          "option " + opt + " must be " + result,
+          AxiosError_default.ERR_BAD_OPTION_VALUE
+        );
+      }
+      continue;
+    }
+    if (allowUnknown !== true) {
+      throw new AxiosError_default("Unknown option " + opt, AxiosError_default.ERR_BAD_OPTION);
+    }
+  }
+}
+var validator_default = {
+  assertOptions,
+  validators
+};
+
+// node_modules/axios/lib/core/Axios.js
+var validators2 = validator_default.validators;
+var Axios = class {
+  constructor(instanceConfig) {
+    this.defaults = instanceConfig || {};
+    this.interceptors = {
+      request: new InterceptorManager_default(),
+      response: new InterceptorManager_default()
+    };
+  }
+  /**
+   * Dispatch a request
+   *
+   * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
+   * @param {?Object} config
+   *
+   * @returns {Promise} The Promise to be fulfilled
+   */
+  async request(configOrUrl, config) {
+    try {
+      return await this._request(configOrUrl, config);
+    } catch (err) {
+      if (err instanceof Error) {
+        let dummy = {};
+        Error.captureStackTrace ? Error.captureStackTrace(dummy) : dummy = new Error();
+        const stack = (() => {
+          if (!dummy.stack) {
+            return "";
+          }
+          const firstNewlineIndex = dummy.stack.indexOf("\n");
+          return firstNewlineIndex === -1 ? "" : dummy.stack.slice(firstNewlineIndex + 1);
+        })();
+        try {
+          if (!err.stack) {
+            err.stack = stack;
+          } else if (stack) {
+            const firstNewlineIndex = stack.indexOf("\n");
+            const secondNewlineIndex = firstNewlineIndex === -1 ? -1 : stack.indexOf("\n", firstNewlineIndex + 1);
+            const stackWithoutTwoTopLines = secondNewlineIndex === -1 ? "" : stack.slice(secondNewlineIndex + 1);
+            if (!String(err.stack).endsWith(stackWithoutTwoTopLines)) {
+              err.stack += "\n" + stack;
+            }
+          }
+        } catch (e) {
+        }
+      }
+      throw err;
+    }
+  }
+  _request(configOrUrl, config) {
+    if (typeof configOrUrl === "string") {
+      config = config || {};
+      config.url = configOrUrl;
+    } else {
+      config = configOrUrl || {};
+    }
+    config = mergeConfig(this.defaults, config);
+    const { transitional: transitional2, paramsSerializer, headers } = config;
+    if (transitional2 !== void 0) {
+      validator_default.assertOptions(
+        transitional2,
+        {
+          silentJSONParsing: validators2.transitional(validators2.boolean),
+          forcedJSONParsing: validators2.transitional(validators2.boolean),
+          clarifyTimeoutError: validators2.transitional(validators2.boolean),
+          legacyInterceptorReqResOrdering: validators2.transitional(validators2.boolean)
+        },
+        false
+      );
+    }
+    if (paramsSerializer != null) {
+      if (utils_default.isFunction(paramsSerializer)) {
+        config.paramsSerializer = {
+          serialize: paramsSerializer
+        };
+      } else {
+        validator_default.assertOptions(
+          paramsSerializer,
+          {
+            encode: validators2.function,
+            serialize: validators2.function
+          },
+          true
+        );
+      }
+    }
+    if (config.allowAbsoluteUrls !== void 0) {
+    } else if (this.defaults.allowAbsoluteUrls !== void 0) {
+      config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
+    } else {
+      config.allowAbsoluteUrls = true;
+    }
+    validator_default.assertOptions(
+      config,
+      {
+        baseUrl: validators2.spelling("baseURL"),
+        withXsrfToken: validators2.spelling("withXSRFToken")
+      },
+      true
+    );
+    config.method = (config.method || this.defaults.method || "get").toLowerCase();
+    let contextHeaders = headers && utils_default.merge(headers.common, headers[config.method]);
+    headers && utils_default.forEach(["delete", "get", "head", "post", "put", "patch", "common"], (method) => {
+      delete headers[method];
+    });
+    config.headers = AxiosHeaders_default.concat(contextHeaders, headers);
+    const requestInterceptorChain = [];
+    let synchronousRequestInterceptors = true;
+    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
+      if (typeof interceptor.runWhen === "function" && interceptor.runWhen(config) === false) {
+        return;
+      }
+      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
+      const transitional3 = config.transitional || transitional_default;
+      const legacyInterceptorReqResOrdering = transitional3 && transitional3.legacyInterceptorReqResOrdering;
+      if (legacyInterceptorReqResOrdering) {
+        requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
+      } else {
+        requestInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
+      }
+    });
+    const responseInterceptorChain = [];
+    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
+      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
+    });
+    let promise;
+    let i = 0;
+    let len;
+    if (!synchronousRequestInterceptors) {
+      const chain = [dispatchRequest.bind(this), void 0];
+      chain.unshift(...requestInterceptorChain);
+      chain.push(...responseInterceptorChain);
+      len = chain.length;
+      promise = Promise.resolve(config);
+      while (i < len) {
+        promise = promise.then(chain[i++], chain[i++]);
+      }
+      return promise;
+    }
+    len = requestInterceptorChain.length;
+    let newConfig = config;
+    while (i < len) {
+      const onFulfilled = requestInterceptorChain[i++];
+      const onRejected = requestInterceptorChain[i++];
+      try {
+        newConfig = onFulfilled(newConfig);
+      } catch (error) {
+        onRejected.call(this, error);
+        break;
+      }
+    }
+    try {
+      promise = dispatchRequest.call(this, newConfig);
+    } catch (error) {
+      return Promise.reject(error);
+    }
+    i = 0;
+    len = responseInterceptorChain.length;
+    while (i < len) {
+      promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
+    }
+    return promise;
+  }
+  getUri(config) {
+    config = mergeConfig(this.defaults, config);
+    const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
+    return buildURL(fullPath, config.params, config.paramsSerializer);
+  }
+};
+utils_default.forEach(["delete", "get", "head", "options"], function forEachMethodNoData(method) {
+  Axios.prototype[method] = function(url, config) {
+    return this.request(
+      mergeConfig(config || {}, {
+        method,
+        url,
+        data: (config || {}).data
+      })
+    );
+  };
+});
+utils_default.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
+  function generateHTTPMethod(isForm) {
+    return function httpMethod(url, data, config) {
+      return this.request(
+        mergeConfig(config || {}, {
+          method,
+          headers: isForm ? {
+            "Content-Type": "multipart/form-data"
+          } : {},
+          url,
+          data
+        })
+      );
+    };
+  }
+  Axios.prototype[method] = generateHTTPMethod();
+  Axios.prototype[method + "Form"] = generateHTTPMethod(true);
+});
+var Axios_default = Axios;
+
+// node_modules/axios/lib/cancel/CancelToken.js
+var CancelToken = class _CancelToken {
+  constructor(executor) {
+    if (typeof executor !== "function") {
+      throw new TypeError("executor must be a function.");
+    }
+    let resolvePromise;
+    this.promise = new Promise(function promiseExecutor(resolve) {
+      resolvePromise = resolve;
+    });
+    const token = this;
+    this.promise.then((cancel) => {
+      if (!token._listeners)
+        return;
+      let i = token._listeners.length;
+      while (i-- > 0) {
+        token._listeners[i](cancel);
+      }
+      token._listeners = null;
+    });
+    this.promise.then = (onfulfilled) => {
+      let _resolve;
+      const promise = new Promise((resolve) => {
+        token.subscribe(resolve);
+        _resolve = resolve;
+      }).then(onfulfilled);
+      promise.cancel = function reject() {
+        token.unsubscribe(_resolve);
+      };
+      return promise;
+    };
+    executor(function cancel(message, config, request) {
+      if (token.reason) {
+        return;
+      }
+      token.reason = new CanceledError_default(message, config, request);
+      resolvePromise(token.reason);
+    });
+  }
+  /**
+   * Throws a `CanceledError` if cancellation has been requested.
+   */
+  throwIfRequested() {
+    if (this.reason) {
+      throw this.reason;
+    }
+  }
+  /**
+   * Subscribe to the cancel signal
+   */
+  subscribe(listener) {
+    if (this.reason) {
+      listener(this.reason);
+      return;
+    }
+    if (this._listeners) {
+      this._listeners.push(listener);
+    } else {
+      this._listeners = [listener];
+    }
+  }
+  /**
+   * Unsubscribe from the cancel signal
+   */
+  unsubscribe(listener) {
+    if (!this._listeners) {
+      return;
+    }
+    const index = this._listeners.indexOf(listener);
+    if (index !== -1) {
+      this._listeners.splice(index, 1);
+    }
+  }
+  toAbortSignal() {
+    const controller = new AbortController();
+    const abort = (err) => {
+      controller.abort(err);
+    };
+    this.subscribe(abort);
+    controller.signal.unsubscribe = () => this.unsubscribe(abort);
+    return controller.signal;
+  }
+  /**
+   * Returns an object that contains a new `CancelToken` and a function that, when called,
+   * cancels the `CancelToken`.
+   */
+  static source() {
+    let cancel;
+    const token = new _CancelToken(function executor(c) {
+      cancel = c;
+    });
+    return {
+      token,
+      cancel
+    };
+  }
+};
+var CancelToken_default = CancelToken;
+
+// node_modules/axios/lib/helpers/spread.js
+function spread(callback) {
+  return function wrap(arr) {
+    return callback.apply(null, arr);
+  };
+}
+
+// node_modules/axios/lib/helpers/isAxiosError.js
+function isAxiosError(payload) {
+  return utils_default.isObject(payload) && payload.isAxiosError === true;
+}
+
+// node_modules/axios/lib/helpers/HttpStatusCode.js
+var HttpStatusCode = {
+  Continue: 100,
+  SwitchingProtocols: 101,
+  Processing: 102,
+  EarlyHints: 103,
+  Ok: 200,
+  Created: 201,
+  Accepted: 202,
+  NonAuthoritativeInformation: 203,
+  NoContent: 204,
+  ResetContent: 205,
+  PartialContent: 206,
+  MultiStatus: 207,
+  AlreadyReported: 208,
+  ImUsed: 226,
+  MultipleChoices: 300,
+  MovedPermanently: 301,
+  Found: 302,
+  SeeOther: 303,
+  NotModified: 304,
+  UseProxy: 305,
+  Unused: 306,
+  TemporaryRedirect: 307,
+  PermanentRedirect: 308,
+  BadRequest: 400,
+  Unauthorized: 401,
+  PaymentRequired: 402,
+  Forbidden: 403,
+  NotFound: 404,
+  MethodNotAllowed: 405,
+  NotAcceptable: 406,
+  ProxyAuthenticationRequired: 407,
+  RequestTimeout: 408,
+  Conflict: 409,
+  Gone: 410,
+  LengthRequired: 411,
+  PreconditionFailed: 412,
+  PayloadTooLarge: 413,
+  UriTooLong: 414,
+  UnsupportedMediaType: 415,
+  RangeNotSatisfiable: 416,
+  ExpectationFailed: 417,
+  ImATeapot: 418,
+  MisdirectedRequest: 421,
+  UnprocessableEntity: 422,
+  Locked: 423,
+  FailedDependency: 424,
+  TooEarly: 425,
+  UpgradeRequired: 426,
+  PreconditionRequired: 428,
+  TooManyRequests: 429,
+  RequestHeaderFieldsTooLarge: 431,
+  UnavailableForLegalReasons: 451,
+  InternalServerError: 500,
+  NotImplemented: 501,
+  BadGateway: 502,
+  ServiceUnavailable: 503,
+  GatewayTimeout: 504,
+  HttpVersionNotSupported: 505,
+  VariantAlsoNegotiates: 506,
+  InsufficientStorage: 507,
+  LoopDetected: 508,
+  NotExtended: 510,
+  NetworkAuthenticationRequired: 511,
+  WebServerIsDown: 521,
+  ConnectionTimedOut: 522,
+  OriginIsUnreachable: 523,
+  TimeoutOccurred: 524,
+  SslHandshakeFailed: 525,
+  InvalidSslCertificate: 526
+};
+Object.entries(HttpStatusCode).forEach(([key, value]) => {
+  HttpStatusCode[value] = key;
+});
+var HttpStatusCode_default = HttpStatusCode;
+
+// node_modules/axios/lib/axios.js
+function createInstance(defaultConfig) {
+  const context = new Axios_default(defaultConfig);
+  const instance = bind(Axios_default.prototype.request, context);
+  utils_default.extend(instance, Axios_default.prototype, context, { allOwnKeys: true });
+  utils_default.extend(instance, context, null, { allOwnKeys: true });
+  instance.create = function create(instanceConfig) {
+    return createInstance(mergeConfig(defaultConfig, instanceConfig));
+  };
+  return instance;
+}
+var axios = createInstance(defaults_default);
+axios.Axios = Axios_default;
+axios.CanceledError = CanceledError_default;
+axios.CancelToken = CancelToken_default;
+axios.isCancel = isCancel;
+axios.VERSION = VERSION;
+axios.toFormData = toFormData_default;
+axios.AxiosError = AxiosError_default;
+axios.Cancel = axios.CanceledError;
+axios.all = function all(promises) {
+  return Promise.all(promises);
+};
+axios.spread = spread;
+axios.isAxiosError = isAxiosError;
+axios.mergeConfig = mergeConfig;
+axios.AxiosHeaders = AxiosHeaders_default;
+axios.formToJSON = (thing) => formDataToJSON_default(utils_default.isHTMLForm(thing) ? new FormData(thing) : thing);
+axios.getAdapter = adapters_default.getAdapter;
+axios.HttpStatusCode = HttpStatusCode_default;
+axios.default = axios;
+var axios_default = axios;
+
+// node_modules/axios/index.js
+var {
+  Axios: Axios2,
+  AxiosError: AxiosError2,
+  CanceledError: CanceledError2,
+  isCancel: isCancel2,
+  CancelToken: CancelToken2,
+  VERSION: VERSION2,
+  all: all2,
+  Cancel,
+  isAxiosError: isAxiosError2,
+  spread: spread2,
+  toFormData: toFormData2,
+  AxiosHeaders: AxiosHeaders2,
+  HttpStatusCode: HttpStatusCode2,
+  formToJSON,
+  getAdapter: getAdapter2,
+  mergeConfig: mergeConfig2
+} = axios_default;
+export {
+  Axios2 as Axios,
+  AxiosError2 as AxiosError,
+  AxiosHeaders2 as AxiosHeaders,
+  Cancel,
+  CancelToken2 as CancelToken,
+  CanceledError2 as CanceledError,
+  HttpStatusCode2 as HttpStatusCode,
+  VERSION2 as VERSION,
+  all2 as all,
+  axios_default as default,
+  formToJSON,
+  getAdapter2 as getAdapter,
+  isAxiosError2 as isAxiosError,
+  isCancel2 as isCancel,
+  mergeConfig2 as mergeConfig,
+  spread2 as spread,
+  toFormData2 as toFormData
+};
+//# sourceMappingURL=axios.js.map

Fișier diff suprimat deoarece este prea mare
+ 2 - 0
frontend/node_modules/.vite/deps/axios.js.map


+ 10 - 0
frontend/node_modules/.vite/deps/chunk-SSYGV25P.js

@@ -0,0 +1,10 @@
+var __defProp = Object.defineProperty;
+var __export = (target, all) => {
+  for (var name in all)
+    __defProp(target, name, { get: all[name], enumerable: true });
+};
+
+export {
+  __export
+};
+//# sourceMappingURL=chunk-SSYGV25P.js.map

+ 7 - 0
frontend/node_modules/.vite/deps/chunk-SSYGV25P.js.map

@@ -0,0 +1,7 @@
+{
+  "version": 3,
+  "sources": [],
+  "sourcesContent": [],
+  "mappings": "",
+  "names": []
+}

+ 3 - 0
frontend/node_modules/.vite/deps/package.json

@@ -0,0 +1,3 @@
+{
+  "type": "module"
+}

Fișier diff suprimat deoarece este prea mare
+ 203 - 0
frontend/node_modules/.vite/deps/vue.js


Fișier diff suprimat deoarece este prea mare
+ 3 - 0
frontend/node_modules/.vite/deps/vue.js.map


+ 22 - 0
frontend/node_modules/@babel/helper-string-parser/LICENSE

@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 19 - 0
frontend/node_modules/@babel/helper-string-parser/README.md

@@ -0,0 +1,19 @@
+# @babel/helper-string-parser
+
+> A utility package to parse strings
+
+See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save @babel/helper-string-parser
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/helper-string-parser
+```

+ 295 - 0
frontend/node_modules/@babel/helper-string-parser/lib/index.js

@@ -0,0 +1,295 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.readCodePoint = readCodePoint;
+exports.readInt = readInt;
+exports.readStringContents = readStringContents;
+var _isDigit = function isDigit(code) {
+  return code >= 48 && code <= 57;
+};
+const forbiddenNumericSeparatorSiblings = {
+  decBinOct: new Set([46, 66, 69, 79, 95, 98, 101, 111]),
+  hex: new Set([46, 88, 95, 120])
+};
+const isAllowedNumericSeparatorSibling = {
+  bin: ch => ch === 48 || ch === 49,
+  oct: ch => ch >= 48 && ch <= 55,
+  dec: ch => ch >= 48 && ch <= 57,
+  hex: ch => ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102
+};
+function readStringContents(type, input, pos, lineStart, curLine, errors) {
+  const initialPos = pos;
+  const initialLineStart = lineStart;
+  const initialCurLine = curLine;
+  let out = "";
+  let firstInvalidLoc = null;
+  let chunkStart = pos;
+  const {
+    length
+  } = input;
+  for (;;) {
+    if (pos >= length) {
+      errors.unterminated(initialPos, initialLineStart, initialCurLine);
+      out += input.slice(chunkStart, pos);
+      break;
+    }
+    const ch = input.charCodeAt(pos);
+    if (isStringEnd(type, ch, input, pos)) {
+      out += input.slice(chunkStart, pos);
+      break;
+    }
+    if (ch === 92) {
+      out += input.slice(chunkStart, pos);
+      const res = readEscapedChar(input, pos, lineStart, curLine, type === "template", errors);
+      if (res.ch === null && !firstInvalidLoc) {
+        firstInvalidLoc = {
+          pos,
+          lineStart,
+          curLine
+        };
+      } else {
+        out += res.ch;
+      }
+      ({
+        pos,
+        lineStart,
+        curLine
+      } = res);
+      chunkStart = pos;
+    } else if (ch === 8232 || ch === 8233) {
+      ++pos;
+      ++curLine;
+      lineStart = pos;
+    } else if (ch === 10 || ch === 13) {
+      if (type === "template") {
+        out += input.slice(chunkStart, pos) + "\n";
+        ++pos;
+        if (ch === 13 && input.charCodeAt(pos) === 10) {
+          ++pos;
+        }
+        ++curLine;
+        chunkStart = lineStart = pos;
+      } else {
+        errors.unterminated(initialPos, initialLineStart, initialCurLine);
+      }
+    } else {
+      ++pos;
+    }
+  }
+  return {
+    pos,
+    str: out,
+    firstInvalidLoc,
+    lineStart,
+    curLine,
+    containsInvalid: !!firstInvalidLoc
+  };
+}
+function isStringEnd(type, ch, input, pos) {
+  if (type === "template") {
+    return ch === 96 || ch === 36 && input.charCodeAt(pos + 1) === 123;
+  }
+  return ch === (type === "double" ? 34 : 39);
+}
+function readEscapedChar(input, pos, lineStart, curLine, inTemplate, errors) {
+  const throwOnInvalid = !inTemplate;
+  pos++;
+  const res = ch => ({
+    pos,
+    ch,
+    lineStart,
+    curLine
+  });
+  const ch = input.charCodeAt(pos++);
+  switch (ch) {
+    case 110:
+      return res("\n");
+    case 114:
+      return res("\r");
+    case 120:
+      {
+        let code;
+        ({
+          code,
+          pos
+        } = readHexChar(input, pos, lineStart, curLine, 2, false, throwOnInvalid, errors));
+        return res(code === null ? null : String.fromCharCode(code));
+      }
+    case 117:
+      {
+        let code;
+        ({
+          code,
+          pos
+        } = readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors));
+        return res(code === null ? null : String.fromCodePoint(code));
+      }
+    case 116:
+      return res("\t");
+    case 98:
+      return res("\b");
+    case 118:
+      return res("\u000b");
+    case 102:
+      return res("\f");
+    case 13:
+      if (input.charCodeAt(pos) === 10) {
+        ++pos;
+      }
+    case 10:
+      lineStart = pos;
+      ++curLine;
+    case 8232:
+    case 8233:
+      return res("");
+    case 56:
+    case 57:
+      if (inTemplate) {
+        return res(null);
+      } else {
+        errors.strictNumericEscape(pos - 1, lineStart, curLine);
+      }
+    default:
+      if (ch >= 48 && ch <= 55) {
+        const startPos = pos - 1;
+        const match = /^[0-7]+/.exec(input.slice(startPos, pos + 2));
+        let octalStr = match[0];
+        let octal = parseInt(octalStr, 8);
+        if (octal > 255) {
+          octalStr = octalStr.slice(0, -1);
+          octal = parseInt(octalStr, 8);
+        }
+        pos += octalStr.length - 1;
+        const next = input.charCodeAt(pos);
+        if (octalStr !== "0" || next === 56 || next === 57) {
+          if (inTemplate) {
+            return res(null);
+          } else {
+            errors.strictNumericEscape(startPos, lineStart, curLine);
+          }
+        }
+        return res(String.fromCharCode(octal));
+      }
+      return res(String.fromCharCode(ch));
+  }
+}
+function readHexChar(input, pos, lineStart, curLine, len, forceLen, throwOnInvalid, errors) {
+  const initialPos = pos;
+  let n;
+  ({
+    n,
+    pos
+  } = readInt(input, pos, lineStart, curLine, 16, len, forceLen, false, errors, !throwOnInvalid));
+  if (n === null) {
+    if (throwOnInvalid) {
+      errors.invalidEscapeSequence(initialPos, lineStart, curLine);
+    } else {
+      pos = initialPos - 1;
+    }
+  }
+  return {
+    code: n,
+    pos
+  };
+}
+function readInt(input, pos, lineStart, curLine, radix, len, forceLen, allowNumSeparator, errors, bailOnError) {
+  const start = pos;
+  const forbiddenSiblings = radix === 16 ? forbiddenNumericSeparatorSiblings.hex : forbiddenNumericSeparatorSiblings.decBinOct;
+  const isAllowedSibling = radix === 16 ? isAllowedNumericSeparatorSibling.hex : radix === 10 ? isAllowedNumericSeparatorSibling.dec : radix === 8 ? isAllowedNumericSeparatorSibling.oct : isAllowedNumericSeparatorSibling.bin;
+  let invalid = false;
+  let total = 0;
+  for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
+    const code = input.charCodeAt(pos);
+    let val;
+    if (code === 95 && allowNumSeparator !== "bail") {
+      const prev = input.charCodeAt(pos - 1);
+      const next = input.charCodeAt(pos + 1);
+      if (!allowNumSeparator) {
+        if (bailOnError) return {
+          n: null,
+          pos
+        };
+        errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine);
+      } else if (Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev) || forbiddenSiblings.has(next)) {
+        if (bailOnError) return {
+          n: null,
+          pos
+        };
+        errors.unexpectedNumericSeparator(pos, lineStart, curLine);
+      }
+      ++pos;
+      continue;
+    }
+    if (code >= 97) {
+      val = code - 97 + 10;
+    } else if (code >= 65) {
+      val = code - 65 + 10;
+    } else if (_isDigit(code)) {
+      val = code - 48;
+    } else {
+      val = Infinity;
+    }
+    if (val >= radix) {
+      if (val <= 9 && bailOnError) {
+        return {
+          n: null,
+          pos
+        };
+      } else if (val <= 9 && errors.invalidDigit(pos, lineStart, curLine, radix)) {
+        val = 0;
+      } else if (forceLen) {
+        val = 0;
+        invalid = true;
+      } else {
+        break;
+      }
+    }
+    ++pos;
+    total = total * radix + val;
+  }
+  if (pos === start || len != null && pos - start !== len || invalid) {
+    return {
+      n: null,
+      pos
+    };
+  }
+  return {
+    n: total,
+    pos
+  };
+}
+function readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors) {
+  const ch = input.charCodeAt(pos);
+  let code;
+  if (ch === 123) {
+    ++pos;
+    ({
+      code,
+      pos
+    } = readHexChar(input, pos, lineStart, curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, errors));
+    ++pos;
+    if (code !== null && code > 0x10ffff) {
+      if (throwOnInvalid) {
+        errors.invalidCodePoint(pos, lineStart, curLine);
+      } else {
+        return {
+          code: null,
+          pos
+        };
+      }
+    }
+  } else {
+    ({
+      code,
+      pos
+    } = readHexChar(input, pos, lineStart, curLine, 4, false, throwOnInvalid, errors));
+  }
+  return {
+    code,
+    pos
+  };
+}
+
+//# sourceMappingURL=index.js.map

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
frontend/node_modules/@babel/helper-string-parser/lib/index.js.map


+ 31 - 0
frontend/node_modules/@babel/helper-string-parser/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "@babel/helper-string-parser",
+  "version": "7.27.1",
+  "description": "A utility package to parse strings",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/babel/babel.git",
+    "directory": "packages/babel-helper-string-parser"
+  },
+  "homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser",
+  "license": "MIT",
+  "publishConfig": {
+    "access": "public"
+  },
+  "main": "./lib/index.js",
+  "devDependencies": {
+    "charcodes": "^0.2.0"
+  },
+  "engines": {
+    "node": ">=6.9.0"
+  },
+  "author": "The Babel Team (https://babel.dev/team)",
+  "exports": {
+    ".": {
+      "types": "./lib/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./package.json": "./package.json"
+  },
+  "type": "commonjs"
+}

+ 22 - 0
frontend/node_modules/@babel/helper-validator-identifier/LICENSE

@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 19 - 0
frontend/node_modules/@babel/helper-validator-identifier/README.md

@@ -0,0 +1,19 @@
+# @babel/helper-validator-identifier
+
+> Validate identifier/keywords name
+
+See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save @babel/helper-validator-identifier
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/helper-validator-identifier
+```

Fișier diff suprimat deoarece este prea mare
+ 8 - 0
frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js.map


+ 57 - 0
frontend/node_modules/@babel/helper-validator-identifier/lib/index.js

@@ -0,0 +1,57 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+Object.defineProperty(exports, "isIdentifierChar", {
+  enumerable: true,
+  get: function () {
+    return _identifier.isIdentifierChar;
+  }
+});
+Object.defineProperty(exports, "isIdentifierName", {
+  enumerable: true,
+  get: function () {
+    return _identifier.isIdentifierName;
+  }
+});
+Object.defineProperty(exports, "isIdentifierStart", {
+  enumerable: true,
+  get: function () {
+    return _identifier.isIdentifierStart;
+  }
+});
+Object.defineProperty(exports, "isKeyword", {
+  enumerable: true,
+  get: function () {
+    return _keyword.isKeyword;
+  }
+});
+Object.defineProperty(exports, "isReservedWord", {
+  enumerable: true,
+  get: function () {
+    return _keyword.isReservedWord;
+  }
+});
+Object.defineProperty(exports, "isStrictBindOnlyReservedWord", {
+  enumerable: true,
+  get: function () {
+    return _keyword.isStrictBindOnlyReservedWord;
+  }
+});
+Object.defineProperty(exports, "isStrictBindReservedWord", {
+  enumerable: true,
+  get: function () {
+    return _keyword.isStrictBindReservedWord;
+  }
+});
+Object.defineProperty(exports, "isStrictReservedWord", {
+  enumerable: true,
+  get: function () {
+    return _keyword.isStrictReservedWord;
+  }
+});
+var _identifier = require("./identifier.js");
+var _keyword = require("./keyword.js");
+
+//# sourceMappingURL=index.js.map

+ 1 - 0
frontend/node_modules/@babel/helper-validator-identifier/lib/index.js.map

@@ -0,0 +1 @@
+{"version":3,"names":["_identifier","require","_keyword"],"sources":["../src/index.ts"],"sourcesContent":["export {\n  isIdentifierName,\n  isIdentifierChar,\n  isIdentifierStart,\n} from \"./identifier.ts\";\nexport {\n  isReservedWord,\n  isStrictBindOnlyReservedWord,\n  isStrictBindReservedWord,\n  isStrictReservedWord,\n  isKeyword,\n} from \"./keyword.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,WAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA","ignoreList":[]}

+ 35 - 0
frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js

@@ -0,0 +1,35 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.isKeyword = isKeyword;
+exports.isReservedWord = isReservedWord;
+exports.isStrictBindOnlyReservedWord = isStrictBindOnlyReservedWord;
+exports.isStrictBindReservedWord = isStrictBindReservedWord;
+exports.isStrictReservedWord = isStrictReservedWord;
+const reservedWords = {
+  keyword: ["break", "case", "catch", "continue", "debugger", "default", "do", "else", "finally", "for", "function", "if", "return", "switch", "throw", "try", "var", "const", "while", "with", "new", "this", "super", "class", "extends", "export", "import", "null", "true", "false", "in", "instanceof", "typeof", "void", "delete"],
+  strict: ["implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"],
+  strictBind: ["eval", "arguments"]
+};
+const keywords = new Set(reservedWords.keyword);
+const reservedWordsStrictSet = new Set(reservedWords.strict);
+const reservedWordsStrictBindSet = new Set(reservedWords.strictBind);
+function isReservedWord(word, inModule) {
+  return inModule && word === "await" || word === "enum";
+}
+function isStrictReservedWord(word, inModule) {
+  return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);
+}
+function isStrictBindOnlyReservedWord(word) {
+  return reservedWordsStrictBindSet.has(word);
+}
+function isStrictBindReservedWord(word, inModule) {
+  return isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word);
+}
+function isKeyword(word) {
+  return keywords.has(word);
+}
+
+//# sourceMappingURL=keyword.js.map

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js.map


+ 31 - 0
frontend/node_modules/@babel/helper-validator-identifier/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "@babel/helper-validator-identifier",
+  "version": "7.28.5",
+  "description": "Validate identifier/keywords name",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/babel/babel.git",
+    "directory": "packages/babel-helper-validator-identifier"
+  },
+  "license": "MIT",
+  "publishConfig": {
+    "access": "public"
+  },
+  "main": "./lib/index.js",
+  "exports": {
+    ".": {
+      "types": "./lib/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./package.json": "./package.json"
+  },
+  "devDependencies": {
+    "@unicode/unicode-17.0.0": "^1.6.10",
+    "charcodes": "^0.2.0"
+  },
+  "engines": {
+    "node": ">=6.9.0"
+  },
+  "author": "The Babel Team (https://babel.dev/team)",
+  "type": "commonjs"
+}

+ 1073 - 0
frontend/node_modules/@babel/parser/CHANGELOG.md

@@ -0,0 +1,1073 @@
+# Changelog
+
+> **Tags:**
+> - :boom:       [Breaking Change]
+> - :eyeglasses: [Spec Compliance]
+> - :rocket:     [New Feature]
+> - :bug:        [Bug Fix]
+> - :memo:       [Documentation]
+> - :house:      [Internal]
+> - :nail_care:  [Polish]
+
+> Semver Policy: https://github.com/babel/babel/tree/main/packages/babel-parser#semver
+
+_Note: Gaps between patch versions are faulty, broken or test releases._
+
+See the [Babel Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) for the pre-6.8.0 version Changelog.
+
+## 6.17.1 (2017-05-10)
+
+### :bug: Bug Fix
+ * Fix typo in flow spread operator error (Brian Ng)
+ * Fixed invalid number literal parsing ([#473](https://github.com/babel/babylon/pull/473)) (Alex Kuzmenko)
+ * Fix number parser ([#433](https://github.com/babel/babylon/pull/433)) (Alex Kuzmenko)
+ * Ensure non pattern shorthand props are checked for reserved words ([#479](https://github.com/babel/babylon/pull/479)) (Brian Ng)
+ * Remove jsx context when parsing arrow functions ([#475](https://github.com/babel/babylon/pull/475)) (Brian Ng)
+ * Allow super in class properties ([#499](https://github.com/babel/babylon/pull/499)) (Brian Ng)
+ * Allow flow class field to be named constructor ([#510](https://github.com/babel/babylon/pull/510)) (Brian Ng)
+
+## 6.17.0 (2017-04-20)
+
+### :bug: Bug Fix
+ * Cherry-pick #418 to 6.x ([#476](https://github.com/babel/babylon/pull/476)) (Sebastian McKenzie)
+ * Add support for invalid escapes in tagged templates ([#274](https://github.com/babel/babylon/pull/274)) (Kevin Gibbons)
+ * Throw error if new.target is used outside of a function ([#402](https://github.com/babel/babylon/pull/402)) (Brian Ng)
+ * Fix parsing of class properties ([#351](https://github.com/babel/babylon/pull/351)) (Kevin Gibbons)
+ * Fix parsing yield with dynamicImport ([#383](https://github.com/babel/babylon/pull/383)) (Brian Ng)
+ * Ensure consistent start args for parseParenItem ([#386](https://github.com/babel/babylon/pull/386)) (Brian Ng)
+
+## 7.0.0-beta.8 (2017-04-04)
+
+### New Feature
+* Add support for flow type spread (#418) (Conrad Buck)
+* Allow statics in flow interfaces (#427) (Brian Ng)
+
+### Bug Fix
+* Fix predicate attachment to match flow parser (#428) (Brian Ng)
+* Add extra.raw back to JSXText and JSXAttribute (#344) (Alex Rattray)
+* Fix rest parameters with array and objects (#424) (Brian Ng)
+* Fix number parser (#433) (Alex Kuzmenko)
+
+### Docs
+* Fix CONTRIBUTING.md [skip ci] (#432) (Alex Kuzmenko)
+
+### Internal
+* Use babel-register script when running babel smoke tests (#442) (Brian Ng)
+
+## 7.0.0-beta.7 (2017-03-22)
+
+### Spec Compliance
+* Remove babylon plugin for template revision since it's stage-4 (#426) (Henry Zhu)
+
+### Bug Fix
+
+* Fix push-pop logic in flow (#405) (Daniel Tschinder)
+
+## 7.0.0-beta.6 (2017-03-21)
+
+### New Feature
+* Add support for invalid escapes in tagged templates (#274) (Kevin Gibbons)
+
+### Polish
+* Improves error message when super is called outside of constructor (#408) (Arshabh Kumar Agarwal)
+
+### Docs
+
+* [7.0] Moved value field in spec from ObjectMember to ObjectProperty as ObjectMethod's don't have it (#415) [skip ci] (James Browning)
+
+## 7.0.0-beta.5 (2017-03-21)
+
+### Bug Fix
+* Throw error if new.target is used outside of a function (#402) (Brian Ng)
+* Fix parsing of class properties (#351) (Kevin Gibbons)
+
+### Other
+ * Test runner: Detect extra property in 'actual' but not in 'expected'. (#407) (Andy)
+ * Optimize travis builds (#419) (Daniel Tschinder)
+ * Update codecov to 2.0 (#412) (Daniel Tschinder)
+ * Fix spec for ClassMethod: It doesn't have a function, it *is* a function. (#406) [skip ci] (Andy)
+ * Changed Non-existent RestPattern to RestElement which is what is actually parsed (#409) [skip ci] (James Browning)
+ * Upgrade flow to 0.41 (Daniel Tschinder)
+ * Fix watch command (#403) (Brian Ng)
+ * Update yarn lock (Daniel Tschinder)
+ * Fix watch command (#403) (Brian Ng)
+ * chore(package): update flow-bin to version 0.41.0 (#395) (greenkeeper[bot])
+ * Add estree test for correct order of directives (Daniel Tschinder)
+ * Add DoExpression to spec (#364) (Alex Kuzmenko)
+ * Mention cloning of repository in CONTRIBUTING.md (#391) [skip ci] (Sumedh Nimkarde)
+ * Explain how to run only one test (#389) [skip ci] (Aaron Ang)
+
+ ## 7.0.0-beta.4 (2017-03-01)
+
+* Don't consume async when checking for async func decl (#377) (Brian Ng)
+* add `ranges` option [skip ci] (Henry Zhu)
+* Don't parse class properties without initializers when classProperties is disabled and Flow is enabled (#300) (Andrew Levine)
+
+## 7.0.0-beta.3 (2017-02-28)
+
+- [7.0] Change RestProperty/SpreadProperty to RestElement/SpreadElement (#384)
+- Merge changes from 6.x
+
+## 7.0.0-beta.2 (2017-02-20)
+
+- estree: correctly change literals in all cases (#368) (Daniel Tschinder)
+
+## 7.0.0-beta.1 (2017-02-20)
+
+- Fix negative number literal typeannotations (#366) (Daniel Tschinder)
+- Update contributing with more test info [skip ci] (#355) (Brian Ng)
+
+## 7.0.0-beta.0 (2017-02-15)
+
+- Reintroduce Variance node (#333) (Daniel Tschinder)
+- Rename NumericLiteralTypeAnnotation to NumberLiteralTypeAnnotation (#332) (Charles Pick)
+- [7.0] Remove ForAwaitStatement, add await flag to ForOfStatement (#349) (Brandon Dail)
+- chore(package): update ava to version 0.18.0 (#345) (greenkeeper[bot])
+- chore(package): update babel-plugin-istanbul to version 4.0.0 (#350) (greenkeeper[bot])
+- Change location of ObjectTypeIndexer to match flow (#228) (Daniel Tschinder)
+- Rename flow AST Type ExistentialTypeParam to ExistsTypeAnnotation (#322) (Toru Kobayashi)
+- Revert "Temporary rollback for erroring on trailing comma with spread (#154)" (#290) (Daniel Tschinder)
+- Remove classConstructorCall plugin (#291) (Brian Ng)
+- Update yarn.lock (Daniel Tschinder)
+- Update cross-env to 3.x (Daniel Tschinder)
+- [7.0] Remove node 0.10, 0.12 and 5 from Travis (#284) (Sergey Rubanov)
+- Remove `String.fromCodePoint` shim (#279) (Mathias Bynens)
+
+## 6.16.1 (2017-02-23)
+
+### :bug: Regression
+
+- Revert "Fix export default async function to be FunctionDeclaration" ([#375](https://github.com/babel/babylon/pull/375))
+
+Need to modify Babel for this AST node change, so moving to 7.0.
+
+- Revert "Don't parse class properties without initializers when classProperties plugin is disabled, and Flow is enabled" ([#376](https://github.com/babel/babylon/pull/376))
+
+[react-native](https://github.com/facebook/react-native/issues/12542) broke with this so we reverted.
+
+## 6.16.0 (2017-02-23)
+
+### :rocket: New Feature
+
+***ESTree*** compatibility as plugin ([#277](https://github.com/babel/babylon/pull/277)) (Daniel Tschinder)
+
+We finally introduce a new compatibility layer for ESTree. To put babylon into ESTree-compatible mode the new plugin `estree` can be enabled. In this mode the parser will output an AST that is compliant to the specs of [ESTree](https://github.com/estree/estree/)
+
+We highly recommend everyone who uses babylon outside of babel to use this plugin. This will make it much easier for users to switch between different ESTree-compatible parsers. We so far tested several projects with different parsers and exchanged their parser to babylon and in nearly all cases it worked out of the box. Some other estree-compatible parsers include `acorn`, `esprima`, `espree`, `flow-parser`, etc.
+
+To enable `estree` mode simply add the plugin in the config:
+```json
+{
+  "plugins": [ "estree" ]
+}
+```
+
+If you want to migrate your project from non-ESTree mode to ESTree, have a look at our [Readme](https://github.com/babel/babylon/#output), where all deviations are mentioned.
+
+Add a parseExpression public method ([#213](https://github.com/babel/babylon/pull/213)) (jeromew)
+
+Babylon exports a new function to parse a single expression
+
+```js
+import { parseExpression } from 'babylon';
+
+const ast = parseExpression('x || y && z', options);
+```
+
+The returned AST will only consist of the expression. The options are the same as for `parse()`
+
+Add startLine option ([#346](https://github.com/babel/babylon/pull/346)) (Raphael Mu)
+
+A new option was added to babylon allowing to change the initial linenumber for the first line which is usually `1`.
+Changing this for example to `100` will make line `1` of the input source to be marked as line `100`, line `2` as `101`, line `3` as `102`, ...
+
+Function predicate declaration ([#103](https://github.com/babel/babylon/pull/103)) (Panagiotis Vekris)
+
+Added support for function predicates which flow introduced in version 0.33.0
+
+```js
+declare function is_number(x: mixed): boolean %checks(typeof x === "number");
+```
+
+Allow imports in declare module ([#315](https://github.com/babel/babylon/pull/315)) (Daniel Tschinder)
+
+Added support for imports within module declarations which flow introduced in version 0.37.0
+
+```js
+declare module "C" {
+  import type { DT } from "D";
+  declare export type CT = { D: DT };
+}
+```
+
+### :eyeglasses: Spec Compliance
+
+Forbid semicolons after decorators in classes ([#352](https://github.com/babel/babylon/pull/352)) (Kevin Gibbons)
+
+This example now correctly throws an error when there is a semicolon after the decorator:
+
+```js
+class A {
+@a;
+foo(){}
+}
+```
+
+Keywords are not allowed as local specifier ([#307](https://github.com/babel/babylon/pull/307)) (Daniel Tschinder)
+
+Using keywords in imports is not allowed anymore:
+
+```js
+import { default } from "foo";
+import { a as debugger } from "foo";
+```
+
+Do not allow overwritting of primitive types ([#314](https://github.com/babel/babylon/pull/314)) (Daniel Tschinder)
+
+In flow it is now forbidden to overwrite the primitive types `"any"`, `"mixed"`, `"empty"`, `"bool"`, `"boolean"`, `"number"`, `"string"`, `"void"` and `"null"` with your own type declaration.
+
+Disallow import type { type a } from … ([#305](https://github.com/babel/babylon/pull/305)) (Daniel Tschinder)
+
+The following code now correctly throws an error
+
+```js
+import type { type a } from "foo";
+```
+
+Don't parse class properties without initializers when classProperties is disabled and Flow is enabled ([#300](https://github.com/babel/babylon/pull/300)) (Andrew Levine)
+
+Ensure that you enable the `classProperties` plugin in order to enable correct parsing of class properties. Prior to this version it was possible to parse them by enabling the `flow` plugin but this was not intended the behaviour.
+
+If you enable the flow plugin you can only define the type of the class properties, but not initialize them.
+
+Fix export default async function to be FunctionDeclaration ([#324](https://github.com/babel/babylon/pull/324)) (Daniel Tschinder)
+
+Parsing the following code now returns a `FunctionDeclaration` AST node instead of `FunctionExpression`.
+
+```js
+export default async function bar() {};
+```
+
+### :nail_care: Polish
+
+Improve error message on attempt to destructure named import ([#288](https://github.com/babel/babylon/pull/288)) (Brian Ng)
+
+### :bug: Bug Fix
+
+Fix negative number literal typeannotations ([#366](https://github.com/babel/babylon/pull/366)) (Daniel Tschinder)
+
+Ensure takeDecorators is called on exported class ([#358](https://github.com/babel/babylon/pull/358)) (Brian Ng)
+
+ESTree: correctly change literals in all cases ([#368](https://github.com/babel/babylon/pull/368)) (Daniel Tschinder)
+
+Correctly convert RestProperty to Assignable ([#339](https://github.com/babel/babylon/pull/339)) (Daniel Tschinder)
+
+Fix #321 by allowing question marks in type params ([#338](https://github.com/babel/babylon/pull/338)) (Daniel Tschinder)
+
+Fix #336 by correctly setting arrow-param ([#337](https://github.com/babel/babylon/pull/337)) (Daniel Tschinder)
+
+Fix parse error when destructuring `set` with default value ([#317](https://github.com/babel/babylon/pull/317)) (Brian Ng)
+
+Fix ObjectTypeCallProperty static ([#298](https://github.com/babel/babylon/pull/298)) (Dan Harper)
+
+
+### :house: Internal
+
+Fix generator-method-with-computed-name spec ([#360](https://github.com/babel/babylon/pull/360)) (Alex Rattray)
+
+Fix flow type-parameter-declaration test with unintended semantic ([#361](https://github.com/babel/babylon/pull/361)) (Alex Rattray)
+
+Cleanup and splitup parser functions ([#295](https://github.com/babel/babylon/pull/295)) (Daniel Tschinder)
+
+chore(package): update flow-bin to version 0.38.0 ([#313](https://github.com/babel/babylon/pull/313)) (greenkeeper[bot])
+
+Call inner function instead of 1:1 copy to plugin ([#294](https://github.com/babel/babylon/pull/294)) (Daniel Tschinder)
+
+Update eslint-config-babel to the latest version 🚀 ([#299](https://github.com/babel/babylon/pull/299)) (greenkeeper[bot])
+
+Update eslint-config-babel to the latest version 🚀 ([#293](https://github.com/babel/babylon/pull/293)) (greenkeeper[bot])
+
+devDeps: remove eslint-plugin-babel ([#292](https://github.com/babel/babylon/pull/292)) (Kai Cataldo)
+
+Correct indent eslint rule config ([#276](https://github.com/babel/babylon/pull/276)) (Daniel Tschinder)
+
+Fail tests that have expected.json and throws-option ([#285](https://github.com/babel/babylon/pull/285)) (Daniel Tschinder)
+
+### :memo: Documentation
+
+Update contributing with more test info [skip ci] ([#355](https://github.com/babel/babylon/pull/355)) (Brian Ng)
+
+Update API documentation ([#330](https://github.com/babel/babylon/pull/330)) (Timothy Gu)
+
+Added keywords to package.json ([#323](https://github.com/babel/babylon/pull/323)) (Dmytro)
+
+AST spec: fix casing of `RegExpLiteral` ([#318](https://github.com/babel/babylon/pull/318)) (Mathias Bynens)
+
+## 6.15.0 (2017-01-10)
+
+### :eyeglasses: Spec Compliance
+
+Add support for Flow shorthand import type ([#267](https://github.com/babel/babylon/pull/267)) (Jeff Morrison)
+
+This change implements flows new shorthand import syntax
+and where previously you had to write this code:
+
+```js
+import {someValue} from "blah";
+import type {someType} from "blah";
+import typeof {someOtherValue} from "blah";
+```
+
+you can now write it like this:
+
+```js
+import {
+  someValue,
+  type someType,
+  typeof someOtherValue,
+} from "blah";
+```
+
+For more information look at [this](https://github.com/facebook/flow/pull/2890) pull request.
+
+flow: allow leading pipes in all positions ([#256](https://github.com/babel/babylon/pull/256)) (Vladimir Kurchatkin)
+
+This change now allows a leading pipe everywhere types can be used:
+```js
+var f = (x): | 1 | 2 => 1;
+```
+
+Throw error when exporting non-declaration ([#241](https://github.com/babel/babylon/pull/241)) (Kai Cataldo)
+
+Previously babylon parsed the following exports, although they are not valid:
+```js
+export typeof foo;
+export new Foo();
+export function() {};
+export for (;;);
+export while(foo);
+```
+
+### :bug: Bug Fix
+
+Don't set inType flag when parsing property names ([#266](https://github.com/babel/babylon/pull/266)) (Vladimir Kurchatkin)
+
+This fixes parsing of this case:
+
+```js
+const map = {
+  [age <= 17] : 'Too young'
+};
+```
+
+Fix source location for JSXEmptyExpression nodes (fixes #248) ([#249](https://github.com/babel/babylon/pull/249)) (James Long)
+
+The following case produced an invalid AST
+```js
+<div>{/* foo */}</div>
+```
+
+Use fromCodePoint to convert high value unicode entities ([#243](https://github.com/babel/babylon/pull/243)) (Ryan Duffy)
+
+When high value unicode entities (e.g. 💩) were used in the input source code they are now correctly encoded in the resulting AST.
+
+Rename folder to avoid Windows-illegal characters ([#281](https://github.com/babel/babylon/pull/281)) (Ryan Plant)
+
+Allow this.state.clone() when parsing decorators ([#262](https://github.com/babel/babylon/pull/262)) (Alex Rattray)
+
+### :house: Internal
+
+User external-helpers ([#254](https://github.com/babel/babylon/pull/254)) (Daniel Tschinder)
+
+Add watch script for dev ([#234](https://github.com/babel/babylon/pull/234)) (Kai Cataldo)
+
+Freeze current plugins list for "*" option, and remove from README.md ([#245](https://github.com/babel/babylon/pull/245)) (Andrew Levine)
+
+Prepare tests for multiple fixture runners. ([#240](https://github.com/babel/babylon/pull/240)) (Daniel Tschinder)
+
+Add some test coverage for decorators stage-0 plugin ([#250](https://github.com/babel/babylon/pull/250)) (Andrew Levine)
+
+Refactor tokenizer types file ([#263](https://github.com/babel/babylon/pull/263)) (Sven SAULEAU)
+
+Update eslint-config-babel to the latest version 🚀 ([#273](https://github.com/babel/babylon/pull/273)) (greenkeeper[bot])
+
+chore(package): update rollup to version 0.41.0 ([#272](https://github.com/babel/babylon/pull/272)) (greenkeeper[bot])
+
+chore(package): update flow-bin to version 0.37.0 ([#255](https://github.com/babel/babylon/pull/255)) (greenkeeper[bot])
+
+## 6.14.1 (2016-11-17)
+
+### :bug: Bug Fix
+
+Allow `"plugins": ["*"]` ([#229](https://github.com/babel/babylon/pull/229)) (Daniel Tschinder)
+
+```js
+{
+  "plugins": ["*"]
+}
+```
+
+Will include all parser plugins instead of specifying each one individually. Useful for tools like babel-eslint, jscodeshift, and ast-explorer.
+
+## 6.14.0 (2016-11-16)
+
+### :eyeglasses: Spec Compliance
+
+Throw error for reserved words `enum` and `await` ([#195](https://github.com/babel/babylon/pull/195)) (Kai Cataldo)
+
+[11.6.2.2 Future Reserved Words](http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words)
+
+Babylon will throw for more reserved words such as `enum` or `await` (in strict mode).
+
+```
+class enum {} // throws
+class await {} // throws in strict mode (module)
+```
+
+Optional names for function types and object type indexers ([#197](https://github.com/babel/babylon/pull/197)) (Gabe Levi)
+
+So where you used to have to write
+
+```js
+type A = (x: string, y: boolean) => number;
+type B = (z: string) => number;
+type C = { [key: string]: number };
+```
+
+you can now write (with flow 0.34.0)
+
+```js
+type A = (string, boolean) => number;
+type B = string => number;
+type C = { [string]: number };
+```
+
+Parse flow nested array type annotations like `number[][]` ([#219](https://github.com/babel/babylon/pull/219)) (Bernhard Häussner)
+
+Supports these form now of specifying array types:
+
+```js
+var a: number[][][][];
+var b: string[][];
+```
+
+### :bug: Bug Fix
+
+Correctly eat semicolon at the end of `DelcareModuleExports` ([#223](https://github.com/babel/babylon/pull/223))  (Daniel Tschinder)
+
+```
+declare module "foo" { declare module.exports: number }
+declare module "foo" { declare module.exports: number; }  // also allowed now
+```
+
+### :house: Internal
+
+ * Count Babel tests towards Babylon code coverage ([#182](https://github.com/babel/babylon/pull/182)) (Moti Zilberman)
+ * Fix strange line endings ([#214](https://github.com/babel/babylon/pull/214)) (Thomas Grainger)
+ * Add node 7 (Daniel Tschinder)
+ * chore(package): update flow-bin to version 0.34.0 ([#204](https://github.com/babel/babylon/pull/204)) (Greenkeeper)
+
+## v6.13.1 (2016-10-26)
+
+### :nail_care: Polish
+
+- Use rollup for bundling to speed up startup time ([#190](https://github.com/babel/babylon/pull/190)) ([@drewml](https://github.com/DrewML))
+
+```js
+const babylon = require('babylon');
+const ast = babylon.parse('var foo = "lol";');
+```
+
+With that test case, there was a ~95ms savings by removing the need for node to build/traverse the dependency graph.
+
+**Without bundling**
+![image](https://cloud.githubusercontent.com/assets/5233399/19420264/3133497e-93ad-11e6-9a6a-2da59c4f5c13.png)
+
+**With bundling**
+![image](https://cloud.githubusercontent.com/assets/5233399/19420267/388f556e-93ad-11e6-813e-7c5c396be322.png)
+
+- add clean command [skip ci] ([#201](https://github.com/babel/babylon/pull/201)) (Henry Zhu)
+- add ForAwaitStatement (async generator already added) [skip ci] ([#196](https://github.com/babel/babylon/pull/196)) (Henry Zhu)
+
+## v6.13.0 (2016-10-21)
+
+### :eyeglasses: Spec Compliance
+
+Property variance type annotations for Flow plugin ([#161](https://github.com/babel/babylon/pull/161)) (Sam Goldman)
+
+> See https://flowtype.org/docs/variance.html for more information
+
+```js
+type T = { +p: T };
+interface T { -p: T };
+declare class T { +[k:K]: V };
+class T { -[k:K]: V };
+class C2 { +p: T = e };
+```
+
+Raise error on duplicate definition of __proto__ ([#183](https://github.com/babel/babylon/pull/183)) (Moti Zilberman)
+
+```js
+({ __proto__: 1, __proto__: 2 }) // Throws an error now
+```
+
+### :bug: Bug Fix
+
+Flow: Allow class properties to be named `static` ([#184](https://github.com/babel/babylon/pull/184)) (Moti Zilberman)
+
+```js
+declare class A {
+  static: T;
+}
+```
+
+Allow "async" as identifier for object literal property shorthand ([#187](https://github.com/babel/babylon/pull/187)) (Andrew Levine)
+
+```js
+var foo = { async, bar };
+```
+
+### :nail_care: Polish
+
+Fix flowtype and add inType to state ([#189](https://github.com/babel/babylon/pull/189)) (Daniel Tschinder)
+
+> This improves the performance slightly (because of hidden classes)
+
+### :house: Internal
+
+Fix .gitattributes line ending setting ([#191](https://github.com/babel/babylon/pull/191)) (Moti Zilberman)
+
+Increase test coverage ([#175](https://github.com/babel/babylon/pull/175) (Moti Zilberman)
+
+Readd missin .eslinignore for IDEs (Daniel Tschinder)
+
+Error on missing expected.json fixture in CI ([#188](https://github.com/babel/babylon/pull/188)) (Moti Zilberman)
+
+Add .gitattributes and .editorconfig for LF line endings ([#179](https://github.com/babel/babylon/pull/179)) (Moti Zilberman)
+
+Fixes two tests that are failing after the merge of #172 ([#177](https://github.com/babel/babylon/pull/177)) (Moti Zilberman)
+
+## v6.12.0 (2016-10-14)
+
+### :eyeglasses: Spec Compliance
+
+Implement import() syntax ([#163](https://github.com/babel/babylon/pull/163)) (Jordan Gensler)
+
+#### Dynamic Import
+
+- Proposal Repo: https://github.com/domenic/proposal-dynamic-import
+- Championed by [@domenic](https://github.com/domenic)
+- stage-2
+- [sept-28 tc39 notes](https://github.com/rwaldron/tc39-notes/blob/master/es7/2016-09/sept-28.md#113a-import)
+
+> This repository contains a proposal for adding a "function-like" import() module loading syntactic form to JavaScript
+
+```js
+import(`./section-modules/${link.dataset.entryModule}.js`)
+.then(module => {
+  module.loadPageInto(main);
+})
+```
+
+Add EmptyTypeAnnotation ([#171](https://github.com/babel/babylon/pull/171)) (Sam Goldman)
+
+#### EmptyTypeAnnotation
+
+Just wasn't covered before.
+
+```js
+type T = empty;
+```
+
+### :bug: Bug Fix
+
+Fix crash when exporting with destructuring and sparse array ([#170](https://github.com/babel/babylon/pull/170)) (Jeroen Engels)
+
+```js
+// was failing due to sparse array
+export const { foo: [ ,, qux7 ] } = bar;
+```
+
+Allow keyword in Flow object declaration property names with type parameters ([#146](https://github.com/babel/babylon/pull/146)) (Dan Harper)
+
+```js
+declare class X {
+  foobar<T>(): void;
+  static foobar<T>(): void;
+}
+```
+
+Allow keyword in object/class property names with Flow type parameters ([#145](https://github.com/babel/babylon/pull/145)) (Dan Harper)
+
+```js
+class Foo {
+  delete<T>(item: T): T {
+    return item;
+  }
+}
+```
+
+Allow typeAnnotations for yield expressions ([#174](https://github.com/babel/babylon/pull/174))) (Daniel Tschinder)
+
+```js
+function *foo() {
+  const x = (yield 5: any);
+}
+```
+
+### :nail_care: Polish
+
+Annotate more errors with expected token ([#172](https://github.com/babel/babylon/pull/172))) (Moti Zilberman)
+
+```js
+// Unexpected token, expected ; (1:6)
+{ set 1 }
+```
+
+### :house: Internal
+
+Remove kcheck ([#173](https://github.com/babel/babylon/pull/173)))  (Daniel Tschinder)
+
+Also run flow, linting, babel tests on separate instances (add back node 0.10)
+
+## v6.11.6 (2016-10-12)
+
+### :bug: Bug Fix/Regression
+
+Fix crash when exporting with destructuring and sparse array ([#170](https://github.com/babel/babylon/pull/170)) (Jeroen Engels)
+
+```js
+// was failing with `Cannot read property 'type' of null` because of null identifiers
+export const { foo: [ ,, qux7 ] } = bar;
+```
+
+## v6.11.5 (2016-10-12)
+
+### :eyeglasses: Spec Compliance
+
+Fix: Check for duplicate named exports in exported destructuring assignments ([#144](https://github.com/babel/babylon/pull/144)) (Kai Cataldo)
+
+```js
+// `foo` has already been exported. Exported identifiers must be unique. (2:20)
+export function foo() {};
+export const { a: [{foo}] } = bar;
+```
+
+Fix: Check for duplicate named exports in exported rest elements/properties ([#164](https://github.com/babel/babylon/pull/164)) (Kai Cataldo)
+
+```js
+// `foo` has already been exported. Exported identifiers must be unique. (2:22)
+export const foo = 1;
+export const [bar, ...foo] = baz;
+```
+
+### :bug: Bug Fix
+
+Fix: Allow identifier `async` for default param in arrow expression ([#165](https://github.com/babel/babylon/pull/165)) (Kai Cataldo)
+
+```js
+// this is ok now
+const test = ({async = true}) => {};
+```
+
+### :nail_care: Polish
+
+Babylon will now print out the token it's expecting if there's a `SyntaxError` ([#150](https://github.com/babel/babylon/pull/150)) (Daniel Tschinder)
+
+```bash
+# So in the case of a missing ending curly (`}`)
+Module build failed: SyntaxError: Unexpected token, expected } (30:0)
+  28 |   }
+  29 |
+> 30 |
+     | ^
+```
+
+## v6.11.4 (2016-10-03)
+
+Temporary rollback for erroring on trailing comma with spread (#154) (Henry Zhu)
+
+## v6.11.3 (2016-10-01)
+
+### :eyeglasses: Spec Compliance
+
+Add static errors for object rest (#149) ([@danez](https://github.com/danez))
+
+> https://github.com/sebmarkbage/ecmascript-rest-spread
+
+Object rest copies the *rest* of properties from the right hand side `obj` starting from the left to right.
+
+```js
+let { x, y, ...z } =  { x: 1, y: 2, z: 3 };
+// x = 1
+// y = 2
+// z = { z: 3 }
+```
+
+#### New Syntax Errors:
+
+**SyntaxError**: The rest element has to be the last element when destructuring (1:10)
+```bash
+> 1 | let { ...x, y, z } = { x: 1, y: 2, z: 3};
+    |           ^
+# Previous behavior:
+# x = { x: 1, y: 2, z: 3 }
+# y = 2
+# z = 3
+```
+
+Before, this was just a more verbose way of shallow copying `obj` since it doesn't actually do what you think.
+
+**SyntaxError**: Cannot have multiple rest elements when destructuring (1:13)
+
+```bash
+> 1 | let { x, ...y, ...z } = { x: 1, y: 2, z: 3};
+    |              ^
+# Previous behavior:
+# x = 1
+# y = { y: 2, z: 3 }
+# z = { y: 2, z: 3 }
+```
+
+Before y and z would just be the same value anyway so there is no reason to need to have both.
+
+**SyntaxError**: A trailing comma is not permitted after the rest element (1:16)
+
+```js
+let { x, y, ...z, } = obj;
+```
+
+The rationale for this is that the use case for trailing comma is that you can add something at the end without affecting the line above. Since a RestProperty always has to be the last property it doesn't make sense.
+
+---
+
+get / set are valid property names in default assignment (#142) ([@jezell](https://github.com/jezell))
+
+```js
+// valid
+function something({ set = null, get = null }) {}
+```
+
+## v6.11.2 (2016-09-23)
+
+### Bug Fix
+
+- [#139](https://github.com/babel/babylon/issues/139) Don't do the duplicate check if not an identifier (#140) @hzoo
+
+```js
+// regression with duplicate export check
+SyntaxError: ./typography.js: `undefined` has already been exported. Exported identifiers must be unique. (22:13)
+  20 |
+  21 | export const { rhythm } = typography;
+> 22 | export const { TypographyStyle } = typography
+```
+
+Bail out for now, and make a change to account for destructuring in the next release.
+
+## 6.11.1 (2016-09-22)
+
+### Bug Fix
+- [#137](https://github.com/babel/babylon/pull/137) - Fix a regression with duplicate exports - it was erroring on all keys in `Object.prototype`. @danez
+
+```javascript
+export toString from './toString';
+```
+
+```bash
+`toString` has already been exported. Exported identifiers must be unique. (1:7)
+> 1 | export toString from './toString';
+    |        ^
+  2 |
+```
+
+## 6.11.0 (2016-09-22)
+
+### Spec Compliance (will break CI)
+
+- Disallow duplicate named exports ([#107](https://github.com/babel/babylon/pull/107)) @kaicataldo
+
+```js
+// Only one default export allowed per module. (2:9)
+export default function() {};
+export { foo as default };
+
+// Only one default export allowed per module. (2:0)
+export default {};
+export default function() {};
+
+// `Foo` has already been exported. Exported identifiers must be unique. (2:0)
+export { Foo };
+export class Foo {};
+```
+
+### New Feature (Syntax)
+
+- Add support for computed class property names ([#121](https://github.com/babel/babylon/pull/121)) @motiz88
+
+```js
+// AST
+interface ClassProperty <: Node {
+  type: "ClassProperty";
+  key: Identifier;
+  value: Expression;
+  computed: boolean; // added
+}
+```
+
+```js
+// with "plugins": ["classProperties"]
+class Foo {
+  [x]
+  ['y']
+}
+
+class Bar {
+  [p]
+  [m] () {}
+}
+ ```
+
+### Bug Fix
+
+- Fix `static` property falling through in the declare class Flow AST ([#135](https://github.com/babel/babylon/pull/135)) @danharper
+
+```js
+declare class X {
+    a: number;
+    static b: number; // static
+    c: number; // this was being marked as static in the AST as well
+}
+```
+
+### Polish
+
+- Rephrase "assigning/binding to rvalue" errors to include context ([#119](https://github.com/babel/babylon/pull/119)) @motiz88
+
+```js
+// Used to error with:
+// SyntaxError: Assigning to rvalue (1:0)
+
+// Now:
+// Invalid left-hand side in assignment expression (1:0)
+3 = 4
+
+// Invalid left-hand side in for-in statement (1:5)
+for (+i in {});
+```
+
+### Internal
+
+- Fix call to `this.parseMaybeAssign` with correct arguments ([#133](https://github.com/babel/babylon/pull/133)) @danez
+- Add semver note to changelog ([#131](https://github.com/babel/babylon/pull/131)) @hzoo
+
+## 6.10.0 (2016-09-19)
+
+> We plan to include some spec compliance bugs in patch versions. An example was the multiple default exports issue.
+
+### Spec Compliance
+
+* Implement ES2016 check for simple parameter list in strict mode ([#106](https://github.com/babel/babylon/pull/106)) (Timothy Gu)
+
+> It is a Syntax Error if ContainsUseStrict of FunctionBody is true and IsSimpleParameterList of FormalParameters is false. https://tc39.github.io/ecma262/2016/#sec-function-definitions-static-semantics-early-errors
+
+More Context: [tc39-notes](https://github.com/rwaldron/tc39-notes/blob/master/es7/2015-07/july-29.md#611-the-scope-of-use-strict-with-respect-to-destructuring-in-parameter-lists)
+
+For example:
+
+```js
+// this errors because it uses destructuring and default parameters
+// in a function with a "use strict" directive
+function a([ option1, option2 ] = []) {
+  "use strict";
+}
+ ```
+
+The solution would be to use a top level "use strict" or to remove the destructuring or default parameters when using a function + "use strict" or to.
+
+### New Feature
+
+* Exact object type annotations for Flow plugin ([#104](https://github.com/babel/babylon/pull/104)) (Basil Hosmer)
+
+Added to flow in https://github.com/facebook/flow/commit/c710c40aa2a115435098d6c0dfeaadb023cd39b8
+
+Looks like:
+
+```js
+var a : {| x: number, y: string |} = { x: 0, y: 'foo' };
+```
+
+### Bug Fixes
+
+* Include `typeParameter` location in `ArrowFunctionExpression` ([#126](https://github.com/babel/babylon/pull/126)) (Daniel Tschinder)
+* Error on invalid flow type annotation with default assignment ([#122](https://github.com/babel/babylon/pull/122)) (Dan Harper)
+* Fix Flow return types on arrow functions ([#124](https://github.com/babel/babylon/pull/124)) (Dan Harper)
+
+### Misc
+
+* Add tests for export extensions ([#127](https://github.com/babel/babylon/pull/127)) (Daniel Tschinder)
+* Fix Contributing guidelines [skip ci] (Daniel Tschinder)
+
+## 6.9.2 (2016-09-09)
+
+The only change is to remove the `babel-runtime` dependency by compiling with Babel's ES2015 loose mode. So using babylon standalone should be smaller.
+
+## 6.9.1 (2016-08-23)
+
+This release contains mainly small bugfixes but also updates babylons default mode to es2017. The features for `exponentiationOperator`, `asyncFunctions` and `trailingFunctionCommas` which previously needed to be activated via plugin are now enabled by default and the plugins are now no-ops.
+
+### Bug Fixes
+
+- Fix issues with default object params in async functions ([#96](https://github.com/babel/babylon/pull/96)) @danez
+- Fix issues with flow-types and async function ([#95](https://github.com/babel/babylon/pull/95)) @danez
+- Fix arrow functions with destructuring, types & default value ([#94](https://github.com/babel/babylon/pull/94)) @danharper
+- Fix declare class with qualified type identifier ([#97](https://github.com/babel/babylon/pull/97)) @danez
+- Remove exponentiationOperator, asyncFunctions, trailingFunctionCommas plugins and enable them by default ([#98](https://github.com/babel/babylon/pull/98)) @danez
+
+## 6.9.0 (2016-08-16)
+
+### New syntax support
+
+- Add JSX spread children ([#42](https://github.com/babel/babylon/pull/42)) @calebmer
+
+(Be aware that React is not going to support this syntax)
+
+```js
+<div>
+  {...todos.map(todo => <Todo key={todo.id} todo={todo}/>)}
+</div>
+```
+
+- Add support for declare module.exports ([#72](https://github.com/babel/babylon/pull/72)) @danez
+
+```js
+declare module "foo" {
+  declare module.exports: {}
+}
+```
+
+### New Features
+
+- If supplied, attach filename property to comment node loc. ([#80](https://github.com/babel/babylon/pull/80)) @divmain
+- Add identifier name to node loc field ([#90](https://github.com/babel/babylon/pull/90)) @kittens
+
+### Bug Fixes
+
+- Fix exponential operator to behave according to spec ([#75](https://github.com/babel/babylon/pull/75)) @danez
+- Fix lookahead to not add comments to arrays which are not cloned ([#76](https://github.com/babel/babylon/pull/76)) @danez
+- Fix accidental fall-through in Flow type parsing. ([#82](https://github.com/babel/babylon/pull/82)) @xiemaisi
+- Only allow declares inside declare module ([#73](https://github.com/babel/babylon/pull/73)) @danez
+- Small fix for parsing type parameter declarations ([#83](https://github.com/babel/babylon/pull/83)) @gabelevi
+- Fix arrow param locations with flow types ([#57](https://github.com/babel/babylon/pull/57)) @danez
+- Fixes SyntaxError position with flow optional type ([#65](https://github.com/babel/babylon/pull/65)) @danez
+
+### Internal
+
+- Add codecoverage to tests @danez
+- Fix tests to not save expected output if we expect the test to fail @danez
+- Make a shallow clone of babel for testing @danez
+- chore(package): update cross-env to version 2.0.0 ([#77](https://github.com/babel/babylon/pull/77)) @greenkeeperio-bot
+- chore(package): update ava to version 0.16.0 ([#86](https://github.com/babel/babylon/pull/86)) @greenkeeperio-bot
+- chore(package): update babel-plugin-istanbul to version 2.0.0 ([#89](https://github.com/babel/babylon/pull/89)) @greenkeeperio-bot
+- chore(package): update nyc to version 8.0.0 ([#88](https://github.com/babel/babylon/pull/88)) @greenkeeperio-bot
+
+## 6.8.4 (2016-07-06)
+
+### Bug Fixes
+
+- Fix the location of params, when flow and default value used ([#68](https://github.com/babel/babylon/pull/68)) @danez
+
+## 6.8.3 (2016-07-02)
+
+### Bug Fixes
+
+- Fix performance regression introduced in 6.8.2 with conditionals ([#63](https://github.com/babel/babylon/pull/63)) @danez
+
+## 6.8.2 (2016-06-24)
+
+### Bug Fixes
+
+- Fix parse error with yielding jsx elements in generators `function* it() { yield <a></a>; }` ([#31](https://github.com/babel/babylon/pull/31)) @eldereal
+- When cloning nodes do not clone its comments ([#24](https://github.com/babel/babylon/pull/24)) @danez
+- Fix parse errors when using arrow functions with an spread element and return type `(...props): void => {}` ([#10](https://github.com/babel/babylon/pull/10)) @danez
+- Fix leading comments added from previous node ([#23](https://github.com/babel/babylon/pull/23)) @danez
+- Fix parse errors with flow's optional arguments `(arg?) => {}` ([#19](https://github.com/babel/babylon/pull/19)) @danez
+- Support negative numeric type literals @kittens
+- Remove line terminator restriction after await keyword @kittens
+- Remove grouped type arrow restriction as it seems flow no longer has it @kittens
+- Fix parse error with generic methods that have the name `get` or `set` `class foo { get() {} }` ([#55](https://github.com/babel/babylon/pull/55)) @vkurchatkin
+- Fix parse error with arrow functions that have flow type parameter declarations `<T>(x: T): T => x;` ([#54](https://github.com/babel/babylon/pull/54)) @gabelevi
+
+### Documentation
+
+- Document AST differences from ESTree ([#41](https://github.com/babel/babylon/pull/41)) @nene
+- Move ast spec from babel/babel ([#46](https://github.com/babel/babylon/pull/46)) @hzoo
+
+### Internal
+
+- Enable skipped tests ([#16](https://github.com/babel/babylon/pull/16)) @danez
+- Add script to test latest version of babylon with babel ([#21](https://github.com/babel/babylon/pull/21)) @danez
+- Upgrade test runner ava @kittens
+- Add missing generate-identifier-regex script @kittens
+- Rename parser context types @kittens
+- Add node v6 to travis testing @hzoo
+- Update to Unicode v9 ([#45](https://github.com/babel/babylon/pull/45)) @mathiasbynens
+
+## 6.8.1 (2016-06-06)
+
+### New Feature
+
+- Parse type parameter declarations with defaults like `type Foo<T = string> = T`
+
+### Bug Fixes
+- Type parameter declarations need 1 or more type parameters.
+- The existential type `*` is not a valid type parameter.
+- The existential type `*` is a primary type
+
+### Spec Compliance
+- The param list for type parameter declarations now consists of `TypeParameter` nodes
+- New `TypeParameter` AST Node (replaces using the `Identifier` node before)
+
+```
+interface TypeParameter <: Node {
+  bound: TypeAnnotation;
+  default: TypeAnnotation;
+  name: string;
+  variance: "plus" | "minus";
+}
+```
+
+## 6.8.0 (2016-05-02)
+
+#### New Feature
+
+##### Parse Method Parameter Decorators ([#12](https://github.com/babel/babylon/pull/12))
+
+> [Method Parameter Decorators](https://goo.gl/8MmCMG) is now a TC39 [stage 0 proposal](https://github.com/tc39/ecma262/blob/master/stage0.md).
+
+Examples:
+
+```js
+class Foo {
+  constructor(@foo() x, @bar({ a: 123 }) @baz() y) {}
+}
+
+export default function func(@foo() x, @bar({ a: 123 }) @baz() y) {}
+
+var obj = {
+  method(@foo() x, @bar({ a: 123 }) @baz() y) {}
+};
+```
+
+##### Parse for-await statements (w/ `asyncGenerators` plugin) ([#17](https://github.com/babel/babylon/pull/17))
+
+There is also a new node type, `ForAwaitStatement`.
+
+> [Async generators and for-await](https://github.com/tc39/proposal-async-iteration) are now a [stage 2 proposal](https://github.com/tc39/ecma262#current-proposals).
+
+Example:
+
+```js
+async function f() {
+  for await (let x of y);
+}
+```

+ 19 - 0
frontend/node_modules/@babel/parser/LICENSE

@@ -0,0 +1,19 @@
+Copyright (C) 2012-2014 by various contributors (see AUTHORS)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 19 - 0
frontend/node_modules/@babel/parser/README.md

@@ -0,0 +1,19 @@
+# @babel/parser
+
+> A JavaScript parser
+
+See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save-dev @babel/parser
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/parser --dev
+```

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff