commit a5f846474ca55b46d520cd4a88871eff91c523b0 Author: Damien Coles Date: Mon Jan 26 10:12:01 2026 -0500 public-ready-init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da9ddf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Environment files +.env +*.env.local + +# SSL certificates +certs/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Google service account keys +google-sa.json +*-sa.json +*.json.key + +# Logs +*.log + +# Docker +docker-compose.override.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d525c9 --- /dev/null +++ b/README.md @@ -0,0 +1,294 @@ +# Nexus 2 + +A field service management and CRM platform built with Django REST Framework and SvelteKit. Designed for managing customers, accounts, service schedules, projects, invoices, and team coordination with integrated Google Workspace services. + +## Improvements Over Nexus 1 + +Nexus 2 is a complete rewrite that addresses limitations in the original Nexus platform: + +### Architecture +- **Modern Frontend Stack**: Migrated from React + Webpack to SvelteKit + Vite for faster builds, better developer experience, and SSR capabilities +- **TypeScript Throughout**: Full TypeScript support in the frontend with typed API interfaces +- **Cleaner Project Structure**: Separated backend and frontend into distinct directories for better organization +- **Simplified Deployment**: Removed Redis dependency - no longer required for basic operation + +### Data Model Enhancements +- **Customer Entity**: Added a Customer model to group related Accounts under a single business entity with centralized billing information +- **Revenue Tracking**: Track revenue per account with date ranges for historical billing data +- **Labor Costs**: Track labor costs on projects for profitability analysis +- **Flexible Scheduling**: New Schedule model defines weekly service patterns with exceptions, enabling automatic service generation + +### New Features +- **Invoice Management**: Full invoicing system with draft/sent/paid/overdue status tracking and payment recording +- **Service Auto-Generation**: Generate monthly service records automatically from defined schedules +- **Role-Based Profiles**: Team members have roles (Admin, Team Leader, Team Member) for access control +- **Dashboard**: Centralized overview of operations + +### API Improvements +- **RESTful CRUD**: Consistent REST API patterns for all entities (vs. custom action-based endpoints in Nexus 1) +- **Pagination**: Built-in pagination with configurable page sizes +- **Better Filtering**: Query parameters for filtering lists by related entities + +### Simplified Integrations +- **Focused Google Integration**: Calendar and Drive/Sheets only (removed Gmail dependency) +- **Optional Google Features**: Application works without Google integration configured + +## Features + +- **Customer Management**: Track customers with contact and billing information +- **Account Management**: Manage service accounts with locations, contacts, and revenue tracking +- **Service Scheduling**: Define weekly service schedules with exceptions and auto-generate service records +- **Service Tracking**: Track service visits with status, team assignments, and notes +- **Project Management**: Plan and track projects with team assignments and labor costs +- **Invoice Management**: Generate and track invoices with payment status +- **Team Profiles**: Manage team members with roles (Admin, Team Leader, Team Member) +- **Reports**: Track team member activity across services and projects +- **Punchlist API**: Backend API for project punchlists with Google Sheets/PDF export (frontend UI requires customization) +- **Google Calendar Integration**: Create calendar events for services and projects +- **JWT Authentication**: Secure API access with token refresh + +## Tech Stack + +### Backend +- Python 3.12 +- Django 5.x with Django REST Framework +- PostgreSQL +- Google APIs (Calendar, Drive, Sheets) +- JWT authentication via djangorestframework-simplejwt +- WhiteNoise for static file serving + +### Frontend +- SvelteKit with TypeScript +- Vite +- Axios for API calls +- SSR-ready + +## Prerequisites + +- Python 3.12+ +- Node.js 22+ +- PostgreSQL +- Google Workspace account with domain-wide delegation (for calendar/drive features) + +## Google Workspace Setup + +This application can integrate with Google Workspace for calendar events and punchlist exports: + +1. Create a service account in Google Cloud Console +2. Enable the following APIs: + - Google Calendar API + - Google Drive API + - Google Sheets API +3. For domain-wide delegation (calendar events on behalf of users): + - Enable domain-wide delegation for the service account + - Grant the following scopes in Google Workspace Admin: + - `https://www.googleapis.com/auth/calendar` + - `https://www.googleapis.com/auth/drive` + - `https://www.googleapis.com/auth/spreadsheets` +4. Download the service account JSON key + +## Configuration + +### Backend + +1. Copy the environment example: +```bash +cd backend +cp .env.example .env +``` + +2. Configure the required environment variables in `.env`: + - `SECRET_KEY`: Django secret key + - `DB_*`: PostgreSQL connection settings + - `GOOGLE_*`: Google API configuration (optional) + +### Frontend + +The frontend uses environment variables set in docker-compose or `.env`: +- `PUBLIC_API_URL`: Backend API URL (e.g., `http://localhost:8000/api`) + +## Development Setup + +### Backend + +```bash +cd backend + +# Create virtual environment +python -m venv venv +source venv/bin/activate # or `venv\Scripts\activate` on Windows + +# Install dependencies +pip install -r requirements.txt + +# Run migrations +python manage.py migrate + +# Create superuser +python manage.py createsuperuser + +# Start development server +python manage.py runserver +``` + +### Frontend + +```bash +cd frontend + +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build +``` + +## Docker Deployment + +### SSL Certificates + +Place your SSL certificates in the `certs/` directory: +- `frontend-cert.pem` and `frontend-key.pem` for the frontend +- `backend-cert.pem` and `backend-key.pem` for the backend (if using HTTPS) + +### Environment Variables + +Create a `.env` file in the root directory for docker-compose: +```bash +BACKEND_PORT=8443 +FRONTEND_PORT=443 +FRONTEND_ORIGIN=https://your-domain.com +PUBLIC_API_URL=https://api.your-domain.com/api +``` + +### Build and Run + +```bash +# Build and start containers +docker-compose up -d --build + +# View logs +docker-compose logs -f +``` + +## Project Structure + +``` +nexus-2/ +├── backend/ # Django backend +│ ├── api/ # Main Django app +│ │ ├── models.py # Database models +│ │ ├── views.py # API views +│ │ ├── serializers.py # DRF serializers +│ │ ├── urls.py # URL routing +│ │ ├── google.py # Google API integrations +│ │ └── migrations/ # Database migrations +│ ├── config/ # Django project settings +│ │ ├── settings.py # Django configuration +│ │ └── urls.py # Root URL routing +│ ├── manage.py +│ ├── requirements.txt +│ └── Dockerfile +├── frontend/ # SvelteKit frontend +│ ├── src/ +│ │ ├── lib/ # Shared utilities +│ │ │ ├── api.ts # API client with types +│ │ │ ├── auth.ts # Authentication helpers +│ │ │ └── components/ # Reusable components +│ │ └── routes/ # SvelteKit routes +│ │ ├── accounts/ # Account management +│ │ ├── customers/ # Customer management +│ │ ├── services/ # Service tracking +│ │ ├── projects/ # Project management +│ │ ├── invoices/ # Invoice management +│ │ ├── schedules/ # Schedule management +│ │ ├── dashboard/ # Dashboard +│ │ ├── profile/ # User profile +│ │ ├── admin/ # Admin features +│ │ ├── calendar/ # Calendar integration +│ │ └── login/ # Authentication +│ ├── package.json +│ └── Dockerfile +├── docker-compose.yml +└── README.md +``` + +## API Endpoints + +### Authentication +- `POST /api/token/` - Obtain JWT tokens +- `POST /api/token/refresh/` - Refresh access token + +### Profiles +- `GET/POST /api/profiles/` - List/create profiles +- `GET/PUT/PATCH/DELETE /api/profiles/{id}/` - Profile detail +- `POST /api/auth/change-password/` - Change password +- `POST /api/auth/reset-password/` - Reset password (admin) + +### Customers +- `GET/POST /api/customers/` - List/create customers +- `GET/PUT/PATCH/DELETE /api/customers/{id}/` - Customer detail + +### Accounts +- `GET/POST /api/accounts/` - List/create accounts +- `GET/PUT/PATCH/DELETE /api/accounts/{id}/` - Account detail + +### Revenues +- `GET/POST /api/revenues/` - List/create revenues +- `GET/PUT/DELETE /api/revenues/{id}/` - Revenue detail + +### Schedules +- `GET/POST /api/schedules/` - List/create schedules +- `GET/PUT/PATCH/DELETE /api/schedules/{id}/` - Schedule detail +- `POST /api/generate-services/` - Generate services from schedules + +### Services +- `GET/POST /api/services/` - List/create services +- `GET/PUT/PATCH/DELETE /api/services/{id}/` - Service detail + +### Projects +- `GET/POST /api/projects/` - List/create projects +- `GET/PUT/PATCH/DELETE /api/projects/{id}/` - Project detail + +### Invoices +- `GET/POST /api/invoices/` - List/create invoices +- `GET/PUT/PATCH/DELETE /api/invoices/{id}/` - Invoice detail +- `POST /api/invoices/{id}/mark_as_paid/` - Mark invoice as paid + +### Reports +- `GET/POST /api/reports/` - List/create reports +- `GET/PUT/PATCH/DELETE /api/reports/{id}/` - Report detail + +### Punchlists +- `GET/POST /api/punchlists/` - List/create punchlists +- `GET/PUT/PATCH/DELETE /api/punchlists/{id}/` - Punchlist detail +- `POST /api/export/punchlist/` - Export punchlist to Google Sheets/PDF + +## Customizing the Punchlist Feature + +The Punchlist model is designed as a customizable checklist for service projects. The backend includes a generic punchlist structure with sections for: + +- **Front area**: ceiling, vents, fixtures, counter +- **Main work area**: equipment, walls, ceiling, vents, floors +- **Equipment stations**: 7 configurable stations plus sinks, dispensers, other +- **Back area**: ceiling, vents +- **End of visit**: trash, cleanup, security check + +**To customize for your use case:** + +1. **Modify the model** (`backend/api/models.py`): Update the `Punchlist` model fields to match your specific checklist requirements + +2. **Update the field mapping** (`backend/api/google.py`): Modify `PUNCHLIST_FIELD_MAPPING` to map your model fields to cells in your Google Sheets template + +3. **Create a Google Sheets template**: Design a template that matches your punchlist layout and configure `PUNCHLIST_TEMPLATE_ID` in your environment + +4. **Build frontend UI**: The frontend punchlist routes were removed as they were specific to a previous implementation. Create new SvelteKit routes under `frontend/src/routes/projects/punchlist/` with forms matching your model fields + +The punchlist API endpoints are fully functional - only the frontend UI needs to be built for your specific workflow. + +## License + +MIT diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..6d206fb --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,33 @@ +# Django settings +SECRET_KEY=your-secret-key-here +DEBUG=False +ALLOWED_HOSTS=localhost,127.0.0.1 + +# Database configuration +DB_NAME=nexus +DB_USER=postgres +DB_PASSWORD=your-database-password +DB_HOST=localhost +DB_PORT=5432 + +# Timezone +TIME_ZONE=America/Detroit + +# JWT settings +JWT_ACCESS_LIFETIME_MINUTES=60 +JWT_REFRESH_LIFETIME_DAYS=14 + +# CORS (comma-separated list of allowed origins) +CORS_ALLOWED_ORIGINS=http://localhost:5173,https://your-domain.com + +# Google API configuration +# Path to your service account JSON key file (relative to app root) +GOOGLE_SERVICE_ACCOUNT_FILE=google-sa.json + +# Calendar settings (for domain-wide delegation) +GOOGLE_CALENDAR_ID=primary +GOOGLE_IMPERSONATOR_EMAIL=your-email@your-domain.com + +# Google Drive punchlist settings (optional) +PUNCHLIST_TEMPLATE_ID=your-google-sheets-template-id +PUNCHLIST_FOLDER_ID=your-google-drive-folder-id diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..f996c00 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,4 @@ +.venv +.env +api/data +google-sa.json diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..5a39f58 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +WORKDIR /app +COPY requirements.txt . +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install gunicorn uvicorn whitenoise +RUN mkdir -p /app/certs/ +RUN mkdir -p /app/staticfiles/ +COPY . . +RUN python manage.py collectstatic --noinput +EXPOSE 8000 +CMD gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker --certfile=/app/certs/backend-cert.pem --keyfile=/app/certs/backend-key.pem --bind=0.0.0.0:8000 \ No newline at end of file diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/admin.py b/backend/api/admin.py new file mode 100644 index 0000000..08794ed --- /dev/null +++ b/backend/api/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from .models import ( + Profile, Customer, Account, Revenue, Labor, + Schedule, Service, Project, Invoice, Report, Punchlist +) + +admin.site.register(Profile) +admin.site.register(Customer) +admin.site.register(Account) +admin.site.register(Revenue) +admin.site.register(Labor) +admin.site.register(Schedule) +admin.site.register(Service) +admin.site.register(Project) +admin.site.register(Invoice) +admin.site.register(Report) +admin.site.register(Punchlist) \ No newline at end of file diff --git a/backend/api/apps.py b/backend/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/backend/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/backend/api/google.py b/backend/api/google.py new file mode 100644 index 0000000..6b55910 --- /dev/null +++ b/backend/api/google.py @@ -0,0 +1,388 @@ +import io +import logging +import os +import re +from django.utils import timezone + +from googleapiclient.discovery import build +from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload +from google.oauth2 import service_account +from google.auth.exceptions import GoogleAuthError +from googleapiclient.errors import HttpError + +from .models import Punchlist + +logger = logging.getLogger(__name__) + +# Configuration from environment +CALENDAR_ID = os.getenv('GOOGLE_CALENDAR_ID', 'primary') +IMPERSONATOR_EMAIL = os.getenv('GOOGLE_IMPERSONATOR_EMAIL') +TIME_ZONE = os.getenv('TIME_ZONE', 'America/Detroit') + +# Punchlist Google Drive configuration from environment +PUNCHLIST_TEMPLATE = os.getenv('PUNCHLIST_TEMPLATE_ID', '') +PUNCHLIST_FOLDER = os.getenv('PUNCHLIST_FOLDER_ID', '') + + +def create_event(key, data): + """ + Create an event on the project calendar using the Google API + Args: + key (dict): Service account credentials dictionary + data (dict): Event data with the following structure: + - summary (str): Event title + - description (str): Event description + - start (str): ISO format start datetime + - end (str): ISO format end datetime + - location (str): Event location + - attendees (list): List of dicts with 'email' keys + - reminders (list, optional): List of dicts with 'method' and 'minutes' keys + Returns: + dict: Created event data or error information + """ + service_account_key = key + # Set up calendar details from environment + calendar_id = CALENDAR_ID + impersonator = IMPERSONATOR_EMAIL + # Validate required fields + required_fields = ['summary', 'start', 'end'] + missing_fields = [field for field in required_fields if field not in data] + if missing_fields: + error_msg = f"Missing required fields: {', '.join(missing_fields)}" + logger.error(error_msg) + return {"error": error_msg} + # Construct event data + event = { + 'summary': data.get('summary'), + 'description': data.get('description', ''), + 'start': { + 'dateTime': data.get('start'), + 'timeZone': TIME_ZONE, + }, + 'end': { + 'dateTime': data.get('end'), + 'timeZone': TIME_ZONE, + }, + 'location': data.get('location', ''), + 'attendees': data.get('attendees', []), + 'reminders': { + 'useDefault': False if data.get('reminders') else True, + 'overrides': data.get('reminders', []) if data.get('reminders') else [], + } + } + try: + # Set up authentication + scopes = ['https://www.googleapis.com/auth/calendar'] + credentials = service_account.Credentials.from_service_account_info( + service_account_key, scopes=scopes, subject=impersonator + ) + # Build and call the service + service = build('calendar', 'v3', credentials=credentials) + created_event = service.events().insert(calendarId=calendar_id, body=event).execute() + logger.info(f"Event created: {created_event.get('htmlLink')}") + return created_event + + except GoogleAuthError as e: + logger.error(f"Authentication error: {e}") + return {"error": "Authentication failed", "details": str(e)} + except HttpError as e: + logger.error(f"Google API error: {e}") + return {"error": "Google Calendar API error", "details": str(e)} + except Exception as e: + logger.error(f"Unexpected error creating event: {e}") + return {"error": "Unexpected error creating event", "details": str(e)} + + +# Field mapping for punchlist export to Google Sheets +# Maps model field names to spreadsheet cell references +# Customize this mapping to match your Google Sheets template +PUNCHLIST_FIELD_MAPPING = { + 'account_name': 'B4', + 'date': 'D4', + # Front area + 'front_ceiling': 'D8', + 'front_vents': 'D9', + 'front_fixtures': 'D10', + 'front_counter': 'D11', + # Main work area + 'main_equipment': 'D15', + 'main_equipment_disassemble': 'D16', + 'main_equipment_reassemble': 'D17', + 'main_equipment_test': 'D18', + 'main_equipment_exterior': 'D19', + 'main_walls': 'D20', + 'main_fixtures': 'D21', + 'main_ceiling': 'D22', + 'main_vents': 'D23', + 'main_floors': 'D24', + # Equipment stations + 'equip_station_1': 'D28', + 'equip_station_2': 'D29', + 'equip_station_3': 'D30', + 'equip_station_4': 'D31', + 'equip_station_5': 'D32', + 'equip_station_6': 'D33', + 'equip_station_7': 'D34', + 'equip_sinks': 'D35', + 'equip_dispensers': 'D36', + 'equip_other': 'D37', + # Back area + 'back_ceiling': 'D41', + 'back_vents': 'D42', + # End of visit + 'end_trash': 'D46', + 'end_clean': 'D47', + 'end_secure': 'D48', + # Notes + 'notes': 'A52' +} + + +def get_google_service(key): + """Create and return Google API service""" + credentials = service_account.Credentials.from_service_account_info( + key, + scopes=['https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/spreadsheets'] + ) + + drive_service = build('drive', 'v3', credentials=credentials) + sheets_service = build('sheets', 'v4', credentials=credentials) + + return drive_service, sheets_service + + +def export_punchlist_to_sheet(key, punchlist_id, template_id=None): + """ + Export a punchlist to Google Sheets and generate a PDF + + Args: + key: Service account credentials dictionary + punchlist_id: The ID of the punchlist to export + template_id: Optional template ID, uses default if not provided + + Returns: + dict: URLs to the created sheet and PDF + """ + # Get the punchlist data + try: + punchlist = Punchlist.objects.get(id=punchlist_id) + + if punchlist.sheet_url and punchlist.pdf_url: + # Try to verify files still exist in Google Drive + try: + drive_service, _ = get_google_service(key) + + # Extract file IDs from URLs + sheet_id = extract_file_id_from_url(punchlist.sheet_url) + pdf_id = extract_file_id_from_url(punchlist.pdf_url) + + # Check if files exist + sheet_exists = check_file_exists(drive_service, sheet_id) + pdf_exists = check_file_exists(drive_service, pdf_id) + + if sheet_exists and pdf_exists: + return { + 'success': True, + 'already_exported': True, + 'sheetUrl': punchlist.sheet_url, + 'pdfUrl': punchlist.pdf_url, + 'message': 'Files already exist' + } + except Exception: + # If there's an error checking, proceed with creating new files + pass + except Punchlist.DoesNotExist: + return { + 'success': False, + 'message': f'Punchlist with ID {punchlist_id} not found' + } + + # Get account and project info + account_name = punchlist.account.name if punchlist.account else 'Unknown Account' + service_date = punchlist.project.date if punchlist.project else punchlist.date + + # Get Google services + drive_service, sheets_service = get_google_service(key) + + # Use provided template ID or default + template_id = template_id or PUNCHLIST_TEMPLATE + + if not template_id: + return { + 'success': False, + 'message': 'No punchlist template ID configured' + } + + try: + # 1. Copy the template + copied_file = drive_service.files().copy( + fileId=template_id, + body={ + 'name': f'Punchlist - {account_name} - {service_date}', + 'parents': [PUNCHLIST_FOLDER] if PUNCHLIST_FOLDER else [] + } + ).execute() + + new_sheet_id = copied_file['id'] + + # 2. Prepare data for the sheet + value_range_body = { + 'valueInputOption': 'USER_ENTERED', + 'data': [] + } + + # Map punchlist data to sheet cells + data_mapping = { + 'account_name': account_name, + 'date': service_date.strftime('%m/%d/%Y') if service_date else '', + 'front_ceiling': 'X' if punchlist.front_ceiling else '', + 'front_vents': 'X' if punchlist.front_vents else '', + 'front_fixtures': 'X' if punchlist.front_fixtures else '', + 'front_counter': 'X' if punchlist.front_counter else '', + 'main_equipment': punchlist.main_equipment.upper() if punchlist.main_equipment else '', + 'main_equipment_disassemble': 'X' if punchlist.main_equipment_disassemble else '', + 'main_equipment_reassemble': 'X' if punchlist.main_equipment_reassemble else '', + 'main_equipment_test': 'X' if punchlist.main_equipment_test else '', + 'main_equipment_exterior': 'X' if punchlist.main_equipment_exterior else '', + 'main_walls': 'X' if punchlist.main_walls else '', + 'main_fixtures': 'X' if punchlist.main_fixtures else '', + 'main_ceiling': 'X' if punchlist.main_ceiling else '', + 'main_vents': 'X' if punchlist.main_vents else '', + 'main_floors': 'X' if punchlist.main_floors else '', + 'equip_station_1': 'X' if punchlist.equip_station_1 else '', + 'equip_station_2': 'X' if punchlist.equip_station_2 else '', + 'equip_station_3': 'X' if punchlist.equip_station_3 else '', + 'equip_station_4': 'X' if punchlist.equip_station_4 else '', + 'equip_station_5': 'X' if punchlist.equip_station_5 else '', + 'equip_station_6': 'X' if punchlist.equip_station_6 else '', + 'equip_station_7': 'X' if punchlist.equip_station_7 else '', + 'equip_sinks': 'X' if punchlist.equip_sinks else '', + 'equip_dispensers': 'X' if punchlist.equip_dispensers else '', + 'equip_other': 'X' if punchlist.equip_other else '', + 'back_ceiling': 'X' if punchlist.back_ceiling else '', + 'back_vents': 'X' if punchlist.back_vents else '', + 'end_trash': 'X' if punchlist.end_trash else '', + 'end_clean': 'X' if punchlist.end_clean else '', + 'end_secure': 'X' if punchlist.end_secure else '', + 'notes': punchlist.notes or '' + } + + # Create value range objects for each cell + for field, cell in PUNCHLIST_FIELD_MAPPING.items(): + if field in data_mapping: + value = data_mapping[field] + + # Extract sheet name and cell reference + if '!' in cell: + sheet_name, cell_ref = cell.split('!') + else: + sheet_name = 'Sheet1' # Default sheet name + cell_ref = cell + + value_range_body['data'].append({ + 'range': f"'{sheet_name}'!{cell_ref}", + 'values': [[value]] + }) + + # 3. Update the sheet with data + sheets_service.spreadsheets().values().batchUpdate( + spreadsheetId=new_sheet_id, + body=value_range_body + ).execute() + + # 4. Export as PDF + request = drive_service.files().export_media( + fileId=new_sheet_id, + mimeType='application/pdf' + ) + + # Create a BytesIO object to store the PDF + pdf_file = io.BytesIO() + downloader = MediaIoBaseDownload(pdf_file, request) + + # Download the PDF + done = False + while not done: + status, done = downloader.next_chunk() + + # 5. Upload the PDF to Drive + pdf_metadata = { + 'name': f'Punchlist - {account_name} - {service_date}.pdf', + 'mimeType': 'application/pdf', + } + if PUNCHLIST_FOLDER: + pdf_metadata['parents'] = [PUNCHLIST_FOLDER] + + pdf_file.seek(0) + pdf_media = MediaIoBaseUpload(pdf_file, mimetype='application/pdf') + + uploaded_pdf = drive_service.files().create( + body=pdf_metadata, + media_body=pdf_media, + fields='id,webViewLink' + ).execute() + + # 6. Make both files accessible via link + for file_id in [new_sheet_id, uploaded_pdf['id']]: + drive_service.permissions().create( + fileId=file_id, + body={ + 'type': 'anyone', + 'role': 'reader' + } + ).execute() + + # Get the web view links + sheet_file = drive_service.files().get( + fileId=new_sheet_id, + fields='webViewLink' + ).execute() + + punchlist.sheet_url = sheet_file['webViewLink'] + punchlist.pdf_url = uploaded_pdf['webViewLink'] + punchlist.exported_at = timezone.now() + punchlist.save() + + return { + 'success': True, + 'sheetUrl': sheet_file['webViewLink'], + 'pdfUrl': uploaded_pdf['webViewLink'] + } + + except Exception as e: + return { + 'success': False, + 'message': f'Error exporting punchlist: {str(e)}' + } + + +def extract_file_id_from_url(url): + """Extract Google Drive file ID from a sharing URL""" + if not url: + return None + + # Google Drive URLs typically have the format: + # https://drive.google.com/file/d/{FILE_ID}/view or + # https://docs.google.com/spreadsheets/d/{FILE_ID}/edit + match = re.search(r'\/d\/([a-zA-Z0-9_-]+)', url) + if match: + return match.group(1) + + # For spreadsheet URLs + match = re.search(r'\/spreadsheets\/d\/([a-zA-Z0-9_-]+)', url) + if match: + return match.group(1) + + return None + + +def check_file_exists(drive_service, file_id): + """Check if a file exists in Google Drive""" + if not file_id: + return False + + try: + drive_service.files().get(fileId=file_id).execute() + return True + except Exception: + return False diff --git a/backend/api/migrations/__init__.py b/backend/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/models.py b/backend/api/models.py new file mode 100644 index 0000000..b03ec41 --- /dev/null +++ b/backend/api/models.py @@ -0,0 +1,310 @@ +import uuid +from django.db import models +from django.contrib.auth.models import User + + +class Profile(models.Model): + """Extension of the Django User model""" + ROLE_CHOICES = ( + ('admin', 'Admin'), + ('team_leader', 'Team Leader'), + ('team_member', 'Team Member'), + ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + primary_phone = models.CharField(max_length=20) + secondary_phone = models.CharField(max_length=20, blank=True, null=True) + email = models.EmailField() + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='team_member') + + class Meta: + ordering = ['last_name', 'first_name'] + verbose_name_plural = "Profiles" + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + +class Customer(models.Model): + """Customer model with contact information""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=200) + primary_contact_first_name = models.CharField(max_length=100) + primary_contact_last_name = models.CharField(max_length=100) + primary_contact_phone = models.CharField(max_length=20) + primary_contact_email = models.EmailField() + secondary_contact_first_name = models.CharField(max_length=100, blank=True, null=True) + secondary_contact_last_name = models.CharField(max_length=100, blank=True, null=True) + secondary_contact_phone = models.CharField(max_length=20, blank=True, null=True) + secondary_contact_email = models.EmailField(blank=True, null=True) + billing_contact_first_name = models.CharField(max_length=100) + billing_contact_last_name = models.CharField(max_length=100) + billing_street_address = models.CharField(max_length=255) + billing_city = models.CharField(max_length=100) + billing_state = models.CharField(max_length=100) + billing_zip_code = models.CharField(max_length=20) + billing_email = models.EmailField() + billing_terms = models.TextField() + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + + class Meta: + ordering = ['name'] + verbose_name_plural = "Customers" + + def __str__(self): + return self.name + + +class Account(models.Model): + """Account model belonging to a customer""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='accounts') + name = models.CharField(max_length=200) + street_address = models.CharField(max_length=255) + city = models.CharField(max_length=100) + state = models.CharField(max_length=100) + zip_code = models.CharField(max_length=20) + contact_first_name = models.CharField(max_length=100) + contact_last_name = models.CharField(max_length=100) + contact_phone = models.CharField(max_length=20) + contact_email = models.EmailField() + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + + class Meta: + ordering = ['name'] + verbose_name_plural = "Accounts" + + def __str__(self): + return f"{self.name} ({self.customer.name})" + + +class Revenue(models.Model): + """Revenue records for accounts""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='revenues') + amount = models.DecimalField(max_digits=10, decimal_places=2) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + + class Meta: + ordering = ['-start_date'] + verbose_name_plural = "Revenues" + + def __str__(self): + return f"{self.account.name} - ${self.amount}" + + +class Labor(models.Model): + """Labor records for accounts""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='labors') + amount = models.DecimalField(max_digits=10, decimal_places=2) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + + class Meta: + ordering = ['-start_date'] + verbose_name_plural = "Labors" + + def __str__(self): + return f"{self.account.name} - ${self.amount}" + + +class Schedule(models.Model): + """Service schedules for accounts""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='schedules') + monday_service = models.BooleanField(default=False) + tuesday_service = models.BooleanField(default=False) + wednesday_service = models.BooleanField(default=False) + thursday_service = models.BooleanField(default=False) + friday_service = models.BooleanField(default=False) + saturday_service = models.BooleanField(default=False) + sunday_service = models.BooleanField(default=False) + weekend_service = models.BooleanField(default=False) + schedule_exception = models.TextField(blank=True, null=True) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + + class Meta: + ordering = ['-start_date'] + verbose_name_plural = "Schedules" + + def __str__(self): + return f"Schedule for {self.account.name}" + + +class Service(models.Model): + """Service records for accounts""" + STATUS_CHOICES = ( + ('scheduled', 'Scheduled'), + ('in_progress', 'In Progress'), + ('completed', 'Completed'), + ('cancelled', 'Cancelled'), + ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='services') + date = models.DateField() + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='scheduled') + team_members = models.ManyToManyField(Profile, related_name='services') + notes = models.TextField(blank=True, null=True) + deadline_start = models.DateTimeField() + deadline_end = models.DateTimeField() + + class Meta: + ordering = ['-date'] + verbose_name_plural = "Services" + + def __str__(self): + return f"Service for {self.account.name} on {self.date}" + + +class Project(models.Model): + """Project records for customers""" + STATUS_CHOICES = ( + ('planned', 'Planned'), + ('in_progress', 'In Progress'), + ('completed', 'Completed'), + ('cancelled', 'Cancelled'), + ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='projects') + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='projects', blank=True, null=True) + date = models.DateField() + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='planned') + team_members = models.ManyToManyField(Profile, related_name='projects') + notes = models.TextField(blank=True, null=True) + labor = models.DecimalField(max_digits=10, decimal_places=2) + amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) + + class Meta: + ordering = ['-date'] + verbose_name_plural = "Projects" + + def __str__(self): + return f"Project for {self.customer.name} on {self.date}" + + +class Invoice(models.Model): + """Invoice records""" + STATUS_CHOICES = ( + ('draft', 'Draft'), + ('sent', 'Sent'), + ('paid', 'Paid'), + ('overdue', 'Overdue'), + ('cancelled', 'Cancelled'), + ) + PAYMENT_CHOICES = ( + ('check', 'Check'), + ('credit_card', 'Credit Card'), + ('bank_transfer', 'Bank Transfer'), + ('cash', 'Cash'), + ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + date = models.DateField() + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='invoices') + accounts = models.ManyToManyField(Account, related_name='invoices', blank=True) + projects = models.ManyToManyField(Project, related_name='invoices', blank=True) + revenues = models.ManyToManyField(Revenue, related_name='invoices', blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft') + date_paid = models.DateField(blank=True, null=True) + payment_type = models.CharField(max_length=20, choices=PAYMENT_CHOICES, blank=True, null=True) + + class Meta: + ordering = ['-date'] + verbose_name_plural = "Invoices" + + def __str__(self): + return f"Invoice for {self.customer.name} on {self.date}" + + +class Report(models.Model): + """Report records""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + date = models.DateField() + team_member = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='reports') + services = models.ManyToManyField(Service, related_name='reports', blank=True) + projects = models.ManyToManyField(Project, related_name='reports', blank=True) + + class Meta: + ordering = ['-date'] + verbose_name_plural = "Reports" + + def __str__(self): + return f"Report by {self.team_member.first_name} {self.team_member.last_name} on {self.date}" + + +class Punchlist(models.Model): + """ + Punchlist records for projects. + This is a customizable checklist for service projects with sections for + different areas and equipment. Modify the fields to match your specific + service requirements. + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + project = models.ForeignKey('Project', on_delete=models.CASCADE, related_name='punchlists') + account = models.ForeignKey('Account', on_delete=models.CASCADE, related_name='punchlists') + date = models.DateField() + second_visit = models.BooleanField(default=False) + second_date = models.DateTimeField(blank=True, null=True) + + # Front area section + front_ceiling = models.BooleanField(default=False) + front_vents = models.BooleanField(default=False) + front_fixtures = models.BooleanField(default=False) + front_counter = models.BooleanField(default=False) + + # Main work area section + main_equipment = models.CharField(max_length=20, blank=True) + main_equipment_disassemble = models.BooleanField(default=False) + main_equipment_reassemble = models.BooleanField(default=False) + main_equipment_test = models.BooleanField(default=False) + main_equipment_exterior = models.BooleanField(default=False) + main_walls = models.BooleanField(default=False) + main_fixtures = models.BooleanField(default=False) + main_ceiling = models.BooleanField(default=False) + main_vents = models.BooleanField(default=False) + main_floors = models.BooleanField(default=False) + + # Equipment section + equip_station_1 = models.BooleanField(default=False) + equip_station_2 = models.BooleanField(default=False) + equip_station_3 = models.BooleanField(default=False) + equip_station_4 = models.BooleanField(default=False) + equip_station_5 = models.BooleanField(default=False) + equip_station_6 = models.BooleanField(default=False) + equip_station_7 = models.BooleanField(default=False) + equip_sinks = models.BooleanField(default=False) + equip_dispensers = models.BooleanField(default=False) + equip_other = models.BooleanField(default=False) + + # Back area section + back_ceiling = models.BooleanField(default=False) + back_vents = models.BooleanField(default=False) + + # End of visit section + end_trash = models.BooleanField(default=False) + end_clean = models.BooleanField(default=False) + end_secure = models.BooleanField(default=False) + + # Notes + notes = models.TextField(blank=True) + + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + sheet_url = models.URLField(blank=True, null=True) + pdf_url = models.URLField(blank=True, null=True) + exported_at = models.DateTimeField(blank=True, null=True) + + class Meta: + ordering = ['-date'] + verbose_name_plural = "Punchlists" + + def __str__(self): + return f"Punchlist for {self.account.name} on {self.date}" diff --git a/backend/api/serializers.py b/backend/api/serializers.py new file mode 100644 index 0000000..a1b5b25 --- /dev/null +++ b/backend/api/serializers.py @@ -0,0 +1,125 @@ +from rest_framework import serializers +from django.contrib.auth.models import User +from .models import ( + Profile, Customer, Account, Revenue, Labor, + Schedule, Service, Project, Invoice, Report, Punchlist +) + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'email'] + +class ProfileSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + + class Meta: + model = Profile + fields = '__all__' + +class RevenueSerializer(serializers.ModelSerializer): + class Meta: + model = Revenue + fields = '__all__' + +class LaborSerializer(serializers.ModelSerializer): + class Meta: + model = Labor + fields = '__all__' + +class ScheduleSerializer(serializers.ModelSerializer): + class Meta: + model = Schedule + fields = '__all__' + +class ServiceSerializer(serializers.ModelSerializer): + team_members_detail = ProfileSerializer(source='team_members', many=True, read_only=True) + + class Meta: + model = Service + fields = '__all__' + + def to_representation(self, instance): + # This method customizes the output format + data = super().to_representation(instance) + # Replace team_members with profile information + if 'team_members_detail' in data: + data['team_members'] = data.pop('team_members_detail') + return data + + def validate_team_members(self, value): + request = self.context.get('request') + if request and request.user and hasattr(request.user, 'profile'): + if request.user.profile.role in ['admin']: + return value + # Default validation + if not value: + raise serializers.ValidationError("At least one team member is required.") + return value + +class ProjectSerializer(serializers.ModelSerializer): + team_members_detail = ProfileSerializer(source='team_members', many=True, read_only=True) + + class Meta: + model = Project + fields = '__all__' + + def to_representation(self, instance): + # This method customizes the output format + data = super().to_representation(instance) + # Replace team_members with profile information + if 'team_members_detail' in data: + data['team_members'] = data.pop('team_members_detail') + return data + + def validate_team_members(self, value): + request = self.context.get('request') + if request and request.user and hasattr(request.user, 'profile'): + if request.user.profile.role in ['admin', 'team_leader']: + return value + # Default validation + if not value: + raise serializers.ValidationError("At least one team member is required.") + return value + +class AccountSerializer(serializers.ModelSerializer): + revenues = RevenueSerializer(many=True, read_only=True) + labors = LaborSerializer(many=True, read_only=True) + schedules = ScheduleSerializer(many=True, read_only=True) + services = ServiceSerializer(many=True, read_only=True) + projects = ProjectSerializer(many=True, read_only=True) + + class Meta: + model = Account + fields = '__all__' + +class CustomerSerializer(serializers.ModelSerializer): + accounts = AccountSerializer(many=True, read_only=True) + projects = ProjectSerializer(many=True, read_only=True) + + class Meta: + model = Customer + fields = '__all__' + +class InvoiceSerializer(serializers.ModelSerializer): + customer = CustomerSerializer(read_only=True) + accounts = AccountSerializer(many=True, read_only=True) + projects = ProjectSerializer(many=True, read_only=True) + + class Meta: + model = Invoice + fields = '__all__' + +class ReportSerializer(serializers.ModelSerializer): + team_member = ProfileSerializer(read_only=True) + services = ServiceSerializer(many=True, read_only=True) + projects = ProjectSerializer(many=True, read_only=True) + + class Meta: + model = Report + fields = '__all__' + +class PunchlistSerializer(serializers.ModelSerializer): + class Meta: + model = Punchlist + fields = '__all__' \ No newline at end of file diff --git a/backend/api/tests.py b/backend/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/api/urls.py b/backend/api/urls.py new file mode 100644 index 0000000..1b7a76e --- /dev/null +++ b/backend/api/urls.py @@ -0,0 +1,37 @@ +from django.urls import path, include +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from rest_framework.routers import DefaultRouter +from .views import ( + UserViewSet, ProfileViewSet, CustomerViewSet, + AccountViewSet, RevenueViewSet, LaborViewSet, + ScheduleViewSet, ServiceViewSet, ProjectViewSet, + InvoiceViewSet, ReportViewSet, current_user, change_password, reset_password, generate_services, + create_calendar_event, PunchlistViewSet, export_punchlist +) + +router = DefaultRouter() +router.register(r'users', UserViewSet) +router.register(r'profiles', ProfileViewSet) +router.register(r'customers', CustomerViewSet) +router.register(r'accounts', AccountViewSet) +router.register(r'revenues', RevenueViewSet) +router.register(r'labors', LaborViewSet) +router.register(r'schedules', ScheduleViewSet) +router.register(r'services', ServiceViewSet) +router.register(r'projects', ProjectViewSet) +router.register(r'invoices', InvoiceViewSet) +router.register(r'reports', ReportViewSet) +router.register(r'punchlists', PunchlistViewSet) + +urlpatterns = [ + path('', include(router.urls)), + path('api-auth/', include('rest_framework.urls')), + path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('auth/user/', current_user, name='current_user'), + path('auth/change-password/', change_password, name='change_password'), + path('auth/reset-password/', reset_password, name='reset_password'), + path('generate-services/', generate_services, name='generate_services'), + path('calendar/create/', create_calendar_event, name='create_calendar_event'), + path('export/punchlist/', export_punchlist, name='export_punchlist') +] diff --git a/backend/api/views.py b/backend/api/views.py new file mode 100644 index 0000000..198e41f --- /dev/null +++ b/backend/api/views.py @@ -0,0 +1,603 @@ +import calendar +import json +import os +from datetime import datetime, timedelta +from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings +from django.db import transaction +from django.db.models import Q +from django.utils import timezone +from rest_framework import viewsets, permissions, status +from rest_framework.decorators import api_view, permission_classes, action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from .google import create_event, export_punchlist_to_sheet +from .models import ( + Profile, Customer, Account, Revenue, Labor, + Schedule, Service, Project, Invoice, Report, Punchlist +) +from .serializers import ( + UserSerializer, ProfileSerializer, CustomerSerializer, + AccountSerializer, RevenueSerializer, LaborSerializer, + ScheduleSerializer, ServiceSerializer, ProjectSerializer, + InvoiceSerializer, ReportSerializer, PunchlistSerializer +) + + +# Users API +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + +# Profiles API +class ProfileViewSet(viewsets.ModelViewSet): + queryset = Profile.objects.all() + serializer_class = ProfileSerializer + permission_classes = [permissions.IsAuthenticated] + + +# Customers API +class CustomerViewSet(viewsets.ModelViewSet): + queryset = Customer.objects.all() + serializer_class = CustomerSerializer + permission_classes = [permissions.IsAuthenticated] + + +# Accounts API +class AccountViewSet(viewsets.ModelViewSet): + queryset = Account.objects.all() + serializer_class = AccountSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Account.objects.all() + customer_id = self.request.query_params.get('customer_id') + if customer_id is not None: + queryset = queryset.filter(customer_id=customer_id) + return queryset + + +# Revenues API +class RevenueViewSet(viewsets.ModelViewSet): + queryset = Revenue.objects.all() + serializer_class = RevenueSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Revenue.objects.all() + account_id = self.request.query_params.get('account_id') + if account_id is not None: + queryset = queryset.filter(account_id=account_id) + return queryset + + +# Labor API +class LaborViewSet(viewsets.ModelViewSet): + queryset = Labor.objects.all() + serializer_class = LaborSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Labor.objects.all() + account_id = self.request.query_params.get('account_id') + if account_id is not None: + queryset = queryset.filter(account_id=account_id) + return queryset + + +# Schedules API +class ScheduleViewSet(viewsets.ModelViewSet): + queryset = Schedule.objects.all() + serializer_class = ScheduleSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Schedule.objects.all() + account_id = self.request.query_params.get('account_id') + if account_id is not None: + queryset = queryset.filter(account_id=account_id) + return queryset + + +# Services API +class ServiceViewSet(viewsets.ModelViewSet): + queryset = Service.objects.all() + serializer_class = ServiceSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Service.objects.all() + + # Filter by account + account_id = self.request.query_params.get('account_id') + if account_id is not None: + queryset = queryset.filter(account_id=account_id) + + # Filter by team member + team_member_id = self.request.query_params.get('team_member_id') + if team_member_id is not None: + queryset = queryset.filter(team_members__id=team_member_id) + + return queryset + + +# Projects API +class ProjectViewSet(viewsets.ModelViewSet): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Project.objects.all() + customer_id = self.request.query_params.get('customer_id') + account_id = self.request.query_params.get('account_id') + + if customer_id is not None: + queryset = queryset.filter(customer_id=customer_id) + if account_id is not None: + queryset = queryset.filter(account_id=account_id) + return queryset + + +# Invoices API +class InvoiceViewSet(viewsets.ModelViewSet): + queryset = Invoice.objects.all() + serializer_class = InvoiceSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Invoice.objects.all() + customer_id = self.request.query_params.get('customer_id') + if customer_id is not None: + queryset = queryset.filter(customer_id=customer_id) + return queryset + + @action(detail=True, methods=['post']) + def mark_as_paid(self, request, pk=None): + """ + Mark an invoice as paid with payment details + """ + invoice = self.get_object() + + # Update invoice with payment information + invoice.status = 'paid' + + # Set payment date if provided, otherwise use today + date_paid = request.data.get('date_paid') + if date_paid: + invoice.date_paid = date_paid + else: + invoice.date_paid = timezone.now().date() + + # Set payment type if provided + payment_type = request.data.get('payment_type') + if payment_type: + invoice.payment_type = payment_type + + invoice.save() + + # Return the updated invoice + serializer = self.get_serializer(invoice) + return Response(serializer.data) + + +# Reports API +class ReportViewSet(viewsets.ModelViewSet): + queryset = Report.objects.all() + serializer_class = ReportSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Report.objects.all() + team_member_id = self.request.query_params.get('team_member_id') + if team_member_id is not None: + queryset = queryset.filter(team_member_id=team_member_id) + return queryset + + +# User Profile API +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def current_user(request): + """ + Endpoint to get the current authenticated user and their profile + """ + user = request.user + user_serializer = UserSerializer(user) + # Get the profile associated with the user + try: + profile = Profile.objects.get(user=user) + profile_serializer = ProfileSerializer(profile) + # Return combined data + return Response({ + 'user': user_serializer.data, + 'profile': profile_serializer.data + }) + except ObjectDoesNotExist: + # Return just the user data if no profile exists + return Response({ + 'user': user_serializer.data, + 'profile': None + }) + + +# Punchlist API +class PunchlistViewSet(viewsets.ModelViewSet): + queryset = Punchlist.objects.all() + serializer_class = PunchlistSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + queryset = Punchlist.objects.all() + project_id = self.request.query_params.get('project_id') + account_id = self.request.query_params.get('account_id') + + if project_id is not None: + queryset = queryset.filter(project_id=project_id) + if account_id is not None: + queryset = queryset.filter(account_id=account_id) + return queryset + + def create(self, request, *args, **kwargs): + """ + Create a new punchlist + """ + # Extract project_id from request data + project_id = request.data.get('project_id') + if not project_id: + return Response( + {"error": "project_id is required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Verify project exists + try: + project = Project.objects.get(id=project_id) + except Project.DoesNotExist: + return Response( + {"error": f"Project with id {project_id} does not exist"}, + status=status.HTTP_404_NOT_FOUND + ) + + # Extract account_id from request data + account_id = request.data.get('account_id') + if not account_id: + return Response( + {"error": "account_id is required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Verify account exists + try: + account = Account.objects.get(id=account_id) + except Account.DoesNotExist: + return Response( + {"error": f"Account with id {account_id} does not exist"}, + status=status.HTTP_404_NOT_FOUND + ) + + # Create a modified data dictionary with project and account fields + modified_data = request.data.copy() + modified_data['project'] = project_id # Map project_id to project + modified_data['account'] = account_id # Map account_id to account + + # Create serializer with modified data + serializer = self.get_serializer(data=modified_data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + + headers = self.get_success_headers(serializer.data) + return Response( + serializer.data, + status=status.HTTP_201_CREATED, + headers=headers + ) + + +# Change Password API +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def change_password(request): + """ + Endpoint to change user password + """ + user = request.user + # Check if required fields are present + if not all(k in request.data for k in ('current_password', 'new_password')): + return Response( + {'error': 'Current password and new password are required'}, + status=status.HTTP_400_BAD_REQUEST + ) + # Verify current password + if not user.check_password(request.data['current_password']): + return Response( + {'error': 'Current password is incorrect'}, + status=status.HTTP_400_BAD_REQUEST + ) + # Set new password + user.set_password(request.data['new_password']) + user.save() + # Update session to prevent logout + update_session_auth_hash(request, user) + return Response({'message': 'Password changed successfully'}, status=status.HTTP_200_OK) + + +# Reset Password API +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def reset_password(request): + """ + Endpoint for admins to reset a user's password + """ + # Check if user is admin + try: + profile = Profile.objects.get(user=request.user) + if profile.role != 'admin': + return Response( + {"error": "Only administrators can reset passwords"}, + status=status.HTTP_403_FORBIDDEN + ) + except ObjectDoesNotExist: + return Response( + {"error": "User profile not found"}, + status=status.HTTP_404_NOT_FOUND + ) + # Get parameters + user_id = request.data.get('user_id') + new_password = request.data.get('new_password') + if not user_id or not new_password: + return Response( + {"error": "Missing required parameters"}, + status=status.HTTP_400_BAD_REQUEST + ) + # Find the target profile and user + try: + target_profile = Profile.objects.get(id=user_id) + target_user = target_profile.user + # Set new password + target_user.set_password(new_password) + target_user.save() + return Response({"message": "Password reset successful"}, status=status.HTTP_200_OK) + except ObjectDoesNotExist: + return Response( + {"error": "Target user profile not found"}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +# Generate Services API +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def generate_services(request): + """ + Batch generate services for a given month based on active schedules + """ + try: + # Expected data from request + month = int(request.data.get('month')) + year = int(request.data.get('year')) + account_ids = request.data.get('account_ids', None) + # Fixed holiday dates for 2025 + holidays = [ + datetime(2025, 1, 1).date(), # New Year's Day + datetime(2025, 1, 20).date(), # MLK Day + datetime(2025, 2, 17).date(), # Presidents' Day + datetime(2025, 5, 26).date(), # Memorial Day + datetime(2025, 7, 4).date(), # Independence Day + datetime(2025, 9, 1).date(), # Labor Day + datetime(2025, 11, 27).date(), # Thanksgiving + datetime(2025, 12, 25).date(), # Christmas + ] + # Validate month + if month < 0 or month > 11: + return Response( + {'error': 'Month must be between 0 (January) and 11 (December)'}, + status=status.HTTP_400_BAD_REQUEST + ) + except (ValueError, TypeError): + return Response( + {'error': 'Invalid parameters. Month and year must be integers.'}, + status=status.HTTP_400_BAD_REQUEST + ) + # Get calendar for specified month + cal = calendar.monthcalendar(year, (month + 1)) + # Get first and last day of month (for filtering schedules) + first_day = datetime(year, (month + 1), 1).date() + last_day = datetime(year, (month + 1), calendar.monthrange(year, (month + 1))[1]).date() + # Get active schedules for the month + schedules_query = Schedule.objects.filter( + start_date__lte=last_day + ).filter( + Q(end_date__isnull=True) | + Q(end_date__gte=first_day) + ) + # Filter by account IDs if provided + if account_ids: + schedules_query = schedules_query.filter(account__id__in=account_ids) + # Initialize service count + services_created = 0 + with transaction.atomic(): + for schedule in schedules_query: + # Use the monthcalendar to iterate through days + for week in cal: + for weekday, day in enumerate(week): + if day == 0: + continue + # Iterated date object + iter_date = datetime(year, month + 1, day).date() + # Check if date is in schedule's range + if not (schedule.start_date <= iter_date and ( + schedule.end_date is None or schedule.end_date >= iter_date)): + continue + # Check if date is a holiday + if iter_date in holidays: + continue + # Initial service requirement + service_needed = False + # Check regular weekday service requirement + if weekday == 0 and schedule.monday_service: + service_needed = True + elif weekday == 1 and schedule.tuesday_service: + service_needed = True + elif weekday == 2 and schedule.wednesday_service: + service_needed = True + elif weekday == 3 and schedule.thursday_service: + service_needed = True + elif weekday == 4 and schedule.friday_service: + service_needed = True + elif weekday == 5 and schedule.saturday_service: + service_needed = True + elif weekday == 6 and schedule.sunday_service: + service_needed = True + # Separate check for weekend service + if (schedule.weekend_service and weekday in [4]) and not schedule.friday_service: + service_needed = True + # Create service if needed and doesn't already exist + if service_needed: + # Check for existing service + existing_service = Service.objects.filter( + account=schedule.account, + date=iter_date + ).exists() + # If service does not exist + if not existing_service: + # Service starts at 6:00PM + deadline_start = datetime.combine( + iter_date, + datetime.min.time() + ).replace(hour=18) + # Service ends at 6:00AM + deadline_end = datetime.combine( + iter_date + timedelta(days=1), + datetime.min.time() + ).replace(hour=6) + # Prepare notes + notes = "" + if schedule.schedule_exception: + notes += f"Schedule exception: {schedule.schedule_exception}" + if schedule.weekend_service and weekday in [4]: + notes += " Weekend Service" + # Create the service + Service.objects.create( + account=schedule.account, + date=iter_date, + status='scheduled', + notes=notes.strip(), + deadline_start=deadline_start, + deadline_end=deadline_end + ) + # Add to the counter + services_created += 1 + # Return a summary of the operation + return Response({ + 'message': f'Successfully generated {services_created} services for {calendar.month_name[month + 1]} {year}.', + 'count': services_created, + 'month': month, + 'year': year, + 'accountsProcessed': schedules_query.count() + }) + + +key_file_path = os.path.join(settings.BASE_DIR, 'google-sa.json') + + +# Google Calendar Event API +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def create_calendar_event(request): + """ + Create a Google Calendar event for a project or service by reading key from file + """ + try: + try: + with open(key_file_path, 'r') as f: + google_service_account_key = json.load(f) + except FileNotFoundError: + return Response( + {"error": "Google service account key file not found"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + except json.JSONDecodeError as e: + return Response( + {"error": "Invalid Google service account key file format", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + result = create_event(google_service_account_key, request.data) + if "error" in result: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + return Response(result, status=status.HTTP_201_CREATED) + except Exception as e: + return Response( + {"error": "Failed to create calendar event", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def export_punchlist(request): + """ + Export a punchlist to Google Sheets and generate a PDF + """ + try: + # Validate required parameters + punchlist_id = request.data.get('punchlistId') + if not punchlist_id: + return Response( + {"error": "punchlistId is required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Get optional template ID + template_id = request.data.get('templateId') + + # Load Google service account key + try: + with open(key_file_path, 'r') as f: + google_service_account_key = json.load(f) + except FileNotFoundError: + return Response( + {"error": "Google service account key file not found"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + except json.JSONDecodeError as e: + return Response( + {"error": "Invalid Google service account key file format", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # Call the export function with the service account key + from .google import export_punchlist_to_sheet + result = export_punchlist_to_sheet(google_service_account_key, punchlist_id, template_id) + + # Handle errors from the export function + if not result.get('success', False): + return Response( + {"error": result.get('message', 'Unknown error during export')}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Return success response with URLs + return Response({ + "success": True, + "message": "Punchlist exported successfully", + "sheetUrl": result.get('sheetUrl'), + "pdfUrl": result.get('pdfUrl'), + "alreadyExported": result.get('already_exported', False) + }, status=status.HTTP_200_OK) + + except Exception as e: + # Handle any unexpected errors + return Response( + {"error": "Failed to export punchlist", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/config/asgi.py b/backend/config/asgi.py new file mode 100644 index 0000000..ed7c431 --- /dev/null +++ b/backend/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 0000000..aa5596f --- /dev/null +++ b/backend/config/settings.py @@ -0,0 +1,119 @@ +import os +from datetime import timedelta +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv("SECRET_KEY") +DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "yes") + +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",") + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', + 'api.apps.ApiConfig', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + +# Database configuration from environment +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('DB_NAME', 'nexus'), + 'USER': os.getenv('DB_USER', 'postgres'), + 'PASSWORD': os.getenv('DB_PASSWORD'), + 'HOST': os.getenv('DB_HOST', 'localhost'), + 'PORT': os.getenv('DB_PORT', '5432'), + } +} + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +LANGUAGE_CODE = 'en-us' +USE_I18N = True +USE_TZ = True +TIME_ZONE = os.getenv('TIME_ZONE', 'America/Detroit') + +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'static'), +] + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=int(os.getenv('JWT_ACCESS_LIFETIME_MINUTES', 60))), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=int(os.getenv('JWT_REFRESH_LIFETIME_DAYS', 14))), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': False, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', +} + +# CORS configuration +CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:5173').split(',') diff --git a/backend/config/urls.py b/backend/config/urls.py new file mode 100644 index 0000000..c318ca8 --- /dev/null +++ b/backend/config/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('api.urls')), +] \ No newline at end of file diff --git a/backend/config/wsgi.py b/backend/config/wsgi.py new file mode 100644 index 0000000..e2fbd58 --- /dev/null +++ b/backend/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/backend/manage.py b/backend/manage.py new file mode 100755 index 0000000..8e7ac79 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f846605 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,40 @@ +asgiref==3.8.1 +beautifulsoup4==4.13.4 +cachetools==5.5.2 +certifi==2025.4.26 +charset-normalizer==3.4.1 +dj-database-url==2.3.0 +Django==5.2 +django-cors-headers==4.7.0 +djangorestframework==3.16.0 +djangorestframework_simplejwt==5.5.0 +dotenv==0.9.9 +google==3.0.0 +google-api-core==2.23.0 +google-api-python-client==2.154.0 +google-auth==2.36.0 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.1 +googleapis-common-protos==1.66.0 +httplib2==0.22.0 +hvac==2.3.0 +idna==3.10 +oauth2==1.9.0.post1 +oauthlib==3.2.2 +proto-plus==1.26.1 +protobuf==5.29.4 +psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +PyJWT==2.9.0 +pyparsing==3.2.3 +python-dotenv==1.1.0 +requests==2.32.3 +requests-oauthlib==2.0.0 +rsa==4.9.1 +soupsieve==2.7 +sqlparse==0.5.3 +typing_extensions==4.13.2 +uritemplate==4.1.1 +urllib3==2.4.0 +whitenoise==6.9.0 diff --git a/backend/static/.gitkeep b/backend/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/templates/.gitkeep b/backend/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..33dafb8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "${BACKEND_PORT:-8443}:8000" + volumes: + - ./backend/.env:/app/.env:ro + - ./backend/google-sa.json:/app/google-sa.json:ro + - ./certs:/app/certs:ro + restart: unless-stopped + networks: + - nexus-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "${FRONTEND_PORT:-443}:3000" + volumes: + - ./certs:/app/certs + environment: + - NODE_ENV=production + - PORT=3000 + - ORIGIN=${FRONTEND_ORIGIN:-https://localhost} + - PUBLIC_API_URL=${PUBLIC_API_URL:-https://localhost:8443/api} + restart: unless-stopped + depends_on: + - backend + networks: + - nexus-network + +networks: + nexus-network: + driver: bridge diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..6562bcb --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,6 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..3f7802c --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..ef10d05 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:18-alpine as builder +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:18-alpine +WORKDIR /app +COPY --from=builder /app/build /app/build +COPY --from=builder /app/package.json /app/package.json +RUN npm install --omit=dev +RUN mkdir -p /app/certs/ +EXPOSE 3000 +ENV NODE_ENV=production +ENV PORT=3000 +ENV ORIGIN=https://localhost:3000 +CMD ["node", "-e", "import('https').then(https => { import('fs').then(fs => { import('path').then(path => { import('./build/handler.js').then(mod => { const handler = mod.handler; const options = { key: fs.readFileSync(path.resolve('/app/certs/frontend-key.pem')), cert: fs.readFileSync(path.resolve('/app/certs/frontend-cert.pem')) }; const server = https.createServer(options, handler); server.listen(3000, () => { console.log('SvelteKit application running on https://localhost:3000'); }); }); }); }); });"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..b5b2950 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..ef07d32 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,36 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { 'no-undef': 'off' } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3e75bc4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3858 @@ +{ + "name": "frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.1", + "dependencies": { + "axios": "^1.8.4", + "bootstrap": "^5.3.5", + "date-fns": "^4.1.0" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "svelte": "^5.28.1", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.2.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.8.tgz", + "integrity": "sha512-LqCYHdWL/QqKIJuZ/ucMAv8d4luKGs4oCPgpt8mWztQAtPrHfXKQ/XAUc8ljCHAfJCn6SvkpTcGt5Tsh8saowA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz", + "integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.20.7", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.7.tgz", + "integrity": "sha512-dVbLMubpJJSLI4OYB+yWYNHGAhgc2bVevWuBjDj8jFUXIJOAnLwYP3vsmtcgoxNGUXoq0rHS5f7MFCsryb6nzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", + "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/type-utils": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", + "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", + "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bootstrap": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.0.tgz", + "integrity": "sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.25.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", + "integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.5.1.tgz", + "integrity": "sha512-Qn1slddZHfqYiDO6IN8/iN3YL+VuHlgYjm30FT+hh0Jf/TX0jeZMTJXQMajFm5f6f6hURi+XO8P+NPYD+T4jkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", + "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", + "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.1.tgz", + "integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.6.tgz", + "integrity": "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.1.3.tgz", + "integrity": "sha512-DUc/z/vk+AFVoxGv54+BOBFqUrmUgNg2gSO2YqrE3OL6ro19/0azPmQj/4wN3s9RxuF5l7G0162q/Ddk4LJhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz", + "integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.30.1", + "@typescript-eslint/parser": "8.30.1", + "@typescript-eslint/utils": "8.30.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", + "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.3", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.12" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3c6a6cb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint ." + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "svelte": "^5.28.1", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.2.5" + }, + "dependencies": { + "axios": "^1.8.4", + "bootstrap": "^5.3.5", + "date-fns": "^4.1.0" + } +} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..150cf1c --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + Nexus v2 + %sveltekit.head% + + +
%sveltekit.body%
+ + + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..07b3255 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,645 @@ +import axios, { + type AxiosInstance, AxiosError, + type AxiosResponse, + type InternalAxiosRequestConfig +} from 'axios'; +import {browser} from '$app/environment'; + +// TYPES +interface ExtendedAxiosRequestConfig extends InternalAxiosRequestConfig { + _retry?: boolean; +} + +export interface Profile { + id: string; + user: User; + first_name: string; + last_name: string; + primary_phone: string; + secondary_phone?: string; + email: string; + role: string; +} + +export interface ProfileParams { + role?: string; + user_id?: string; + search?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface User { + id: number; + username: string; + email: string; +} + +export interface Customer { + id: string; + name: string; + primary_contact_first_name: string; + primary_contact_last_name: string; + primary_contact_phone: string; + primary_contact_email: string; + secondary_contact_first_name?: string; + secondary_contact_last_name?: string; + secondary_contact_phone?: string; + secondary_contact_email?: string; + billing_contact_first_name: string; + billing_contact_last_name: string; + billing_street_address: string; + billing_city: string; + billing_state: string; + billing_zip_code: string; + billing_email: string; + billing_terms: string; + start_date: string; + end_date?: string; + accounts?: Account[]; + projects?: Project[]; +} + +export interface Account { + id: string; + customer: string | Customer; + name: string; + street_address: string; + city: string; + state: string; + zip_code: string; + contact_first_name: string; + contact_last_name: string; + contact_phone: string; + contact_email: string; + start_date: string; + end_date?: string; + revenues?: Revenue[]; + labors?: Labor[]; + schedules?: Schedule[]; + services?: Service[]; + projects?: Project[]; +} + +export interface Schedule { + id: string; + account: string | Account; + monday_service: boolean; + tuesday_service: boolean; + wednesday_service: boolean; + thursday_service: boolean; + friday_service: boolean; + saturday_service: boolean; + sunday_service: boolean; + weekend_service: boolean; + schedule_exception?: string; + start_date: string; + end_date?: string; +} + +export interface Revenue { + id: string; + account: string | Account; + amount: number; + start_date: string; + end_date?: string; +} + +export interface Labor { + id: string; + account: string | Account; + amount: number; + start_date: string; + end_date?: string; +} + +export interface Schedule { + id: string; + account: string | Account; + monday_service: boolean; + tuesday_service: boolean; + wednesday_service: boolean; + thursday_service: boolean; + friday_service: boolean; + saturday_service: boolean; + sunday_service: boolean; + weekend_service: boolean; + schedule_exception?: string; + start_date: string; + end_date?: string; +} + +export interface Service { + id: string; + account: string | Account; + date: string; + status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled'; + team_members: string[] | Profile[]; + notes?: string; + deadline_start: string; + deadline_end: string; +} + +export interface Project { + id: string; + customer: string | Customer; + account?: string | Account; + date: string; + status: 'planned' | 'in_progress' | 'completed' | 'cancelled'; + team_members: string[] | Profile[]; // Change this to match Service + notes?: string; + labor: number; + amount: number; +} + +export interface Invoice { + id: string; + date: string; + customer: string | Customer; + accounts: Account[]; + revenues: Revenue[]; + projects: Project[]; + status: 'draft' | 'sent' | 'paid' | 'overdue' | 'cancelled'; + date_paid?: string; + payment_type?: 'check' | 'credit_card' | 'bank_transfer' | 'cash'; +} + +export interface Report { + id: string; + date: string; + team_member: string | Profile; + services: Service[]; + projects: Project[]; +} + +export interface CustomerParams { + name?: string; + primary_contact_email?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface AccountParams { + customer_id?: string; + name?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface ScheduleParams { + account_id?: string; + active?: boolean; + start_date_from?: string; + start_date_to?: string; + end_date_from?: string; + end_date_to?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface Holiday { + date: string; + name: string; +} + +export interface ServiceParams { + account_id?: string; + status?: string; + date_from?: string; + date_to?: string; + team_member_id?: string; + page?: number; + + [key: string]: string | number | boolean | undefined; +} + +export interface ProjectParams { + customer_id?: string; + account_id?: string; + status?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface InvoiceParams { + customer_id?: string; + status?: string; + date_from?: string; + date_to?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface ReportParams { + team_member_id?: string; + date_from?: string; + date_to?: string; + + [key: string]: string | number | boolean | undefined; +} + +interface TokenRefreshResponse { + access: string; +} + +export interface PaginatedResponse { + count: number; + next: string | null; + previous: string | null; + results: T[]; +} + +export interface ServiceGenerationResponse { + message: string; + count: number; + month: number; + year: number; + accountsProcessed: number; +} + +export interface PasswordChangeRequest extends Record { + current_password: string; + new_password: string; +} + +export interface PasswordResetRequest extends Record { + user_id: string; + new_password: string; +} + +/** + * Get the base URL for API requests based on environment + * Uses PUBLIC_API_URL environment variable if set, otherwise defaults to localhost + * @returns The base URL for API requests + */ +const getBaseUrl = (): string => { + // Check for environment variable (set in .env or docker-compose) + const envApiUrl = import.meta.env.PUBLIC_API_URL; + if (envApiUrl) { + return envApiUrl; + } + // Default to localhost for development + return 'http://localhost:8000/api'; +}; +/** + * Create an Axios instance with custom configuration + */ +const apiClient: AxiosInstance = axios.create({ + baseURL: getBaseUrl(), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + timeout: 30000 // 30 seconds +}); +/** + * Request interceptor for API calls + */ +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { // Change to InternalAxiosRequestConfig + const token = browser ? localStorage.getItem('access_token') : null; + if (token && config.headers) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error: Error) => { + return Promise.reject(error); + } +); +/** + * Response interceptor for API calls + * Handles token refresh on 401 Unauthorized errors + */ +apiClient.interceptors.response.use( + (response: AxiosResponse): AxiosResponse => response, + async (error: AxiosError) => { + const originalRequest = error.config as ExtendedAxiosRequestConfig; + if (!originalRequest) { + return Promise.reject(error); + } + if (error.response?.status === 401 && !originalRequest._retry && browser) { + originalRequest._retry = true; + const refreshToken = localStorage.getItem('refresh_token'); + if (!refreshToken) { + console.error('No refresh token available. Redirecting to login...'); + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + window.location.href = '/login'; + return Promise.reject(new Error('Authentication required')); + } + try { + const response = await axios.post( + `${getBaseUrl()}/token/refresh/`, + {refresh: refreshToken} + ); + const {access} = response.data; + localStorage.setItem('access_token', access); + if (originalRequest.headers) { + originalRequest.headers['Authorization'] = `Bearer ${access}`; + } + return axios(originalRequest); + } catch (refreshError) { + console.error('Token refresh failed:', refreshError); + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + window.location.href = '/login'; + return Promise.reject(refreshError); + } + } + return Promise.reject(error); + } +); +/** + * API service object with methods for common HTTP operations + */ +export const apiService = { + /** + * Make a GET request + * @param url - Endpoint URL (will be appended to base URL) + * @param params - Query parameters + * @returns Promise with response data + */ + get: async (url: string, params?: Record): Promise => { + const response = await apiClient.get(url, {params}); + return response.data; + }, + /** + * Make a POST request + * @param url - Endpoint URL (will be appended to base URL) + * @param data - Request body + * @returns Promise with response data + */ + post: async (url: string, data?: Record): Promise => { + const response = await apiClient.post(url, data); + return response.data; + }, + /** + * Make a PUT request + * @param url - Endpoint URL (will be appended to base URL) + * @param data - Request body + * @returns Promise with response data + */ + put: async (url: string, data?: Record): Promise => { + const response = await apiClient.put(url, data); + return response.data; + }, + /** + * Make a PATCH request + * @param url - Endpoint URL (will be appended to base URL) + * @param data - Request body + * @returns Promise with response data + */ + patch: async (url: string, data?: Record): Promise => { + const response = await apiClient.patch(url, data); + return response.data; + }, + /** + * Make a DELETE request + * @param url - Endpoint URL (will be appended to base URL) + * @returns Promise with response data + */ + delete: async (url: string): Promise => { + const response = await apiClient.delete(url); + return response.data; + } +}; +export const profileService = { + getAll: async (params?: ProfileParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/profiles/', params); + let allProfiles = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/profiles/', nextPageParams); + allProfiles = [...allProfiles, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allProfiles + }; + }, + getById: (id: string) => apiService.get(`/profiles/${id}/`), + create: (data: Omit) => apiService.post('/profiles/', data), + update: (id: string, data: Partial) => apiService.put(`/profiles/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/profiles/${id}/`, data), + delete: (id: string) => apiService.delete(`/profiles/${id}/`), + changePassword: (data: PasswordChangeRequest) => apiService.post('/auth/change-password/', data), + resetPassword: (data: PasswordResetRequest) => apiService.post('/auth/reset-password/', data) +}; +export const customerService = { + getAll: async (params?: CustomerParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/customers/', params); + let allCustomers = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/customers/', nextPageParams); + allCustomers = [...allCustomers, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allCustomers + }; + }, + getById: (id: string) => apiService.get(`/customers/${id}/`), + create: (data: Omit) => apiService.post('/customers/', data), + update: (id: string, data: Partial) => apiService.put(`/customers/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/customers/${id}/`, data), + delete: (id: string) => apiService.delete(`/customers/${id}/`), + getAccounts: (id: string) => apiService.get>(`/accounts/`, {customer_id: id}) +}; +export const accountService = { + getAll: async (params?: AccountParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/accounts/', params); + let allAccounts = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/accounts/', nextPageParams); + allAccounts = [...allAccounts, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allAccounts + }; + }, + getById: (id: string) => apiService.get(`/accounts/${id}/`), + create: (data: Omit) => apiService.post('/accounts/', data), + update: (id: string, data: Partial) => apiService.put(`/accounts/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/accounts/${id}/`, data), + delete: (id: string) => apiService.delete(`/accounts/${id}/`), + getServices: (id: string) => apiService.get>(`/services/`, {account_id: id}), + getRevenues: async (id: string) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/revenues/', {account_id: id}); + let allRevenues = [...firstPageResponse.results]; + + // Get total number of pages based on count and page size + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {account_id: id, page}; + const nextPageResponse = await apiService.get>('/revenues/', nextPageParams); + allRevenues = [...allRevenues, ...nextPageResponse.results]; + } + + return allRevenues; + } + , + createRevenue: (data: Omit) => apiService.post('/revenues/', data), + updateRevenue: (id: string, data: Partial) => apiService.put(`/revenues/${id}/`, data), + deleteRevenue: (id: string) => apiService.delete(`/revenues/${id}/`) +}; +export const scheduleService = { + getAll: async (params?: ScheduleParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/schedules/', params); + let allSchedules = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/schedules/', nextPageParams); + allSchedules = [...allSchedules, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allSchedules + }; + }, + getById: (id: string) => apiService.get(`/schedules/${id}/`), + create: (data: Omit) => apiService.post('/schedules/', data), + update: (id: string, data: Partial) => apiService.put(`/schedules/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/schedules/${id}/`, data), + delete: (id: string) => apiService.delete(`/schedules/${id}/`), + generateServices: (month: number, year: number, accountIds?: string[]) => + apiService.post('/generate-services/', { + month, + year, + account_ids: accountIds + }) +}; +export const serviceService = { + getAll: async (params?: ServiceParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/services/', params); + let allServices = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/services/', nextPageParams); + allServices = [...allServices, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allServices + }; + }, + getById: (id: string) => apiService.get(`/services/${id}/`), + create: (data: Omit) => apiService.post('/services/', data), + update: (id: string, data: Partial) => apiService.put(`/services/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/services/${id}/`, data), + delete: (id: string) => apiService.delete(`/services/${id}/`) +}; +export const projectService = { + getAll: async (params?: ProjectParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/projects/', params); + let allProjects = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/projects/', nextPageParams); + allProjects = [...allProjects, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allProjects + }; + }, + getById: (id: string) => apiService.get(`/projects/${id}/`), + create: (data: Omit) => apiService.post('/projects/', data), + update: (id: string, data: Partial) => apiService.put(`/projects/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/projects/${id}/`, data), + delete: (id: string) => apiService.delete(`/projects/${id}/`) +}; +export const invoiceService = { + getAll: async (params?: InvoiceParams) => { + // Initial request for the first page + const firstPageResponse = await apiService.get>('/invoices/', params); + let allInvoices = [...firstPageResponse.results]; + // Get total number of pages based on count and page size (assuming default page size of 10) + const pageSize = 10; // Adjust if your API uses a different page size + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + // Fetch subsequent pages if they exist + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/invoices/', nextPageParams); + allInvoices = [...allInvoices, ...nextPageResponse.results]; + } + // Return a response that matches the PaginatedResponse structure but contains all results + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allInvoices + }; + }, + getById: (id: string) => apiService.get(`/invoices/${id}/`), + create: (data: Omit) => apiService.post('/invoices/', data), + update: (id: string, data: Partial) => apiService.put(`/invoices/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/invoices/${id}/`, data), + delete: (id: string) => apiService.delete(`/invoices/${id}/`), + markAsPaid: (id: string, data: Record) => + apiService.post(`/invoices/${id}/mark_as_paid/`, data) +}; +export const reportService = { + getAll: (params?: ReportParams) => apiService.get>('/reports/', params), + getById: (id: string) => apiService.get(`/reports/${id}/`), + create: (data: Omit) => apiService.post('/reports/', data), + update: (id: string, data: Partial) => apiService.put(`/reports/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/reports/${id}/`, data), + delete: (id: string) => apiService.delete(`/reports/${id}/`) +}; +export default apiClient; \ No newline at end of file diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts new file mode 100644 index 0000000..67ce44e --- /dev/null +++ b/frontend/src/lib/auth.ts @@ -0,0 +1,218 @@ +import {browser} from '$app/environment'; +import {writable, derived} from 'svelte/store'; +import axios from 'axios'; +import apiClient, {type User, type Profile} from './api.js'; + +// TYPES +interface AuthState { + isAuthenticated: boolean; + user: User | null; + profile: Profile | null; + accessToken: string | null; + refreshToken: string | null; + loading: boolean; + error: string | null; +} + +interface LoginCredentials { + username: string; + password: string; +} + +interface TokenResponse { + access: string; + refresh: string; +} + +// INITIAL STATE +const initialState: AuthState = { + isAuthenticated: false, + user: null, + profile: null, + accessToken: browser ? localStorage.getItem('access_token') : null, + refreshToken: browser ? localStorage.getItem('refresh_token') : null, + loading: false, + error: null +}; + +// AUTH STORE FACTORY FUNCTION +const createAuthStore = () => { + const {subscribe, set, update} = writable(initialState); + if (browser && initialState.accessToken) { + update(state => ({ + ...state, + isAuthenticated: true, + loading: true + })); + (async () => { + try { + await loadUserProfile(); + } catch (error) { + console.error('Error during initial profile loading:', error); + } + })(); + } + + // LOAD USER PROFILE + async function loadUserProfile() { + try { + const response = await apiClient.get('/auth/user/'); + const data = response.data; + const userData = data.user as User; + const profileData = data.profile as Profile | null; + update(state => ({ + ...state, + isAuthenticated: true, + user: userData, + profile: profileData, + loading: false, + error: null + })); + } catch (error) { + console.error('Error loading user profile:', error); + if (axios.isAxiosError(error) && error.response?.status === 401) { + logout(); + } else { + update(state => ({ + ...state, + loading: false, + error: 'Failed to load user profile' + })); + } + } + } + + // LOGIN FUNCTION - SETS ACCESS AND REFRESH TOKENS + async function login(credentials: LoginCredentials) { + update(state => ({...state, loading: true, error: null})); + try { + const response = await apiClient.post( + `/token/`, + credentials + ); + const {access, refresh} = response.data; + if (browser) { + localStorage.setItem('access_token', access); + localStorage.setItem('refresh_token', refresh); + } + update(state => ({ + ...state, + accessToken: access, + refreshToken: refresh, + isAuthenticated: true, + })); + await loadUserProfile(); + return true; + } catch (error) { + console.error('Login failed:', error); + let errorMessage = 'Authentication failed'; + if (axios.isAxiosError(error)) { + if (error.response?.status === 401) { + errorMessage = 'Invalid username or password'; + } else if (error.response?.data?.detail) { + errorMessage = error.response.data.detail; + } + } + update(state => ({ + ...state, + loading: false, + error: errorMessage, + isAuthenticated: false, + user: null, + profile: null, + accessToken: null, + refreshToken: null + })); + return false; + } + } + + // LOGOUT FUNCTION - REMOVES TOKENS AND CLEARS STORES + function logout() { + if (browser) { + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + } + set({ + isAuthenticated: false, + user: null, + profile: null, + accessToken: null, + refreshToken: null, + loading: false, + error: null + }); + if (browser) { + window.location.href = '/login'; + } + } + + // TOKEN REFRESH FUNCTION + async function refreshToken(): Promise { + const currentState = getAuthState(); + if (!currentState.refreshToken) { + logout(); + return null; + } + try { + const response = await apiClient.post<{ access: string }>( + `/token/refresh/`, + {refresh: currentState.refreshToken} + ); + const newAccessToken = response.data.access; + if (browser) { + localStorage.setItem('access_token', newAccessToken); + } + update(state => ({ + ...state, + accessToken: newAccessToken + })); + return newAccessToken; + } catch (error) { + console.error('Token refresh failed:', error); + logout(); + return null; + } + } + + // HELPER FUNCTION FOR CURRENT STATE + function getAuthState(): AuthState { + let value: AuthState = initialState; + subscribe(state => { + value = state; + })(); + return value; + } + + return { + subscribe, + login, + logout, + refreshToken, + loadUserProfile + }; +}; + +// CREATE AND EXPORT AUTH STORE +export const auth = createAuthStore(); + +// DERIVED STORES FOR COMPONENTS +export const isAuthenticated = derived( + auth, + $auth => $auth.isAuthenticated +); +export const profile = derived( + auth, + $auth => $auth.profile +); +export const loading = derived( + auth, + $auth => $auth.loading +); +export const error = derived( + auth, + $auth => $auth.error +); + +// EXPORT TYPES FOR COMPONENTS +export type {AuthState, LoginCredentials, TokenResponse}; \ No newline at end of file diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte new file mode 100644 index 0000000..0b04277 --- /dev/null +++ b/frontend/src/lib/components/Navbar.svelte @@ -0,0 +1,218 @@ + + + \ No newline at end of file diff --git a/frontend/src/lib/google.ts b/frontend/src/lib/google.ts new file mode 100644 index 0000000..66c5feb --- /dev/null +++ b/frontend/src/lib/google.ts @@ -0,0 +1,201 @@ +import apiClient, {type Service, type Project} from './api.js'; + +// Calendar Event type definition +export interface CalendarEvent { + summary: string; + description: string; + start: string; // ISO format datetime string + end: string; // ISO format datetime string + location: string; + attendees: { email: string }[]; + reminders?: { method: string; minutes: number }[]; +} + +// Interface for the response we expect from the API +export interface CalendarEventResponse { + id?: string; + htmlLink?: string; + error?: string; + details?: string; +} + +// Type for API errors +interface ApiError { + response?: { + data?: { + error?: string; + } + }; + message: string; +} + +// Service for handling Google Calendar API +export const calendarService = { + /** + * Create a calendar event + * @param event - Calendar event data + * @returns Promise with the created event data + */ + createEvent: async (event: CalendarEvent): Promise => { + try { + const response = await apiClient.post('/calendar/create/', event); + return response.data; + } catch (error: unknown) { + console.error('Error creating calendar event:', error); + const apiError = error as ApiError; + return { + error: 'Failed to create calendar event', + details: apiError.response?.data?.error || apiError.message + }; + } + }, + + /** + * Create a calendar event from a service + * @param service - The service to create an event for + * @param additionalAttendees - Additional email addresses to invite + * @returns Promise with the created event data + */ + createEventFromService: async (service: Service, additionalAttendees: string[] = []): Promise => { + + // Extract account and location information + const account = typeof service.account === 'object' ? service.account : null; + if (!account) { + return { + error: 'Service account information missing', + details: 'Cannot create calendar event without account information' + }; + } + + const location = `${account.street_address}, ${account.city}, ${account.state} ${account.zip_code}`; + + // Extract team members for attendees + const attendees = [ + // Add team members as attendees + ...(service.team_members || []).map(member => ({ + email: typeof member === 'object' ? member.email : '' + })), + // Add additional attendees + ...additionalAttendees.map(email => ({email})) + ].filter(attendee => attendee.email); // Filter out empty emails + + try { + // Create event data + const eventData: CalendarEvent = { + summary: `Service: ${account.name}`, + description: `Service scheduled for ${account.name}.\n\nNotes: ${service.notes || 'None'}`, + start: service.deadline_start, + end: service.deadline_end, + location, + attendees, + reminders: [ + {method: 'email', minutes: 60}, // 2 hours before + {method: 'popup', minutes: 30} // 30 minutes before + ] + }; + + return await calendarService.createEvent(eventData); + } catch (error: unknown) { + console.error('Error creating event from service:', error); + const typedError = error as Error; + return { + error: 'Failed to create calendar event from service', + details: typedError.message + }; + } + }, + + /** + * Create a calendar event from a project + * @param project - The project to create an event for + * @param additionalAttendees - Additional email addresses to invite + * @param startTime - Start time in HH:MM format (optional) + * @param endTime - End time in HH:MM format (optional) + * @returns Promise with the created event data + */ + createEventFromProject: async ( + project: Project, + additionalAttendees: string[] = [], + startTime: string = '09:00', + endTime: string = '17:00' + ): Promise => { + // Extract customer and account information + const customer = typeof project.customer === 'object' ? project.customer : null; + const account = typeof project.account === 'object' ? project.account : null; + + if (!customer) { + return { + error: 'Project customer information missing', + details: 'Cannot create calendar event without customer information' + }; + } + + // Determine location based on account or customer + const location = account + ? `${account.street_address}, ${account.city}, ${account.state} ${account.zip_code}` + : `${customer.billing_street_address}, ${customer.billing_city}, ${customer.billing_state} ${customer.billing_zip_code}`; + + // Create date strings with time + const projectDate = project.date.split('T')[0]; // Get just the date part + + // Parse the time strings to determine if this is an overnight event + const startHour = parseInt(startTime.split(':')[0], 10); + const startMinute = parseInt(startTime.split(':')[1], 10); + const endHour = parseInt(endTime.split(':')[0], 10); + const endMinute = parseInt(endTime.split(':')[1], 10); + + // Check if end time is earlier than start time (overnight event) + const isOvernightEvent = (endHour < startHour) || + (endHour === startHour && endMinute < startMinute); + + // Create start datetime + const startDateTime = `${projectDate}T${startTime}:00`; + + // For end datetime, add a day if it's an overnight event + let endDateTime; + if (isOvernightEvent) { + // Create a Date object from the project date + const endDate = new Date(projectDate); + // Add one day + endDate.setDate(endDate.getDate() + 1); + // Format the date as YYYY-MM-DD + const nextDay = endDate.toISOString().split('T')[0]; + // Create the end datetime with the next day + endDateTime = `${nextDay}T${endTime}:00`; + } else { + // Same day event + endDateTime = `${projectDate}T${endTime}:00`; + } + + // Rest of your function remains the same... + const attendees = [ + ...(project.team_members || []).map(member => ({ + email: typeof member === 'object' ? member.email : '' + })), + ...additionalAttendees.map(email => ({email})) + ].filter(attendee => attendee.email); + + try { + const eventData = { + summary: `Project: ${customer.name}${account ? ` - ${account.name}` : ''}`, + description: project.notes || `Project for ${customer.name}`, + start: startDateTime, + end: endDateTime, + location, + attendees + }; + + return await calendarService.createEvent(eventData); + } catch (error) { + console.error('Error creating calendar event from project:', error); + return { + error: 'Failed to create calendar event', + details: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + + +}; + +export default calendarService; \ No newline at end of file diff --git a/frontend/src/lib/punchlist.ts b/frontend/src/lib/punchlist.ts new file mode 100644 index 0000000..7b52b0a --- /dev/null +++ b/frontend/src/lib/punchlist.ts @@ -0,0 +1,141 @@ +import {apiService, type Account, type Project, type PaginatedResponse} from "./api"; + +export interface Punchlist { + id: string; + project: string | Project; + account: string | Account; + date: string; + second_visit: boolean; + second_date: string | null; + // Front area + front_ceiling: boolean; + front_vents: boolean; + front_fixtures: boolean; + front_counter: boolean; + // Main work area + main_equipment: string; + main_equipment_disassemble: boolean; + main_equipment_reassemble: boolean; + main_equipment_test: boolean; + main_equipment_exterior: boolean; + main_walls: boolean; + main_fixtures: boolean; + main_ceiling: boolean; + main_vents: boolean; + main_floors: boolean; + // Equipment stations + equip_station_1: boolean; + equip_station_2: boolean; + equip_station_3: boolean; + equip_station_4: boolean; + equip_station_5: boolean; + equip_station_6: boolean; + equip_station_7: boolean; + equip_sinks: boolean; + equip_dispensers: boolean; + equip_other: boolean; + // Back area + back_ceiling: boolean; + back_vents: boolean; + // End of visit + end_trash: boolean; + end_clean: boolean; + end_secure: boolean; + // Notes and metadata + notes: string; + created_at: string; + updated_at: string; + sheet_url?: string; + pdf_url?: string; + exported_at?: string; +} + +export interface PunchlistCreateRequest { + project_id: string; + account_id: string; + date: string; + second_visit: boolean; + second_date: string | null; + // Front area + front_ceiling: boolean; + front_vents: boolean; + front_fixtures: boolean; + front_counter: boolean; + // Main work area + main_equipment: string; + main_equipment_disassemble: boolean; + main_equipment_reassemble: boolean; + main_equipment_test: boolean; + main_equipment_exterior: boolean; + main_walls: boolean; + main_fixtures: boolean; + main_ceiling: boolean; + main_vents: boolean; + main_floors: boolean; + // Equipment stations + equip_station_1: boolean; + equip_station_2: boolean; + equip_station_3: boolean; + equip_station_4: boolean; + equip_station_5: boolean; + equip_station_6: boolean; + equip_station_7: boolean; + equip_sinks: boolean; + equip_dispensers: boolean; + equip_other: boolean; + // Back area + back_ceiling: boolean; + back_vents: boolean; + // End of visit + end_trash: boolean; + end_clean: boolean; + end_secure: boolean; + // Notes + notes: string; + + // Index signature for dynamic access + [key: string]: string | boolean | null | undefined; +} + +export interface PunchlistParams { + project_id?: string; + account_id?: string; + + [key: string]: string | number | boolean | undefined; +} + +export interface ExportPunchlistResponse { + success: boolean; + message?: string; + sheetUrl?: string; + pdfUrl?: string; + alreadyExported?: boolean; +} + +export const punchlistService = { + getAll: async (params?: PunchlistParams) => { + const firstPageResponse = await apiService.get>('/punchlists/', params); + let allPunchlists = [...firstPageResponse.results]; + + const pageSize = 10; + const totalPages = Math.ceil(firstPageResponse.count / pageSize); + + for (let page = 2; page <= totalPages; page++) { + const nextPageParams = {...params, page}; + const nextPageResponse = await apiService.get>('/punchlists/', nextPageParams); + allPunchlists = [...allPunchlists, ...nextPageResponse.results]; + } + + return { + count: firstPageResponse.count, + next: null, + previous: null, + results: allPunchlists + }; + }, + getById: (id: string) => apiService.get(`/punchlists/${id}/`), + create: (data: PunchlistCreateRequest) => apiService.post('/punchlists/', data), + update: (id: string, data: Partial) => apiService.put(`/punchlists/${id}/`, data), + patch: (id: string, data: Partial) => apiService.patch(`/punchlists/${id}/`, data), + delete: (id: string) => apiService.delete(`/punchlists/${id}/`) +}; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..b0e99c0 --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,6 @@ + + +{@render children()} \ No newline at end of file diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..aa1556e --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,109 @@ + + + + Nexus Portal | Employee Login + + +
+
+ +
+
+

Nexus Employee Portal

+

Your centralized platform for managing customers, projects, invoices, and reports.

+ +
+
+
+ +
+
+

Customer Management

+

Track customer information and billing details

+
+
+ +
+
+ +
+
+

Project Tracking

+

Manage projects and punchlists efficiently

+
+
+ +
+
+ +
+
+

Invoice Management

+

Create and track invoices and payments

+
+
+
+
+
+ + +
+
+
+ {#if loading} +
+
+ Loading... +
+
+ {:else} +

Employee Access

+ + {#if error} + + {/if} + +

+ Please sign in to access the employee portal and its features. +

+ + + Sign In + + +
+

Forgot your password? Contact your administrator.

+
+ {/if} +
+
+
+
+ +
+

© {new Date().getFullYear()} Nexus Portal. All rights reserved.

+
+
diff --git a/frontend/src/routes/accounts/+page.svelte b/frontend/src/routes/accounts/+page.svelte new file mode 100644 index 0000000..2b29bec --- /dev/null +++ b/frontend/src/routes/accounts/+page.svelte @@ -0,0 +1,243 @@ + + +
+ +
+

Accounts

+ {#if isAdmin} + Add Account + {/if} +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredAccounts.length === 0} + + {:else} +
+ + + + + + + + + + + + + + {#each filteredAccounts as account (account.id)} + {@const isActive = !account.end_date || new Date(account.end_date) > new Date()} + + + + + + + + + + {/each} + +
Account NameCustomerContactEmailPhoneStatusActions
{account.name}{getCustomerName(account)}{account.contact_first_name} {account.contact_last_name} + {account.contact_email} + {account.contact_phone} + + {isActive ? 'Active' : 'Inactive'} + + +
+ + View + + {#if isAdmin && isActive} + + {/if} +
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/accounts/[id]/+page.svelte b/frontend/src/routes/accounts/[id]/+page.svelte new file mode 100644 index 0000000..ed04ce2 --- /dev/null +++ b/frontend/src/routes/accounts/[id]/+page.svelte @@ -0,0 +1,789 @@ + + +
+ +
+
+ + Back to Accounts + +

{account?.name || 'Account Details'}

+ {#if account && customer} +

Customer: {customer.name}

+ {/if} +
+ + {#if isAdmin && account} +
+ + + {#if account && isAccountActive(account)} + + {/if} +
+ {/if} +
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if !account} + + {:else} + + {#if showEditForm} +
+
+
Edit Account
+
+
+
+
+
+ + +
+
+ + +
+
+
Address
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
Contact Information
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ {/if} + + +
+ +
+
+
+
Account Information
+
+
+
+
+

Status

+ + {isAccountActive(account) ? 'Active' : 'Inactive'} + +
+
+

Start Date

+

{formatDate(account.start_date)}

+
+
+ +
Address
+

+ {account.street_address}
+ {account.city}, {account.state} {account.zip_code} +

+ +
Contact Information
+
+
+

Contact Name

+

{account.contact_first_name} {account.contact_last_name}

+
+
+

Contact Phone

+

+ {account.contact_phone} +

+
+
+ +
+
+

Contact Email

+

+ {account.contact_email} +

+
+
+
+
+ + +
+
+
Services
+ + Add Service + +
+
+ {#if services.length === 0} +
+

No services found for this account.

+
+ {:else} +
+ + + + + + + + + + + {#each services as service (service.id)} + + + + + + + {/each} + +
DateStatusTeam MembersActions
{formatDate(service.date)} + + {service.status.replace('_', ' ')} + + + {#if service.team_members && service.team_members.length > 0} + {service.team_members.map(tm => + typeof tm === 'object' && tm ? + `${tm.first_name || ''} ${tm.last_name || ''}` : + '' + ).join(', ')} + {:else} + Not assigned + {/if} + + + + View + +
+
+ {/if} +
+ +
+
+ + +
+ + {#if showRevenueForm} +
+
+
{editingRevenueId ? 'Edit Revenue' : 'Add Revenue'}
+
+
+
+
+ +
+ $ + +
+
+ +
+ + +
+ +
+ + +
Leave blank if this is current revenue
+
+ +
+ + +
+
+
+
+ {/if} + + +
+
+
Revenue
+ {#if isAdmin && !showRevenueForm} + + {/if} +
+
+ {#if revenues.length === 0} +

No revenue records found.

+ {:else} + {#each revenues as revenue (revenue.id)} +
+
+
+
{formatCurrency(revenue.amount)}
+ + From: {formatDate(revenue.start_date)} + {#if revenue.end_date} + to {formatDate(revenue.end_date)} + {:else} + (Current) + {/if} + +
+ {#if isAdmin} +
+ + +
+ {/if} +
+
+ {/each} + {/if} +
+
+ + +
+
+
Schedule
+ {#if isAdmin} + + Manage Schedules + + {/if} +
+
+ {#if schedules.length === 0} +

No schedule records found.

+ {:else} + {#each schedules as schedule (schedule.id)} +
+
+
Service Days
+
    + {#if schedule.monday_service} +
  • Monday
  • + {/if} + {#if schedule.tuesday_service} +
  • Tuesday
  • + {/if} + {#if schedule.wednesday_service} +
  • Wednesday
  • + {/if} + {#if schedule.thursday_service} +
  • Thursday
  • + {/if} + {#if schedule.friday_service} +
  • Friday
  • + {/if} + {#if schedule.saturday_service} +
  • Saturday
  • + {/if} + {#if schedule.sunday_service} +
  • Sunday
  • + {/if} + {#if schedule.weekend_service} +
  • Weekend Service
  • + {/if} +
+ {#if schedule.schedule_exception} +
+
Exceptions:
+

{schedule.schedule_exception}

+
+ {/if} + + From: {formatDate(schedule.start_date)} + {#if schedule.end_date} + to {formatDate(schedule.end_date)} + {:else} + (Current) + {/if} + +
+
+ {/each} + {/if} +
+
+ + + +
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/accounts/new/+page.svelte b/frontend/src/routes/accounts/new/+page.svelte new file mode 100644 index 0000000..25fb620 --- /dev/null +++ b/frontend/src/routes/accounts/new/+page.svelte @@ -0,0 +1,231 @@ + + +
+ +
+

Add New Account

+
+ + +
+
+
Account Information
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
Address
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
Contact Information
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + Cancel + + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/routes/admin/users/+page.svelte b/frontend/src/routes/admin/users/+page.svelte new file mode 100644 index 0000000..681c57c --- /dev/null +++ b/frontend/src/routes/admin/users/+page.svelte @@ -0,0 +1,521 @@ + + +
+
+

User Management

+ {#if isAdmin} + + {/if} +
+ + +
+
+
+ + +
+
+
+ +
+
+ + + {#if showCreateForm} +
+
+ {editingUser ? 'Edit User' : 'Add New User'} +
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + + {#if editingUser} + Username cannot be changed after creation + {/if} +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + {#if !editingUser} +
+ + A user account will be created with a temporary password that the user will need to change on first login. +
+ {/if} + +
+ + +
+
+
+
+ {/if} + + + {#if showResetPasswordForm} + + {/if} + + + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredUsers.length === 0} + + {:else} +
+ + + + + + + + + + + + + {#each filteredUsers as user (user.id)} + + + + + + + + + {/each} + +
NameUsernameEmailRolePrimary PhoneActions
{user.first_name} {user.last_name}{user.user?.username || 'N/A'}{user.email} + + {user.role.replace('_', ' ')} + + {user.primary_phone} +
+ + + +
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/calendar/create/+page.svelte b/frontend/src/routes/calendar/create/+page.svelte new file mode 100644 index 0000000..6c8bb2c --- /dev/null +++ b/frontend/src/routes/calendar/create/+page.svelte @@ -0,0 +1,377 @@ + + +
+
+

Create Calendar Event

+
+ + + + {#if loading} +
+
+ Loading... +
+
+ {:else} +
+
+
Calendar Event Details
+
+
+
+
+ +
+ + + + + +
+
+ + {#if selectedType === 'service'} +
+ + {#if services.length === 0} +
+ No active services found. Services must be in "Scheduled" or "In Progress" status. +
+ {:else} + + {/if} +
+ {/if} + + {#if selectedType === 'project'} +
+ + {#if projects.length === 0} +
+ No active projects found. Projects must be in "Planned" or "In Progress" status. +
+ {:else} + + {/if} +
+ + +
+
+ + +
+
+ + +
+
+ {/if} + +
+ + +
+ Team members associated with the selected item will be added automatically. +
+
+ +
+ +
+
+
+
+ + {#if showResult && result} +
+ {#if result.error} +
+
Error
+

{result.error}

+ {#if result.details} +
+

{result.details}

+ {/if} +
+ {:else} +
+
Success!
+

Calendar event created successfully.

+ {#if result.htmlLink} +
+

+ + View Event in Google Calendar + +

+ {/if} +
+ {/if} +
+ {/if} + {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/customers/+page.svelte b/frontend/src/routes/customers/+page.svelte new file mode 100644 index 0000000..dafc84f --- /dev/null +++ b/frontend/src/routes/customers/+page.svelte @@ -0,0 +1,183 @@ + + + +
+
+

Customers

+ {#if isAdmin} + Add Customer + {/if} +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredCustomers.length === 0} + + {:else} +
+ + + + + + + + + + + + + {#each filteredCustomers as customer (customer.id)} + {@const isActive = !customer.end_date || new Date(customer.end_date) > new Date()} + + + + + + + + + {/each} + +
Company NamePrimary ContactEmailPhoneStatusActions
{customer.name}{customer.primary_contact_first_name} {customer.primary_contact_last_name} + {customer.primary_contact_email} + {customer.primary_contact_phone} + + {isActive ? 'Active' : 'Inactive'} + + +
+ + View + + {#if isAdmin && isActive} + + {/if} +
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/customers/[id]/+page.svelte b/frontend/src/routes/customers/[id]/+page.svelte new file mode 100644 index 0000000..75cbc6d --- /dev/null +++ b/frontend/src/routes/customers/[id]/+page.svelte @@ -0,0 +1,617 @@ + + +
+ +
+
+ + Back to Customers + +

{customer?.name || 'Customer Details'}

+ {#if customer} +

+ + {isCustomerActive(customer) ? 'Active' : 'Inactive'} + + Since {formatDate(customer.start_date)} +

+ {/if} +
+ {#if isAdmin && customer} +
+ + + {#if customer && isCustomerActive(customer)} + + {/if} +
+ {/if} +
+ {#if loading && !customer} +
+
+ Loading... +
+
+ {:else if !customer} + + {:else} + + {#if showEditForm} +
+
+
Edit Customer
+
+
+
+
+
+ + +
+
+ + +
+
+
Primary Contact
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
Secondary Contact
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
Billing Information
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ {/if} + +
+ +
+
+
+
Customer Information
+
+
+
Primary Contact
+
+
+

Contact Name

+

{customer.primary_contact_first_name} {customer.primary_contact_last_name}

+
+
+

Contact Phone

+

+ {customer.primary_contact_phone} +

+
+
+
+
+

Contact Email

+

+ {customer.primary_contact_email} +

+
+
+ {#if customer.secondary_contact_first_name || customer.secondary_contact_last_name} +
Secondary Contact
+
+
+

Contact Name

+

{customer.secondary_contact_first_name} {customer.secondary_contact_last_name}

+
+
+

Contact Phone

+

+ {#if customer.secondary_contact_phone} + {customer.secondary_contact_phone} + {:else} + Not provided + {/if} +

+
+
+
+
+

Contact Email

+

+ {#if customer.secondary_contact_email} + {customer.secondary_contact_email} + {:else} + Not provided + {/if} +

+
+
+ {/if} +
Billing Information
+
+
+

Billing Contact

+

{customer.billing_contact_first_name} {customer.billing_contact_last_name}

+
+
+

Billing Email

+

+ {customer.billing_email} +

+
+
+
+
+

Billing Address

+

+ {customer.billing_street_address}
+ {customer.billing_city}, {customer.billing_state} {customer.billing_zip_code} +

+
+
+
+
+

Billing Terms

+

{customer.billing_terms}

+
+
+
+
+
+ +
+ +
+
+
Quick Actions
+
+
+
+ + + Generate Invoice + + + Create Project + +
+
+
+ +
+
+
Customer Statistics
+
+
+
+ Total Accounts: + {accounts.length} +
+
+ Active Accounts: + + {accounts.filter(acc => !acc.end_date || new Date(acc.end_date) > new Date()).length} + +
+ +
+
+
+
+ +
+
+
Customer Accounts
+ {#if isAdmin} + + {/if} +
+
+ {#if accounts.length === 0} +
+

No accounts found for this customer.

+ {#if isAdmin} + + {/if} +
+ {:else} +
+ + + + + + + + + + + + {#each accounts as account (account.id)} + {@const isActive = !account.end_date || new Date(account.end_date) > new Date()} + + + + + + + + {/each} + +
Account NameLocationContactStatusActions
{account.name}{account.city}, {account.state}{account.contact_first_name} {account.contact_last_name} + + {isActive ? 'Active' : 'Inactive'} + + + + View + +
+
+ {/if} +
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/customers/new/+page.svelte b/frontend/src/routes/customers/new/+page.svelte new file mode 100644 index 0000000..33e29b0 --- /dev/null +++ b/frontend/src/routes/customers/new/+page.svelte @@ -0,0 +1,286 @@ + + +
+ +
+

Add New Customer

+
+ + +
+
+
Customer Information
+
+
+
+
+
+ + +
+
+ + +
+
+
Primary Contact
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
Secondary Contact
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
Billing Information
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + Cancel + + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..6070320 --- /dev/null +++ b/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1,399 @@ + + +
+ +
+

Dashboard

+

Welcome back, {username}!

+
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if error} + + {:else} + +
+
+
+
+
+
+
Active Customers
+

{getActiveCustomersCount()}

+
+
+ +
+
+ View All +
+
+
+ +
+
+
+
+
+
Active Accounts
+

{getActiveAccountsCount()}

+
+
+ +
+
+ View All +
+
+
+ +
+
+
+
+
+
Today's Services
+

{getTodaysServicesCount()}

+
+
+ +
+
+ View All +
+
+
+ +
+
+
+
+
+
This Week's Services
+

{getThisWeeksServicesCount()}

+
+
+ +
+
+ View All +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+
+
+
Upcoming Services
+ View All +
+ +
+
+ + +
+
+
+
Recent Projects
+ View All +
+
+ {#if recentProjects.length === 0} +
+

No recent projects found.

+
+ {:else} + + {/if} +
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/invoices/+page.svelte b/frontend/src/routes/invoices/+page.svelte new file mode 100644 index 0000000..169a1ec --- /dev/null +++ b/frontend/src/routes/invoices/+page.svelte @@ -0,0 +1,340 @@ + + +
+
+

Invoices

+ {#if isAdmin} + Create New Invoice + {/if} +
+ + +
+
+
Filters
+
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+ + + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredInvoices.length === 0} + + {:else} +
+ + + + + + + + + + + + + + {#each filteredInvoices as invoice (invoice.id)} + + + + + + + + + + {/each} + +
Invoice IDCustomerDateAmountStatusDate PaidActions
{invoice.id}{getCustomerName(invoice)}{formatDate(invoice.date)}{formatCurrency(getInvoiceTotal(invoice))} + + {invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)} + + {invoice.date_paid ? formatDate(invoice.date_paid) : 'Not paid'} + +
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/invoices/[id]/+page.svelte b/frontend/src/routes/invoices/[id]/+page.svelte new file mode 100644 index 0000000..5c3d489 --- /dev/null +++ b/frontend/src/routes/invoices/[id]/+page.svelte @@ -0,0 +1,550 @@ +// frontend/src/routes/invoices/[id]/+page.svelte + + + +
+
+
+ + Back to Invoices + +

Invoice Details

+
+ {#if isAdmin && !isEditing && invoice && invoice.status !== 'paid' && invoice.status !== 'cancelled'} +
+ + {#if invoice.status === 'sent'} + + {/if} +
+ {/if} +
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if error} + + {:else if !invoice} + + {:else} +
+ +
+
+
+
Invoice Information
+ + {invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)} + +
+
+ {#if isEditing} + +
+
+
+ + +
+
+ + +
+
+ + {#if invoice.status === 'paid' || isEditing} +
+
+ + +
+
+ + +
+
+ {/if} + +
+ + +
+
+ {:else} + +
+
+
Invoice Date
+

{formatDate(invoice.date)}

+
+
+
Invoice ID
+

{invoice.id}

+
+
+ + {#if invoice.status === 'paid' && invoice.date_paid} +
+
+
Date Paid
+

{formatDate(invoice.date_paid)}

+
+
+
Payment Method
+

+ {invoice.payment_type ? + invoice.payment_type.charAt(0).toUpperCase() + invoice.payment_type.slice(1).replace('_', ' ') : + 'Not specified'} +

+
+
+ {/if} + + + + {#if customer} +
+
+
Billing Contact
+

+ {customer.billing_contact_first_name} {customer.billing_contact_last_name}
+ {customer.billing_email} +

+
+
+
Billing Address
+

+ {customer.billing_street_address}
+ {customer.billing_city}, {customer.billing_state} {customer.billing_zip_code} +

+
+
+ {/if} + {/if} +
+
+ + +
+
+
Invoice Items
+
+
+ + {#if revenues && revenues.length > 0} +
+ + + + + + + + + + {#each revenues as revenue (revenue.id)} + + + + + + {/each} + +
AccountDescriptionAmount
+ {typeof revenue.account === 'object' ? + revenue.account.name : + accounts.find(a => a.id === revenue.account)?.name || 'Unknown Account'} + Monthly Service ({formatDate(revenue.start_date)} - {revenue.end_date ? formatDate(revenue.end_date) : 'Ongoing'}){formatCurrency(revenue.amount)}
+
+ {/if} + + + {#if projects && projects.length > 0} +
+ + + + + + + + + + {#each projects as project (project.id)} + + + + + + {/each} + +
ProjectDateAmount
+ + Project for {typeof project.customer === 'object' ? + project.customer.name : + customer?.name || 'Unknown Customer'} + + {formatDate(project.date)}{formatCurrency(project.amount || 0)}
+
+ {/if} + + {#if (!revenues || revenues.length === 0) && (!projects || projects.length === 0)} +
+

No items attached to this invoice.

+
+ {/if} +
+ +
+
+ + +
+
+
+
Actions
+
+
+
+ + + + + {#if invoice.status === 'draft'} + + {/if} + + {#if invoice.status === 'sent'} + + + {/if} +
+
+
+ + + {#if invoice.status === 'paid' && invoice.date_paid} +
+
+
Payment History
+
+
+
+
+ +
+
+
Payment Received
+

{formatDate(invoice.date_paid)}

+
+
+ {formatCurrency(getInvoiceTotal())} +
+
+
+

Payment Method:

+

+ {invoice.payment_type ? + invoice.payment_type.charAt(0).toUpperCase() + invoice.payment_type.slice(1).replace('_', ' ') : + 'Not specified'} +

+
+
+
+ {/if} + + + {#if customer} +
+
+
Customer Information
+
+
+
{customer.name}
+

+ Primary Contact:
+ {customer.primary_contact_first_name} {customer.primary_contact_last_name}
+ {customer.primary_contact_phone}
+ {customer.primary_contact_email} +

+

+ Billing Contact:
+ {customer.billing_contact_first_name} {customer.billing_contact_last_name}
+ {customer.billing_email} +

+ +
+
+ {/if} +
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/invoices/new/+page.svelte b/frontend/src/routes/invoices/new/+page.svelte new file mode 100644 index 0000000..66c3d5c --- /dev/null +++ b/frontend/src/routes/invoices/new/+page.svelte @@ -0,0 +1,328 @@ + + +
+
+

Create New Invoice

+
+ + + + {#if loading} +
+
+ Loading... +
+
+ {:else} + +
+
+
Invoice Information
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + {#if selectedCustomer} + +
+ + {#if accounts.length === 0} +

No active accounts found for this customer.

+ {:else} +
+ + + + + + + + + + + {#each accounts as account (account.id)} + {#if !account.end_date || new Date(account.end_date) > new Date()} + + + + + + + {/if} + {/each} + +
Account NameLocationMonthly Revenue
+
+ +
+
{account.name}{account.city}, {account.state} + {account.revenues && account.revenues.length > 0 + ? formatCurrency(account.revenues[0].amount) + : 'No revenue data'} +
+
+ {/if} +
+ + +
+ + {#if projects.length === 0} +

No completed projects available for invoicing.

+ {:else} +
+ + + + + + + + + + + {#each projects as project (project.id)} + + + + + + + {/each} + +
Project DateAccountAmount
+
+ +
+
{formatDate(project.date)} + {project.account && typeof project.account === 'object' + ? project.account.name + : project.account + ? accounts.find(a => a.id === project.account)?.name || 'Unknown Account' + : 'No Account'} + {formatCurrency(project.amount || 0)}
+
+ {/if} +
+ {/if} + + +
+ + Cancel + + +
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..b8ecd30 --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,92 @@ + +
+
+
+ +
+

Nexus 2 Login

+

Login to your account

+
+
+ {#if $error} + + {/if} + {#if $loading} +
+
+ Loading... +
+

Authenticating...

+
+ {:else} +
+
+ + + {#if $error && !username.trim()} +
Please enter your username
+ {/if} +
+
+ + + {#if $error && !password.trim()} +
Please enter your password
+ {/if} +
+ +
+ {/if} +
+
+
+
\ No newline at end of file diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte new file mode 100644 index 0000000..9f39175 --- /dev/null +++ b/frontend/src/routes/profile/+page.svelte @@ -0,0 +1,433 @@ + + +
+
+

Your Profile

+
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if !profileData} + + {:else} + +
+
+
+
+
Profile Information
+
+ +
+
+
+ {#if showEditForm} + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ {:else} + +
+
+

Name

+

{profileData.first_name} {profileData.last_name}

+
+
+

Email

+

+ {profileData.email} +

+
+
+
+
+

Primary Phone

+

+ {profileData.primary_phone} +

+
+
+

Secondary Phone

+ {#if profileData.secondary_phone} +

+ {profileData.secondary_phone} +

+ {:else} +

Not provided

+ {/if} +
+
+
+
+

Role

+

+ {profileData.role.replace('_', ' ')} +

+
+
+ {/if} +
+
+ + +
+
+
Security
+
+ +
+
+
+ {#if showPasswordForm} + +
+ {#if passwordError} + + {/if} + {#if passwordSuccess} + + {/if} +
+ + +
+
+ + +
+ Password must be at least 8 characters long. +
+
+
+ + +
+ +
+ + +
+
+ {:else} +

To change your password, click the "Change Password" button.

+ {/if} +
+
+
+ + +
+ +
+
+
Account Status
+
+
+
+ Status: + Active +
+
+ Last Login: + Today +
+
+
+ + +
+
+
Quick Actions
+
+
+ +
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/profile/schedule/+page.svelte b/frontend/src/routes/profile/schedule/+page.svelte new file mode 100644 index 0000000..b532c49 --- /dev/null +++ b/frontend/src/routes/profile/schedule/+page.svelte @@ -0,0 +1,357 @@ + + +
+
+
+

My Services

+

+ Services assigned to {$profile?.first_name} {$profile?.last_name} +

+
+
+ + +
+
+
Filters
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + + {#if error} + + {/if} + + + {#if loading} +
+
+ Loading... +
+
+ {:else if services.length === 0} + + {:else} + +
+

Upcoming Services

+ +
+ + + + + + + + + + + + {#each services.filter(s => { + const serviceDate = new Date(s.date); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return serviceDate >= today && (statusFilter === 'all' || s.status === statusFilter); + }) as service (service.id)} + + + + + + + + {:else} + + + + {/each} + +
AccountDateTime WindowStatusActions
{getAccountName(service)}{formatDate(service.date)}{formatTime(service.deadline_start)} - {formatTime(service.deadline_end)} + + {service.status.replace('_', ' ')} + + + + View Details + +
No upcoming services
+
+
+ + +
+

Past Services

+ +
+ + + + + + + + + + + + {#each services.filter(s => { + const serviceDate = new Date(s.date); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return serviceDate < today && (statusFilter === 'all' || s.status === statusFilter); + }).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) as service (service.id)} + + + + + + + + {:else} + + + + {/each} + +
AccountDateTime WindowStatusActions
{getAccountName(service)}{formatDate(service.date)}{formatTime(service.deadline_start)} - {formatTime(service.deadline_end)} + + {service.status.replace('_', ' ')} + + + + View Details + +
No past services
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/projects/+page.svelte b/frontend/src/routes/projects/+page.svelte new file mode 100644 index 0000000..098a6fe --- /dev/null +++ b/frontend/src/routes/projects/+page.svelte @@ -0,0 +1,363 @@ + + +
+
+

Projects

+ {#if isAdmin || isTeamLeader} + Create New Project + {/if} +
+ + +
+
+
Filters
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+ + + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredProjects.length === 0} + + {:else} +
+ + + + + + + + + + + + + + + {#each filteredProjects as project (project.id)} + + + + + + + + + + + {/each} + +
CustomerAccountDateStatusLabor CostBilling AmountTeam MembersActions
{getCustomerName(project)}{getAccountName(project)}{formatDate(project.date)} + + {project.status.replace('_', ' ')} + + {formatCurrency(project.labor)}{formatCurrency(project.amount)} + {#if project.team_members && project.team_members.length > 0} + {project.team_members.map(tm => `${tm.first_name} ${tm.last_name}`).join(', ')} + {:else} + Not assigned + {/if} + + +
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/projects/[id]/+page.svelte b/frontend/src/routes/projects/[id]/+page.svelte new file mode 100644 index 0000000..c30ab99 --- /dev/null +++ b/frontend/src/routes/projects/[id]/+page.svelte @@ -0,0 +1,374 @@ + + +
+ +
+
+ + Back to Projects + +

Project Details

+ {#if project && customer} +

+ Customer: {customer.name} + {#if account} + | Account: {account.name} + {/if} +

+ {/if} +
+ + {#if (isAdmin || isTeamLeader) && project && project.status !== 'completed' && project.status !== 'cancelled'} + + {/if} +
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if !project} + + {:else} + +
+ +
+
+
+
Project Information
+ + {project.status.replace('_', ' ')} + +
+
+
+
+

Project Date

+

{formatDate(project.date)}

+
+
+

Financial Information

+

+ Labor Cost: {formatCurrency(project.labor)} + Billing Amount: {formatCurrency(project.amount)} +

+
+
+ + {#if account} +
+
+

Account Address

+

+ {account.street_address}
+ {account.city}, {account.state} {account.zip_code} +

+
+
+ {/if} + +
+
+

Team Members

+ {#if project.team_members && project.team_members.length > 0} +
    + {#each project.team_members as member (typeof member === 'object' ? member.id : member)} +
  • +
    +
    + {typeof member === 'object' ? `${member.first_name} ${member.last_name}` : 'Unknown Member'} +
    {typeof member === 'object' ? member.role.replace('_', ' ') : ''}
    +
    + {#if typeof member === 'object'} + + {/if} +
    +
  • + {/each} +
+ {:else} +

No team members assigned

+ {/if} +
+
+ + {#if project.notes} +
+
+

Notes

+
+

{project.notes}

+
+
+
+ {/if} +
+
+
+ + +
+ {#if isAdmin || isTeamLeader} + +
+
+
Status Management
+
+
+ {#if project.status === 'planned'} +

This project is currently planned.

+
+ + + +
+ {:else if project.status === 'in_progress'} +

This project is currently in progress.

+
+ + +
+ {:else if project.status === 'completed'} +

+ + This project has been completed. +

+
+ +
+ {:else if project.status === 'cancelled'} +

+ + This project has been cancelled. +

+
+ +
+ {/if} +
+
+ {/if} + + + {#if customer} +
+
+
Customer Contact
+
+
+

{customer.primary_contact_first_name} {customer.primary_contact_last_name} +

+

+ {customer.primary_contact_phone}
+ {customer.primary_contact_email} +

+ +
+
+ {/if} + + +
+
+
Quick Actions
+
+
+
+ {#if customer} + + Create New Project + + + View Customer + + {/if} + {#if account} + + View Account + + {/if} + + View All Projects + +
+
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/projects/[id]/edit/+page.svelte b/frontend/src/routes/projects/[id]/edit/+page.svelte new file mode 100644 index 0000000..33b6a5d --- /dev/null +++ b/frontend/src/routes/projects/[id]/edit/+page.svelte @@ -0,0 +1,340 @@ + + +
+
+
+ + Back to Project Details + +

Edit Project

+ {#if project && customer} +

+ Customer: {customer.name} +

+ {/if} +
+
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if !project} + + {:else if !isAdmin && !isTeamLeader} + + {:else} + {#if apiError} + + {/if} + +
+
+
Edit Project
+
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Select team members to assign to this project.

+ + {#if allProfiles.length === 0} +

No team members available.

+ {:else} +
+ {#each allProfiles as profileObj (profileObj.id)} +
+
+ + +
+
+ {/each} +
+ {/if} +
+ + +
+ + +
+ + +
+ + Cancel + + +
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/projects/new/+page.svelte b/frontend/src/routes/projects/new/+page.svelte new file mode 100644 index 0000000..8cca4d9 --- /dev/null +++ b/frontend/src/routes/projects/new/+page.svelte @@ -0,0 +1,337 @@ + + +
+
+

Create New Project

+
+ + + + {#if loading} +
+
+ Loading... +
+
+ {:else} + +
+
+
Project Information
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {#if allProfiles.length === 0} +

No team members available.

+ {:else} +
+ {#each allProfiles as profileObj (profileObj.id)} +
+
+ + +
+
+ {/each} +
+ {/if} +
+ + +
+ + +
+ + +
+ + Cancel + + +
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/schedules/+page.svelte b/frontend/src/routes/schedules/+page.svelte new file mode 100644 index 0000000..2164019 --- /dev/null +++ b/frontend/src/routes/schedules/+page.svelte @@ -0,0 +1,480 @@ + + +
+
+

Schedules

+
+ {#if isAdmin || isTeamLeader} + + Generate Services + + + {/if} +
+
+ + + {#if showCreateForm} +
+
+
{scheduleForm.id ? 'Edit Schedule' : 'Add New Schedule'}
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+ {#each weekdayOptions as day (day.value)} +
+
+ + +
+
+ {/each} +
+
+
+
+ + +
+
+ + +
+
+
+
+ {/if} + + +
+
+
Filters
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredSchedules.length === 0} + + {:else} +
+ + + + + + + + + + + + + + {#each filteredSchedules as schedule (schedule.id)} + {@const isActive = !schedule.end_date || new Date(schedule.end_date) > new Date()} + + + + + + + + + + {/each} + +
AccountService DaysStart DateEnd DateExceptionsStatusActions
{getAccountName(schedule)}{getScheduleDays(schedule)}{formatDate(schedule.start_date)}{schedule.end_date ? formatDate(schedule.end_date) : 'Active'}{schedule.schedule_exception || 'None'} + + {isActive ? 'Active' : 'Inactive'} + + +
+ {#if isAdmin || isTeamLeader} + + + {#if isActive} + + {/if} + {/if} +
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/services/+page.svelte b/frontend/src/routes/services/+page.svelte new file mode 100644 index 0000000..bc39a15 --- /dev/null +++ b/frontend/src/routes/services/+page.svelte @@ -0,0 +1,318 @@ + + +
+
+

Services

+ {#if isAdmin || isTeamLeader} + Schedule New Service + {/if} +
+ + +
+
+
Filters
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + + {#if loading} +
+
+ Loading... +
+
+ {:else if filteredServices.length === 0} + + {:else} +
+ + + + + + + + + + + + + {#each filteredServices as service (service.id)} + + + + + + + + + {/each} + +
AccountDateTime WindowStatusTeam MembersActions
{getAccountName(service)}{formatDate(service.date)} + {formatTime(service.deadline_start)} - {formatTime(service.deadline_end)} + + + {service.status.replace('_', ' ')} + + + {#if service.team_members && service.team_members.length > 0} + {service.team_members.map(tm => typeof tm === 'string' ? tm : `${tm.first_name} ${tm.last_name}`).join(', ')} + {:else} + Not assigned + {/if} + + +
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/services/[id]/+page.svelte b/frontend/src/routes/services/[id]/+page.svelte new file mode 100644 index 0000000..ceeb419 --- /dev/null +++ b/frontend/src/routes/services/[id]/+page.svelte @@ -0,0 +1,358 @@ + + +
+ +
+
+ + Back to Services + +

Service Details

+ {#if service && account} +

+ Account: {account.name} +

+ {/if} +
+ + {#if (isAdmin || isTeamLeader) && service && service.status !== 'completed' && service.status !== 'cancelled'} + + {/if} +
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if !service} + + {:else} + +
+ +
+
+
+
Service Information
+ + {service.status.replace('_', ' ')} + +
+
+
+
+

Service Date

+

{formatDate(service.date)}

+
+
+

Time Window

+

{formatTime(service.deadline_start)} + - {formatTime(service.deadline_end)}

+
+
+ +
+
+

Account Address

+ {#if account} +

+ {account.street_address}
+ {account.city}, {account.state} {account.zip_code} +

+ {:else} +

Account details not available

+ {/if} +
+
+ +
+
+

Team Members

+ {#if service.team_members && service.team_members.length > 0} +
    + {#each service.team_members as member (typeof member === 'object' ? member.id : member)} +
  • +
    +
    + {typeof member === 'object' ? `${member.first_name} ${member.last_name}` : 'Unknown Member'} +
    {typeof member === 'object' ? member.role.replace('_', ' ') : ''}
    +
    + {#if typeof member === 'object'} + + {/if} +
    +
  • + {/each} +
+ {:else} +

No team members assigned

+ {/if} +
+
+ + {#if service.notes} +
+
+

Notes

+
+

{service.notes}

+
+
+
+ {/if} +
+
+
+ + +
+ {#if isAdmin || isTeamLeader} + +
+
+
Status Management
+
+
+ {#if service.status === 'scheduled'} +

This service is currently scheduled.

+
+ + + +
+ {:else if service.status === 'in_progress'} +

This service is currently in progress.

+
+ + +
+ {:else if service.status === 'completed'} +

+ + This service has been completed. +

+
+ +
+ {:else if service.status === 'cancelled'} +

+ + This service has been cancelled. +

+
+ +
+ {/if} +
+
+ {/if} + + + {#if account} +
+
+
Account Contact
+
+
+

{account.contact_first_name} {account.contact_last_name} +

+

+ {account.contact_phone}
+ {account.contact_email} +

+ +
+
+ {/if} + + +
+
+
Quick Actions
+
+ +
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/routes/services/[id]/edit/+page.svelte b/frontend/src/routes/services/[id]/edit/+page.svelte new file mode 100644 index 0000000..ddad7f8 --- /dev/null +++ b/frontend/src/routes/services/[id]/edit/+page.svelte @@ -0,0 +1,339 @@ + + +
+
+
+ + Back to Service Details + +

Edit Service

+ {#if service && account} +

+ Account: {account.name} +

+ {/if} +
+
+ + {#if loading} +
+
+ Loading... +
+
+ {:else if !service} + + {:else if !isAdmin && !isTeamLeader} + + {:else} + {#if apiError} + + {/if} + +
+
+
Edit Service
+
+
+
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Select team members to assign to this service.

+ + {#if allProfiles.length === 0} +

No team members available.

+ {:else} +
+ {#each allProfiles as profileObj (profileObj.id)} +
+
+ + +
+
+ {/each} +
+ {/if} +
+ + +
+ + +
+ + +
+ + Cancel + + +
+
+
+
+ {/if} +
diff --git a/frontend/src/routes/services/generate/+page.svelte b/frontend/src/routes/services/generate/+page.svelte new file mode 100644 index 0000000..e72899a --- /dev/null +++ b/frontend/src/routes/services/generate/+page.svelte @@ -0,0 +1,410 @@ + + +
+ +
+

Generate Services

+ + Back to Services + +
+ {#if successMessage} + + {/if} + {#if errorMessage} + + {/if} +
+
+ +
+
+
Service Generation
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + This tool will generate service records for the selected month based on active schedules. + Services will be created according to each schedule's service days, excluding common holidays. + Team members will need to be assigned to the services separately. +
+
+
+ +
+
+
+
Select Accounts for Generation
+
+ + +
+
+
+
+

+ Choose which accounts to generate services for. If none are selected, services will be generated + for all accounts. +

+
+ {#each accounts.filter(account => schedules.some(s => + (typeof s.account === 'object' ? s.account.id : s.account) === account.id)) as account (account.id)} +
+
+ toggleAccountSelection(account.id)} + > + +
+
+ {/each} +
+
+
+ +
+
+
+ Active Schedules + {#if accountFilter} + for {getAccountName(accountFilter)} + {/if} +
+
+
+ {#if loading} +
+
+ Loading... +
+
+ {:else if schedules.length === 0} +
+

No active schedules found.

+
+ {:else} +
+ + + + + + + + + + + {#each schedules as schedule (schedule.id)} + + + + + + + {/each} + +
AccountService DaysStart DateExceptions
{getAccountName(schedule.account)}{getScheduleDays(schedule)}{formatDate(schedule.start_date)}{schedule.schedule_exception || 'None'}
+
+ {/if} +
+
+
+
+ + + +
+
+
About Service Generation
+
+
+

This tool automatically creates service records for the selected month based on active schedule + patterns.

+
    +
  • Services are only created for active schedules
  • +
  • Common holidays are automatically excluded
  • +
  • Weekend settings are respected
  • +
  • Duplicate services are prevented
  • +
+
+ + After generation, you'll need to assign team members to each service. +
+
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/routes/services/new/+page.svelte b/frontend/src/routes/services/new/+page.svelte new file mode 100644 index 0000000..e0bab89 --- /dev/null +++ b/frontend/src/routes/services/new/+page.svelte @@ -0,0 +1,270 @@ + + +
+
+

Schedule New Service

+
+ + + + {#if loading} +
+
+ Loading... +
+
+ {:else} + +
+
+
Service Information
+
+
+
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {#if allProfiles.length === 0} +

No team members available.

+ {:else} +
+ {#each allProfiles as profileObj (profileObj.id)} +
+
+ + +
+
+ {/each} +
+ {/if} +
+ + +
+ + +
+ + +
+ + Cancel + + +
+
+
+
+ {/if} +
\ No newline at end of file diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/frontend/static/favicon.png differ diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..415a6c3 --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,9 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +const config = { + preprocess: vitePreprocess(), + kit: { adapter: adapter() } +}; + +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});