diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..4f6f59e
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,84 @@
+/**
+ * This is intended to be a basic starting point for linting in your app.
+ * It relies on recommended configs out of the box for simplicity, but you can
+ * and should modify this configuration to best suit your team's needs.
+ */
+
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ root: true,
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ env: {
+ browser: true,
+ commonjs: true,
+ es6: true,
+ },
+ ignorePatterns: ["!**/.server", "!**/.client"],
+
+ // Base config
+ extends: ["eslint:recommended"],
+
+ overrides: [
+ // React
+ {
+ files: ["**/*.{js,jsx,ts,tsx}"],
+ plugins: ["react", "jsx-a11y"],
+ extends: [
+ "plugin:react/recommended",
+ "plugin:react/jsx-runtime",
+ "plugin:react-hooks/recommended",
+ "plugin:jsx-a11y/recommended",
+ ],
+ settings: {
+ react: {
+ version: "detect",
+ },
+ formComponents: ["Form"],
+ linkComponents: [
+ { name: "Link", linkAttribute: "to" },
+ { name: "NavLink", linkAttribute: "to" },
+ ],
+ "import/resolver": {
+ typescript: {},
+ },
+ },
+ },
+
+ // Typescript
+ {
+ files: ["**/*.{ts,tsx}"],
+ plugins: ["@typescript-eslint", "import"],
+ parser: "@typescript-eslint/parser",
+ settings: {
+ "import/internal-regex": "^~/",
+ "import/resolver": {
+ node: {
+ extensions: [".ts", ".tsx"],
+ },
+ typescript: {
+ alwaysTryTypes: true,
+ },
+ },
+ },
+ extends: [
+ "plugin:@typescript-eslint/recommended",
+ "plugin:import/recommended",
+ "plugin:import/typescript",
+ ],
+ },
+
+ // Node
+ {
+ files: [".eslintrc.cjs"],
+ env: {
+ node: true,
+ },
+ },
+ ],
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c76d0e0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+
+/.cache
+/build
+.env
+
+/generated/prisma
diff --git a/.kiro/specs/car-maintenance-system/design.md b/.kiro/specs/car-maintenance-system/design.md
new file mode 100644
index 0000000..778d4ee
--- /dev/null
+++ b/.kiro/specs/car-maintenance-system/design.md
@@ -0,0 +1,399 @@
+# Design Document
+
+## Overview
+
+The car maintenance management system is designed as a modern, responsive web application built with Remix, featuring Arabic language support with RTL layout, role-based authentication, and comprehensive business management capabilities. The system follows a modular architecture with clear separation of concerns, utilizing Prisma for database operations and Tailwind CSS for styling.
+
+## Architecture
+
+### High-Level Architecture
+
+```mermaid
+graph TB
+ A[Client Browser] --> B[Remix Frontend]
+ B --> C[Remix Backend/API Routes]
+ C --> D[Prisma ORM]
+ D --> E[SQLite Database]
+
+ F[Authentication Middleware] --> C
+ G[Session Management] --> C
+ H[Route Protection] --> B
+
+ subgraph "Frontend Components"
+ I[Dashboard Layout]
+ J[RTL Components]
+ K[Responsive Navigation]
+ L[Form Components]
+ end
+
+ B --> I
+ B --> J
+ B --> K
+ B --> L
+```
+
+### Technology Stack
+
+- **Frontend Framework**: Remix with React 18
+- **Backend**: Remix server-side rendering and API routes
+- **Database**: SQLite with Prisma ORM
+- **Styling**: Tailwind CSS with RTL support
+- **Authentication**: Custom session-based authentication
+- **TypeScript**: Full type safety throughout the application
+
+## Components and Interfaces
+
+### 1. Authentication System
+
+#### Session Management
+- Custom session-based authentication using Remix sessions
+- Secure cookie storage with httpOnly and secure flags
+- Session expiration and renewal mechanisms
+- CSRF protection for form submissions
+
+#### User Authentication Flow
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant A as Auth Route
+ participant S as Session Store
+ participant D as Database
+
+ U->>A: Submit login credentials
+ A->>D: Validate user credentials
+ D-->>A: Return user data
+ A->>S: Create session
+ S-->>A: Return session cookie
+ A-->>U: Redirect with session cookie
+```
+
+#### Route Protection Middleware
+- Higher-order component for protecting routes based on auth levels
+- Automatic redirection to login for unauthenticated users
+- Role-based access control for different user types
+
+### 2. Database Schema Design
+
+#### Core Entities and Relationships
+
+```mermaid
+erDiagram
+ User {
+ int id PK
+ string name
+ string username UK
+ string email UK
+ string password
+ enum status
+ int authLevel
+ datetime createdDate
+ datetime editDate
+ }
+
+ Customer {
+ int id PK
+ string name
+ string phone
+ string email
+ string address
+ datetime createdDate
+ datetime updateDate
+ }
+
+ Vehicle {
+ int id PK
+ string plateNumber UK
+ string bodyType
+ string manufacturer
+ string model
+ string trim
+ int year
+ string transmission
+ string fuel
+ int cylinders
+ decimal engineDisplacement
+ enum useType
+ int ownerId FK
+ datetime lastVisitDate
+ datetime suggestedNextVisitDate
+ datetime createdDate
+ datetime updateDate
+ }
+
+ MaintenanceVisit {
+ int id PK
+ int vehicleId FK
+ int customerId FK
+ string maintenanceType
+ string description
+ decimal cost
+ string paymentStatus
+ int kilometers
+ datetime visitDate
+ datetime createdDate
+ datetime updateDate
+ }
+
+ Expense {
+ int id PK
+ string description
+ string category
+ decimal amount
+ datetime expenseDate
+ datetime createdDate
+ datetime updateDate
+ }
+
+ Income {
+ int id PK
+ int maintenanceVisitId FK
+ decimal amount
+ datetime incomeDate
+ datetime createdDate
+ datetime updateDate
+ }
+
+ Customer ||--o{ Vehicle : owns
+ Vehicle ||--o{ MaintenanceVisit : has
+ Customer ||--o{ MaintenanceVisit : books
+ MaintenanceVisit ||--|| Income : generates
+```
+
+### 3. UI/UX Design System
+
+#### RTL Layout Implementation
+- CSS logical properties for directional layouts
+- Arabic font integration (Noto Sans Arabic, Cairo)
+- Tailwind CSS RTL plugin configuration
+- Component-level RTL support with `dir="rtl"` attribute
+
+#### Responsive Design Breakpoints
+```css
+/* Mobile First Approach */
+sm: 640px /* Small devices */
+md: 768px /* Medium devices */
+lg: 1024px /* Large devices */
+xl: 1280px /* Extra large devices */
+2xl: 1536px /* 2X Extra large devices */
+```
+
+#### Color Scheme and Theming
+- Primary colors: Blue gradient (#3B82F6 to #1E40AF)
+- Secondary colors: Gray scale for neutral elements
+- Success: Green (#10B981)
+- Warning: Amber (#F59E0B)
+- Error: Red (#EF4444)
+- Dark mode support for future enhancement
+- Design : use constant design for all pages, forms and buttons.
+
+### 4. Navigation and Layout System
+
+#### Dashboard Layout Structure
+```mermaid
+graph LR
+ A[App Shell] --> B[Sidebar Navigation]
+ A --> C[Main Content Area]
+ A --> D[Header Bar]
+
+ B --> E[Logo Area]
+ B --> F[Navigation Menu]
+ B --> G[User Profile]
+
+ C --> H[Page Content]
+ C --> I[Breadcrumbs]
+
+ D --> J[Mobile Menu Toggle]
+ D --> K[User Actions]
+```
+
+#### Sidebar Navigation Features
+- Collapsible sidebar with full/icon-only modes
+- Mobile-responsive hamburger menu
+- Active state indicators
+- Smooth animations and transitions
+- Persistent state across page navigation
+
+### 5. Form and Data Management
+
+#### Form Validation Strategy
+- Client-side validation using HTML5 and custom validators
+- Server-side validation with Zod schemas
+- Real-time validation feedback
+- Internationalized error messages in Arabic
+
+#### Data Table Components
+- Sortable columns with Arabic text support
+- Pagination with RTL navigation
+- Search and filtering capabilities
+- Responsive table design with horizontal scrolling
+- Bulk actions for data management
+
+## Data Models
+
+### User Model
+```typescript
+interface User {
+ id: number;
+ name: string;
+ username: string;
+ email: string;
+ password: string; // hashed
+ status: 'active' | 'inactive';
+ authLevel: 1 | 2 | 3; // 1=superadmin, 2=admin, 3=user
+ createdDate: Date;
+ editDate: Date;
+}
+```
+
+### Vehicle Model
+```typescript
+interface Vehicle {
+ id: number;
+ plateNumber: string;
+ bodyType: string;
+ manufacturer: string;
+ model: string;
+ trim?: string;
+ year: number;
+ transmission: 'Automatic' | 'Manual';
+ fuel: 'Gasoline' | 'Diesel' | 'Hybrid' | 'Mild Hybrid' | 'Electric';
+ cylinders?: number;
+ engineDisplacement?: number;
+ useType: 'personal' | 'taxi' | 'apps' | 'loading' | 'travel';
+ ownerId: number;
+ lastVisitDate?: Date;
+ suggestedNextVisitDate?: Date;
+ createdDate: Date;
+ updateDate: Date;
+}
+```
+
+### Maintenance Visit Model
+```typescript
+interface MaintenanceVisit {
+ id: number;
+ vehicleId: number;
+ customerId: number;
+ maintenanceType: string;
+ description: string;
+ cost: number;
+ paymentStatus: string;
+ kilometers: number;
+ visitDate: Date;
+ nextVisitDelay: 1 | 2 | 3 | 4; // months
+ createdDate: Date;
+ updateDate: Date;
+}
+```
+
+## Error Handling
+
+### Client-Side Error Handling
+- Global error boundary for React components
+- Form validation error display
+- Network error handling with retry mechanisms
+- User-friendly error messages in Arabic
+
+### Server-Side Error Handling
+- Centralized error handling middleware
+- Database constraint violation handling
+- Authentication and authorization error responses
+- Logging and monitoring for production debugging
+
+### Error Response Format
+```typescript
+interface ErrorResponse {
+ success: false;
+ error: {
+ code: string;
+ message: string;
+ details?: any;
+ };
+}
+```
+
+## Testing Strategy
+
+### Unit Testing
+- Component testing with React Testing Library
+- Business logic testing for utility functions
+- Database model testing with Prisma
+- Authentication flow testing
+
+### Integration Testing
+- API route testing with Remix testing utilities
+- Database integration testing
+- Authentication middleware testing
+- Form submission and validation testing
+
+### End-to-End Testing
+- User journey testing for critical paths
+- Cross-browser compatibility testing
+- Mobile responsiveness testing
+- RTL layout verification
+
+### Testing Tools
+- Jest for unit testing
+- React Testing Library for component testing
+- Playwright for E2E testing
+- MSW (Mock Service Worker) for API mocking
+
+## Security Considerations
+
+### Authentication Security
+- Password hashing using bcrypt
+- Secure session management
+- CSRF protection
+- Rate limiting for authentication attempts
+
+### Data Protection
+- Input sanitization and validation
+- SQL injection prevention through Prisma
+- XSS protection through proper escaping
+- Secure headers configuration
+
+### Access Control
+- Role-based access control (RBAC)
+- Route-level protection
+- API endpoint authorization
+- Data filtering based on user permissions
+
+## Performance Optimization
+
+### Frontend Performance
+- Code splitting with Remix route-based splitting
+- Image optimization and lazy loading
+- CSS optimization with Tailwind purging
+- Bundle size monitoring and optimization
+
+### Backend Performance
+- Database query optimization with Prisma
+- Caching strategies for frequently accessed data
+- Connection pooling for database connections
+- Response compression and minification
+
+### Mobile Performance
+- Touch-friendly interface design
+- Optimized images for different screen densities
+- Reduced JavaScript bundle size for mobile
+- Progressive Web App (PWA) capabilities
+
+## Deployment and Infrastructure
+
+### Development Environment
+- Local SQLite database for development
+- Hot module replacement with Vite
+- TypeScript compilation and type checking
+- ESLint and Prettier for code quality
+
+### Production Deployment
+- Build optimization with Remix production build
+- Database migration strategy
+- Environment variable management
+- Health check endpoints for monitoring
+
+### Monitoring and Logging
+- Application performance monitoring
+- Error tracking and reporting
+- User analytics and usage metrics
+- Database performance monitoring
\ No newline at end of file
diff --git a/.kiro/specs/car-maintenance-system/requirements.md b/.kiro/specs/car-maintenance-system/requirements.md
new file mode 100644
index 0000000..0121166
--- /dev/null
+++ b/.kiro/specs/car-maintenance-system/requirements.md
@@ -0,0 +1,165 @@
+# Requirements Document
+
+## Introduction
+
+This document outlines the requirements for a comprehensive car maintenance management system built with Remix, SQLite, and Prisma. The system is designed for business owners to track vehicle visits and maintenance records with a focus on Arabic language support, RTL design, and mobile-friendly responsive UI/UX.
+
+## Requirements
+
+### Requirement 1: Core Framework and Database Setup
+
+**User Story:** As a business owner, I want a robust web application built on modern technologies, so that I can reliably manage my car maintenance business operations.
+
+#### Acceptance Criteria
+
+1. WHEN the application is deployed THEN the system SHALL use Remix as the web framework
+2. WHEN data needs to be stored THEN the system SHALL use SQLite database with Prisma ORM
+3. WHEN the application starts THEN the system SHALL initialize properly with all database connections established
+
+### Requirement 2: Arabic Language and RTL Support
+
+**User Story:** As an Arabic-speaking business owner, I want the interface to support Arabic language with proper RTL layout, so that I can use the system naturally in my native language.
+
+#### Acceptance Criteria
+
+1. WHEN the application loads THEN the system SHALL display all text in Arabic with RTL text direction
+2. WHEN viewing any page THEN the system SHALL render Arabic fonts correctly
+3. WHEN navigating the interface THEN all UI components SHALL support RTL layout orientation
+4. WHEN using forms THEN input fields SHALL align properly for RTL text entry
+
+### Requirement 3: Responsive Design and Mobile Support
+
+**User Story:** As a business owner who works on different devices, I want a responsive interface that works seamlessly on desktop, tablet, and mobile, so that I can manage my business from anywhere.
+
+#### Acceptance Criteria
+
+1. WHEN accessing the application on desktop THEN the system SHALL display a full dashboard layout
+2. WHEN accessing the application on tablet THEN the system SHALL adapt the layout for medium screens
+3. WHEN accessing the application on mobile THEN the system SHALL provide a mobile-optimized interface
+4. WHEN resizing the browser window THEN the system SHALL smoothly transition between responsive breakpoints
+
+### Requirement 4: Dashboard Navigation System
+
+**User Story:** As a user, I want an intuitive navigation system with collapsible sidebar, so that I can efficiently access different sections of the application.
+
+#### Acceptance Criteria
+
+1. WHEN viewing the dashboard THEN the system SHALL display a collapsible sidebar navigation menu
+2. WHEN the sidebar is collapsed THEN the system SHALL show icon-only navigation
+3. WHEN on mobile devices THEN the system SHALL provide a hamburger menu for navigation
+4. WHEN navigating between sections THEN the system SHALL provide smooth transitions
+5. WHEN the sidebar state changes THEN the system SHALL maintain the state across page navigation
+
+### Requirement 5: User Authentication System
+
+**User Story:** As a system administrator, I want a secure custom authentication system, so that I can control access to the application and protect sensitive business data.
+
+#### Acceptance Criteria
+
+1. WHEN a user attempts to sign in THEN the system SHALL authenticate using username or email with password
+2. WHEN storing passwords THEN the system SHALL hash passwords securely
+3. WHEN managing sessions THEN the system SHALL implement proper session management
+4. WHEN authentication fails THEN the system SHALL display appropriate error messages
+5. WHEN authentication succeeds THEN the system SHALL redirect to the appropriate dashboard
+
+### Requirement 6: User Management and Access Control
+
+**User Story:** As a superadmin, I want to manage user accounts with different access levels, so that I can control who has access to what features in the system.
+
+#### Acceptance Criteria
+
+1. WHEN creating users THEN the system SHALL support three auth levels: 1=superadmin, 2=admin, 3=user
+2. WHEN an admin views users THEN the system SHALL hide superadmin accounts from admin users
+3. WHEN a superadmin views users THEN the system SHALL display all user accounts
+4. WHEN managing user status THEN the system SHALL support "active" and "inactive" status
+5. WHEN no admin users exist THEN the system SHALL allow access to the signup page
+6. WHEN admin users exist THEN the system SHALL restrict access to the signup page
+
+### Requirement 7: Customer Management
+
+**User Story:** As a business owner, I want to manage customer information, so that I can maintain records of vehicle owners and their contact details.
+
+#### Acceptance Criteria
+
+1. WHEN adding a customer THEN the system SHALL store customer contact information
+2. WHEN viewing customers THEN the system SHALL display a searchable list of all customers
+3. WHEN editing customer information THEN the system SHALL update records with proper validation
+4. WHEN deleting a customer THEN the system SHALL handle associated vehicle relationships appropriately
+5. WHEN creating a customer THEN the system SHALL validate required fields and uniqueness constraints
+
+### Requirement 7.1: Enhanced Customer Details View
+
+**User Story:** As a business owner, I want a comprehensive customer details view that shows all related information in one place, so that I can quickly access customer vehicles, maintenance history, and take relevant actions.
+
+#### Acceptance Criteria
+
+1. WHEN viewing a customer's details THEN the system SHALL display a redesigned "المعلومات الأساسية" (Basic Information) section with improved layout
+2. WHEN viewing customer details THEN the system SHALL show all vehicles owned by the customer
+3. WHEN clicking on a vehicle in the customer details THEN the system SHALL navigate to the vehicles page filtered by that specific vehicle's plate number
+4. WHEN viewing customer details THEN the system SHALL provide a "Show All Vehicles" button that opens the vehicles page filtered by the customer
+5. WHEN viewing customer details THEN the system SHALL display the latest 3 maintenance visits for the customer
+6. WHEN viewing customer details THEN the system SHALL provide a button to view all maintenance visits filtered by the customer
+7. WHEN no maintenance visits exist THEN the system SHALL display an appropriate message encouraging the first visit
+8. WHEN viewing vehicle information THEN the system SHALL show key details like plate number, manufacturer, model, and year
+9. WHEN viewing maintenance visit information THEN the system SHALL show visit date, maintenance type, cost, and payment status
+
+### Requirement 8: Vehicle Management
+
+**User Story:** As a business owner, I want to manage detailed vehicle information, so that I can track each vehicle's specifications and maintenance history.
+
+#### Acceptance Criteria
+
+1. WHEN adding a vehicle THEN the system SHALL store plate number, body type, manufacturer, model, year, transmission, fuel type, and use type
+2. WHEN entering vehicle details THEN the system SHALL provide dropdown lists for body type, transmission, and fuel type options
+3. WHEN saving a vehicle THEN the system SHALL ensure plate number uniqueness
+4. WHEN linking vehicles THEN the system SHALL associate each vehicle with a customer owner
+5. WHEN viewing vehicles THEN the system SHALL display last visit date and suggested next visit date
+6. WHEN editing vehicle information THEN the system SHALL update records with proper validation
+
+### Requirement 9: Maintenance Visit Management
+
+**User Story:** As a service technician, I want to record maintenance visits with detailed information, so that I can track what work was performed and when.
+
+#### Acceptance Criteria
+
+1. WHEN creating a maintenance visit THEN the system SHALL link the visit to a specific vehicle and customer
+2. WHEN recording visit details THEN the system SHALL store maintenance type, description, cost, and kilometers
+3. WHEN completing a visit THEN the system SHALL update the vehicle's last visit date to the current date
+4. WHEN setting next visit THEN the system SHALL calculate suggested next visit date based on selected delay period
+5. WHEN selecting visit delay THEN the system SHALL provide options for 1, 2, 3, and 4 months
+6. WHEN saving a visit THEN the system SHALL generate corresponding income records
+
+### Requirement 10: Financial Management
+
+**User Story:** As a business owner, I want to track expenses and income, so that I can monitor the financial performance of my maintenance business.
+
+#### Acceptance Criteria
+
+1. WHEN recording expenses THEN the system SHALL store expense details with proper categorization
+2. WHEN a maintenance visit is completed THEN the system SHALL automatically generate income records
+3. WHEN viewing financial data THEN the system SHALL provide summaries of income and expenses
+4. WHEN managing financial records THEN the system SHALL support full CRUD operations
+5. WHEN calculating totals THEN the system SHALL provide accurate financial reporting
+
+### Requirement 11: Data Validation and Error Handling
+
+**User Story:** As a user, I want proper validation and clear error messages, so that I can understand and correct any input errors quickly.
+
+#### Acceptance Criteria
+
+1. WHEN submitting forms THEN the system SHALL validate all required fields
+2. WHEN validation fails THEN the system SHALL display clear, localized error messages
+3. WHEN database operations fail THEN the system SHALL handle errors gracefully
+4. WHEN unique constraints are violated THEN the system SHALL provide specific error feedback
+5. WHEN network errors occur THEN the system SHALL provide appropriate user feedback
+
+### Requirement 12: Database Seeding and Initial Setup
+
+**User Story:** As a system administrator, I want the system to initialize with essential data, so that I can start using the application immediately after deployment.
+
+#### Acceptance Criteria
+
+1. WHEN the database is first created THEN the system SHALL seed one superadmin account
+2. WHEN seeding data THEN the system SHALL create essential system configuration data
+3. WHEN running seed scripts THEN the system SHALL handle existing data appropriately
+4. WHEN initializing THEN the system SHALL set up proper database relationships and constraints
\ No newline at end of file
diff --git a/.kiro/specs/car-maintenance-system/tasks.md b/.kiro/specs/car-maintenance-system/tasks.md
new file mode 100644
index 0000000..4caa1b9
--- /dev/null
+++ b/.kiro/specs/car-maintenance-system/tasks.md
@@ -0,0 +1,201 @@
+# Implementation Plan
+
+- [x] 1. Set up project foundation and database schema
+
+
+
+
+
+ - Configure Prisma schema with all required models and relationships
+ - Set up database migrations and seed data
+ - Configure TypeScript types for all models
+ - _Requirements: 1.1, 1.2, 1.3, 12.1, 12.2, 12.3, 12.4_
+
+- [x] 2. Implement authentication system foundation
+
+
+
+
+
+ - Create session management utilities and middleware
+ - Implement password hashing and validation functions
+ - Create authentication helper functions and types
+ - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
+
+- [x] 3. Create authentication routes and forms
+
+
+
+
+
+ - Implement signin route with username/email login support
+ - Create signup route with conditional access control
+ - Build authentication form components with validation
+ - Implement proper error handling and user feedback
+ - _Requirements: 5.1, 5.4, 5.5, 6.5, 6.6, 11.1, 11.2, 11.4_
+
+- [x] 4. Set up RTL layout and Arabic language support
+
+
+
+
+ - Configure Tailwind CSS for RTL support and Arabic fonts
+ - Create base layout components with RTL orientation
+ - Implement responsive design utilities and breakpoints
+ - Test Arabic text rendering and layout behavior
+ - _Requirements: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4_
+
+- [x] 5. Build dashboard layout and navigation system
+
+
+
+
+
+ - Create responsive sidebar navigation component
+ - Implement collapsible sidebar with icon-only mode
+ - Build mobile hamburger menu and responsive behavior
+ - Add smooth transitions and state persistence
+ - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
+
+- [x] 6. Implement route protection and access control
+
+
+
+
+
+ - Create route protection middleware for different auth levels
+ - Implement role-based access control logic
+ - Add automatic redirection for unauthorized access
+ - Test access control for all user types
+ - _Requirements: 6.1, 6.2, 6.3, 6.4_
+
+- [x] 7. Create user management system
+
+
+
+
+
+
+ - Build user listing page with role-based filtering
+ - Implement user creation, editing, and status management
+ - Create user deletion functionality with proper validation
+ - Add search and filtering capabilities for user management
+ - _Requirements: 6.1, 6.2, 6.3, 6.4, 7.1, 7.2, 7.3, 7.4, 7.5_
+
+- [x] 8. Implement customer management CRUD operations
+
+
+
+
+
+ - Create customer model and database operations
+ - Build customer listing page with search functionality
+ - Implement customer creation and editing forms
+ - Add customer deletion with relationship handling
+ - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 11.1, 11.2, 11.3_
+
+- [x] 9. Build vehicle management system
+
+
+
+
+
+ - Create vehicle model with all required fields and relationships
+ - Implement vehicle registration form with dropdown selections
+ - Build vehicle listing and search functionality
+ - Add vehicle editing and deletion capabilities
+ - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 11.1, 11.2, 11.4_
+
+- [x] 10. Implement maintenance visit management
+
+
+
+
+
+ - Create maintenance visit model and database operations
+ - Build visit registration form with vehicle and customer linking
+ - Implement visit delay selection and next visit date calculation
+ - Add visit listing, editing, and deletion functionality
+ - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6_
+
+- [x] 11. Create financial management system
+
+
+
+
+
+ - Implement expense management CRUD operations
+ - Build automatic income generation from maintenance visits
+ - Create financial reporting and summary views
+ - Add expense and income listing with filtering
+ - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
+
+- [x] 12. Build reusable form components and validation
+
+
+
+
+
+ - Create reusable form input components with RTL support
+ - Implement client-side and server-side validation
+ - Build data table components with Arabic text support
+ - Add pagination, sorting, and filtering utilities
+ - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_
+
+
+
+
+
+- [x] 13. Enhance customer details view with comprehensive information display (note that i do not use dynamic page $... i use popup forms for update and view details)
+
+
+
+
+
+
+
+
+
+ - Redesign the "المعلومات الأساسية" (Basic Information) section with improved layout
+ - Display all customer vehicles with clickable navigation to filtered vehicle pages
+ - Add "Show All Vehicles" button that opens vehicles page filtered by customer
+ - Show latest 3 maintenance visits with key information (date, type, cost, payment status)
+ - Add "View All Visits" button that opens maintenance visits page filtered by customer
+ - Implement proper navigation with URL parameters for filtering
+ - Add responsive design for mobile and tablet views
+ - _Requirements: 7.1.1, 7.1.2, 7.1.3, 7.1.4, 7.1.5, 7.1.6, 7.1.7, 7.1.8, 7.1.9_
+
+- [ ] 14. Implement comprehensive error handling
+ - Create global error boundary components
+ - Implement centralized error handling middleware
+ - Add proper error logging and user feedback
+ - Create Arabic error message translations
+ - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_
+
+- [ ] 15. Add responsive design and mobile optimization
+ - Optimize all components for mobile devices
+ - Test and refine responsive breakpoints
+ - Implement touch-friendly interactions
+ - Verify RTL layout on all screen sizes
+ - _Requirements: 3.1, 3.2, 3.3, 3.4, 4.3, 4.4_
+
+- [ ] 16. Create database seeding and initial setup
+ - Implement database seed script with superadmin account
+ - Create essential system data initialization
+ - Add proper error handling for existing data
+ - Test database initialization and migration process
+ - _Requirements: 12.1, 12.2, 12.3, 12.4_
+
+- [ ] 17. Implement comprehensive testing suite
+ - Write unit tests for all business logic functions
+ - Create integration tests for API routes and database operations
+ - Add component tests for UI components
+ - Implement end-to-end tests for critical user journeys
+ - _Requirements: All requirements validation through testing_
+
+- [ ] 18. Final integration and system testing
+ - Test complete user workflows from authentication to data management
+ - Verify all CRUD operations work correctly with proper validation
+ - Test role-based access control across all features
+ - Validate Arabic language support and RTL layout throughout the system
+ - _Requirements: All requirements integration testing_
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5480842
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "kiroAgent.configureMCP": "Disabled"
+}
\ No newline at end of file
diff --git a/MAINTENANCE_TYPE_MIGRATION_SUMMARY.md b/MAINTENANCE_TYPE_MIGRATION_SUMMARY.md
new file mode 100644
index 0000000..11f2a22
--- /dev/null
+++ b/MAINTENANCE_TYPE_MIGRATION_SUMMARY.md
@@ -0,0 +1,117 @@
+# Maintenance Type Migration Summary
+
+## Overview
+Updated the maintenance system to use a separate `MaintenanceType` table instead of storing maintenance types as simple strings. This provides better data consistency, allows for better management of maintenance types, and enables features like descriptions and active/inactive status.
+
+## Database Changes
+
+### New Table: `maintenance_types`
+- `id` (Primary Key)
+- `name` (Unique, required)
+- `description` (Optional)
+- `isActive` (Boolean, default true)
+- `createdDate` (Timestamp)
+- `updateDate` (Timestamp)
+
+### Updated Table: `maintenance_visits`
+- Replaced `maintenanceType` (string) with `maintenanceTypeId` (foreign key)
+- Added foreign key constraint to `maintenance_types` table
+
+## Code Changes
+
+### 1. Schema Updates
+- **File**: `prisma/schema.prisma`
+- Added `MaintenanceType` model
+- Updated `MaintenanceVisit` model to use `maintenanceTypeId`
+- Added relationship between models
+
+### 2. Type Definitions
+- **File**: `app/types/database.ts`
+- Added `MaintenanceType` export
+- Added `CreateMaintenanceTypeData` and `UpdateMaintenanceTypeData` interfaces
+- Updated `CreateMaintenanceVisitData` and `UpdateMaintenanceVisitData` to use `maintenanceTypeId`
+- Updated `MaintenanceVisitWithRelations` to include `maintenanceType` relationship
+
+### 3. New Server Functions
+- **File**: `app/lib/maintenance-type-management.server.ts` (NEW)
+- `getMaintenanceTypes()` - Get all maintenance types
+- `getMaintenanceTypesForSelect()` - Get maintenance types for dropdowns
+- `getMaintenanceTypeById()` - Get single maintenance type
+- `createMaintenanceType()` - Create new maintenance type
+- `updateMaintenanceType()` - Update existing maintenance type
+- `deleteMaintenanceType()` - Soft delete maintenance type
+- `toggleMaintenanceTypeStatus()` - Toggle active/inactive status
+- `getMaintenanceTypeStats()` - Get statistics for maintenance type
+
+### 4. Updated Server Functions
+- **File**: `app/lib/maintenance-visit-management.server.ts`
+- Updated all functions to include `maintenanceType` relationship in queries
+- Updated search functionality to search within maintenance type names
+- Updated create/update functions to use `maintenanceTypeId`
+
+### 5. Validation Updates
+- **File**: `app/lib/validation.ts`
+- Updated `validateMaintenanceVisit()` to validate `maintenanceTypeId` instead of `maintenanceType`
+
+### 6. Route Updates
+- **File**: `app/routes/maintenance-visits.tsx`
+- Added import for `getMaintenanceTypesForSelect`
+- Updated loader to fetch maintenance types
+- Updated action handlers to work with `maintenanceTypeId`
+- Updated form data processing
+
+### 7. Component Updates
+- **File**: `app/components/maintenance-visits/MaintenanceVisitForm.tsx`
+- Updated to accept `maintenanceTypes` prop
+- Changed maintenance type field from text input to select dropdown
+- Updated form field name from `maintenanceType` to `maintenanceTypeId`
+
+- **File**: `app/components/maintenance-visits/MaintenanceVisitDetailsView.tsx`
+- Updated to display `visit.maintenanceType.name` instead of `visit.maintenanceType`
+- Added display of maintenance type description if available
+
+- **File**: `app/components/vehicles/VehicleDetailsView.tsx`
+- Updated to display maintenance type name with fallback for undefined types
+
+### 8. Seed Data Updates
+- **File**: `prisma/seed.ts`
+- Added creation of default maintenance types
+- Updated sample maintenance visits to use maintenance type IDs
+
+## Default Maintenance Types
+The system now includes these default maintenance types:
+1. صيانة دورية (Periodic Maintenance)
+2. تغيير زيت المحرك (Engine Oil Change)
+3. إصلاح الفرامل (Brake Repair)
+4. إصلاح المحرك (Engine Repair)
+5. إصلاح ناقل الحركة (Transmission Repair)
+6. إصلاح التكييف (AC Repair)
+7. إصلاح الإطارات (Tire Repair)
+8. إصلاح الكهرباء (Electrical Repair)
+9. فحص شامل (Comprehensive Inspection)
+10. أخرى (Other)
+
+## Migration Steps
+1. Run the database migration: `prisma/migrations/add_maintenance_types.sql`
+2. Update your database schema
+3. Run the seed script to populate default maintenance types
+4. Test the application to ensure all functionality works correctly
+
+## Benefits
+- **Data Consistency**: Standardized maintenance type names
+- **Extensibility**: Easy to add new maintenance types without code changes
+- **Management**: Can activate/deactivate maintenance types
+- **Descriptions**: Each maintenance type can have a detailed description
+- **Statistics**: Can track usage and revenue by maintenance type
+- **Validation**: Better form validation with predefined options
+
+## Breaking Changes
+- API endpoints now expect `maintenanceTypeId` instead of `maintenanceType`
+- Database queries need to include the maintenance type relationship
+- Forms now use dropdown selection instead of free text input
+
+## Future Enhancements
+- Admin interface for managing maintenance types
+- Maintenance type categories/grouping
+- Pricing suggestions per maintenance type
+- Maintenance type templates with common descriptions
\ No newline at end of file
diff --git a/ROUTE_PROTECTION.md b/ROUTE_PROTECTION.md
new file mode 100644
index 0000000..142d007
--- /dev/null
+++ b/ROUTE_PROTECTION.md
@@ -0,0 +1,250 @@
+# Route Protection and Access Control Implementation
+
+This document describes the comprehensive route protection and access control system implemented for the car maintenance management system.
+
+## Overview
+
+The route protection system provides:
+- **Authentication middleware** for all protected routes
+- **Role-based access control** (RBAC) with three levels: Superadmin, Admin, User
+- **Automatic redirection** for unauthorized access attempts
+- **Session validation** and user status checking
+- **Route-specific protection** functions
+- **Permission checking** utilities
+- **Redirect authenticated users** from auth pages (signin/signup)
+
+## Authentication Levels
+
+The system uses a hierarchical authentication system where lower numbers indicate higher privileges:
+
+| Level | Name | Value | Description |
+|-------|------------|-------|---------------------------------------|
+| 1 | Superadmin | 1 | Full system access, can manage admins |
+| 2 | Admin | 2 | Can manage users and business data |
+| 3 | User | 3 | Basic access to operational features |
+
+## Route Protection Matrix
+
+| Route | Auth Required | Min Level | Superadmin | Admin | User |
+|--------------------------|---------------|-----------|------------|-------|------|
+| `/dashboard` | ✓ | User (3) | ✓ | ✓ | ✓ |
+| `/users` | ✓ | Admin (2) | ✓ | ✓ | ✗ |
+| `/customers` | ✓ | User (3) | ✓ | ✓ | ✓ |
+| `/vehicles` | ✓ | User (3) | ✓ | ✓ | ✓ |
+| `/maintenance-visits` | ✓ | User (3) | ✓ | ✓ | ✓ |
+| `/financial` | ✓ | Admin (2) | ✓ | ✓ | ✗ |
+| `/admin/enable-signup` | ✓ | Super (1) | ✓ | ✗ | ✗ |
+| `/signin` | ✗ | None | ✓* | ✓* | ✓* |
+| `/signup` | ✗ | None | ✓* | ✓* | ✓* |
+
+*\* Authenticated users are redirected away from auth pages*
+
+## Implementation Details
+
+### Core Middleware Functions
+
+#### `requireAuthentication(request, options?)`
+- Ensures user is logged in and active
+- Options: `allowInactive`, `redirectTo`
+- Redirects to `/signin` if not authenticated
+- Checks user status (active/inactive)
+
+#### `requireAdmin(request, options?)`
+- Requires Admin level (2) or higher
+- Calls `requireAuthLevel` with `AUTH_LEVELS.ADMIN`
+
+#### `requireSuperAdmin(request, options?)`
+- Requires Superadmin level (1)
+- Calls `requireAuthLevel` with `AUTH_LEVELS.SUPERADMIN`
+
+#### `redirectIfAuthenticated(request, redirectTo?)`
+- Redirects authenticated users away from auth pages
+- Default redirect: `/dashboard`
+- Only redirects active users
+
+### Route-Specific Protection Functions
+
+#### `protectUserManagementRoute(request)`
+- Used by `/users` route
+- Requires Admin level or higher
+- Additional business logic validation
+
+#### `protectFinancialRoute(request)`
+- Used by `/financial` route
+- Requires Admin level or higher
+
+#### `protectCustomerRoute(request)`
+- Used by `/customers` route
+- Requires any authenticated user
+
+#### `protectVehicleRoute(request)`
+- Used by `/vehicles` route
+- Requires any authenticated user
+
+#### `protectMaintenanceRoute(request)`
+- Used by `/maintenance-visits` route
+- Requires any authenticated user
+
+### Permission Checking
+
+#### `checkPermission(user, permission)`
+Checks specific permissions for a user:
+
+| Permission | Superadmin | Admin | User |
+|-------------------|------------|-------|------|
+| `view_all_users` | ✓ | ✗ | ✗ |
+| `create_users` | ✓ | ✓ | ✗ |
+| `manage_finances` | ✓ | ✓ | ✗ |
+| `view_reports` | ✓ | ✓ | ✗ |
+
+### Session Validation
+
+#### `validateSession(request)`
+Returns session validation result:
+```typescript
+{
+ isValid: boolean;
+ user: SafeUser | null;
+ error?: 'no_user' | 'inactive_user' | 'session_error';
+}
+```
+
+### Error Handling
+
+#### `createUnauthorizedResponse(message?)`
+- Creates 403 Forbidden response
+- Default message in Arabic
+- Custom message support
+
+## Usage Examples
+
+### Protecting a Route
+```typescript
+// In route loader
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireAdmin(request);
+ return json({ user });
+}
+```
+
+### Custom Protection
+```typescript
+// In route loader with custom options
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireAuthentication(request, {
+ redirectTo: "/custom-login",
+ allowInactive: false
+ });
+ return json({ user });
+}
+```
+
+### Permission Checking
+```typescript
+// In component or route
+const canManageUsers = checkPermission(user, 'create_users');
+if (canManageUsers) {
+ // Show user management UI
+}
+```
+
+### Conditional Access Control
+```typescript
+// Signup route with conditional access
+export async function loader({ request }: LoaderFunctionArgs) {
+ await redirectIfAuthenticated(request);
+
+ const signupAllowed = await isSignupAllowed();
+ if (!signupAllowed && !adminOverride) {
+ return redirect("/signin?error=signup_disabled");
+ }
+
+ return json({ signupAllowed });
+}
+```
+
+## Security Features
+
+### Authentication Security
+- ✓ Secure session management with httpOnly cookies
+- ✓ Password hashing with bcrypt
+- ✓ Session expiration handling
+- ✓ CSRF protection through Remix forms
+
+### Authorization Security
+- ✓ Role-based access control (RBAC)
+- ✓ Route-level protection
+- ✓ API endpoint authorization
+- ✓ User status validation (active/inactive)
+
+### Error Handling
+- ✓ Graceful error responses
+- ✓ Proper HTTP status codes
+- ✓ Localized error messages (Arabic)
+- ✓ Secure error information disclosure
+
+## Testing
+
+The route protection system includes comprehensive tests:
+
+### Integration Tests
+- Permission checking logic
+- Auth level hierarchy validation
+- Error response creation
+- User status validation
+
+### Route Tests
+- Authentication requirement verification
+- Role-based access control
+- Redirection behavior
+- Error handling
+
+## Requirements Compliance
+
+This implementation satisfies the following requirements:
+
+### Requirement 6.1: User Authentication Levels
+✓ Supports three auth levels: 1=superadmin, 2=admin, 3=user
+
+### Requirement 6.2: Admin User Visibility
+✓ Admins cannot see superadmin accounts
+✓ Superadmins can see all user accounts
+
+### Requirement 6.3: User Status Management
+✓ Supports "active" and "inactive" status
+✓ Inactive users are properly handled
+
+### Requirement 6.4: Conditional Signup Access
+✓ Signup restricted when admin users exist
+✓ Signup allowed when no admin users exist
+
+## Future Enhancements
+
+Potential improvements for the route protection system:
+
+1. **Rate Limiting**: Add rate limiting for authentication attempts
+2. **Audit Logging**: Log access attempts and authorization failures
+3. **Session Management**: Advanced session management with refresh tokens
+4. **Multi-Factor Authentication**: Add MFA support for admin accounts
+5. **API Key Authentication**: Support for API access with keys
+6. **Permission Caching**: Cache permission checks for performance
+7. **Dynamic Permissions**: Database-driven permission system
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Redirect Loops**: Check for circular redirects in auth logic
+2. **Session Expiry**: Ensure proper session renewal
+3. **Permission Denied**: Verify user auth level and status
+4. **Inactive Users**: Check user status in database
+
+### Debug Information
+
+Enable debug logging by setting environment variables:
+```bash
+DEBUG=auth:*
+LOG_LEVEL=debug
+```
+
+This will provide detailed information about authentication and authorization decisions.
\ No newline at end of file
diff --git a/SETTINGS_IMPLEMENTATION_SUMMARY.md b/SETTINGS_IMPLEMENTATION_SUMMARY.md
new file mode 100644
index 0000000..7c3e1d6
--- /dev/null
+++ b/SETTINGS_IMPLEMENTATION_SUMMARY.md
@@ -0,0 +1,233 @@
+# 🔧 Settings System Implementation Summary
+
+## ✅ What Has Been Completed
+
+### 1. **Database Schema & Migration**
+- ✅ Added `Settings` model to Prisma schema
+- ✅ Created migration SQL file (`prisma/migrations/add_settings.sql`)
+- ✅ Created settings seeding script (`prisma/settingsSeed.ts`)
+- ✅ Database schema updated with `npx prisma db push`
+
+### 2. **Server-Side Settings Management**
+- ✅ Created `app/lib/settings-management.server.ts` with comprehensive settings API
+- ✅ Implemented settings CRUD operations
+- ✅ Added settings formatter utilities
+- ✅ Error handling for missing settings table
+- ✅ Default settings fallback system
+
+### 3. **React Context & Hooks**
+- ✅ Created `app/contexts/SettingsContext.tsx` for global settings access
+- ✅ Implemented formatting hooks (`useSettings`, `useFormatters`)
+- ✅ Integrated settings provider in root layout
+
+### 4. **Settings Management UI**
+- ✅ Created comprehensive settings page (`app/routes/settings.tsx`)
+- ✅ Real-time preview of formatting changes
+- ✅ Admin-only access control
+- ✅ Form validation and error handling
+- ✅ Settings reset functionality
+
+### 5. **Component Integration**
+- ✅ Updated `MaintenanceVisitDetailsView` with settings formatting
+- ✅ Updated `VehicleDetailsView` with settings formatting
+- ✅ Updated `CustomerDetailsView` with settings formatting
+- ✅ Added settings link to sidebar navigation
+
+### 6. **Documentation**
+- ✅ Created comprehensive documentation (`SETTINGS_SYSTEM_README.md`)
+- ✅ Implementation guide and troubleshooting
+- ✅ API reference and usage examples
+
+## 🔄 Next Steps to Complete Setup
+
+### Step 1: Generate Prisma Client
+The current issue is that Prisma Client needs to be regenerated to include the new Settings model.
+
+```bash
+# Stop the dev server first (Ctrl+C)
+npx prisma generate
+```
+
+### Step 2: Seed Default Settings
+```bash
+npx tsx prisma/settingsSeed.ts
+```
+
+### Step 3: Restart Development Server
+```bash
+npm run dev
+```
+
+### Step 4: Test Settings System
+1. Navigate to `http://localhost:5173/login`
+2. Login with admin credentials
+3. Go to Settings page (`/settings`)
+4. Test different format configurations
+5. Verify changes appear across all pages
+
+## 🎯 Features Implemented
+
+### **Date Format Configuration**
+- **Arabic (ar-SA)**: ٩/١١/٢٠٢٥ (Arabic numerals)
+- **English (en-US)**: 11/9/2025 (Western numerals)
+- Applied to: Visit dates, creation dates, update dates
+
+### **Currency Configuration**
+- **Supported**: JOD (د.أ), USD ($), EUR (€), SAR (ر.س), AED (د.إ)
+- **Custom symbols**: User-configurable currency symbols
+- Applied to: All cost displays, financial reports
+
+### **Number Format Configuration**
+- **Arabic (ar-SA)**: ١٬٢٣٤٫٥٦ (Arabic numerals with Arabic separators)
+- **English (en-US)**: 1,234.56 (Western numerals with Western separators)
+- Applied to: Kilometers, costs, all numeric displays
+
+## 📁 Files Created/Modified
+
+### New Files:
+```
+app/lib/settings-management.server.ts
+app/contexts/SettingsContext.tsx
+app/routes/settings.tsx
+prisma/settingsSeed.ts
+prisma/migrations/add_settings.sql
+SETTINGS_SYSTEM_README.md
+SETTINGS_IMPLEMENTATION_SUMMARY.md
+```
+
+### Modified Files:
+```
+prisma/schema.prisma (Added Settings model)
+app/root.tsx (Added settings provider)
+app/components/layout/Sidebar.tsx (Added settings link)
+app/components/maintenance-visits/MaintenanceVisitDetailsView.tsx
+app/components/vehicles/VehicleDetailsView.tsx
+app/components/customers/CustomerDetailsView.tsx
+app/routes/maintenance-visits.tsx (Added settings import)
+```
+
+## 🔧 Settings API Reference
+
+### Server Functions:
+- `getAppSettings()`: Get all settings as typed object
+- `updateSettings(settings)`: Update multiple settings
+- `getSetting(key)`: Get specific setting value
+- `updateSetting(key, value)`: Update single setting
+- `initializeDefaultSettings()`: Create default settings
+
+### React Hooks:
+- `useSettings()`: Access settings and formatters
+- `useFormatters(settings)`: Create formatters without context
+
+### Formatting Functions:
+- `formatCurrency(amount)`: Format currency with symbol
+- `formatDate(date)`: Format date according to locale
+- `formatNumber(number)`: Format numbers according to locale
+- `formatDateTime(date)`: Format date and time
+
+## 🎨 Usage Examples
+
+### In Components:
+```typescript
+import { useSettings } from '~/contexts/SettingsContext';
+
+function MyComponent() {
+ const { formatCurrency, formatDate, formatNumber } = useSettings();
+
+ return (
+
+
Cost: {formatCurrency(250.75)}
+
Date: {formatDate(new Date())}
+
Kilometers: {formatNumber(45000)} كم
+
+ );
+}
+```
+
+### In Loaders:
+```typescript
+import { getAppSettings, createFormatter } from '~/lib/settings-management.server';
+
+export async function loader() {
+ const settings = await getAppSettings();
+ const formatter = await createFormatter();
+
+ return json({
+ formattedCost: formatter.formatCurrency(1234.56),
+ settings
+ });
+}
+```
+
+## 🚀 Benefits Achieved
+
+### **Centralized Configuration**
+- Single source of truth for all formatting
+- Consistent formatting across entire application
+- Easy to change formats globally
+
+### **Localization Support**
+- Arabic and English number formats
+- Proper RTL/LTR date formatting
+- Cultural currency display preferences
+
+### **Admin Control**
+- Real-time format changes
+- Preview before applying
+- Reset to defaults functionality
+
+### **Developer Experience**
+- Type-safe settings API
+- Easy-to-use React hooks
+- Comprehensive error handling
+
+### **Performance**
+- Settings cached in React context
+- Minimal database queries
+- Efficient formatting utilities
+
+## 🔍 Troubleshooting
+
+### Common Issues:
+
+1. **"Cannot read properties of undefined (reading 'settings')"**
+ - Solution: Run `npx prisma generate` to update Prisma Client
+
+2. **Settings not loading**
+ - Solution: Run `npx tsx prisma/settingsSeed.ts` to seed defaults
+
+3. **Formatting not applied**
+ - Solution: Ensure components use `useSettings()` hook
+
+4. **Settings page not accessible**
+ - Solution: Login with admin account (authLevel 2 or higher)
+
+## 🎉 Success Criteria
+
+When setup is complete, you should be able to:
+
+1. ✅ Access settings page at `/settings` (admin only)
+2. ✅ Change date format and see immediate preview
+3. ✅ Switch currency and symbol
+4. ✅ Toggle number format between Arabic/English
+5. ✅ See formatting changes across all pages:
+ - Maintenance visit costs and dates
+ - Vehicle information and kilometers
+ - Customer creation dates
+ - All numeric displays
+
+## 📈 Future Enhancements
+
+The system is designed to be extensible for:
+- Time zone configuration
+- Additional currency support
+- Custom date format patterns
+- Language switching (full Arabic/English UI)
+- User-specific settings
+- Settings import/export
+
+---
+
+**Status**: Implementation Complete - Requires Prisma Client Regeneration
+**Next Action**: Run `npx prisma generate` and seed settings
+**Estimated Time**: 2-3 minutes to complete setup
\ No newline at end of file
diff --git a/SETTINGS_SYSTEM_README.md b/SETTINGS_SYSTEM_README.md
new file mode 100644
index 0000000..6a87cf7
--- /dev/null
+++ b/SETTINGS_SYSTEM_README.md
@@ -0,0 +1,298 @@
+# 🔧 Settings System Documentation
+
+## Overview
+The Settings System provides centralized configuration management for the Car Maintenance Management System, allowing administrators to customize date formats, currency, and number formatting across the entire application.
+
+## 🎯 Features
+
+### 1. **Date Format Configuration**
+- **Arabic (ar-SA)**: ٢٠٢٥/١١/٩ (Arabic numerals)
+- **English (en-US)**: 11/9/2025 (Western numerals)
+- **Display Patterns**: dd/MM/yyyy, MM/dd/yyyy, yyyy-MM-dd
+
+### 2. **Currency Configuration**
+- **Supported Currencies**: JOD, USD, EUR, SAR, AED
+- **Custom Currency Symbols**: د.أ, $, €, ر.س, د.إ
+- **Automatic Formatting**: 1,234.56 د.أ
+
+### 3. **Number Format Configuration**
+- **Arabic (ar-SA)**: ١٬٢٣٤٫٥٦ (Arabic numerals with Arabic separators)
+- **English (en-US)**: 1,234.56 (Western numerals with Western separators)
+- **Applied to**: Costs, kilometers, all numeric displays
+
+## 🏗️ Architecture
+
+### Database Schema
+```sql
+CREATE TABLE "settings" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "key" TEXT NOT NULL UNIQUE,
+ "value" TEXT NOT NULL,
+ "description" TEXT,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### Settings Keys
+| Key | Description | Default Value | Options |
+|-----|-------------|---------------|---------|
+| `dateFormat` | Date format locale | `ar-SA` | `ar-SA`, `en-US` |
+| `currency` | Currency code | `JOD` | `JOD`, `USD`, `EUR`, `SAR`, `AED` |
+| `numberFormat` | Number format locale | `ar-SA` | `ar-SA`, `en-US` |
+| `currencySymbol` | Currency symbol | `د.أ` | Any string |
+| `dateDisplayFormat` | Date pattern | `dd/MM/yyyy` | Various patterns |
+
+## 📁 File Structure
+
+```
+app/
+├── lib/
+│ └── settings-management.server.ts # Server-side settings management
+├── contexts/
+│ └── SettingsContext.tsx # React context for settings
+├── routes/
+│ └── settings.tsx # Settings management page
+├── components/
+│ ├── layout/
+│ │ └── Sidebar.tsx # Updated with settings link
+│ ├── maintenance-visits/
+│ │ └── MaintenanceVisitDetailsView.tsx # Uses settings formatting
+│ ├── vehicles/
+│ │ └── VehicleDetailsView.tsx # Uses settings formatting
+│ └── customers/
+│ └── CustomerDetailsView.tsx # Uses settings formatting
+└── root.tsx # Settings provider integration
+
+prisma/
+├── schema.prisma # Settings model
+├── settingsSeed.ts # Settings seeding script
+└── migrations/
+ └── add_settings.sql # Settings table migration
+```
+
+## 🔧 Implementation Details
+
+### 1. Settings Management Server (`settings-management.server.ts`)
+
+#### Key Functions:
+- `getAppSettings()`: Retrieves all settings as typed object
+- `updateSettings()`: Updates multiple settings atomically
+- `createFormatter()`: Creates formatter instance with current settings
+- `SettingsFormatter`: Utility class for consistent formatting
+
+#### Usage Example:
+```typescript
+import { getAppSettings, createFormatter } from '~/lib/settings-management.server';
+
+// In loader
+const settings = await getAppSettings();
+
+// Create formatter
+const formatter = await createFormatter();
+const formattedCurrency = formatter.formatCurrency(1234.56);
+const formattedDate = formatter.formatDate(new Date());
+```
+
+### 2. Settings Context (`SettingsContext.tsx`)
+
+#### Provides:
+- `settings`: Current settings object
+- `formatNumber(value)`: Format numbers according to settings
+- `formatCurrency(value)`: Format currency with symbol
+- `formatDate(date)`: Format dates according to settings
+- `formatDateTime(date)`: Format date and time
+
+#### Usage Example:
+```typescript
+import { useSettings } from '~/contexts/SettingsContext';
+
+function MyComponent() {
+ const { formatCurrency, formatDate, formatNumber } = useSettings();
+
+ return (
+
+
Cost: {formatCurrency(250.75)}
+
Date: {formatDate(new Date())}
+
Kilometers: {formatNumber(45000)} كم
+
+ );
+}
+```
+
+### 3. Root Integration (`root.tsx`)
+
+The root layout loads settings and provides them to all components:
+
+```typescript
+export async function loader({ request }: LoaderFunctionArgs) {
+ await initializeDefaultSettings();
+ const settings = await getAppSettings();
+ return json({ settings });
+}
+
+export default function App() {
+ const { settings } = useLoaderData();
+
+ return (
+
+
+
+ );
+}
+```
+
+## 🎨 Settings Page Features
+
+### Admin Interface (`/settings`)
+- **Real-time Preview**: See formatting changes immediately
+- **Validation**: Ensures valid locale and currency codes
+- **Reset Functionality**: Restore default settings
+- **Visual Examples**: Shows how settings affect different data types
+
+### Security
+- **Admin Only**: Requires authentication level 2 (Admin) or higher
+- **Input Validation**: Validates all setting values
+- **Error Handling**: Graceful fallback to defaults on errors
+
+## 🌍 Localization Support
+
+### Arabic (ar-SA)
+- **Numbers**: ١٬٢٣٤٫٥٦ (Arabic-Indic digits)
+- **Dates**: ٩/١١/٢٠٢٥ (Arabic calendar format)
+- **Currency**: ١٬٢٣٤٫٥٦ د.أ
+
+### English (en-US)
+- **Numbers**: 1,234.56 (Western digits)
+- **Dates**: 11/9/2025 (Gregorian format)
+- **Currency**: 1,234.56 JOD
+
+## 🔄 Migration Guide
+
+### From Hardcoded Formatting
+Replace hardcoded formatting:
+
+```typescript
+// Before
+{visit.cost.toLocaleString('ar-SA')} د.أ
+{new Date(visit.date).toLocaleDateString('ar-SA')}
+{visit.kilometers.toLocaleString('ar-SA')} كم
+
+// After
+{formatCurrency(visit.cost)}
+{formatDate(visit.date)}
+{formatNumber(visit.kilometers)} كم
+```
+
+### Adding New Components
+1. Import settings context: `import { useSettings } from '~/contexts/SettingsContext';`
+2. Use formatting functions: `const { formatCurrency, formatDate, formatNumber } = useSettings();`
+3. Apply formatting: `{formatCurrency(amount)}` instead of hardcoded formatting
+
+## 🧪 Testing
+
+### Manual Testing
+1. Navigate to `/settings` (admin required)
+2. Change date format from Arabic to English
+3. Verify changes appear across all pages:
+ - Maintenance visits
+ - Vehicle details
+ - Customer information
+ - Financial reports
+
+### Automated Testing
+```typescript
+// Test settings formatting
+import { SettingsFormatter } from '~/lib/settings-management.server';
+
+const arabicSettings = {
+ dateFormat: 'ar-SA' as const,
+ numberFormat: 'ar-SA' as const,
+ currency: 'JOD',
+ currencySymbol: 'د.أ'
+};
+
+const formatter = new SettingsFormatter(arabicSettings);
+expect(formatter.formatCurrency(1234.56)).toBe('١٬٢٣٤٫٥٦ د.أ');
+```
+
+## 🚀 Performance Considerations
+
+### Caching
+- Settings are loaded once at application startup
+- Context provides settings to all components without re-fetching
+- Database queries are minimized through upsert operations
+
+### Memory Usage
+- Settings object is lightweight (< 1KB)
+- Formatter instances are created on-demand
+- No memory leaks from event listeners
+
+## 🔮 Future Enhancements
+
+### Planned Features
+1. **Time Zone Support**: Configure display time zones
+2. **Language Switching**: Full Arabic/English interface toggle
+3. **Custom Date Patterns**: User-defined date format strings
+4. **Decimal Precision**: Configurable decimal places for currency
+5. **Export/Import**: Settings backup and restore functionality
+
+### Technical Improvements
+1. **Settings Validation**: JSON schema validation for settings
+2. **Audit Trail**: Track settings changes with timestamps
+3. **Role-based Settings**: Different settings per user role
+4. **Real-time Updates**: WebSocket-based settings synchronization
+
+## 📋 Troubleshooting
+
+### Common Issues
+
+#### Settings Not Loading
+```typescript
+// Check if settings are initialized
+await initializeDefaultSettings();
+const settings = await getAppSettings();
+console.log('Settings:', settings);
+```
+
+#### Formatting Not Applied
+```typescript
+// Ensure component uses settings context
+import { useSettings } from '~/contexts/SettingsContext';
+
+function MyComponent() {
+ const { formatCurrency } = useSettings();
+ // Use formatCurrency instead of hardcoded formatting
+}
+```
+
+#### Database Errors
+```bash
+# Reset settings table
+npx prisma db push --force-reset
+npx tsx prisma/settingsSeed.ts
+```
+
+### Debug Mode
+Enable debug logging in settings management:
+
+```typescript
+// In settings-management.server.ts
+const DEBUG = process.env.NODE_ENV === 'development';
+
+if (DEBUG) {
+ console.log('Settings loaded:', settings);
+}
+```
+
+## 📚 Related Documentation
+- [Database Schema](./prisma/schema.prisma)
+- [Authentication System](./app/lib/auth-middleware.server.ts)
+- [Component Architecture](./app/components/README.md)
+- [Localization Guide](./LOCALIZATION.md)
+
+---
+
+**Last Updated**: November 9, 2025
+**Version**: 1.0.0
+**Maintainer**: Car MMS Development Team
\ No newline at end of file
diff --git a/SETTINGS_TESTING_GUIDE.md b/SETTINGS_TESTING_GUIDE.md
new file mode 100644
index 0000000..50786e8
--- /dev/null
+++ b/SETTINGS_TESTING_GUIDE.md
@@ -0,0 +1,144 @@
+# 🧪 Settings System Testing Guide
+
+## ✅ Setup Complete!
+
+The settings system is now fully operational. Here's how to test it:
+
+## 🔗 Access URLs
+- **Application**: http://localhost:5174/
+- **Settings Page**: http://localhost:5174/settings (Admin only)
+
+## 🧪 Testing Steps
+
+### 1. **Login as Admin**
+```
+URL: http://localhost:5174/login
+Credentials: Use admin account (authLevel 2 or higher)
+```
+
+### 2. **Access Settings Page**
+```
+URL: http://localhost:5174/settings
+Expected: Settings management interface with:
+- Date format options (ar-SA / en-US)
+- Currency selection (JOD, USD, EUR, SAR, AED)
+- Number format options (ar-SA / en-US)
+- Currency symbol input
+- Real-time preview section
+```
+
+### 3. **Test Date Format Changes**
+1. Change date format from Arabic to English
+2. Click "حفظ الإعدادات" (Save Settings)
+3. Navigate to maintenance visits, vehicles, or customers
+4. Verify dates now show in English format (11/9/2025 vs ٩/١١/٢٠٢٥)
+
+### 4. **Test Currency Changes**
+1. Change currency from JOD to USD
+2. Update currency symbol from "د.أ" to "$"
+3. Save settings
+4. Check maintenance visit costs and financial displays
+5. Verify format changes from "1,234.56 د.أ" to "1,234.56 $"
+
+### 5. **Test Number Format Changes**
+1. Change number format from Arabic to English
+2. Save settings
+3. Check kilometer displays and numeric values
+4. Verify format changes from "١٬٢٣٤" to "1,234"
+
+### 6. **Test Real-time Preview**
+- Change any setting and observe the preview section
+- Should show immediate formatting changes before saving
+
+## 📍 Where to See Changes
+
+### **Maintenance Visits** (`/maintenance-visits`)
+- Visit costs (currency formatting)
+- Visit dates (date formatting)
+- Kilometer readings (number formatting)
+
+### **Vehicle Details** (`/vehicles`)
+- Last visit dates
+- Creation/update dates
+- Maintenance history costs
+
+### **Customer Details** (`/customers`)
+- Customer creation dates
+- Last update dates
+- Associated vehicle information
+
+## 🎯 Expected Behavior
+
+### **Arabic Settings (ar-SA)**
+```
+Date: ٩/١١/٢٠٢٥
+Currency: ١٬٢٣٤٫٥٦ د.أ
+Numbers: ١٬٢٣٤٫٥٦
+Kilometers: ٤٥٬٠٠٠ كم
+```
+
+### **English Settings (en-US)**
+```
+Date: 11/9/2025
+Currency: 1,234.56 $
+Numbers: 1,234.56
+Kilometers: 45,000 كم
+```
+
+## 🔧 Admin Features
+
+### **Settings Management**
+- ✅ Real-time preview of changes
+- ✅ Form validation
+- ✅ Reset to defaults button
+- ✅ Success/error messages
+- ✅ Admin-only access control
+
+### **Supported Currencies**
+- **JOD** (د.أ) - Jordanian Dinar
+- **USD** ($) - US Dollar
+- **EUR** (€) - Euro
+- **SAR** (ر.س) - Saudi Riyal
+- **AED** (د.إ) - UAE Dirham
+
+## 🚨 Troubleshooting
+
+### **Settings Page Not Accessible**
+- Ensure you're logged in as admin (authLevel 2+)
+- Check URL: http://localhost:5174/settings
+
+### **Changes Not Appearing**
+- Refresh the page after saving settings
+- Check browser console for errors
+- Verify settings were saved (check preview section)
+
+### **Formatting Not Applied**
+- Clear browser cache
+- Restart development server
+- Check that components use `useSettings()` hook
+
+## ✨ Success Indicators
+
+When working correctly, you should see:
+
+1. **Settings page loads** without errors
+2. **Preview updates** in real-time as you change settings
+3. **Save confirmation** message appears after updating
+4. **Formatting changes** appear across all pages immediately
+5. **Consistent formatting** throughout the application
+
+## 🎉 Features Verified
+
+- ✅ Centralized settings management
+- ✅ Real-time formatting preview
+- ✅ Admin access control
+- ✅ Database persistence
+- ✅ Error handling and fallbacks
+- ✅ Cross-component formatting consistency
+- ✅ Arabic/English localization support
+
+---
+
+**Status**: Ready for Testing
+**Server**: http://localhost:5174/
+**Settings**: http://localhost:5174/settings
\ No newline at end of file
diff --git a/app/components/README.md b/app/components/README.md
new file mode 100644
index 0000000..e5d4e8d
--- /dev/null
+++ b/app/components/README.md
@@ -0,0 +1,363 @@
+# Reusable Form Components and Validation
+
+This document describes the enhanced reusable form components and validation utilities implemented for the car maintenance management system.
+
+## Overview
+
+The system now includes a comprehensive set of reusable form components with RTL support, client-side and server-side validation, and enhanced data table functionality with Arabic text support.
+
+## Components
+
+### Form Input Components
+
+#### Input Component (`app/components/ui/Input.tsx`)
+Enhanced input component with RTL support, validation, and icons.
+
+```tsx
+import { Input } from '~/components/ui/Input';
+
+ }
+ required
+/>
+```
+
+**Props:**
+- `label`: Field label
+- `error`: Error message to display
+- `helperText`: Helper text below input
+- `startIcon`/`endIcon`: Icons for input decoration
+- `fullWidth`: Whether input takes full width (default: true)
+- All standard HTML input props
+
+#### Select Component (`app/components/ui/Select.tsx`)
+Dropdown select component with RTL support and validation.
+
+```tsx
+import { Select } from '~/components/ui/Select';
+
+
+```
+
+**Props:**
+- `options`: Array of `{ value, label, disabled? }` objects
+- `placeholder`: Placeholder text
+- Other props same as Input component
+
+#### Textarea Component (`app/components/ui/Textarea.tsx`)
+Multi-line text input with RTL support.
+
+```tsx
+import { Textarea } from '~/components/ui/Textarea';
+
+
+```
+
+**Props:**
+- `resize`: Resize behavior ('none', 'vertical', 'horizontal', 'both')
+- `rows`: Number of visible rows
+- Other props same as Input component
+
+#### FormField Component (`app/components/ui/FormField.tsx`)
+Wrapper component for consistent field styling and validation display.
+
+```tsx
+import { FormField } from '~/components/ui/FormField';
+
+
+
+
+```
+
+### Form Layout Components
+
+#### Form Component (`app/components/ui/Form.tsx`)
+Main form wrapper with title, description, and error handling.
+
+```tsx
+import { Form, FormActions, FormSection, FormGrid } from '~/components/ui/Form';
+
+
+```
+
+**Components:**
+- `Form`: Main form wrapper
+- `FormActions`: Action buttons container
+- `FormSection`: Grouped form fields with title
+- `FormGrid`: Responsive grid layout for fields
+
+### Enhanced Data Table
+
+#### DataTable Component (`app/components/ui/DataTable.tsx`)
+Advanced data table with search, filtering, sorting, and pagination.
+
+```tsx
+import { DataTable } from '~/components/ui/DataTable';
+
+ {customer.name}
+ },
+ {
+ key: 'phone',
+ header: 'الهاتف',
+ filterable: true,
+ filterType: 'text'
+ }
+ ]}
+ searchable
+ searchPlaceholder="البحث في العملاء..."
+ filterable
+ pagination={{
+ enabled: true,
+ pageSize: 10,
+ currentPage: 1,
+ onPageChange: handlePageChange
+ }}
+ actions={{
+ label: 'الإجراءات',
+ render: (item) => (
+ edit(item)}>تعديل
+ )
+ }}
+/>
+```
+
+**Features:**
+- Search across multiple fields
+- Column-based filtering
+- Sorting with Arabic text support
+- Pagination
+- Custom action buttons
+- RTL layout support
+- Loading and empty states
+
+## Validation
+
+### Server-Side Validation (`app/lib/form-validation.ts`)
+
+Zod-based validation schemas with Arabic error messages.
+
+```tsx
+import { validateCustomerData } from '~/lib/form-validation';
+
+const result = validateCustomerData(formData);
+if (!result.success) {
+ return json({ errors: result.errors });
+}
+```
+
+**Available Validators:**
+- `validateUserData(data)`
+- `validateCustomerData(data)`
+- `validateVehicleData(data)`
+- `validateMaintenanceVisitData(data)`
+- `validateExpenseData(data)`
+
+### Client-Side Validation Hook (`app/hooks/useFormValidation.ts`)
+
+React hook for real-time form validation.
+
+```tsx
+import { useFormValidation } from '~/hooks/useFormValidation';
+import { customerSchema } from '~/lib/form-validation';
+
+const {
+ values,
+ errors,
+ isValid,
+ setValue,
+ getFieldProps,
+ validate
+} = useFormValidation({
+ schema: customerSchema,
+ initialValues: { name: '', email: '' },
+ validateOnChange: true,
+ validateOnBlur: true
+});
+
+// Use with form fields
+
+```
+
+### Validation Utilities (`app/lib/validation-utils.ts`)
+
+Utility functions for field-level validation.
+
+```tsx
+import { validateField, validateEmail, PATTERNS } from '~/lib/validation-utils';
+
+// Single field validation
+const result = validateField(value, {
+ required: true,
+ minLength: 3,
+ email: true
+});
+
+// Specific validators
+const emailResult = validateEmail('test@example.com');
+const phoneResult = validatePhone('+966501234567');
+
+// Pattern matching
+const isValidEmail = PATTERNS.email.test(email);
+```
+
+## Table Utilities (`app/lib/table-utils.ts`)
+
+Utilities for data processing with Arabic text support.
+
+```tsx
+import {
+ searchData,
+ filterData,
+ sortData,
+ processTableData
+} from '~/lib/table-utils';
+
+// Process table data with search, filter, sort, and pagination
+const result = processTableData(
+ data,
+ {
+ search: 'محمد',
+ filters: { status: 'active' },
+ sort: { key: 'name', direction: 'asc' },
+ pagination: { page: 1, pageSize: 10 }
+ },
+ ['name', 'email'] // searchable fields
+);
+```
+
+## Example Forms
+
+### Enhanced Customer Form (`app/components/forms/EnhancedCustomerForm.tsx`)
+
+Complete example showing all components working together:
+
+```tsx
+import { EnhancedCustomerForm } from '~/components/forms/EnhancedCustomerForm';
+
+ setShowForm(false)}
+ errors={actionData?.errors}
+ isLoading={navigation.state === 'submitting'}
+ onSubmit={(data) => submit(data, { method: 'post' })}
+/>
+```
+
+### Enhanced Vehicle Form (`app/components/forms/EnhancedVehicleForm.tsx`)
+
+Complex form with multiple sections and validation:
+
+```tsx
+import { EnhancedVehicleForm } from '~/components/forms/EnhancedVehicleForm';
+
+ setShowForm(false)}
+ errors={actionData?.errors}
+ isLoading={isSubmitting}
+/>
+```
+
+## Features
+
+### RTL Support
+- All components support right-to-left layout
+- Arabic text rendering and alignment
+- Proper icon and element positioning
+
+### Validation
+- Client-side real-time validation
+- Server-side validation with Zod schemas
+- Arabic error messages
+- Field-level and form-level validation
+
+### Accessibility
+- Proper ARIA labels and descriptions
+- Keyboard navigation support
+- Screen reader compatibility
+- Focus management
+
+### Performance
+- Memoized components to prevent unnecessary re-renders
+- Debounced search functionality
+- Efficient data processing utilities
+- Lazy loading for large datasets
+
+## Usage Guidelines
+
+1. **Always use FormField wrapper** for consistent styling and error display
+2. **Implement both client and server validation** for security and UX
+3. **Use the validation hook** for real-time feedback
+4. **Leverage table utilities** for consistent data processing
+5. **Follow RTL design patterns** for Arabic text and layout
+6. **Test with Arabic content** to ensure proper rendering
+
+## Migration from Old Components
+
+To migrate existing forms to use the new components:
+
+1. Replace basic inputs with the new Input/Select/Textarea components
+2. Wrap fields with FormField for consistent styling
+3. Add validation using the useFormValidation hook
+4. Update data tables to use the enhanced DataTable component
+5. Use Form layout components for better structure
+
+## Testing
+
+All components include comprehensive tests:
+- `app/lib/__tests__/form-validation.test.ts`
+- `app/lib/__tests__/validation-utils.test.ts`
+
+Run tests with:
+```bash
+npm run test -- --run app/lib/__tests__/form-validation.test.ts
+```
\ No newline at end of file
diff --git a/app/components/customers/CustomerDetailsView.tsx b/app/components/customers/CustomerDetailsView.tsx
new file mode 100644
index 0000000..45959e6
--- /dev/null
+++ b/app/components/customers/CustomerDetailsView.tsx
@@ -0,0 +1,313 @@
+import { Link } from "@remix-run/react";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import { useSettings } from "~/contexts/SettingsContext";
+import { PAYMENT_STATUS_NAMES } from "~/lib/constants";
+import type { CustomerWithVehicles } from "~/types/database";
+
+interface CustomerDetailsViewProps {
+ customer: CustomerWithVehicles;
+ onEdit: () => void;
+ onClose: () => void;
+}
+
+export function CustomerDetailsView({
+ customer,
+ onEdit,
+ onClose,
+}: CustomerDetailsViewProps) {
+ const { formatDate, formatCurrency } = useSettings();
+ return (
+
+ {/* Enhanced Basic Information Section */}
+
+
+
+ 👤
+ المعلومات الأساسية
+
+
+ العميل #{customer.id}
+
+
+
+
+
+
اسم العميل
+
{customer.name}
+
+
+
+
+
+
+
+
تاريخ الإنشاء
+
+ {formatDate(customer.createdDate)}
+
+
+
+
+
آخر تحديث
+
+ {formatDate(customer.updateDate)}
+
+
+
+ {customer.address && (
+
+
العنوان
+
{customer.address}
+
+ )}
+
+
+
+ {/* Customer Vehicles Section */}
+
+
+
+
+ 🚗
+
+ مركبات العميل ({customer.vehicles.length})
+
+
+
+ {customer.vehicles.length > 0 && (
+
+ 🔍
+ عرض جميع المركبات
+
+ )}
+
+
+
+
+ {customer.vehicles.length === 0 ? (
+
+
🚗
+
لا توجد مركبات مسجلة
+
لم يتم تسجيل أي مركبات لهذا العميل بعد
+
+ إضافة مركبة جديدة
+
+
+ ) : (
+
+ {customer.vehicles.map((vehicle) => (
+
+
+
+
+ {vehicle.plateNumber}
+
+
#{vehicle.id}
+
+
+ انقر للعرض
+
+
+
+
+
+ الصانع:
+ {vehicle.manufacturer}
+
+
+ الموديل:
+ {vehicle.model}
+
+
+ سنة الصنع:
+ {vehicle.year}
+
+
+ آخر زيارة:
+
+ {vehicle.lastVisitDate
+ ? formatDate(vehicle.lastVisitDate)
+ : لا توجد زيارات
+ }
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Latest Maintenance Visits Section */}
+
+
+
+
+ 🔧
+
+ آخر زيارات الصيانة ({customer.maintenanceVisits.length > 3 ? '3 من ' + customer.maintenanceVisits.length : customer.maintenanceVisits.length})
+
+
+
+ {customer.maintenanceVisits.length > 0 && (
+
+ 📋
+ عرض جميع الزيارات
+
+ )}
+
+
+
+
+ {customer.maintenanceVisits.length === 0 ? (
+
+
🔧
+
لا توجد زيارات صيانة
+
لم يتم تسجيل أي زيارات صيانة لهذا العميل بعد
+
+ ابدأ بتسجيل أول زيارة صيانة لتتبع تاريخ الخدمات المقدمة
+
+
+ تسجيل زيارة صيانة جديدة
+
+
+ ) : (
+
+ {customer.maintenanceVisits.slice(0, 3).map((visit) => (
+
+
+
+
+ {(() => {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ return jobs.length > 1
+ ? `${jobs.length} أعمال صيانة`
+ : jobs[0]?.job || 'نوع صيانة غير محدد';
+ } catch {
+ return 'نوع صيانة غير محدد';
+ }
+ })()}
+
+
زيارة #{visit.id}
+
+
+
+ {formatCurrency(visit.cost)}
+
+
+
+
+
+
+ تاريخ الزيارة:
+
+ {formatDate(visit.visitDate)}
+
+
+
+ المركبة:
+
+ {visit.vehicle?.plateNumber || ''}
+
+
+ {visit.description && (
+
+
الوصف:
+
{visit.description}
+
+ )}
+
+
+ ))}
+
+ {customer.maintenanceVisits.length > 3 && (
+
+
+ عرض 3 من أصل {customer.maintenanceVisits.length} زيارة صيانة
+
+
+
📋
+ عرض جميع الزيارات ({customer.maintenanceVisits.length})
+
+
+ )}
+
+ )}
+
+
+
+ {/* Action Buttons */}
+
+
+ ✏️
+ تعديل العميل
+
+
+
+ إغلاق
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/customers/CustomerForm.tsx b/app/components/customers/CustomerForm.tsx
new file mode 100644
index 0000000..80d0b96
--- /dev/null
+++ b/app/components/customers/CustomerForm.tsx
@@ -0,0 +1,183 @@
+import { Form } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { Input } from "~/components/ui/Input";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import type { Customer } from "~/types/database";
+
+interface CustomerFormProps {
+ customer?: Customer;
+ onCancel: () => void;
+ errors?: Record;
+ isLoading: boolean;
+}
+
+export function CustomerForm({
+ customer,
+ onCancel,
+ errors = {},
+ isLoading,
+}: CustomerFormProps) {
+ const [formData, setFormData] = useState({
+ name: customer?.name || "",
+ phone: customer?.phone || "",
+ email: customer?.email || "",
+ address: customer?.address || "",
+ });
+
+ // Reset form data when customer changes
+ useEffect(() => {
+ if (customer) {
+ setFormData({
+ name: customer.name || "",
+ phone: customer.phone || "",
+ email: customer.email || "",
+ address: customer.address || "",
+ });
+ } else {
+ setFormData({
+ name: "",
+ phone: "",
+ email: "",
+ address: "",
+ });
+ }
+ }, [customer]);
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData(prev => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const isEditing = !!customer;
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/customers/CustomerList.tsx b/app/components/customers/CustomerList.tsx
new file mode 100644
index 0000000..ab50940
--- /dev/null
+++ b/app/components/customers/CustomerList.tsx
@@ -0,0 +1,282 @@
+import { Form } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import { useSettings } from "~/contexts/SettingsContext";
+import type { CustomerWithVehicles } from "~/types/database";
+
+interface CustomerListProps {
+ customers: CustomerWithVehicles[];
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ onViewCustomer: (customer: CustomerWithVehicles) => void;
+ onEditCustomer: (customer: CustomerWithVehicles) => void;
+ isLoading: boolean;
+ actionData?: any;
+}
+
+export function CustomerList({
+ customers,
+ currentPage,
+ totalPages,
+ onPageChange,
+ onViewCustomer,
+ onEditCustomer,
+ isLoading,
+ actionData,
+}: CustomerListProps) {
+ const { formatDate } = useSettings();
+ const [deletingCustomerId, setDeletingCustomerId] = useState(null);
+
+ // Reset deleting state when delete action completes
+ useEffect(() => {
+ if (actionData?.success && actionData.action === "delete") {
+ setDeletingCustomerId(null);
+ }
+ }, [actionData]);
+
+ const columns = [
+ {
+ key: "name",
+ header: "اسم العميل",
+ render: (customer: CustomerWithVehicles) => (
+
+
{customer.name}
+
+ {/* العميل رقم: {customer.id} */}
+
+
+ ),
+ },
+ {
+ key: "contact",
+ header: "معلومات الاتصال",
+ render: (customer: CustomerWithVehicles) => (
+
+ {customer.phone && (
+
+ 📞 {customer.phone}
+
+ )}
+ {customer.email && (
+
+ ✉️ {customer.email}
+
+ )}
+ {!customer.phone && !customer.email && (
+
+ لا توجد معلومات اتصال
+
+ )}
+
+ ),
+ },
+ {
+ key: "address",
+ header: "العنوان",
+ render: (customer: CustomerWithVehicles) => (
+
+ {customer.address || (
+ غير محدد
+ )}
+
+ ),
+ },
+ {
+ key: "vehicles",
+ header: "المركبات",
+ render: (customer: CustomerWithVehicles) => (
+
+
+ {customer.vehicles.length} مركبة
+
+ {customer.vehicles.length > 0 && (
+
+ {customer.vehicles.slice(0, 2).map((vehicle) => (
+
+ {vehicle.plateNumber} - {vehicle.manufacturer} {vehicle.model}
+
+ ))}
+ {customer.vehicles.length > 2 && (
+
+ و {customer.vehicles.length - 2} مركبة أخرى...
+
+ )}
+
+ )}
+
+ ),
+ },
+ {
+ key: "visits",
+ header: "الزيارات",
+ render: (customer: CustomerWithVehicles) => (
+
+
+ {customer.maintenanceVisits.length} زيارة
+
+ {customer.maintenanceVisits.length > 0 && (
+
+ آخر زيارة: {formatDate(customer.maintenanceVisits[0].visitDate)}
+
+ )}
+
+ ),
+ },
+ {
+ key: "createdDate",
+ header: "تاريخ الإنشاء",
+ render: (customer: CustomerWithVehicles) => (
+
+ {formatDate(customer.createdDate)}
+
+ ),
+ },
+ {
+ key: "actions",
+ header: "الإجراءات",
+ render: (customer: CustomerWithVehicles) => (
+
+ onViewCustomer(customer)}
+ >
+ عرض
+
+
+ onEditCustomer(customer)}
+ disabled={isLoading}
+ >
+ تعديل
+
+
+
+
+
+ {
+ e.preventDefault();
+ if (window.confirm("هل أنت متأكد من حذف هذا العميل؟")) {
+ setDeletingCustomerId(customer.id);
+ (e.target as HTMLButtonElement).form?.submit();
+ }
+ }}
+ >
+ {deletingCustomerId === customer.id ? "جاري الحذف..." : "حذف"}
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ {customers.length === 0 ? (
+
+
👥
+
+ لا يوجد عملاء
+
+
+ لم يتم العثور على أي عملاء. قم بإضافة عميل جديد للبدء.
+
+
+ ) : (
+ <>
+
+
+
+
+ {columns.map((column) => (
+
+ {column.header}
+
+ ))}
+
+
+
+ {customers.map((customer) => (
+
+ {columns.map((column) => (
+
+ {column.render ? column.render(customer) : String(customer[column.key as keyof CustomerWithVehicles] || '')}
+
+ ))}
+
+ ))}
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+
+
onPageChange(currentPage - 1)}
+ disabled={currentPage === 1 || isLoading}
+ className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ السابق
+
+
+
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
+ const page = i + 1;
+ return (
+ onPageChange(page)}
+ disabled={isLoading}
+ className={`px-3 py-2 text-sm font-medium rounded-md ${
+ currentPage === page
+ ? 'bg-blue-600 text-white'
+ : 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50'
+ } disabled:opacity-50 disabled:cursor-not-allowed`}
+ >
+ {page}
+
+ );
+ })}
+
+
+
onPageChange(currentPage + 1)}
+ disabled={currentPage === totalPages || isLoading}
+ className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ التالي
+
+
+
+
+ صفحة {currentPage} من {totalPages}
+
+
+
+ )}
+ >
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/expenses/ExpenseForm.tsx b/app/components/expenses/ExpenseForm.tsx
new file mode 100644
index 0000000..83002b9
--- /dev/null
+++ b/app/components/expenses/ExpenseForm.tsx
@@ -0,0 +1,195 @@
+import { Form } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { Input } from "~/components/ui/Input";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import { EXPENSE_CATEGORIES } from "~/lib/constants";
+import type { Expense } from "@prisma/client";
+
+interface ExpenseFormProps {
+ expense?: Expense;
+ onCancel: () => void;
+ errors?: Record;
+ isLoading: boolean;
+}
+
+export function ExpenseForm({
+ expense,
+ onCancel,
+ errors = {},
+ isLoading,
+}: ExpenseFormProps) {
+ const [formData, setFormData] = useState({
+ description: expense?.description || "",
+ category: expense?.category || "",
+ amount: expense?.amount?.toString() || "",
+ expenseDate: expense?.expenseDate
+ ? new Date(expense.expenseDate).toISOString().split('T')[0]
+ : new Date().toISOString().split('T')[0],
+ });
+
+ // Reset form data when expense changes
+ useEffect(() => {
+ if (expense) {
+ setFormData({
+ description: expense.description || "",
+ category: expense.category || "",
+ amount: expense.amount?.toString() || "",
+ expenseDate: expense.expenseDate
+ ? new Date(expense.expenseDate).toISOString().split('T')[0]
+ : new Date().toISOString().split('T')[0],
+ });
+ } else {
+ setFormData({
+ description: "",
+ category: "",
+ amount: "",
+ expenseDate: new Date().toISOString().split('T')[0],
+ });
+ }
+ }, [expense]);
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData(prev => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const isEditing = !!expense;
+
+ return (
+
+
+ {isEditing && (
+
+ )}
+
+ {/* Description */}
+
+
+ وصف المصروف *
+
+
handleInputChange("description", e.target.value)}
+ placeholder="أدخل وصف المصروف"
+ error={errors.description}
+ required
+ disabled={isLoading}
+ />
+ {errors.description && (
+
{errors.description}
+ )}
+
+
+ {/* Category */}
+
+
+ الفئة *
+
+
handleInputChange("category", e.target.value)}
+ required
+ disabled={isLoading}
+ className={`
+ w-full px-3 py-2 border rounded-lg shadow-sm
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
+ disabled:bg-gray-50 disabled:text-gray-500
+ ${errors.category
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }
+ `}
+ >
+ اختر الفئة
+ {EXPENSE_CATEGORIES.map((cat) => (
+
+ {cat.label}
+
+ ))}
+
+ {errors.category && (
+
{errors.category}
+ )}
+
+
+ {/* Amount */}
+
+
+ المبلغ *
+
+
handleInputChange("amount", e.target.value)}
+ placeholder="0.00"
+ error={errors.amount}
+ required
+ disabled={isLoading}
+ dir="ltr"
+ />
+ {errors.amount && (
+
{errors.amount}
+ )}
+
+
+ {/* Expense Date */}
+
+
+ تاريخ المصروف
+
+
handleInputChange("expenseDate", e.target.value)}
+ error={errors.expenseDate}
+ disabled={isLoading}
+ />
+ {errors.expenseDate && (
+
{errors.expenseDate}
+ )}
+
+
+ {/* Form Actions */}
+
+
+ إلغاء
+
+
+
+ {isLoading
+ ? (isEditing ? "جاري التحديث..." : "جاري الإنشاء...")
+ : (isEditing ? "تحديث المصروف" : "إنشاء المصروف")
+ }
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/forms/EnhancedCustomerForm.tsx b/app/components/forms/EnhancedCustomerForm.tsx
new file mode 100644
index 0000000..22d1b21
--- /dev/null
+++ b/app/components/forms/EnhancedCustomerForm.tsx
@@ -0,0 +1,199 @@
+import { useEffect } from 'react';
+import { Form as RemixForm } from "@remix-run/react";
+import { Input } from "~/components/ui/Input";
+import { Textarea } from "~/components/ui/Textarea";
+import { Button } from "~/components/ui/Button";
+import { FormField } from "~/components/ui/FormField";
+import { Form, FormActions, FormSection, FormGrid } from "~/components/ui/Form";
+import { useFormValidation } from "~/hooks/useFormValidation";
+import { customerSchema } from "~/lib/form-validation";
+import type { Customer } from "~/types/database";
+
+interface EnhancedCustomerFormProps {
+ customer?: Customer;
+ onCancel: () => void;
+ errors?: Record;
+ isLoading: boolean;
+ onSubmit?: (data: any) => void;
+}
+
+export function EnhancedCustomerForm({
+ customer,
+ onCancel,
+ errors = {},
+ isLoading,
+ onSubmit,
+}: EnhancedCustomerFormProps) {
+ const {
+ values,
+ errors: validationErrors,
+ touched,
+ isValid,
+ setValue,
+ setTouched,
+ reset,
+ validate,
+ getFieldProps,
+ } = useFormValidation({
+ schema: customerSchema,
+ initialValues: {
+ name: customer?.name || "",
+ phone: customer?.phone || "",
+ email: customer?.email || "",
+ address: customer?.address || "",
+ },
+ validateOnChange: true,
+ validateOnBlur: true,
+ });
+
+ // Reset form when customer changes
+ useEffect(() => {
+ if (customer) {
+ reset({
+ name: customer.name || "",
+ phone: customer.phone || "",
+ email: customer.email || "",
+ address: customer.address || "",
+ });
+ } else {
+ reset({
+ name: "",
+ phone: "",
+ email: "",
+ address: "",
+ });
+ }
+ }, [customer, reset]);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const { isValid: formIsValid } = validate();
+
+ if (formIsValid && onSubmit) {
+ onSubmit(values);
+ }
+ };
+
+ const isEditing = !!customer;
+ const combinedErrors = { ...validationErrors, ...errors };
+
+ return (
+
+
+ {isEditing && (
+
+ )}
+
+
+
+ {/* Customer Name */}
+
+
+
+
+ {/* Phone Number */}
+
+
+
+
+
+
+
+ {/* Email */}
+
+
+
+
+ {/* Address */}
+
+
+
+
+
+
+
+ إلغاء
+
+
+
+ {isEditing ? "تحديث العميل" : "إنشاء العميل"}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/forms/EnhancedVehicleForm.tsx b/app/components/forms/EnhancedVehicleForm.tsx
new file mode 100644
index 0000000..37e9897
--- /dev/null
+++ b/app/components/forms/EnhancedVehicleForm.tsx
@@ -0,0 +1,400 @@
+import { useEffect } from 'react';
+import { Form as RemixForm } from "@remix-run/react";
+import { Input } from "~/components/ui/Input";
+import { Select } from "~/components/ui/Select";
+import { Button } from "~/components/ui/Button";
+import { FormField } from "~/components/ui/FormField";
+import { Form, FormActions, FormSection, FormGrid } from "~/components/ui/Form";
+import { useFormValidation } from "~/hooks/useFormValidation";
+import { vehicleSchema } from "~/lib/form-validation";
+import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, BODY_TYPES, MANUFACTURERS, VALIDATION } from "~/lib/constants";
+import type { Vehicle } from "~/types/database";
+
+interface EnhancedVehicleFormProps {
+ vehicle?: Vehicle;
+ customers: { id: number; name: string; phone?: string | null }[];
+ onCancel: () => void;
+ errors?: Record;
+ isLoading: boolean;
+ onSubmit?: (data: any) => void;
+}
+
+export function EnhancedVehicleForm({
+ vehicle,
+ customers,
+ onCancel,
+ errors = {},
+ isLoading,
+ onSubmit,
+}: EnhancedVehicleFormProps) {
+ const {
+ values,
+ errors: validationErrors,
+ touched,
+ isValid,
+ setValue,
+ setTouched,
+ reset,
+ validate,
+ getFieldProps,
+ } = useFormValidation({
+ schema: vehicleSchema,
+ initialValues: {
+ plateNumber: vehicle?.plateNumber || "",
+ bodyType: vehicle?.bodyType || "",
+ manufacturer: vehicle?.manufacturer || "",
+ model: vehicle?.model || "",
+ trim: vehicle?.trim || "",
+ year: vehicle?.year || new Date().getFullYear(),
+ transmission: vehicle?.transmission || "",
+ fuel: vehicle?.fuel || "",
+ cylinders: vehicle?.cylinders || null,
+ engineDisplacement: vehicle?.engineDisplacement || null,
+ useType: vehicle?.useType || "",
+ ownerId: vehicle?.ownerId || 0,
+ },
+ validateOnChange: true,
+ validateOnBlur: true,
+ });
+
+ // Reset form when vehicle changes
+ useEffect(() => {
+ if (vehicle) {
+ reset({
+ plateNumber: vehicle.plateNumber || "",
+ bodyType: vehicle.bodyType || "",
+ manufacturer: vehicle.manufacturer || "",
+ model: vehicle.model || "",
+ trim: vehicle.trim || "",
+ year: vehicle.year || new Date().getFullYear(),
+ transmission: vehicle.transmission || "",
+ fuel: vehicle.fuel || "",
+ cylinders: vehicle.cylinders || null,
+ engineDisplacement: vehicle.engineDisplacement || null,
+ useType: vehicle.useType || "",
+ ownerId: vehicle.ownerId || 0,
+ });
+ } else {
+ reset({
+ plateNumber: "",
+ bodyType: "",
+ manufacturer: "",
+ model: "",
+ trim: "",
+ year: new Date().getFullYear(),
+ transmission: "",
+ fuel: "",
+ cylinders: null,
+ engineDisplacement: null,
+ useType: "",
+ ownerId: 0,
+ });
+ }
+ }, [vehicle, reset]);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const { isValid: formIsValid } = validate();
+
+ if (formIsValid && onSubmit) {
+ onSubmit(values);
+ }
+ };
+
+ const isEditing = !!vehicle;
+ const combinedErrors = { ...validationErrors, ...errors };
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+
+ {isEditing && (
+
+ )}
+
+
+
+ {/* Plate Number */}
+
+
+
+
+ {/* Owner */}
+
+ ({
+ value: customer.id.toString(),
+ label: `${customer.name}${customer.phone ? ` (${customer.phone})` : ''}`,
+ }))}
+ value={values.ownerId?.toString() || ''}
+ onChange={(e) => setValue('ownerId', parseInt(e.target.value) || 0)}
+ />
+
+
+
+
+
+
+ {/* Body Type */}
+
+ ({
+ value: type.value,
+ label: type.label,
+ }))}
+ {...getFieldProps('bodyType')}
+ />
+
+
+ {/* Manufacturer */}
+
+ ({
+ value: manufacturer.value,
+ label: manufacturer.label,
+ }))}
+ {...getFieldProps('manufacturer')}
+ />
+
+
+ {/* Model */}
+
+
+
+
+ {/* Trim */}
+
+
+
+
+ {/* Year */}
+
+ setValue('year', parseInt(e.target.value) || currentYear)}
+ />
+
+
+ {/* Use Type */}
+
+ ({
+ value: useType.value,
+ label: useType.label,
+ }))}
+ {...getFieldProps('useType')}
+ />
+
+
+
+
+
+
+ {/* Transmission */}
+
+ ({
+ value: transmission.value,
+ label: transmission.label,
+ }))}
+ {...getFieldProps('transmission')}
+ />
+
+
+ {/* Fuel */}
+
+ ({
+ value: fuel.value,
+ label: fuel.label,
+ }))}
+ {...getFieldProps('fuel')}
+ />
+
+
+ {/* Cylinders */}
+
+ setValue('cylinders', e.target.value ? parseInt(e.target.value) : null)}
+ />
+
+
+ {/* Engine Displacement */}
+
+ setValue('engineDisplacement', e.target.value ? parseFloat(e.target.value) : null)}
+ />
+
+
+
+
+
+
+ إلغاء
+
+
+
+ {isEditing ? "تحديث المركبة" : "إنشاء المركبة"}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/layout/Container.tsx b/app/components/layout/Container.tsx
new file mode 100644
index 0000000..f937117
--- /dev/null
+++ b/app/components/layout/Container.tsx
@@ -0,0 +1,41 @@
+import { ReactNode } from 'react';
+import { getResponsiveClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface ContainerProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
+ padding?: boolean;
+}
+
+export function Container({
+ children,
+ className = '',
+ config = {},
+ maxWidth = 'full',
+ padding = true
+}: ContainerProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const classes = getResponsiveClasses(layoutConfig);
+
+ const maxWidthClasses = {
+ sm: 'max-w-sm',
+ md: 'max-w-md',
+ lg: 'max-w-lg',
+ xl: 'max-w-xl',
+ '2xl': 'max-w-2xl',
+ full: 'max-w-full',
+ };
+
+ const paddingClass = padding ? 'px-4 sm:px-6 lg:px-8' : '';
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/layout/DashboardLayout.tsx b/app/components/layout/DashboardLayout.tsx
new file mode 100644
index 0000000..9b1e9ea
--- /dev/null
+++ b/app/components/layout/DashboardLayout.tsx
@@ -0,0 +1,170 @@
+import { ReactNode, useState, useEffect } from 'react';
+import { Form } from '@remix-run/react';
+import { Sidebar } from './Sidebar';
+import { Container } from './Container';
+import { Flex } from './Flex';
+import { Text, Button } from '../ui';
+
+interface DashboardLayoutProps {
+ children: ReactNode;
+ user: {
+ id: number;
+ name: string;
+ authLevel: number;
+ };
+}
+
+export function DashboardLayout({ children, user }: DashboardLayoutProps) {
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [isMobile, setIsMobile] = useState(false);
+
+ // Handle responsive behavior
+ useEffect(() => {
+ const checkMobile = () => {
+ const mobile = window.innerWidth < 768;
+ setIsMobile(mobile);
+
+ // Auto-collapse sidebar on mobile
+ if (mobile) {
+ setSidebarCollapsed(true);
+ }
+ };
+
+ checkMobile();
+ window.addEventListener('resize', checkMobile);
+
+ return () => window.removeEventListener('resize', checkMobile);
+ }, []);
+
+ // Load sidebar state from localStorage
+ useEffect(() => {
+ const savedState = localStorage.getItem('sidebarCollapsed');
+ if (savedState !== null && !isMobile) {
+ setSidebarCollapsed(JSON.parse(savedState));
+ }
+ }, [isMobile]);
+
+ // Save sidebar state to localStorage
+ const handleSidebarToggle = () => {
+ const newState = !sidebarCollapsed;
+ setSidebarCollapsed(newState);
+ if (!isMobile) {
+ localStorage.setItem('sidebarCollapsed', JSON.stringify(newState));
+ }
+ };
+
+ const handleMobileMenuClose = () => {
+ setMobileMenuOpen(false);
+ };
+
+ const getAuthLevelText = (authLevel: number) => {
+ switch (authLevel) {
+ case 1:
+ return "مدير عام";
+ case 2:
+ return "مدير";
+ case 3:
+ return "مستخدم";
+ default:
+ return "غير محدد";
+ }
+ };
+
+ return (
+
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+ {/* Header */}
+
+
+
+ {/* Mobile menu button and title */}
+
+ {isMobile && (
+
setMobileMenuOpen(true)}
+ className="p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+
+
+
+
+ )}
+
+ {/* Page title - only show on mobile when sidebar is closed */}
+ {isMobile && (
+
+ لوحة التحكم
+
+ )}
+
+
+ {/* User info and actions */}
+
+
+
+ مرحباً، {user.name}
+
+
+ {getAuthLevelText(user.authLevel)}
+
+
+
+
+
+
+
+
+ خروج
+
+
+
+
+
+
+
+ {/* Page Content */}
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/layout/Flex.tsx b/app/components/layout/Flex.tsx
new file mode 100644
index 0000000..e85fd60
--- /dev/null
+++ b/app/components/layout/Flex.tsx
@@ -0,0 +1,102 @@
+import { ReactNode } from 'react';
+import { getResponsiveClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface FlexProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse';
+ align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
+ wrap?: boolean;
+ gap?: 'sm' | 'md' | 'lg' | 'xl';
+ responsive?: {
+ sm?: Partial>;
+ md?: Partial>;
+ lg?: Partial>;
+ xl?: Partial>;
+ };
+}
+
+export function Flex({
+ children,
+ className = '',
+ config = {},
+ direction = 'row',
+ align = 'start',
+ justify = 'start',
+ wrap = false,
+ gap = 'md',
+ responsive = {}
+}: FlexProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const classes = getResponsiveClasses(layoutConfig);
+
+ const directionClasses = {
+ row: layoutConfig.direction === 'rtl' ? 'flex-row-reverse' : 'flex-row',
+ col: 'flex-col',
+ 'row-reverse': layoutConfig.direction === 'rtl' ? 'flex-row' : 'flex-row-reverse',
+ 'col-reverse': 'flex-col-reverse',
+ };
+
+ const alignClasses = {
+ start: 'items-start',
+ center: 'items-center',
+ end: 'items-end',
+ stretch: 'items-stretch',
+ baseline: 'items-baseline',
+ };
+
+ const justifyClasses = {
+ start: 'justify-start',
+ center: 'justify-center',
+ end: 'justify-end',
+ between: 'justify-between',
+ around: 'justify-around',
+ evenly: 'justify-evenly',
+ };
+
+ const gapClasses = {
+ sm: 'gap-2',
+ md: 'gap-4',
+ lg: 'gap-6',
+ xl: 'gap-8',
+ };
+
+ const wrapClass = wrap ? 'flex-wrap' : '';
+
+ // Build responsive classes
+ const responsiveClasses = Object.entries(responsive)
+ .map(([breakpoint, props]) => {
+ const responsiveClassList = [];
+
+ if (props.direction) {
+ const responsiveDirection = props.direction === 'row' && layoutConfig.direction === 'rtl'
+ ? 'flex-row-reverse'
+ : props.direction === 'row-reverse' && layoutConfig.direction === 'rtl'
+ ? 'flex-row'
+ : directionClasses[props.direction];
+ responsiveClassList.push(`${breakpoint}:${responsiveDirection}`);
+ }
+
+ if (props.align) {
+ responsiveClassList.push(`${breakpoint}:${alignClasses[props.align]}`);
+ }
+
+ if (props.justify) {
+ responsiveClassList.push(`${breakpoint}:${justifyClasses[props.justify]}`);
+ }
+
+ return responsiveClassList.join(' ');
+ })
+ .join(' ');
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/layout/Grid.tsx b/app/components/layout/Grid.tsx
new file mode 100644
index 0000000..17bfeb4
--- /dev/null
+++ b/app/components/layout/Grid.tsx
@@ -0,0 +1,56 @@
+import { ReactNode } from 'react';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface GridProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ cols?: 1 | 2 | 3 | 4 | 6 | 12;
+ gap?: 'sm' | 'md' | 'lg' | 'xl';
+ responsive?: {
+ sm?: 1 | 2 | 3 | 4 | 6 | 12;
+ md?: 1 | 2 | 3 | 4 | 6 | 12;
+ lg?: 1 | 2 | 3 | 4 | 6 | 12;
+ xl?: 1 | 2 | 3 | 4 | 6 | 12;
+ };
+}
+
+export function Grid({
+ children,
+ className = '',
+ config = {},
+ cols = 1,
+ gap = 'md',
+ responsive = {}
+}: GridProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const colsClasses = {
+ 1: 'grid-cols-1',
+ 2: 'grid-cols-2',
+ 3: 'grid-cols-3',
+ 4: 'grid-cols-4',
+ 6: 'grid-cols-6',
+ 12: 'grid-cols-12',
+ };
+
+ const gapClasses = {
+ sm: 'gap-2',
+ md: 'gap-4',
+ lg: 'gap-6',
+ xl: 'gap-8',
+ };
+
+ const responsiveClasses = Object.entries(responsive)
+ .map(([breakpoint, cols]) => `${breakpoint}:${colsClasses[cols]}`)
+ .join(' ');
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..295fbba
--- /dev/null
+++ b/app/components/layout/Sidebar.tsx
@@ -0,0 +1,257 @@
+import { ReactNode, useState, useEffect } from 'react';
+import { Link, useLocation } from '@remix-run/react';
+import { getResponsiveClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface SidebarProps {
+ isCollapsed: boolean;
+ onToggle: () => void;
+ isMobile: boolean;
+ isOpen: boolean;
+ onClose: () => void;
+ userAuthLevel: number;
+}
+
+interface NavigationItem {
+ name: string;
+ href: string;
+ icon: ReactNode;
+ authLevel?: number; // Minimum auth level required
+}
+
+const navigationItems: NavigationItem[] = [
+ {
+ name: 'لوحة التحكم',
+ href: '/dashboard',
+ icon: (
+
+
+
+
+ ),
+ },
+ {
+ name: 'العملاء',
+ href: '/customers',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ name: 'المركبات',
+ href: '/vehicles',
+ icon: (
+
+
+
+
+ ),
+ },
+ {
+ name: 'زيارات الصيانة',
+ href: '/maintenance-visits',
+ icon: (
+
+
+
+
+ ),
+ },
+ {
+ name: 'المصروفات',
+ href: '/expenses',
+ icon: (
+
+
+
+ ),
+ authLevel: 2, // Admin and above
+ },
+ {
+ name: 'التقارير المالية',
+ href: '/financial-reports',
+ icon: (
+
+
+
+ ),
+ authLevel: 2, // Admin and above
+ },
+ {
+ name: 'إدارة المستخدمين',
+ href: '/users',
+ icon: (
+
+
+
+ ),
+ authLevel: 2, // Admin and above
+ },
+ {
+ name: 'إعدادات النظام',
+ href: '/settings',
+ icon: (
+
+
+
+
+ ),
+ authLevel: 2, // Admin and above
+ },
+];
+
+export function Sidebar({ isCollapsed, onToggle, isMobile, isOpen, onClose, userAuthLevel }: SidebarProps) {
+ const location = useLocation();
+
+ // Filter navigation items based on user auth level
+ const filteredNavItems = navigationItems.filter(item =>
+ !item.authLevel || userAuthLevel <= item.authLevel
+ );
+
+ // Close sidebar on route change for mobile
+ useEffect(() => {
+ if (isMobile && isOpen) {
+ onClose();
+ }
+ }, [location.pathname, isMobile, isOpen, onClose]);
+
+ if (isMobile) {
+ return (
+ <>
+ {/* Mobile overlay */}
+ {isOpen && (
+
+ )}
+
+ {/* Mobile Sidebar */}
+
+ {/* Mobile Header */}
+
+
+ {/* Mobile Navigation */}
+
+
+ {filteredNavItems.map((item) => {
+ const isActive = location.pathname === item.href;
+ return (
+
+ {isActive &&
}
+
+ {item.icon}
+
+
{item.name}
+
+ );
+ })}
+
+
+
+ >
+ );
+ }
+
+ // Desktop Sidebar
+ return (
+
+ {/* Desktop Header */}
+
+
+
+ {!isCollapsed && (
+
نظام الصيانة
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Desktop Navigation */}
+
+
+ {filteredNavItems.map((item) => {
+ const isActive = location.pathname === item.href;
+ return (
+
+ {isActive &&
}
+
+ {item.icon}
+
+ {!isCollapsed && (
+
{item.name}
+ )}
+
+ );
+ })}
+
+
+
+ {/* Desktop Footer */}
+ {!isCollapsed && (
+
+
+ نظام إدارة صيانة السيارات
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/layout/index.ts b/app/components/layout/index.ts
new file mode 100644
index 0000000..3668ec5
--- /dev/null
+++ b/app/components/layout/index.ts
@@ -0,0 +1,5 @@
+export { Container } from './Container';
+export { Grid } from './Grid';
+export { Flex } from './Flex';
+export { Sidebar } from './Sidebar';
+export { DashboardLayout } from './DashboardLayout';
\ No newline at end of file
diff --git a/app/components/maintenance-visits/MaintenanceVisitDetailsView.tsx b/app/components/maintenance-visits/MaintenanceVisitDetailsView.tsx
new file mode 100644
index 0000000..661b885
--- /dev/null
+++ b/app/components/maintenance-visits/MaintenanceVisitDetailsView.tsx
@@ -0,0 +1,311 @@
+import { Link } from "@remix-run/react";
+import { useSettings } from "~/contexts/SettingsContext";
+import type { MaintenanceVisitWithRelations } from "~/types/database";
+
+interface MaintenanceVisitDetailsViewProps {
+ visit: MaintenanceVisitWithRelations;
+}
+
+export function MaintenanceVisitDetailsView({ visit }: MaintenanceVisitDetailsViewProps) {
+ const { formatCurrency, formatDate, formatNumber } = useSettings();
+ const getPaymentStatusColor = (status: string) => {
+ switch (status) {
+ case 'paid':
+ return 'bg-green-100 text-green-800 border-green-200';
+ case 'pending':
+ return 'bg-yellow-100 text-yellow-800 border-yellow-200';
+ case 'partial':
+ return 'bg-blue-100 text-blue-800 border-blue-200';
+ case 'cancelled':
+ return 'bg-red-100 text-red-800 border-red-200';
+ default:
+ return 'bg-gray-100 text-gray-800 border-gray-200';
+ }
+ };
+
+ const getPaymentStatusLabel = (status: string) => {
+ switch (status) {
+ case 'paid': return 'مدفوع';
+ case 'pending': return 'معلق';
+ case 'partial': return 'مدفوع جزئياً';
+ case 'cancelled': return 'ملغي';
+ default: return status;
+ }
+ };
+
+ const getDelayLabel = (months: number) => {
+ const delayOptions = [
+ { value: 1, label: 'شهر واحد' },
+ { value: 2, label: 'شهرين' },
+ { value: 3, label: '3 أشهر' },
+ { value: 4, label: '4 أشهر' },
+ { value: 6, label: '6 أشهر' },
+ { value: 12, label: 'سنة واحدة' },
+ ];
+ const option = delayOptions.find(opt => opt.value === months);
+ return option ? option.label : `${months} أشهر`;
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
+
+
🔧
+
+
+ زيارة صيانة #{visit.id}
+
+
+ {formatDate(visit.visitDate)}
+
+
+
+
+
+ {formatCurrency(visit.cost)}
+
+
+ {getPaymentStatusLabel(visit.paymentStatus)}
+
+
+
+
+
+ {/* Main Details Grid */}
+
+ {/* Visit Information */}
+
+
+
+ 📋
+ تفاصيل الزيارة
+
+
+
+
+
أعمال الصيانة المنجزة
+
+ {(() => {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ return jobs.map((job: any, index: number) => (
+
+
+
+
{job.job}
+ {job.notes && (
+
{job.notes}
+ )}
+
+
+
+ #{index + 1}
+
+ {job.cost !== undefined && (
+
+ {formatCurrency(job.cost)}
+
+ )}
+
+
+
+ ));
+ } catch {
+ return (
+
+
لا توجد تفاصيل أعمال الصيانة
+
+ );
+ }
+ })()}
+
+
+
+
+
+
عداد الكيلومترات
+
+ {formatNumber(visit.kilometers)} كم
+
+
+
+
الزيارة التالية بعد
+
+ {getDelayLabel(visit.nextVisitDelay)}
+
+
+
+
+
+
وصف الأعمال المنجزة
+
+
+ {visit.description}
+
+
+
+
+
+
+ {/* Vehicle & Customer Information */}
+
+ {/* Vehicle Info */}
+
+
+
+ 🚗
+ معلومات المركبة
+
+
+
+
+
+
رقم اللوحة
+
+ {visit.vehicle.plateNumber}
+
+
+
+
السنة
+
+ {visit.vehicle.year}
+
+
+
+
الشركة المصنعة
+
+ {visit.vehicle.manufacturer}
+
+
+
+
الموديل
+
+ {visit.vehicle.model}
+
+
+
+
+
+
+ {/* Customer Info */}
+
+
+
+ 👤
+ معلومات العميل
+
+
+
+
+
+
اسم العميل
+
+ {visit.customer.name}
+
+
+
+ {(visit.customer.phone || visit.customer.email) && (
+
+ {visit.customer.phone && (
+
+ )}
+
+ {visit.customer.email && (
+
+ )}
+
+ )}
+
+ {visit.customer.address && (
+
+
العنوان
+
+ {visit.customer.address}
+
+
+ )}
+
+
+
+
+
+
+ {/* Income Information */}
+ {visit.income && visit.income.length > 0 && (
+
+
+
+ 💰
+ سجل الدخل
+
+
+
+
+ {visit.income.map((income) => (
+
+
+
تاريخ الدخل
+
+ {formatDate(income.incomeDate)}
+
+
+
+
المبلغ
+
+ {formatCurrency(income.amount)}
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Action Buttons */}
+
+
+ 🚗
+ عرض تفاصيل المركبة
+
+
+ 👤
+ عرض تفاصيل العميل
+
+
+ 📋
+ جميع زيارات هذه المركبة
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/maintenance-visits/MaintenanceVisitForm.tsx b/app/components/maintenance-visits/MaintenanceVisitForm.tsx
new file mode 100644
index 0000000..3a6131c
--- /dev/null
+++ b/app/components/maintenance-visits/MaintenanceVisitForm.tsx
@@ -0,0 +1,564 @@
+import { useState, useEffect } from "react";
+import { Form, useActionData, useNavigation } from "@remix-run/react";
+import { Button, Input, Select, Text, Card, CardHeader, CardBody, MultiSelect } from "~/components/ui";
+import { AutocompleteInput } from "~/components/ui/AutocompleteInput";
+import { useSettings } from "~/contexts/SettingsContext";
+import type { MaintenanceVisitWithRelations, Customer, Vehicle, MaintenanceType, MaintenanceJob } from "~/types/database";
+import { PAYMENT_STATUS_NAMES, VISIT_DELAY_OPTIONS } from "~/lib/constants";
+
+interface MaintenanceVisitFormProps {
+ visit?: MaintenanceVisitWithRelations;
+ customers: Customer[];
+ vehicles: Vehicle[];
+ maintenanceTypes: { id: number; name: string; }[];
+ onCancel?: () => void;
+}
+
+export function MaintenanceVisitForm({
+ visit,
+ customers,
+ vehicles,
+ maintenanceTypes,
+ onCancel
+}: MaintenanceVisitFormProps) {
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === "submitting";
+ const { settings } = useSettings();
+
+ // Form state
+ const [plateNumberInput, setPlateNumberInput] = useState(
+ visit?.vehicle?.plateNumber || ""
+ );
+ const [selectedCustomerId, setSelectedCustomerId] = useState(
+ visit?.customerId?.toString() || ""
+ );
+ const [selectedVehicleId, setSelectedVehicleId] = useState(
+ visit?.vehicleId?.toString() || ""
+ );
+ const [filteredVehicles, setFilteredVehicles] = useState(vehicles);
+
+ // Maintenance jobs state (with costs)
+ const [maintenanceJobs, setMaintenanceJobs] = useState(() => {
+ if (visit?.maintenanceJobs) {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ return jobs;
+ } catch {
+ return [];
+ }
+ }
+ return [];
+ });
+
+ // Current maintenance type being added
+ const [currentTypeId, setCurrentTypeId] = useState("");
+ const [currentCost, setCurrentCost] = useState("");
+
+ // Create autocomplete options for plate numbers
+ const plateNumberOptions = vehicles.map(vehicle => {
+ const customer = customers.find(c => c.id === vehicle.ownerId);
+ return {
+ value: vehicle.plateNumber,
+ label: `${vehicle.plateNumber} - ${vehicle.manufacturer} ${vehicle.model} (${customer?.name || 'غير محدد'})`,
+ data: {
+ vehicle,
+ customer
+ }
+ };
+ });
+
+ // Handle plate number selection
+ const handlePlateNumberSelect = (option: any) => {
+ const { vehicle, customer } = option.data;
+ setPlateNumberInput(vehicle.plateNumber);
+ setSelectedCustomerId(customer?.id?.toString() || "");
+ setSelectedVehicleId(vehicle.id.toString());
+ };
+
+ // Reset form state when visit prop changes (switching between create/edit modes)
+ useEffect(() => {
+ if (visit) {
+ // Editing mode - populate with visit data
+ setPlateNumberInput(visit.vehicle?.plateNumber || "");
+ setSelectedCustomerId(visit.customerId?.toString() || "");
+ setSelectedVehicleId(visit.vehicleId?.toString() || "");
+
+ // Parse maintenance jobs from JSON
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ setMaintenanceJobs(jobs);
+ } catch {
+ setMaintenanceJobs([]);
+ }
+ } else {
+ // Create mode - reset to empty state
+ setPlateNumberInput("");
+ setSelectedCustomerId("");
+ setSelectedVehicleId("");
+ setMaintenanceJobs([]);
+ setCurrentTypeId("");
+ setCurrentCost("");
+ }
+ }, [visit]);
+
+ // Filter vehicles based on selected customer
+ useEffect(() => {
+ if (selectedCustomerId) {
+ const customerId = parseInt(selectedCustomerId);
+ const customerVehicles = vehicles.filter(v => v.ownerId === customerId);
+ setFilteredVehicles(customerVehicles);
+
+ // Reset vehicle selection if current vehicle doesn't belong to selected customer
+ if (selectedVehicleId) {
+ const vehicleId = parseInt(selectedVehicleId);
+ const vehicleBelongsToCustomer = customerVehicles.some(v => v.id === vehicleId);
+ if (!vehicleBelongsToCustomer) {
+ setSelectedVehicleId("");
+ }
+ }
+ } else {
+ setFilteredVehicles(vehicles);
+ }
+ }, [selectedCustomerId, vehicles, selectedVehicleId]);
+
+ // Format date for input
+ const formatDateForInput = (date: Date | string | null) => {
+ if (!date) return "";
+ const d = new Date(date);
+ return d.toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM format
+ };
+
+ // Calculate total cost from maintenance jobs
+ const totalCost = maintenanceJobs.reduce((sum, job) => sum + job.cost, 0);
+
+ // Add maintenance job
+ const handleAddMaintenanceJob = () => {
+ if (!currentTypeId || !currentCost) return;
+
+ const typeIdNum = parseInt(currentTypeId);
+ const costNum = parseFloat(currentCost);
+
+ if (isNaN(typeIdNum) || isNaN(costNum) || costNum <= 0) return;
+
+ // Check if type already exists
+ if (maintenanceJobs.some(job => job.typeId === typeIdNum)) {
+ alert("هذا النوع من الصيانة موجود بالفعل");
+ return;
+ }
+
+ const type = maintenanceTypes.find(t => t.id === typeIdNum);
+ if (!type) return;
+
+ const newJob: MaintenanceJob = {
+ typeId: typeIdNum,
+ job: type.name,
+ cost: costNum,
+ notes: ''
+ };
+
+ setMaintenanceJobs([...maintenanceJobs, newJob]);
+ setCurrentTypeId("");
+ setCurrentCost("");
+ };
+
+ // Remove maintenance job
+ const handleRemoveMaintenanceJob = (typeId: number) => {
+ setMaintenanceJobs(maintenanceJobs.filter(job => job.typeId !== typeId));
+ };
+
+ return (
+
+
+
+ {visit ? "تعديل زيارة الصيانة" : "إضافة زيارة صيانة جديدة"}
+
+
+
+
+ {visit && (
+
+ )}
+
+ {/* Plate Number Autocomplete - Only show for new visits */}
+ {!visit && (
+
+
+
+ ابدأ بكتابة رقم اللوحة لاختيار المركبة والعميل تلقائياً
+
+
+ )}
+
+
+ {/* Customer Selection */}
+
+
+
+ العميل
+ *
+
+
setSelectedCustomerId(e.target.value)}
+ required
+ disabled={false}
+ className={`w-full px-3 py-2 border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 appearance-none bg-white ${actionData?.errors?.customerId
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }`}
+ >
+ اختر العميل
+ {customers.map((customer) => (
+
+ {customer.name}{customer.phone ? ` - ${customer.phone}` : ''}
+
+ ))}
+
+
+
+ {actionData?.errors?.customerId && (
+
+ {actionData.errors.customerId}
+
+ )}
+ {!visit && plateNumberInput && selectedCustomerId && (
+
+ تم اختيار العميل تلقائياً من رقم اللوحة
+
+ )}
+
+
+ {/* Vehicle Selection */}
+
+
+
+ المركبة
+ *
+
+
setSelectedVehicleId(e.target.value)}
+ required
+ disabled={false}
+ className={`w-full px-3 py-2 border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 appearance-none bg-white ${actionData?.errors?.vehicleId
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }`}
+ >
+ اختر المركبة
+ {filteredVehicles.map((vehicle) => (
+
+ {vehicle.plateNumber} - {vehicle.manufacturer} {vehicle.model} ({vehicle.year})
+
+ ))}
+
+
+
+ {actionData?.errors?.vehicleId && (
+
+ {actionData.errors.vehicleId}
+
+ )}
+ {!selectedCustomerId && !plateNumberInput && (
+
+ يرجى اختيار العميل أولاً أو البحث برقم اللوحة
+
+ )}
+ {!visit && plateNumberInput && selectedVehicleId && (
+
+ تم اختيار المركبة تلقائياً من رقم اللوحة
+
+ )}
+
+
+
+ {/* Maintenance Types Selection */}
+
+
+ أنواع الصيانة
+ *
+
+
+ {/* Add Maintenance Type Form */}
+
+
+ setCurrentTypeId(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ >
+ اختر نوع الصيانة
+ {maintenanceTypes
+ .filter(type => !maintenanceJobs.some(job => job.typeId === type.id))
+ .map((type) => (
+
+ {type.name}
+
+ ))}
+
+
+
+ setCurrentCost(e.target.value)}
+ placeholder="التكلفة"
+ step="0.01"
+ min="0"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+ إضافة
+
+
+
+ {/* List of Added Maintenance Types */}
+ {maintenanceJobs.length > 0 && (
+
+ {maintenanceJobs.map((job) => (
+
+
+ {job.job}
+
+
+
{job.cost.toFixed(2)} {settings.currency}
+
handleRemoveMaintenanceJob(job.typeId)}
+ className="text-red-600 hover:text-red-800"
+ >
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {maintenanceJobs.length === 0 && (
+
+ لم يتم إضافة أي نوع صيانة بعد
+
+ )}
+
+ {/* Hidden input to pass maintenance jobs data */}
+
+
+ {actionData?.errors?.maintenanceJobs && (
+
+ {actionData.errors.maintenanceJobs}
+
+ )}
+
+
+
+ {/* Payment Status */}
+
+ ({
+ value: value,
+ label: label
+ }))}
+ />
+
+
+
+ {/* Description */}
+
+
+ وصف الصيانة
+ *
+
+
+ {actionData?.errors?.description && (
+
+ {actionData.errors.description}
+
+ )}
+
+
+
+ {/* Cost */}
+
+
+
+ يتم حساب التكلفة تلقائياً من أنواع الصيانة المضافة
+
+
+
+ {/* Kilometers */}
+
+
+
+
+ {/* Next Visit Delay */}
+
+ ({
+ value: option.value.toString(),
+ label: option.label
+ }))}
+ />
+
+
+
+ {/* Visit Date */}
+
+
+
+
+ {/* Action Buttons */}
+ {/* Debug Info */}
+ {!visit && (
+
+ Debug Info:
+ Customer ID: {selectedCustomerId || "Not selected"}
+ Vehicle ID: {selectedVehicleId || "Not selected"}
+ Plate Number: {plateNumberInput || "Not entered"}
+ Selected Maintenance Types: {maintenanceJobs.length} types
+ Types: {maintenanceJobs.map(j => j.job).join(', ') || "None selected"}
+
+ )}
+
+
+ {onCancel && (
+
+ إلغاء
+
+ )}
+ {
+ // Client-side validation before submission
+ if (!visit) {
+ const form = e.currentTarget.form;
+ if (!form) return;
+
+ const formData = new FormData(form);
+ const customerId = formData.get("customerId") as string;
+ const vehicleId = formData.get("vehicleId") as string;
+ const description = formData.get("description") as string;
+ const cost = formData.get("cost") as string;
+ const kilometers = formData.get("kilometers") as string;
+
+ const hasValidCustomer = customerId && customerId !== "";
+ const hasValidVehicle = vehicleId && vehicleId !== "";
+ const hasValidJobs = maintenanceJobs.length > 0;
+ const hasValidDescription = description && description.trim() !== "";
+ const hasValidCost = cost && cost.trim() !== "" && parseFloat(cost) > 0;
+ const hasValidKilometers = kilometers && kilometers.trim() !== "" && parseInt(kilometers) >= 0;
+
+ const missingFields = [];
+ if (!hasValidCustomer) missingFields.push("العميل");
+ if (!hasValidVehicle) missingFields.push("المركبة");
+ if (!hasValidJobs) missingFields.push("نوع صيانة واحد على الأقل");
+ if (!hasValidDescription) missingFields.push("وصف الصيانة");
+ if (!hasValidCost) missingFields.push("التكلفة");
+ if (!hasValidKilometers) missingFields.push("عدد الكيلومترات");
+
+ if (missingFields.length > 0) {
+ e.preventDefault();
+ alert(`يرجى ملء الحقول المطلوبة التالية:\n- ${missingFields.join('\n- ')}`);
+ return;
+ }
+ }
+ }}
+ >
+ {isSubmitting
+ ? visit
+ ? "جاري التحديث..."
+ : "جاري الحفظ..."
+ : visit
+ ? "تحديث الزيارة"
+ : "حفظ الزيارة"}
+
+
+
+
+
+ );
+}
diff --git a/app/components/maintenance-visits/MaintenanceVisitForm.tsx.backup b/app/components/maintenance-visits/MaintenanceVisitForm.tsx.backup
new file mode 100644
index 0000000..7f71ad7
--- /dev/null
+++ b/app/components/maintenance-visits/MaintenanceVisitForm.tsx.backup
@@ -0,0 +1,462 @@
+import { useState, useEffect } from "react";
+import { Form, useActionData, useNavigation } from "@remix-run/react";
+import { Button, Input, Select, Text, Card, CardHeader, CardBody, MultiSelect } from "~/components/ui";
+import { AutocompleteInput } from "~/components/ui/AutocompleteInput";
+import type { MaintenanceVisitWithRelations, Customer, Vehicle, MaintenanceType, MaintenanceJob } from "~/types/database";
+import { PAYMENT_STATUS_NAMES, VISIT_DELAY_OPTIONS } from "~/lib/constants";
+
+interface MaintenanceVisitFormProps {
+ visit?: MaintenanceVisitWithRelations;
+ customers: Customer[];
+ vehicles: Vehicle[];
+ maintenanceTypes: { id: number; name: string; }[];
+ onCancel?: () => void;
+}
+
+export function MaintenanceVisitForm({
+ visit,
+ customers,
+ vehicles,
+ maintenanceTypes,
+ onCancel
+}: MaintenanceVisitFormProps) {
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === "submitting";
+
+ // Form state
+ const [plateNumberInput, setPlateNumberInput] = useState(
+ visit?.vehicle?.plateNumber || ""
+ );
+ const [selectedCustomerId, setSelectedCustomerId] = useState(
+ visit?.customerId?.toString() || ""
+ );
+ const [selectedVehicleId, setSelectedVehicleId] = useState(
+ visit?.vehicleId?.toString() || ""
+ );
+ const [filteredVehicles, setFilteredVehicles] = useState(vehicles);
+
+ // Selected maintenance types state
+ const [selectedMaintenanceTypes, setSelectedMaintenanceTypes] = useState(() => {
+ if (visit?.maintenanceJobs) {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ return jobs.map((job: MaintenanceJob) => job.typeId).filter((id: number) => id > 0);
+ } catch {
+ return [];
+ }
+ }
+ return [];
+ });
+
+ // Create autocomplete options for plate numbers
+ const plateNumberOptions = vehicles.map(vehicle => {
+ const customer = customers.find(c => c.id === vehicle.ownerId);
+ return {
+ value: vehicle.plateNumber,
+ label: `${vehicle.plateNumber} - ${vehicle.manufacturer} ${vehicle.model} (${customer?.name || 'غير محدد'})`,
+ data: {
+ vehicle,
+ customer
+ }
+ };
+ });
+
+ // Handle plate number selection
+ const handlePlateNumberSelect = (option: any) => {
+ const { vehicle, customer } = option.data;
+ setPlateNumberInput(vehicle.plateNumber);
+ setSelectedCustomerId(customer?.id?.toString() || "");
+ setSelectedVehicleId(vehicle.id.toString());
+ };
+
+ // Reset form state when visit prop changes (switching between create/edit modes)
+ useEffect(() => {
+ if (visit) {
+ // Editing mode - populate with visit data
+ setPlateNumberInput(visit.vehicle?.plateNumber || "");
+ setSelectedCustomerId(visit.customerId?.toString() || "");
+ setSelectedVehicleId(visit.vehicleId?.toString() || "");
+
+ // Parse maintenance jobs from JSON
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ const typeIds = jobs.map((job: MaintenanceJob) => job.typeId).filter((id: number) => id > 0);
+ setSelectedMaintenanceTypes(typeIds);
+ } catch {
+ setSelectedMaintenanceTypes([]);
+ }
+ } else {
+ // Create mode - reset to empty state
+ setPlateNumberInput("");
+ setSelectedCustomerId("");
+ setSelectedVehicleId("");
+ setSelectedMaintenanceTypes([]);
+ }
+ }, [visit]);
+
+ // Filter vehicles based on selected customer
+ useEffect(() => {
+ if (selectedCustomerId) {
+ const customerId = parseInt(selectedCustomerId);
+ const customerVehicles = vehicles.filter(v => v.ownerId === customerId);
+ setFilteredVehicles(customerVehicles);
+
+ // Reset vehicle selection if current vehicle doesn't belong to selected customer
+ if (selectedVehicleId) {
+ const vehicleId = parseInt(selectedVehicleId);
+ const vehicleBelongsToCustomer = customerVehicles.some(v => v.id === vehicleId);
+ if (!vehicleBelongsToCustomer) {
+ setSelectedVehicleId("");
+ }
+ }
+ } else {
+ setFilteredVehicles(vehicles);
+ }
+ }, [selectedCustomerId, vehicles, selectedVehicleId]);
+
+ // Format date for input
+ const formatDateForInput = (date: Date | string | null) => {
+ if (!date) return "";
+ const d = new Date(date);
+ return d.toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM format
+ };
+
+ // Convert selected maintenance types to jobs format for submission
+ const getMaintenanceJobsForSubmission = () => {
+ return selectedMaintenanceTypes.map(typeId => {
+ const type = maintenanceTypes.find(t => t.id === typeId);
+ return {
+ typeId,
+ job: type?.name || '',
+ cost: 0,
+ notes: ''
+ };
+ });
+ };
+
+ return (
+
+
+
+ {visit ? "تعديل زيارة الصيانة" : "إضافة زيارة صيانة جديدة"}
+
+
+
+
+ {visit && (
+
+ )}
+
+ {/* Plate Number Autocomplete - Only show for new visits */}
+ {!visit && (
+
+
+
+ ابدأ بكتابة رقم اللوحة لاختيار المركبة والعميل تلقائياً
+
+
+ )}
+
+
+ {/* Customer Selection */}
+
+
+
+ العميل
+ *
+
+
setSelectedCustomerId(e.target.value)}
+ required
+ disabled={false}
+ className={`w-full px-3 py-2 border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 appearance-none bg-white ${actionData?.errors?.customerId
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }`}
+ >
+ اختر العميل
+ {customers.map((customer) => (
+
+ {customer.name}{customer.phone ? ` - ${customer.phone}` : ''}
+
+ ))}
+
+
+
+ {actionData?.errors?.customerId && (
+
+ {actionData.errors.customerId}
+
+ )}
+ {!visit && plateNumberInput && selectedCustomerId && (
+
+ تم اختيار العميل تلقائياً من رقم اللوحة
+
+ )}
+
+
+ {/* Vehicle Selection */}
+
+
+
+ المركبة
+ *
+
+
setSelectedVehicleId(e.target.value)}
+ required
+ disabled={false}
+ className={`w-full px-3 py-2 border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 appearance-none bg-white ${actionData?.errors?.vehicleId
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }`}
+ >
+ اختر المركبة
+ {filteredVehicles.map((vehicle) => (
+
+ {vehicle.plateNumber} - {vehicle.manufacturer} {vehicle.model} ({vehicle.year})
+
+ ))}
+
+
+
+ {actionData?.errors?.vehicleId && (
+
+ {actionData.errors.vehicleId}
+
+ )}
+ {!selectedCustomerId && !plateNumberInput && (
+
+ يرجى اختيار العميل أولاً أو البحث برقم اللوحة
+
+ )}
+ {!visit && plateNumberInput && selectedVehicleId && (
+
+ تم اختيار المركبة تلقائياً من رقم اللوحة
+
+ )}
+
+
+
+ {/* Maintenance Types Selection */}
+
+ ({
+ value: type.id,
+ label: type.name
+ }))}
+ value={selectedMaintenanceTypes}
+ onChange={setSelectedMaintenanceTypes}
+ placeholder="اختر أنواع الصيانة المطلوبة..."
+ error={actionData?.errors?.maintenanceJobs}
+ required
+ />
+
+ يمكنك اختيار أكثر من نوع صيانة واحد
+
+
+ {/* Hidden input to pass maintenance jobs data in the expected format */}
+
+
+
+
+ {/* Payment Status */}
+
+ ({
+ value: value,
+ label: label
+ }))}
+ />
+
+
+
+ {/* Description */}
+
+
+ وصف الصيانة
+ *
+
+
+ {actionData?.errors?.description && (
+
+ {actionData.errors.description}
+
+ )}
+
+
+
+ {/* Cost */}
+
+
+
+
+ {/* Kilometers */}
+
+
+
+
+ {/* Next Visit Delay */}
+
+ ({
+ value: option.value,
+ label: option.label
+ }))}
+ />
+
+
+
+ {/* Visit Date */}
+
+
+
+
+ {/* Action Buttons */}
+ {/* Debug Info */}
+ {!visit && (
+
+ Debug Info:
+ Customer ID: {selectedCustomerId || "Not selected"}
+ Vehicle ID: {selectedVehicleId || "Not selected"}
+ Plate Number: {plateNumberInput || "Not entered"}
+ Selected Maintenance Types: {selectedMaintenanceTypes.length} types
+ Types: {selectedMaintenanceTypes.join(', ') || "None selected"}
+
+ )}
+
+
+ {onCancel && (
+
+ إلغاء
+
+ )}
+ {
+ // Client-side validation before submission
+ if (!visit) {
+ const form = e.currentTarget.form;
+ if (!form) return;
+
+ const formData = new FormData(form);
+ const customerId = formData.get("customerId") as string;
+ const vehicleId = formData.get("vehicleId") as string;
+ const description = formData.get("description") as string;
+ const cost = formData.get("cost") as string;
+ const kilometers = formData.get("kilometers") as string;
+
+ const hasValidCustomer = customerId && customerId !== "";
+ const hasValidVehicle = vehicleId && vehicleId !== "";
+ const hasValidJobs = selectedMaintenanceTypes.length > 0;
+ const hasValidDescription = description && description.trim() !== "";
+ const hasValidCost = cost && cost.trim() !== "" && parseFloat(cost) > 0;
+ const hasValidKilometers = kilometers && kilometers.trim() !== "" && parseInt(kilometers) >= 0;
+
+ const missingFields = [];
+ if (!hasValidCustomer) missingFields.push("العميل");
+ if (!hasValidVehicle) missingFields.push("المركبة");
+ if (!hasValidJobs) missingFields.push("نوع صيانة واحد على الأقل");
+ if (!hasValidDescription) missingFields.push("وصف الصيانة");
+ if (!hasValidCost) missingFields.push("التكلفة");
+ if (!hasValidKilometers) missingFields.push("عدد الكيلومترات");
+
+ if (missingFields.length > 0) {
+ e.preventDefault();
+ alert(`يرجى ملء الحقول المطلوبة التالية:\n- ${missingFields.join('\n- ')}`);
+ return;
+ }
+ }
+ }}
+ >
+ {isSubmitting
+ ? visit
+ ? "جاري التحديث..."
+ : "جاري الحفظ..."
+ : visit
+ ? "تحديث الزيارة"
+ : "حفظ الزيارة"}
+
+
+
+
+
+ );
+}
diff --git a/app/components/maintenance-visits/MaintenanceVisitList.tsx b/app/components/maintenance-visits/MaintenanceVisitList.tsx
new file mode 100644
index 0000000..46e1366
--- /dev/null
+++ b/app/components/maintenance-visits/MaintenanceVisitList.tsx
@@ -0,0 +1,253 @@
+import { useState } from "react";
+import { Link, Form } from "@remix-run/react";
+import { Button } from "~/components/ui/Button";
+import { Text } from "~/components/ui/Text";
+import { DataTable } from "~/components/ui/DataTable";
+import type { MaintenanceVisitWithRelations } from "~/types/database";
+import { PAYMENT_STATUS_NAMES } from "~/lib/constants";
+import { useSettings } from "~/contexts/SettingsContext";
+
+interface MaintenanceVisitListProps {
+ visits: MaintenanceVisitWithRelations[];
+ onEdit?: (visit: MaintenanceVisitWithRelations) => void;
+ onView?: (visit: MaintenanceVisitWithRelations) => void;
+}
+
+export function MaintenanceVisitList({
+ visits,
+ onEdit,
+ onView
+}: MaintenanceVisitListProps) {
+ const { formatDate, formatCurrency, formatNumber, formatDateTime } = useSettings();
+ const [deleteVisitId, setDeleteVisitId] = useState(null);
+
+ const handleDelete = (visitId: number) => {
+ setDeleteVisitId(visitId);
+ };
+
+ const confirmDelete = () => {
+ if (deleteVisitId) {
+ // Submit delete form
+ const form = document.createElement('form');
+ form.method = 'post';
+ form.style.display = 'none';
+
+ const intentInput = document.createElement('input');
+ intentInput.type = 'hidden';
+ intentInput.name = 'intent';
+ intentInput.value = 'delete';
+
+ const idInput = document.createElement('input');
+ idInput.type = 'hidden';
+ idInput.name = 'id';
+ idInput.value = deleteVisitId.toString();
+
+ form.appendChild(intentInput);
+ form.appendChild(idInput);
+ document.body.appendChild(form);
+ form.submit();
+ }
+ };
+
+ const getPaymentStatusColor = (status: string) => {
+ switch (status) {
+ case 'paid':
+ return 'text-green-600 bg-green-50';
+ case 'pending':
+ return 'text-yellow-600 bg-yellow-50';
+ case 'partial':
+ return 'text-blue-600 bg-blue-50';
+ case 'cancelled':
+ return 'text-red-600 bg-red-50';
+ default:
+ return 'text-gray-600 bg-gray-50';
+ }
+ };
+
+ const columns = [
+ {
+ key: 'visitDate',
+ header: 'تاريخ الزيارة',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+
+ {formatDate(visit.visitDate)}
+
+
+ {formatDateTime(visit.visitDate).split(' ')[1]}
+
+
+ ),
+ },
+ {
+ key: 'vehicle',
+ header: 'المركبة',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+ {visit.vehicle.plateNumber}
+
+ {visit.vehicle.manufacturer} {visit.vehicle.model} ({visit.vehicle.year})
+
+
+ ),
+ },
+ {
+ key: 'customer',
+ header: 'العميل',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+ {visit.customer.name}
+ {visit.customer.phone && (
+ {visit.customer.phone}
+ )}
+
+ ),
+ },
+ {
+ key: 'maintenanceJobs',
+ header: 'أعمال الصيانة',
+ render: (visit: MaintenanceVisitWithRelations) => {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ return (
+
+
+ {jobs.length > 1 ? `${jobs.length} أعمال صيانة` : jobs[0]?.job || 'غير محدد'}
+
+
+ {visit.description}
+
+
+ );
+ } catch {
+ return (
+
+ غير محدد
+
+ {visit.description}
+
+
+ );
+ }
+ },
+ },
+ {
+ key: 'cost',
+ header: 'التكلفة',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+ {formatCurrency(visit.cost)}
+
+ ),
+ },
+ {
+ key: 'paymentStatus',
+ header: 'حالة الدفع',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+ {PAYMENT_STATUS_NAMES[visit.paymentStatus as keyof typeof PAYMENT_STATUS_NAMES]}
+
+ ),
+ },
+ {
+ key: 'kilometers',
+ header: 'الكيلومترات',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+ {formatNumber(visit.kilometers)} كم
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: 'الإجراءات',
+ render: (visit: MaintenanceVisitWithRelations) => (
+
+ {onView ? (
+ onView(visit)}
+ >
+ عرض
+
+ ) : (
+
+
+ عرض
+
+
+ )}
+ {onEdit && (
+ onEdit(visit)}
+ >
+ تعديل
+
+ )}
+ handleDelete(visit.id)}
+ >
+ حذف
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ {visits.length === 0 ? (
+
+
🔧
+
+ لا توجد زيارات صيانة
+
+
+ لم يتم العثور على أي زيارات صيانة. قم بإضافة زيارة جديدة للبدء.
+
+
+ ) : (
+
+ )}
+
+
+ {/* Delete Confirmation Modal */}
+ {deleteVisitId !== null && (
+
+
+
تأكيد الحذف
+
+ هل أنت متأكد من حذف زيارة الصيانة هذه؟ سيتم حذف جميع البيانات المرتبطة بها نهائياً.
+
+
+ setDeleteVisitId(null)}
+ >
+ إلغاء
+
+
+ حذف
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/maintenance-visits/index.ts b/app/components/maintenance-visits/index.ts
new file mode 100644
index 0000000..1ff6677
--- /dev/null
+++ b/app/components/maintenance-visits/index.ts
@@ -0,0 +1,2 @@
+export { MaintenanceVisitForm } from './MaintenanceVisitForm';
+export { MaintenanceVisitList } from './MaintenanceVisitList';
\ No newline at end of file
diff --git a/app/components/tables/EnhancedCustomerTable.tsx b/app/components/tables/EnhancedCustomerTable.tsx
new file mode 100644
index 0000000..7c51d8f
--- /dev/null
+++ b/app/components/tables/EnhancedCustomerTable.tsx
@@ -0,0 +1,212 @@
+import { useState, useMemo } from 'react';
+import { DataTable } from '~/components/ui/DataTable';
+import { Button } from '~/components/ui/Button';
+import { Text } from '~/components/ui/Text';
+import { processTableData, type TableState } from '~/lib/table-utils';
+import { useSettings } from '~/contexts/SettingsContext';
+import type { Customer } from '~/types/database';
+
+interface EnhancedCustomerTableProps {
+ customers: Customer[];
+ loading?: boolean;
+ onEdit?: (customer: Customer) => void;
+ onDelete?: (customer: Customer) => void;
+ onView?: (customer: Customer) => void;
+}
+
+export function EnhancedCustomerTable({
+ customers,
+ loading = false,
+ onEdit,
+ onDelete,
+ onView,
+}: EnhancedCustomerTableProps) {
+ const { formatDate } = useSettings();
+ const [tableState, setTableState] = useState({
+ search: '',
+ filters: {},
+ sort: { key: 'name', direction: 'asc' },
+ pagination: { page: 1, pageSize: 10 },
+ });
+
+ // Define searchable fields
+ const searchableFields = ['name', 'phone', 'email', 'address'] as const;
+
+ // Process table data
+ const processedData = useMemo(() => {
+ return processTableData(customers, tableState, searchableFields);
+ }, [customers, tableState]);
+
+ // Handle sorting
+ const handleSort = (key: string, direction: 'asc' | 'desc') => {
+ setTableState(prev => ({
+ ...prev,
+ sort: { key, direction },
+ }));
+ };
+
+ // Handle page change
+ const handlePageChange = (page: number) => {
+ setTableState(prev => ({
+ ...prev,
+ pagination: { ...prev.pagination, page },
+ }));
+ };
+
+ // Define table columns
+ const columns = [
+ {
+ key: 'name' as keyof Customer,
+ header: 'اسم العميل',
+ sortable: true,
+ filterable: true,
+ render: (customer: Customer) => (
+
+ {customer.name}
+
+ ),
+ },
+ {
+ key: 'phone' as keyof Customer,
+ header: 'رقم الهاتف',
+ sortable: true,
+ filterable: true,
+ render: (customer: Customer) => (
+
+ {customer.phone || '-'}
+
+ ),
+ },
+ {
+ key: 'email' as keyof Customer,
+ header: 'البريد الإلكتروني',
+ sortable: true,
+ filterable: true,
+ render: (customer: Customer) => (
+
+ {customer.email || '-'}
+
+ ),
+ },
+ {
+ key: 'address' as keyof Customer,
+ header: 'العنوان',
+ filterable: true,
+ render: (customer: Customer) => (
+
+ {customer.address || '-'}
+
+ ),
+ },
+ {
+ key: 'createdDate' as keyof Customer,
+ header: 'تاريخ الإنشاء',
+ sortable: true,
+ render: (customer: Customer) => (
+
+ {formatDate(customer.createdDate)}
+
+ ),
+ },
+ ];
+
+ return (
+
+ {/* Table Header */}
+
+
+
+ قائمة العملاء
+
+
+ إدارة بيانات العملاء
+
+
+
+
+
+ المجموع: {processedData.originalCount}
+
+ {processedData.filteredCount !== processedData.originalCount && (
+
+ (مفلتر: {processedData.filteredCount})
+
+ )}
+
+
+
+ {/* Enhanced Data Table */}
+
(
+
+ {onView && (
+
onView(customer)}
+ icon={
+
+
+
+
+ }
+ >
+ عرض
+
+ )}
+
+ {onEdit && (
+
onEdit(customer)}
+ icon={
+
+
+
+ }
+ >
+ تعديل
+
+ )}
+
+ {onDelete && (
+
onDelete(customer)}
+ icon={
+
+
+
+ }
+ >
+ حذف
+
+ )}
+
+ ),
+ }}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/AutocompleteInput.tsx b/app/components/ui/AutocompleteInput.tsx
new file mode 100644
index 0000000..6e302b8
--- /dev/null
+++ b/app/components/ui/AutocompleteInput.tsx
@@ -0,0 +1,214 @@
+import { useState, useRef, useEffect } from "react";
+import { Input } from "./Input";
+
+interface AutocompleteOption {
+ value: string;
+ label: string;
+ data?: any;
+}
+
+interface AutocompleteInputProps {
+ name?: string;
+ label?: string;
+ placeholder?: string;
+ value: string;
+ onChange: (value: string) => void;
+ onSelect?: (option: AutocompleteOption) => void;
+ options: AutocompleteOption[];
+ error?: string;
+ required?: boolean;
+ disabled?: boolean;
+ loading?: boolean;
+}
+
+export function AutocompleteInput({
+ name,
+ label,
+ placeholder,
+ value,
+ onChange,
+ onSelect,
+ options,
+ error,
+ required,
+ disabled,
+ loading
+}: AutocompleteInputProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
+ const inputRef = useRef(null);
+ const listRef = useRef(null);
+ const containerRef = useRef(null);
+
+ // Filter options based on input value
+ const filteredOptions = options.filter(option =>
+ option.label.toLowerCase().includes(value.toLowerCase()) ||
+ option.value.toLowerCase().includes(value.toLowerCase())
+ );
+
+ // Handle input change
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ onChange(newValue);
+ setIsOpen(true);
+ setHighlightedIndex(-1);
+ };
+
+ // Handle option selection
+ const handleOptionSelect = (option: AutocompleteOption) => {
+ onChange(option.value);
+ onSelect?.(option);
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ };
+
+ // Handle keyboard navigation
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!isOpen) {
+ if (e.key === "ArrowDown" || e.key === "Enter") {
+ setIsOpen(true);
+ return;
+ }
+ return;
+ }
+
+ switch (e.key) {
+ case "ArrowDown":
+ e.preventDefault();
+ setHighlightedIndex(prev =>
+ prev < filteredOptions.length - 1 ? prev + 1 : 0
+ );
+ break;
+ case "ArrowUp":
+ e.preventDefault();
+ setHighlightedIndex(prev =>
+ prev > 0 ? prev - 1 : filteredOptions.length - 1
+ );
+ break;
+ case "Enter":
+ e.preventDefault();
+ if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
+ handleOptionSelect(filteredOptions[highlightedIndex]);
+ }
+ break;
+ case "Escape":
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ break;
+ }
+ };
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ // Scroll highlighted option into view
+ useEffect(() => {
+ if (highlightedIndex >= 0 && listRef.current) {
+ const highlightedElement = listRef.current.children[highlightedIndex] as HTMLElement;
+ if (highlightedElement) {
+ highlightedElement.scrollIntoView({
+ block: "nearest",
+ behavior: "smooth"
+ });
+ }
+ }
+ }, [highlightedIndex]);
+
+ return (
+
+
setIsOpen(true)}
+ error={error}
+ required={required}
+ disabled={disabled}
+ endIcon={
+
+ {loading ? (
+
+
+
+
+ ) : isOpen ? (
+
setIsOpen(false)}
+ className="text-gray-400 hover:text-gray-600"
+ >
+
+
+
+
+ ) : (
+
setIsOpen(true)}
+ className="text-gray-400 hover:text-gray-600"
+ >
+
+
+
+
+ )}
+
+ }
+ />
+
+ {/* Dropdown */}
+ {isOpen && filteredOptions.length > 0 && (
+
+
+ {filteredOptions.map((option, index) => (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ handleOptionSelect(option);
+ }}
+ onMouseDown={(e) => {
+ e.preventDefault(); // Prevent input from losing focus
+ }}
+ onMouseEnter={() => setHighlightedIndex(index)}
+ >
+ {option.value}
+ {option.label !== option.value && (
+ {option.label}
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* No results */}
+ {isOpen && filteredOptions.length === 0 && value.length > 0 && (
+
+
+ لا توجد نتائج مطابقة
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/Button.tsx b/app/components/ui/Button.tsx
new file mode 100644
index 0000000..e554644
--- /dev/null
+++ b/app/components/ui/Button.tsx
@@ -0,0 +1,92 @@
+import { ReactNode, ButtonHTMLAttributes } from 'react';
+import { getButtonClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface ButtonProps extends Omit, 'className'> {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost';
+ size?: 'sm' | 'md' | 'lg';
+ fullWidth?: boolean;
+ loading?: boolean;
+ icon?: ReactNode;
+ iconPosition?: 'start' | 'end';
+}
+
+export function Button({
+ children,
+ className = '',
+ config = {},
+ variant = 'primary',
+ size = 'md',
+ fullWidth = false,
+ loading = false,
+ icon,
+ iconPosition = 'start',
+ disabled,
+ ...props
+}: ButtonProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const baseClasses = getButtonClasses(variant as any, size);
+
+ const variantClasses = {
+ primary: 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500 disabled:bg-blue-300',
+ secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900 focus:ring-gray-500 disabled:bg-gray-100',
+ danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500 disabled:bg-red-300',
+ outline: 'border border-gray-300 bg-white hover:bg-gray-50 text-gray-700 focus:ring-blue-500 disabled:bg-gray-50',
+ ghost: 'bg-transparent hover:bg-gray-100 text-gray-700 focus:ring-gray-500 disabled:text-gray-400',
+ };
+
+ const fullWidthClass = fullWidth ? 'w-full' : '';
+ const disabledClass = (disabled || loading) ? 'cursor-not-allowed opacity-50' : '';
+
+ const iconSpacing = layoutConfig.direction === 'rtl' ? 'space-x-reverse' : '';
+ const iconOrderClass = iconPosition === 'end' ? 'flex-row-reverse' : '';
+
+ return (
+
+ {loading && (
+
+
+
+
+ )}
+
+ {icon && iconPosition === 'start' && !loading && (
+
+ {icon}
+
+ )}
+
+ {children}
+
+ {icon && iconPosition === 'end' && !loading && (
+
+ {icon}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/Card.tsx b/app/components/ui/Card.tsx
new file mode 100644
index 0000000..1f52c11
--- /dev/null
+++ b/app/components/ui/Card.tsx
@@ -0,0 +1,118 @@
+import { ReactNode } from 'react';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface CardProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ padding?: 'none' | 'sm' | 'md' | 'lg';
+ shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
+ border?: boolean;
+ hover?: boolean;
+}
+
+export function Card({
+ children,
+ className = '',
+ config = {},
+ padding = 'md',
+ shadow = 'md',
+ rounded = 'lg',
+ border = true,
+ hover = false
+}: CardProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const paddingClasses = {
+ none: '',
+ sm: 'p-3',
+ md: 'p-4 sm:p-6',
+ lg: 'p-6 sm:p-8',
+ };
+
+ const shadowClasses = {
+ none: '',
+ sm: 'shadow-sm',
+ md: 'shadow-md',
+ lg: 'shadow-lg',
+ xl: 'shadow-xl',
+ };
+
+ const roundedClasses = {
+ none: '',
+ sm: 'rounded-sm',
+ md: 'rounded-md',
+ lg: 'rounded-lg',
+ xl: 'rounded-xl',
+ };
+
+ const borderClass = border ? 'border border-gray-200' : '';
+ const hoverClass = hover ? 'hover:shadow-lg transition-shadow duration-200' : '';
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface CardHeaderProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+}
+
+export function CardHeader({ children, className = '', config = {} }: CardHeaderProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface CardBodyProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+}
+
+export function CardBody({ children, className = '', config = {} }: CardBodyProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface CardFooterProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+}
+
+export function CardFooter({ children, className = '', config = {} }: CardFooterProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/DataTable.tsx b/app/components/ui/DataTable.tsx
new file mode 100644
index 0000000..aac5592
--- /dev/null
+++ b/app/components/ui/DataTable.tsx
@@ -0,0 +1,422 @@
+import { ReactNode, memo, useState, useMemo } from 'react';
+import { Text } from './Text';
+import { Button } from './Button';
+import { Input } from './Input';
+import { Select } from './Select';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface Column {
+ key: keyof T | string;
+ header: string;
+ render?: (item: T) => ReactNode;
+ sortable?: boolean;
+ filterable?: boolean;
+ filterType?: 'text' | 'select' | 'date' | 'number';
+ filterOptions?: { value: string; label: string }[];
+ className?: string;
+ width?: string;
+}
+
+interface FilterState {
+ [key: string]: string;
+}
+
+interface DataTableProps {
+ data: T[];
+ columns: Column[];
+ loading?: boolean;
+ emptyMessage?: string;
+ className?: string;
+ config?: Partial;
+ onSort?: (key: string, direction: 'asc' | 'desc') => void;
+ sortKey?: string;
+ sortDirection?: 'asc' | 'desc';
+ searchable?: boolean;
+ searchPlaceholder?: string;
+ filterable?: boolean;
+ pagination?: {
+ enabled: boolean;
+ pageSize?: number;
+ currentPage?: number;
+ totalItems?: number;
+ onPageChange?: (page: number) => void;
+ };
+ actions?: {
+ label: string;
+ render: (item: T) => ReactNode;
+ };
+}
+
+export const DataTable = memo(function DataTable>({
+ data,
+ columns,
+ loading = false,
+ emptyMessage = "لا توجد بيانات",
+ className = '',
+ config = {},
+ onSort,
+ sortKey,
+ sortDirection,
+ searchable = false,
+ searchPlaceholder = "البحث...",
+ filterable = false,
+ pagination,
+ actions,
+}: DataTableProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filters, setFilters] = useState({});
+
+ const handleSort = (key: string) => {
+ if (!onSort) return;
+
+ const newDirection = sortKey === key && sortDirection === 'asc' ? 'desc' : 'asc';
+ onSort(key, newDirection);
+ };
+
+ const handleFilterChange = (columnKey: string, value: string) => {
+ setFilters(prev => ({
+ ...prev,
+ [columnKey]: value,
+ }));
+ };
+
+ // Filter and search data
+ const filteredData = useMemo(() => {
+ let result = [...data];
+
+ // Apply search
+ if (searchable && searchTerm) {
+ result = result.filter(item => {
+ return columns.some(column => {
+ const value = item[column.key];
+ if (value == null) return false;
+ return String(value).toLowerCase().includes(searchTerm.toLowerCase());
+ });
+ });
+ }
+
+ // Apply column filters
+ if (filterable) {
+ Object.entries(filters).forEach(([columnKey, filterValue]) => {
+ if (filterValue) {
+ result = result.filter(item => {
+ const value = item[columnKey];
+ if (value == null) return false;
+ return String(value).toLowerCase().includes(filterValue.toLowerCase());
+ });
+ }
+ });
+ }
+
+ return result;
+ }, [data, searchTerm, filters, columns, searchable, filterable]);
+
+ // Paginate data
+ const paginatedData = useMemo(() => {
+ if (!pagination?.enabled) return filteredData;
+
+ const pageSize = pagination.pageSize || 10;
+ const currentPage = pagination.currentPage || 1;
+ const startIndex = (currentPage - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+
+ return filteredData.slice(startIndex, endIndex);
+ }, [filteredData, pagination]);
+
+ const totalPages = pagination?.enabled
+ ? Math.ceil(filteredData.length / (pagination.pageSize || 10))
+ : 1;
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+
+
+
+ لا توجد بيانات
+
+
{emptyMessage}
+
+
+ );
+ }
+
+ return (
+
+ {/* Search and Filters */}
+ {(searchable || filterable) && (
+
+
+ {/* Search */}
+ {searchable && (
+
+
setSearchTerm(e.target.value)}
+ startIcon={
+
+
+
+ }
+ />
+
+ )}
+
+ {/* Column Filters */}
+ {filterable && (
+
+ {columns
+ .filter(column => column.filterable)
+ .map(column => (
+
+ {column.filterType === 'select' && column.filterOptions ? (
+ handleFilterChange(column.key as string, e.target.value)}
+ options={[
+ { value: '', label: `جميع ${column.header}` },
+ ...column.filterOptions
+ ]}
+ />
+ ) : (
+ handleFilterChange(column.key as string, e.target.value)}
+ />
+ )}
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Table */}
+
+
+
+
+ {columns.map((column) => (
+
+ {column.sortable && onSort ? (
+ handleSort(column.key as string)}
+ className="group inline-flex items-center space-x-1 space-x-reverse hover:text-gray-700"
+ >
+ {column.header}
+
+ {sortKey === column.key ? (
+ sortDirection === 'asc' ? (
+
+
+
+ ) : (
+
+
+
+ )
+ ) : (
+
+
+
+ )}
+
+
+ ) : (
+ column.header
+ )}
+
+ ))}
+ {actions && (
+
+ {actions.label}
+
+ )}
+
+
+
+ {paginatedData.map((item, rowIndex) => {
+ // Use item.id if available, otherwise fall back to rowIndex
+ const rowKey = item.id ? `row-${item.id}` : `row-${rowIndex}`;
+ return (
+
+ {columns.map((column) => (
+
+ {column.render
+ ? column.render(item)
+ : String(item[column.key] || '')
+ }
+
+ ))}
+ {actions && (
+
+ {actions.render(item)}
+
+ )}
+
+ );
+ })}
+
+
+
+
+ {/* Pagination */}
+ {pagination?.enabled && totalPages > 1 && (
+
+
{})}
+ config={config}
+ />
+
+ )}
+
+ {/* Results Summary */}
+ {(searchable || filterable || pagination?.enabled) && (
+
+
+ عرض {paginatedData.length} من {filteredData.length}
+ {filteredData.length !== data.length && ` (مفلتر من ${data.length})`}
+
+
+ )}
+
+ );
+}) as >(props: DataTableProps) => JSX.Element;
+
+interface PaginationProps {
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ className?: string;
+ config?: Partial;
+}
+
+export const Pagination = memo(function Pagination({
+ currentPage,
+ totalPages,
+ onPageChange,
+ className = '',
+ config = {},
+}: PaginationProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ if (totalPages <= 1) return null;
+
+ const getPageNumbers = () => {
+ const pages = [];
+ const maxVisible = 5;
+
+ if (totalPages <= maxVisible) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ if (currentPage <= 3) {
+ for (let i = 1; i <= 4; i++) {
+ pages.push(i);
+ }
+ pages.push('...');
+ pages.push(totalPages);
+ } else if (currentPage >= totalPages - 2) {
+ pages.push(1);
+ pages.push('...');
+ for (let i = totalPages - 3; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+ pages.push('...');
+ for (let i = currentPage - 1; i <= currentPage + 1; i++) {
+ pages.push(i);
+ }
+ pages.push('...');
+ pages.push(totalPages);
+ }
+ }
+
+ return pages;
+ };
+
+ return (
+
+
+
onPageChange(currentPage - 1)}
+ disabled={currentPage === 1}
+ >
+ السابق
+
+
+
+ {getPageNumbers().map((page, index) => (
+
+ {page === '...' ? (
+ ...
+ ) : (
+ onPageChange(page as number)}
+ >
+ {page}
+
+ )}
+
+ ))}
+
+
+
onPageChange(currentPage + 1)}
+ disabled={currentPage === totalPages}
+ >
+ التالي
+
+
+
+
+ صفحة {currentPage} من {totalPages}
+
+
+ );
+});
\ No newline at end of file
diff --git a/app/components/ui/Form.tsx b/app/components/ui/Form.tsx
new file mode 100644
index 0000000..49bd42c
--- /dev/null
+++ b/app/components/ui/Form.tsx
@@ -0,0 +1,233 @@
+import { ReactNode, FormHTMLAttributes } from 'react';
+import { Form as RemixForm } from '@remix-run/react';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+import { Button } from './Button';
+import { Text } from './Text';
+
+interface FormProps extends Omit, 'className'> {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ title?: string;
+ description?: string;
+ loading?: boolean;
+ error?: string;
+ success?: string;
+ actions?: ReactNode;
+ spacing?: 'sm' | 'md' | 'lg';
+}
+
+export function Form({
+ children,
+ className = '',
+ config = {},
+ title,
+ description,
+ loading = false,
+ error,
+ success,
+ actions,
+ spacing = 'md',
+ ...props
+}: FormProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const spacingClasses = {
+ sm: 'space-y-4',
+ md: 'space-y-6',
+ lg: 'space-y-8',
+ };
+
+ return (
+
+ {/* Form Header */}
+ {(title || description) && (
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+
+ )}
+
+ {/* Success Message */}
+ {success && (
+
+ )}
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+ {/* Form Content */}
+
+ {children}
+
+ {/* Form Actions */}
+ {actions && (
+
+ {actions}
+
+ )}
+
+
+ {/* Loading Overlay */}
+ {loading && (
+
+ )}
+
+ );
+}
+
+// Form Actions Component
+interface FormActionsProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ justify?: 'start' | 'center' | 'end' | 'between';
+ spacing?: 'sm' | 'md' | 'lg';
+}
+
+export function FormActions({
+ children,
+ className = '',
+ config = {},
+ justify = 'end',
+ spacing = 'md',
+}: FormActionsProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const justifyClasses = {
+ start: 'justify-start',
+ center: 'justify-center',
+ end: 'justify-end',
+ between: 'justify-between',
+ };
+
+ const spacingClasses = {
+ sm: 'space-x-2 space-x-reverse',
+ md: 'space-x-4 space-x-reverse',
+ lg: 'space-x-6 space-x-reverse',
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Form Section Component
+interface FormSectionProps {
+ children: ReactNode;
+ title?: string;
+ description?: string;
+ className?: string;
+ config?: Partial;
+}
+
+export function FormSection({
+ children,
+ title,
+ description,
+ className = '',
+ config = {},
+}: FormSectionProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ return (
+
+ {(title || description) && (
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+
+ )}
+
+ {children}
+
+
+ );
+}
+
+// Form Grid Component
+interface FormGridProps {
+ children: ReactNode;
+ columns?: 1 | 2 | 3 | 4;
+ className?: string;
+ config?: Partial;
+ gap?: 'sm' | 'md' | 'lg';
+}
+
+export function FormGrid({
+ children,
+ columns = 2,
+ className = '',
+ config = {},
+ gap = 'md',
+}: FormGridProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const columnClasses = {
+ 1: 'grid-cols-1',
+ 2: 'grid-cols-1 md:grid-cols-2',
+ 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
+ };
+
+ const gapClasses = {
+ sm: 'gap-4',
+ md: 'gap-6',
+ lg: 'gap-8',
+ };
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/FormField.tsx b/app/components/ui/FormField.tsx
new file mode 100644
index 0000000..3fc9803
--- /dev/null
+++ b/app/components/ui/FormField.tsx
@@ -0,0 +1,54 @@
+import { ReactNode } from 'react';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface FormFieldProps {
+ children: ReactNode;
+ label?: string;
+ error?: string;
+ helperText?: string;
+ required?: boolean;
+ className?: string;
+ config?: Partial;
+ htmlFor?: string;
+}
+
+export function FormField({
+ children,
+ label,
+ error,
+ helperText,
+ required = false,
+ className = '',
+ config = {},
+ htmlFor,
+}: FormFieldProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ return (
+
+ {label && (
+
+ {label}
+ {required && * }
+
+ )}
+
+ {children}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {helperText && !error && (
+
+ {helperText}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/Input.tsx b/app/components/ui/Input.tsx
new file mode 100644
index 0000000..eb316b9
--- /dev/null
+++ b/app/components/ui/Input.tsx
@@ -0,0 +1,86 @@
+import { InputHTMLAttributes, forwardRef, useId } from 'react';
+import { getFormInputClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface InputProps extends Omit, 'className'> {
+ className?: string;
+ config?: Partial;
+ label?: string;
+ error?: string;
+ helperText?: string;
+ fullWidth?: boolean;
+ startIcon?: React.ReactNode;
+ endIcon?: React.ReactNode;
+}
+
+export const Input = forwardRef(({
+ className = '',
+ config = {},
+ label,
+ error,
+ helperText,
+ fullWidth = true,
+ startIcon,
+ endIcon,
+ id,
+ ...props
+}, ref) => {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const inputClasses = getFormInputClasses(!!error);
+
+ const fullWidthClass = fullWidth ? 'w-full' : '';
+ const hasIconsClass = (startIcon || endIcon) ? 'relative' : '';
+
+ const generatedId = useId();
+ const inputId = id || generatedId;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+ {startIcon && (
+
+
+ {startIcon}
+
+
+ )}
+
+
+
+ {endIcon && (
+
+
+ {endIcon}
+
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {helperText && !error && (
+
+ {helperText}
+
+ )}
+
+ );
+});
\ No newline at end of file
diff --git a/app/components/ui/Modal.tsx b/app/components/ui/Modal.tsx
new file mode 100644
index 0000000..b6e577f
--- /dev/null
+++ b/app/components/ui/Modal.tsx
@@ -0,0 +1,220 @@
+import { ReactNode, useEffect, useState } from 'react';
+import { createPortal } from 'react-dom';
+import { Button } from './Button';
+import { Text } from './Text';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ children: ReactNode;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ className?: string;
+ config?: Partial;
+ showCloseButton?: boolean;
+}
+
+export function Modal({
+ isOpen,
+ onClose,
+ title,
+ children,
+ size = 'md',
+ className = '',
+ config = {},
+ showCloseButton = true,
+}: ModalProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ }
+
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, [isOpen]);
+
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onClose();
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('keydown', handleEscape);
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape);
+ };
+ }, [isOpen, onClose]);
+
+ if (!isOpen || !isMounted) return null;
+
+ const sizeClasses = {
+ sm: 'max-w-md',
+ md: 'max-w-lg',
+ lg: 'max-w-2xl',
+ xl: 'max-w-4xl',
+ };
+
+ const modalContent = (
+
+
+ {/* Backdrop */}
+
+
+ {/* Modal panel */}
+
+ {/* Header */}
+
+
+
+ {title}
+
+ {showCloseButton && (
+
+ إغلاق
+
+
+
+
+ )}
+
+
+ {/* Content */}
+
+ {children}
+
+
+
+
+
+ );
+
+ return createPortal(modalContent, document.body);
+}
+
+interface ConfirmModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ variant?: 'danger' | 'warning' | 'info';
+ config?: Partial;
+}
+
+export function ConfirmModal({
+ isOpen,
+ onClose,
+ onConfirm,
+ title,
+ message,
+ confirmText = "تأكيد",
+ cancelText = "إلغاء",
+ variant = 'info',
+ config = {},
+}: ConfirmModalProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ const handleConfirm = () => {
+ onConfirm();
+ onClose();
+ };
+
+ const getIcon = () => {
+ switch (variant) {
+ case 'danger':
+ return (
+
+ );
+ case 'warning':
+ return (
+
+ );
+ default:
+ return (
+
+ );
+ }
+ };
+
+ return (
+
+
+ {getIcon()}
+
+
+ {title}
+
+
+ {message}
+
+
+
+
+
+
+ {confirmText}
+
+
+ {cancelText}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/MultiSelect.tsx b/app/components/ui/MultiSelect.tsx
new file mode 100644
index 0000000..3f0a117
--- /dev/null
+++ b/app/components/ui/MultiSelect.tsx
@@ -0,0 +1,171 @@
+import { useState, useRef, useEffect } from "react";
+import { Text } from "./Text";
+
+interface Option {
+ value: string | number;
+ label: string;
+}
+
+interface MultiSelectProps {
+ name?: string;
+ label?: string;
+ options: Option[];
+ value: (string | number)[];
+ onChange: (values: (string | number)[]) => void;
+ placeholder?: string;
+ error?: string;
+ required?: boolean;
+ disabled?: boolean;
+ className?: string;
+}
+
+export function MultiSelect({
+ name,
+ label,
+ options,
+ value,
+ onChange,
+ placeholder = "اختر العناصر...",
+ error,
+ required,
+ disabled,
+ className = ""
+}: MultiSelectProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const containerRef = useRef(null);
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ const handleToggleOption = (optionValue: string | number) => {
+ if (disabled) return;
+
+ const newValue = value.includes(optionValue)
+ ? value.filter(v => v !== optionValue)
+ : [...value, optionValue];
+
+ onChange(newValue);
+ };
+
+ const handleRemoveItem = (optionValue: string | number) => {
+ if (disabled) return;
+ onChange(value.filter(v => v !== optionValue));
+ };
+
+ const selectedOptions = options.filter(option => value.includes(option.value));
+ const displayText = selectedOptions.length > 0
+ ? `تم اختيار ${selectedOptions.length} عنصر`
+ : placeholder;
+
+ return (
+
+ {label && (
+
+ {label}
+ {required && * }
+
+ )}
+
+ {/* Hidden input for form submission */}
+ {name && (
+
+ )}
+
+ {/* Selected items display */}
+ {selectedOptions.length > 0 && (
+
+ {selectedOptions.map((option) => (
+
+ {option.label}
+ {!disabled && (
+ handleRemoveItem(option.value)}
+ className="mr-1 text-blue-600 hover:text-blue-800"
+ >
+ ×
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* Dropdown trigger */}
+
!disabled && setIsOpen(!isOpen)}
+ >
+
+
0 ? 'text-gray-900' : 'text-gray-500'}>
+ {displayText}
+
+
+
+
+
+
+
+ {/* Dropdown menu */}
+ {isOpen && (
+
+ {options.length === 0 ? (
+
+ لا توجد خيارات متاحة
+
+ ) : (
+ options.map((option) => {
+ const isSelected = value.includes(option.value);
+ return (
+
handleToggleOption(option.value)}
+ >
+
{option.label}
+ {isSelected && (
+
+
+
+ )}
+
+ );
+ })
+ )}
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/SearchInput.tsx b/app/components/ui/SearchInput.tsx
new file mode 100644
index 0000000..689853f
--- /dev/null
+++ b/app/components/ui/SearchInput.tsx
@@ -0,0 +1,79 @@
+import { useState, useEffect } from 'react';
+import { Input } from './Input';
+import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface SearchInputProps {
+ placeholder?: string;
+ onSearch: (query: string) => void;
+ debounceMs?: number;
+ className?: string;
+ config?: Partial;
+ initialValue?: string;
+}
+
+export function SearchInput({
+ placeholder = "البحث...",
+ onSearch,
+ debounceMs = 300,
+ className = '',
+ config = {},
+ initialValue = '',
+}: SearchInputProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const [query, setQuery] = useState(initialValue);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ onSearch(query);
+ }, debounceMs);
+
+ return () => clearTimeout(timer);
+ }, [query, onSearch, debounceMs]);
+
+ return (
+
+
+
setQuery(e.target.value)}
+ className="pr-10"
+ />
+ {query && (
+
setQuery('')}
+ className="absolute inset-y-0 left-0 pl-3 flex items-center"
+ >
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/Select.tsx b/app/components/ui/Select.tsx
new file mode 100644
index 0000000..762cbbe
--- /dev/null
+++ b/app/components/ui/Select.tsx
@@ -0,0 +1,97 @@
+import { SelectHTMLAttributes, forwardRef } from 'react';
+import { getFormInputClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface SelectOption {
+ value: string;
+ label: string;
+ disabled?: boolean;
+}
+
+interface SelectProps extends Omit, 'className'> {
+ className?: string;
+ config?: Partial;
+ label?: string;
+ error?: string;
+ helperText?: string;
+ fullWidth?: boolean;
+ options: SelectOption[];
+ placeholder?: string;
+}
+
+export const Select = forwardRef(({
+ className = '',
+ config = {},
+ label,
+ error,
+ helperText,
+ fullWidth = true,
+ options,
+ placeholder,
+ id,
+ ...props
+}, ref) => {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const inputClasses = getFormInputClasses(!!error);
+
+ const fullWidthClass = fullWidth ? 'w-full' : '';
+ const inputId = id || `select-${Math.random().toString(36).substr(2, 9)}`;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+
+ {placeholder && (
+
+ {placeholder}
+
+ )}
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+ {/* Custom dropdown arrow */}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {helperText && !error && (
+
+ {helperText}
+
+ )}
+
+ );
+});
+
+Select.displayName = 'Select';
\ No newline at end of file
diff --git a/app/components/ui/Text.tsx b/app/components/ui/Text.tsx
new file mode 100644
index 0000000..5538254
--- /dev/null
+++ b/app/components/ui/Text.tsx
@@ -0,0 +1,73 @@
+import { ReactNode } from 'react';
+import { getArabicTextClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface TextProps {
+ children: ReactNode;
+ className?: string;
+ config?: Partial;
+ size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
+ weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
+ color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'muted';
+ align?: 'left' | 'center' | 'right' | 'justify';
+ as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
+}
+
+export function Text({
+ children,
+ className = '',
+ config = {},
+ size = 'base',
+ weight = 'normal',
+ color = 'primary',
+ align,
+ as: Component = 'p'
+}: TextProps) {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+
+ const sizeClasses = {
+ xs: 'text-xs',
+ sm: 'text-sm',
+ base: 'text-base',
+ lg: 'text-lg',
+ xl: 'text-xl',
+ '2xl': 'text-2xl',
+ '3xl': 'text-3xl',
+ '4xl': 'text-4xl',
+ };
+
+ const weightClasses = {
+ light: 'font-light',
+ normal: 'font-normal',
+ medium: 'font-medium',
+ semibold: 'font-semibold',
+ bold: 'font-bold',
+ };
+
+ const colorClasses = {
+ primary: 'text-gray-900 dark:text-gray-100',
+ secondary: 'text-gray-600 dark:text-gray-400',
+ success: 'text-green-600 dark:text-green-400',
+ warning: 'text-amber-600 dark:text-amber-400',
+ error: 'text-red-600 dark:text-red-400',
+ muted: 'text-gray-500 dark:text-gray-500',
+ };
+
+ const alignClasses = {
+ left: layoutConfig.direction === 'rtl' ? 'text-right' : 'text-left',
+ center: 'text-center',
+ right: layoutConfig.direction === 'rtl' ? 'text-left' : 'text-right',
+ justify: 'text-justify',
+ };
+
+ const alignClass = align ? alignClasses[align] : (layoutConfig.direction === 'rtl' ? 'text-right' : 'text-left');
+ const arabicClasses = getArabicTextClasses(size as any);
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/Textarea.tsx b/app/components/ui/Textarea.tsx
new file mode 100644
index 0000000..02a0359
--- /dev/null
+++ b/app/components/ui/Textarea.tsx
@@ -0,0 +1,72 @@
+import { TextareaHTMLAttributes, forwardRef } from 'react';
+import { getFormInputClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
+
+interface TextareaProps extends Omit, 'className'> {
+ className?: string;
+ config?: Partial;
+ label?: string;
+ error?: string;
+ helperText?: string;
+ fullWidth?: boolean;
+ resize?: 'none' | 'vertical' | 'horizontal' | 'both';
+}
+
+export const Textarea = forwardRef(({
+ className = '',
+ config = {},
+ label,
+ error,
+ helperText,
+ fullWidth = true,
+ resize = 'vertical',
+ id,
+ ...props
+}, ref) => {
+ const layoutConfig = { ...defaultLayoutConfig, ...config };
+ const inputClasses = getFormInputClasses(!!error);
+
+ const fullWidthClass = fullWidth ? 'w-full' : '';
+ const resizeClass = {
+ none: 'resize-none',
+ vertical: 'resize-y',
+ horizontal: 'resize-x',
+ both: 'resize',
+ }[resize];
+
+ const inputId = id || `textarea-${Math.random().toString(36).substr(2, 9)}`;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {helperText && !error && (
+
+ {helperText}
+
+ )}
+
+ );
+});
+
+Textarea.displayName = 'Textarea';
\ No newline at end of file
diff --git a/app/components/ui/index.ts b/app/components/ui/index.ts
new file mode 100644
index 0000000..2aba204
--- /dev/null
+++ b/app/components/ui/index.ts
@@ -0,0 +1,10 @@
+export { Text } from './Text';
+export { Button } from './Button';
+export { Input } from './Input';
+export { AutocompleteInput } from './AutocompleteInput';
+export { Card, CardHeader, CardBody, CardFooter } from './Card';
+export { DataTable, Pagination } from './DataTable';
+export { SearchInput } from './SearchInput';
+export { Modal, ConfirmModal } from './Modal';
+export { Select } from './Select';
+export { MultiSelect } from './MultiSelect';
\ No newline at end of file
diff --git a/app/components/users/UserForm.tsx b/app/components/users/UserForm.tsx
new file mode 100644
index 0000000..439faae
--- /dev/null
+++ b/app/components/users/UserForm.tsx
@@ -0,0 +1,217 @@
+import { useState, useEffect } from 'react';
+import { Form } from '@remix-run/react';
+import { Input, Button, Select, Text } from '~/components/ui';
+import { validateUser } from '~/lib/validation';
+import { AUTH_LEVELS, USER_STATUS } from '~/types/auth';
+import type { UserWithoutPassword } from '~/types/database';
+
+interface UserFormProps {
+ user?: UserWithoutPassword;
+ onSubmit: (data: FormData) => void;
+ onCancel: () => void;
+ loading?: boolean;
+ currentUserAuthLevel: number;
+}
+
+export function UserForm({
+ user,
+ onSubmit,
+ onCancel,
+ loading = false,
+ currentUserAuthLevel,
+}: UserFormProps) {
+ const [formData, setFormData] = useState({
+ name: user?.name || '',
+ username: user?.username || '',
+ email: user?.email || '',
+ password: '',
+ confirmPassword: '',
+ authLevel: user?.authLevel || AUTH_LEVELS.USER,
+ status: user?.status || USER_STATUS.ACTIVE,
+ });
+
+ const [errors, setErrors] = useState>({});
+ const [touched, setTouched] = useState>({});
+
+ const isEditing = !!user;
+
+ // Validate form data
+ useEffect(() => {
+ const validationData = {
+ name: formData.name,
+ username: formData.username,
+ email: formData.email,
+ authLevel: formData.authLevel,
+ status: formData.status,
+ };
+
+ // Only validate password for new users or when password is provided
+ if (!isEditing || formData.password) {
+ validationData.password = formData.password;
+ }
+
+ const validation = validateUser(validationData);
+
+ // Add confirm password validation
+ const newErrors = { ...validation.errors };
+ if (!isEditing || formData.password) {
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
+ }
+ }
+
+ setErrors(newErrors);
+ }, [formData, isEditing]);
+
+ const handleInputChange = (field: string, value: string | number) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ setTouched(prev => ({ ...prev, [field]: true }));
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Mark all fields as touched
+ const allFields = Object.keys(formData);
+ setTouched(allFields.reduce((acc, field) => ({ ...acc, [field]: true }), {}));
+
+ // Check if form is valid
+ if (Object.keys(errors).length > 0) {
+ return;
+ }
+
+ // Create FormData
+ const submitData = new FormData();
+ submitData.append('name', formData.name);
+ submitData.append('username', formData.username);
+ submitData.append('email', formData.email);
+ submitData.append('authLevel', formData.authLevel.toString());
+ submitData.append('status', formData.status);
+
+ if (!isEditing || formData.password) {
+ submitData.append('password', formData.password);
+ }
+
+ if (isEditing) {
+ submitData.append('userId', user.id.toString());
+ submitData.append('_action', 'update');
+ } else {
+ submitData.append('_action', 'create');
+ }
+
+ onSubmit(submitData);
+ };
+
+ // Get available auth levels based on current user's level
+ const getAuthLevelOptions = () => {
+ const options = [];
+
+ if (currentUserAuthLevel === AUTH_LEVELS.SUPERADMIN) {
+ options.push({ value: AUTH_LEVELS.SUPERADMIN, label: 'مدير عام' });
+ }
+
+ if (currentUserAuthLevel <= AUTH_LEVELS.ADMIN) {
+ options.push({ value: AUTH_LEVELS.ADMIN, label: 'مدير' });
+ }
+
+ options.push({ value: AUTH_LEVELS.USER, label: 'مستخدم' });
+
+ return options;
+ };
+
+ const statusOptions = [
+ { value: USER_STATUS.ACTIVE, label: 'نشط' },
+ { value: USER_STATUS.INACTIVE, label: 'غير نشط' },
+ ];
+
+ return (
+
+
+ handleInputChange('name', e.target.value)}
+ error={touched.name ? errors.name : undefined}
+ required
+ />
+
+ handleInputChange('username', e.target.value)}
+ error={touched.username ? errors.username : undefined}
+ required
+ />
+
+
+ handleInputChange('email', e.target.value)}
+ error={touched.email ? errors.email : undefined}
+ required
+ />
+
+
+ handleInputChange('password', e.target.value)}
+ error={touched.password ? errors.password : undefined}
+ required={!isEditing}
+ />
+
+ handleInputChange('confirmPassword', e.target.value)}
+ error={touched.confirmPassword ? errors.confirmPassword : undefined}
+ required={!isEditing || !!formData.password}
+ />
+
+
+
+ handleInputChange('authLevel', parseInt(e.target.value))}
+ options={getAuthLevelOptions()}
+ error={touched.authLevel ? errors.authLevel : undefined}
+ required
+ />
+
+ handleInputChange('status', e.target.value)}
+ options={statusOptions}
+ error={touched.status ? errors.status : undefined}
+ required
+ />
+
+
+
+
+ 0}
+ >
+ {isEditing ? 'تحديث المستخدم' : 'إنشاء المستخدم'}
+
+
+
+ إلغاء
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/users/UserList.tsx b/app/components/users/UserList.tsx
new file mode 100644
index 0000000..2a70b40
--- /dev/null
+++ b/app/components/users/UserList.tsx
@@ -0,0 +1,233 @@
+import { useState, memo } from 'react';
+import { Form } from '@remix-run/react';
+import { DataTable, Pagination, Button, Text, ConfirmModal } from '~/components/ui';
+import { getAuthLevelName, getStatusName } from '~/lib/user-utils';
+import { useSettings } from '~/contexts/SettingsContext';
+import { AUTH_LEVELS } from '~/types/auth';
+import type { UserWithoutPassword } from '~/types/database';
+
+interface UserListProps {
+ users: UserWithoutPassword[];
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ onEdit: (user: UserWithoutPassword) => void;
+ onDelete: (userId: number) => void;
+ onToggleStatus: (userId: number) => void;
+ currentUserAuthLevel: number;
+ loading?: boolean;
+}
+
+export const UserList = memo(function UserList({
+ users,
+ currentPage,
+ totalPages,
+ onPageChange,
+ onEdit,
+ onDelete,
+ onToggleStatus,
+ currentUserAuthLevel,
+ loading = false,
+}: UserListProps) {
+ const { formatDate } = useSettings();
+ const [deleteModal, setDeleteModal] = useState<{
+ isOpen: boolean;
+ user: UserWithoutPassword | null;
+ }>({ isOpen: false, user: null });
+
+ const [statusModal, setStatusModal] = useState<{
+ isOpen: boolean;
+ user: UserWithoutPassword | null;
+ }>({ isOpen: false, user: null });
+
+ const handleDeleteClick = (user: UserWithoutPassword) => {
+ setDeleteModal({ isOpen: true, user });
+ };
+
+ const handleStatusClick = (user: UserWithoutPassword) => {
+ setStatusModal({ isOpen: true, user });
+ };
+
+ const handleDeleteConfirm = () => {
+ if (deleteModal.user) {
+ onDelete(deleteModal.user.id);
+ }
+ setDeleteModal({ isOpen: false, user: null });
+ };
+
+ const handleStatusConfirm = () => {
+ if (statusModal.user) {
+ onToggleStatus(statusModal.user.id);
+ }
+ setStatusModal({ isOpen: false, user: null });
+ };
+
+ const canEditUser = (user: UserWithoutPassword) => {
+ // Superadmin can edit anyone
+ if (currentUserAuthLevel === AUTH_LEVELS.SUPERADMIN) return true;
+
+ // Admin cannot edit superadmin
+ if (currentUserAuthLevel === AUTH_LEVELS.ADMIN && user.authLevel === AUTH_LEVELS.SUPERADMIN) {
+ return false;
+ }
+
+ return true;
+ };
+
+ const canDeleteUser = (user: UserWithoutPassword) => {
+ // Same rules as edit
+ return canEditUser(user);
+ };
+
+ const columns = [
+ {
+ key: 'name',
+ header: 'الاسم',
+ sortable: true,
+ render: (user: UserWithoutPassword) => (
+
+ {user.name}
+ @{user.username}
+
+ ),
+ },
+ {
+ key: 'email',
+ header: 'البريد الإلكتروني',
+ sortable: true,
+ },
+ {
+ key: 'authLevel',
+ header: 'مستوى الصلاحية',
+ render: (user: UserWithoutPassword) => {
+ const levelName = getAuthLevelName(user.authLevel);
+ const colorClass = user.authLevel === AUTH_LEVELS.SUPERADMIN
+ ? 'text-purple-600 bg-purple-100'
+ : user.authLevel === AUTH_LEVELS.ADMIN
+ ? 'text-blue-600 bg-blue-100'
+ : 'text-gray-600 bg-gray-100';
+
+ return (
+
+ {levelName}
+
+ );
+ },
+ },
+ {
+ key: 'status',
+ header: 'الحالة',
+ render: (user: UserWithoutPassword) => {
+ const statusName = getStatusName(user.status);
+ const colorClass = user.status === 'active'
+ ? 'text-green-600 bg-green-100'
+ : 'text-red-600 bg-red-100';
+
+ return (
+
+ {statusName}
+
+ );
+ },
+ },
+ {
+ key: 'createdDate',
+ header: 'تاريخ الإنشاء',
+ sortable: true,
+ render: (user: UserWithoutPassword) => (
+
+ {formatDate(user.createdDate)}
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: 'الإجراءات',
+ render: (user: UserWithoutPassword) => (
+
+ {canEditUser(user) && (
+ onEdit(user)}
+ className='w-24'
+ >
+ تعديل
+
+ )}
+
+ {canEditUser(user) && (
+ handleStatusClick(user)}
+ className='w-24'
+ >
+ {user.status === 'active' ? 'إلغاء تفعيل' : 'تفعيل'}
+
+ )}
+
+ {canDeleteUser(user) && (
+ handleDeleteClick(user)}
+ className='w-24'
+ >
+ حذف
+
+ )}
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+
+
+ {totalPages > 1 && (
+
+ )}
+
+ {/* Delete Confirmation Modal */}
+ setDeleteModal({ isOpen: false, user: null })}
+ onConfirm={handleDeleteConfirm}
+ title="تأكيد الحذف"
+ message={`هل أنت متأكد من حذف المستخدم "${deleteModal.user?.name}"؟ هذا الإجراء لا يمكن التراجع عنه.`}
+ confirmText="حذف"
+ cancelText="إلغاء"
+ variant="danger"
+ />
+
+ {/* Status Toggle Confirmation Modal */}
+ setStatusModal({ isOpen: false, user: null })}
+ onConfirm={handleStatusConfirm}
+ title={statusModal.user?.status === 'active' ? 'إلغاء تفعيل المستخدم' : 'تفعيل المستخدم'}
+ message={
+ statusModal.user?.status === 'active'
+ ? `هل أنت متأكد من إلغاء تفعيل المستخدم "${statusModal.user?.name}"؟`
+ : `هل أنت متأكد من تفعيل المستخدم "${statusModal.user?.name}"؟`
+ }
+ confirmText={statusModal.user?.status === 'active' ? 'إلغاء تفعيل' : 'تفعيل'}
+ cancelText="إلغاء"
+ variant={statusModal.user?.status === 'active' ? 'warning' : 'info'}
+ />
+ >
+ );
+});
\ No newline at end of file
diff --git a/app/components/vehicles/VehicleDetailsView.tsx b/app/components/vehicles/VehicleDetailsView.tsx
new file mode 100644
index 0000000..3c26683
--- /dev/null
+++ b/app/components/vehicles/VehicleDetailsView.tsx
@@ -0,0 +1,388 @@
+import { Link } from "@remix-run/react";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import { useSettings } from "~/contexts/SettingsContext";
+import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, BODY_TYPES } from "~/lib/constants";
+import type { VehicleWithOwner, VehicleWithRelations } from "~/types/database";
+
+interface VehicleDetailsViewProps {
+ vehicle: VehicleWithOwner | VehicleWithRelations;
+ onEdit?: () => void;
+ onClose?: () => void;
+ isLoadingVisits?: boolean;
+}
+
+export function VehicleDetailsView({ vehicle, onEdit, onClose, isLoadingVisits }: VehicleDetailsViewProps) {
+ const { formatDate, formatCurrency, formatNumber } = useSettings();
+ // Helper functions to get display labels
+ const getTransmissionLabel = (value: string) => {
+ return TRANSMISSION_TYPES.find(t => t.value === value)?.label || value;
+ };
+
+ const getFuelLabel = (value: string) => {
+ return FUEL_TYPES.find(f => f.value === value)?.label || value;
+ };
+
+ const getUseTypeLabel = (value: string) => {
+ return USE_TYPES.find(u => u.value === value)?.label || value;
+ };
+
+ const getBodyTypeLabel = (value: string) => {
+ return BODY_TYPES.find(b => b.value === value)?.label || value;
+ };
+
+ return (
+
+ {/* Enhanced Vehicle Information Section */}
+
+
+
+ 🚗
+ معلومات المركبة
+
+
+ المركبة #{vehicle.id}
+
+
+
+
+
+
رقم اللوحة
+
+ {vehicle.plateNumber}
+
+
+
+
+
الشركة المصنعة
+
{vehicle.manufacturer}
+
+
+
+
الموديل
+
{vehicle.model}
+
+
+
+
سنة الصنع
+
{vehicle.year}
+
+
+
+
نوع الهيكل
+
{getBodyTypeLabel(vehicle.bodyType)}
+
+
+ {vehicle.trim && (
+
+
الفئة
+
{vehicle.trim}
+
+ )}
+
+
+
+ {/* Technical Specifications */}
+
+
+
+ ⚙️
+ المواصفات التقنية
+
+
+
+
+
+
+
ناقل الحركة
+
{getTransmissionLabel(vehicle.transmission)}
+
+
+
+
نوع الوقود
+
{getFuelLabel(vehicle.fuel)}
+
+
+
+
نوع الاستخدام
+
{getUseTypeLabel(vehicle.useType)}
+
+
+ {vehicle.cylinders && (
+
+
عدد الأسطوانات
+
{vehicle.cylinders}
+
+ )}
+
+ {vehicle.engineDisplacement && (
+
+
سعة المحرك
+
{vehicle.engineDisplacement} لتر
+
+ )}
+
+
+
+
+ {/* Owner Information */}
+
+
+
+
+ 👤
+ معلومات المالك
+
+
+ 👁️
+ عرض تفاصيل المالك
+
+
+
+
+
+
+
+
اسم المالك
+
{vehicle.owner.name}
+
+
+ {vehicle.owner.phone && (
+
+ )}
+
+ {vehicle.owner.email && (
+
+ )}
+
+ {vehicle.owner.address && (
+
+
العنوان
+
{vehicle.owner.address}
+
+ )}
+
+
+
+
+ {/* Maintenance Status */}
+
+
+
+
+ 🔧
+ حالة الصيانة
+
+
+ 📋
+ عرض جميع زيارات الصيانة
+
+
+
+
+
+
+
+
آخر زيارة صيانة
+
+ {vehicle.lastVisitDate
+ ? formatDate(vehicle.lastVisitDate)
+ : لا توجد زيارات
+ }
+
+
+
+ {vehicle.suggestedNextVisitDate && (
+
+
الزيارة المقترحة التالية
+
+ {formatDate(vehicle.suggestedNextVisitDate)}
+
+
+ )}
+
+
+
تاريخ التسجيل
+
+ {formatDate(vehicle.createdDate)}
+
+
+
+
+
آخر تحديث
+
+ {formatDate(vehicle.updateDate)}
+
+
+
+
+
+
+ {/* Recent Maintenance Visits
+
+
+
+ 🔧
+ آخر زيارات الصيانة
+
+
+
+
+ {isLoadingVisits ? (
+
+
🔧
+
جاري تحميل زيارات الصيانة...
+
+
+ ) : !('maintenanceVisits' in vehicle) || !vehicle.maintenanceVisits || vehicle.maintenanceVisits.length === 0 ? (
+
+
🔧
+
لا توجد زيارات صيانة
+
لم يتم تسجيل أي زيارات صيانة لهذه المركبة بعد
+
+ تسجيل زيارة صيانة جديدة
+
+
+ ) : (
+
+ {('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).slice(0, 3).map((visit: any) => (
+
+
+
+
+ {(() => {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ return jobs.length > 1
+ ? `${jobs.length} أعمال صيانة`
+ : jobs[0]?.job || 'نوع صيانة غير محدد';
+ } catch {
+ return 'نوع صيانة غير محدد';
+ }
+ })()}
+
+
زيارة #{visit.id}
+
+
+
+ {formatCurrency(visit.cost)}
+
+
+ {visit.paymentStatus === 'paid' ? 'مدفوع' :
+ visit.paymentStatus === 'pending' ? 'معلق' : 'غير مدفوع'}
+
+
+
+
+
+
+ تاريخ الزيارة:
+
+ {formatDate(visit.visitDate)}
+
+
+
+ عداد الكيلومترات:
+
+ {visit.kilometers ? formatNumber(visit.kilometers) : 'غير محدد'} كم
+
+
+ {visit.description && (
+
+
الوصف:
+
{visit.description}
+
+ )}
+
+
+ ))}
+
+ {('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).length > 3 && (
+
+
+ عرض 3 من أصل {('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).length} زيارة صيانة
+
+
+
📋
+ عرض جميع الزيارات ({('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).length})
+
+
+ )}
+
+ )}
+
+
*/}
+
+ {/* Action Buttons */}
+ {(onEdit || onClose) && (
+
+ {onEdit && (
+
+ ✏️
+ تعديل المركبة
+
+ )}
+
+ {onClose && (
+
+ إغلاق
+
+ )}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/vehicles/VehicleForm.tsx b/app/components/vehicles/VehicleForm.tsx
new file mode 100644
index 0000000..f8b8b21
--- /dev/null
+++ b/app/components/vehicles/VehicleForm.tsx
@@ -0,0 +1,576 @@
+import { Form } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { Input } from "~/components/ui/Input";
+import { AutocompleteInput } from "~/components/ui/AutocompleteInput";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, VALIDATION } from "~/lib/constants";
+import type { Vehicle } from "~/types/database";
+
+interface VehicleFormProps {
+ vehicle?: Vehicle;
+ customers: { id: number; name: string; phone?: string | null }[];
+ onCancel: () => void;
+ errors?: Record;
+ isLoading: boolean;
+}
+
+export function VehicleForm({
+ vehicle,
+ customers,
+ onCancel,
+ errors = {},
+ isLoading,
+}: VehicleFormProps) {
+ const [formData, setFormData] = useState({
+ plateNumber: vehicle?.plateNumber || "",
+ bodyType: vehicle?.bodyType || "",
+ manufacturer: vehicle?.manufacturer || "",
+ model: vehicle?.model || "",
+ trim: vehicle?.trim || "",
+ year: vehicle?.year?.toString() || "",
+ transmission: vehicle?.transmission || "",
+ fuel: vehicle?.fuel || "",
+ cylinders: vehicle?.cylinders?.toString() || "",
+ engineDisplacement: vehicle?.engineDisplacement?.toString() || "",
+ useType: vehicle?.useType || "",
+ ownerId: vehicle?.ownerId?.toString() || "",
+ });
+
+ // Car dataset state
+ const [manufacturers, setManufacturers] = useState([]);
+ const [models, setModels] = useState<{model: string; bodyType: string}[]>([]);
+ const [isLoadingManufacturers, setIsLoadingManufacturers] = useState(false);
+ const [isLoadingModels, setIsLoadingModels] = useState(false);
+
+ // Autocomplete state
+ const [manufacturerSearchValue, setManufacturerSearchValue] = useState(vehicle?.manufacturer || "");
+ const [modelSearchValue, setModelSearchValue] = useState(vehicle?.model || "");
+ const [ownerSearchValue, setOwnerSearchValue] = useState(() => {
+ if (vehicle?.ownerId) {
+ const owner = customers.find(c => c.id === vehicle.ownerId);
+ return owner ? owner.name : "";
+ }
+ return "";
+ });
+
+ // Load manufacturers on component mount
+ useEffect(() => {
+ const loadManufacturers = async () => {
+ setIsLoadingManufacturers(true);
+ try {
+ const response = await fetch('/api/car-dataset?action=manufacturers');
+ const result = await response.json();
+ if (result.success) {
+ setManufacturers(result.data);
+ }
+ } catch (error) {
+ console.error('Error loading manufacturers:', error);
+ } finally {
+ setIsLoadingManufacturers(false);
+ }
+ };
+
+ loadManufacturers();
+ }, []);
+
+ // Load models when manufacturer changes
+ useEffect(() => {
+ if (formData.manufacturer) {
+ const loadModels = async () => {
+ setIsLoadingModels(true);
+ try {
+ const response = await fetch(`/api/car-dataset?action=models&manufacturer=${encodeURIComponent(formData.manufacturer)}`);
+ const result = await response.json();
+ if (result.success) {
+ setModels(result.data);
+ }
+ } catch (error) {
+ console.error('Error loading models:', error);
+ } finally {
+ setIsLoadingModels(false);
+ }
+ };
+
+ loadModels();
+ } else {
+ setModels([]);
+ }
+ }, [formData.manufacturer]);
+
+ // Create autocomplete options
+ const manufacturerOptions = manufacturers.map(manufacturer => ({
+ value: manufacturer,
+ label: manufacturer,
+ data: manufacturer
+ }));
+
+ const modelOptions = models.map(item => ({
+ value: item.model,
+ label: item.model,
+ data: item
+ }));
+
+ const ownerOptions = customers.map(customer => ({
+ value: customer.name,
+ label: `${customer.name}${customer.phone ? ` - ${customer.phone}` : ''}`,
+ data: customer
+ }));
+
+ // Handle manufacturer selection
+ const handleManufacturerSelect = (option: any) => {
+ const manufacturer = option.data;
+ setManufacturerSearchValue(manufacturer);
+ setFormData(prev => ({
+ ...prev,
+ manufacturer,
+ model: "", // Reset model when manufacturer changes
+ bodyType: "" // Reset body type when manufacturer changes
+ }));
+ setModelSearchValue(""); // Reset model search
+ };
+
+ // Handle model selection
+ const handleModelSelect = (option: any) => {
+ const modelData = option.data;
+ setModelSearchValue(modelData.model);
+ setFormData(prev => ({
+ ...prev,
+ model: modelData.model,
+ bodyType: modelData.bodyType // Auto-set body type from dataset
+ }));
+ };
+
+ // Handle owner selection from autocomplete
+ const handleOwnerSelect = (option: any) => {
+ const customer = option.data;
+ setOwnerSearchValue(customer.name);
+ setFormData(prev => ({
+ ...prev,
+ ownerId: customer.id.toString()
+ }));
+ };
+
+ // Reset form data when vehicle changes
+ useEffect(() => {
+ if (vehicle) {
+ const owner = customers.find(c => c.id === vehicle.ownerId);
+ setFormData({
+ plateNumber: vehicle.plateNumber || "",
+ bodyType: vehicle.bodyType || "",
+ manufacturer: vehicle.manufacturer || "",
+ model: vehicle.model || "",
+ trim: vehicle.trim || "",
+ year: vehicle.year?.toString() || "",
+ transmission: vehicle.transmission || "",
+ fuel: vehicle.fuel || "",
+ cylinders: vehicle.cylinders?.toString() || "",
+ engineDisplacement: vehicle.engineDisplacement?.toString() || "",
+ useType: vehicle.useType || "",
+ ownerId: vehicle.ownerId?.toString() || "",
+ });
+ setManufacturerSearchValue(vehicle.manufacturer || "");
+ setModelSearchValue(vehicle.model || "");
+ setOwnerSearchValue(owner ? owner.name : "");
+ } else {
+ setFormData({
+ plateNumber: "",
+ bodyType: "",
+ manufacturer: "",
+ model: "",
+ trim: "",
+ year: "",
+ transmission: "",
+ fuel: "",
+ cylinders: "",
+ engineDisplacement: "",
+ useType: "",
+ ownerId: "",
+ });
+ setManufacturerSearchValue("");
+ setModelSearchValue("");
+ setOwnerSearchValue("");
+ }
+ }, [vehicle, customers]);
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData(prev => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const isEditing = !!vehicle;
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+
+ {isEditing && (
+
+ )}
+
+
+ {/* Plate Number */}
+
+
+ رقم اللوحة *
+
+
handleInputChange("plateNumber", e.target.value)}
+ placeholder="أدخل رقم اللوحة"
+ error={errors.plateNumber}
+ required
+ disabled={isLoading}
+ dir="ltr"
+ />
+ {errors.plateNumber && (
+
{errors.plateNumber}
+ )}
+
+
+ {/* Manufacturer with Autocomplete */}
+
+
+ {/* Hidden input for form submission */}
+
+ {formData.manufacturer && manufacturerSearchValue && (
+
+ ✓ تم اختيار الشركة المصنعة: {manufacturerSearchValue}
+
+ )}
+
+
+ {/* Model with Autocomplete */}
+
+
+ {/* Hidden input for form submission */}
+
+ {formData.model && modelSearchValue && (
+
+ ✓ تم اختيار الموديل: {modelSearchValue}
+
+ )}
+ {!formData.manufacturer && (
+
+ يرجى اختيار الشركة المصنعة أولاً
+
+ )}
+
+
+ {/* Body Type (Auto-filled, Read-only) */}
+
+
+ نوع الهيكل *
+
+
+ {formData.bodyType && (
+
+ ℹ️ تم تعبئة نوع الهيكل تلقائياً من قاعدة البيانات
+
+ )}
+ {errors.bodyType && (
+
{errors.bodyType}
+ )}
+
+
+ {/* Trim */}
+
+
+ الفئة
+
+
handleInputChange("trim", e.target.value)}
+ placeholder="أدخل الفئة (اختياري)"
+ error={errors.trim}
+ disabled={isLoading}
+ />
+ {errors.trim && (
+
{errors.trim}
+ )}
+
+
+ {/* Year */}
+
+
+ سنة الصنع *
+
+
handleInputChange("year", e.target.value)}
+ placeholder={`${VALIDATION.MIN_YEAR} - ${currentYear}`}
+ error={errors.year}
+ required
+ disabled={isLoading}
+ />
+ {errors.year && (
+
{errors.year}
+ )}
+
+
+ {/* Transmission */}
+
+
+ ناقل الحركة *
+
+
handleInputChange("transmission", e.target.value)}
+ required
+ disabled={isLoading}
+ className={`
+ w-full px-3 py-2 border rounded-lg shadow-sm
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
+ disabled:bg-gray-50 disabled:text-gray-500
+ ${errors.transmission
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }
+ `}
+ >
+ اختر ناقل الحركة
+ {TRANSMISSION_TYPES.map((transmission) => (
+
+ {transmission.label}
+
+ ))}
+
+ {errors.transmission && (
+
{errors.transmission}
+ )}
+
+
+ {/* Fuel */}
+
+
+ نوع الوقود *
+
+
handleInputChange("fuel", e.target.value)}
+ required
+ disabled={isLoading}
+ className={`
+ w-full px-3 py-2 border rounded-lg shadow-sm
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
+ disabled:bg-gray-50 disabled:text-gray-500
+ ${errors.fuel
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }
+ `}
+ >
+ اختر نوع الوقود
+ {FUEL_TYPES.map((fuel) => (
+
+ {fuel.label}
+
+ ))}
+
+ {errors.fuel && (
+
{errors.fuel}
+ )}
+
+
+ {/* Cylinders */}
+
+
+ عدد الأسطوانات
+
+
handleInputChange("cylinders", e.target.value)}
+ placeholder="عدد الأسطوانات (اختياري)"
+ error={errors.cylinders}
+ disabled={isLoading}
+ />
+ {errors.cylinders && (
+
{errors.cylinders}
+ )}
+
+
+ {/* Engine Displacement */}
+
+
+ سعة المحرك (لتر)
+
+
handleInputChange("engineDisplacement", e.target.value)}
+ placeholder="سعة المحرك (اختياري)"
+ error={errors.engineDisplacement}
+ disabled={isLoading}
+ />
+ {errors.engineDisplacement && (
+
{errors.engineDisplacement}
+ )}
+
+
+ {/* Use Type */}
+
+
+ نوع الاستخدام *
+
+
handleInputChange("useType", e.target.value)}
+ required
+ disabled={isLoading}
+ className={`
+ w-full px-3 py-2 border rounded-lg shadow-sm
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
+ disabled:bg-gray-50 disabled:text-gray-500
+ ${errors.useType
+ ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
+ : 'border-gray-300'
+ }
+ `}
+ >
+ اختر نوع الاستخدام
+ {USE_TYPES.map((useType) => (
+
+ {useType.label}
+
+ ))}
+
+ {errors.useType && (
+
{errors.useType}
+ )}
+
+
+ {/* Owner with Autocomplete */}
+
+
+ {/* Hidden input for form submission */}
+
+ {formData.ownerId && ownerSearchValue && (
+
+ ✓ تم اختيار المالك: {ownerSearchValue}
+
+ )}
+ {!formData.ownerId && ownerSearchValue && (
+
+ يرجى اختيار المالك من القائمة المنسدلة
+
+ )}
+
+
+
+ {/* Form Actions */}
+
+
+ إلغاء
+
+
+
+ {isLoading
+ ? (isEditing ? "جاري التحديث..." : "جاري الإنشاء...")
+ : (isEditing ? "تحديث المركبة" : "إنشاء المركبة")
+ }
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/vehicles/VehicleList.tsx b/app/components/vehicles/VehicleList.tsx
new file mode 100644
index 0000000..8dd8bce
--- /dev/null
+++ b/app/components/vehicles/VehicleList.tsx
@@ -0,0 +1,272 @@
+import { Form, Link } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { DataTable, Pagination } from "~/components/ui/DataTable";
+import { Button } from "~/components/ui/Button";
+import { Flex } from "~/components/layout/Flex";
+import { useSettings } from "~/contexts/SettingsContext";
+import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, BODY_TYPES } from "~/lib/constants";
+import type { VehicleWithOwner } from "~/types/database";
+
+interface VehicleListProps {
+ vehicles: VehicleWithOwner[];
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ onEditVehicle: (vehicle: VehicleWithOwner) => void;
+ onViewVehicle?: (vehicle: VehicleWithOwner) => void;
+ isLoading: boolean;
+ actionData?: any;
+}
+
+export function VehicleList({
+ vehicles,
+ currentPage,
+ totalPages,
+ onPageChange,
+ onEditVehicle,
+ onViewVehicle,
+ isLoading,
+ actionData,
+}: VehicleListProps) {
+ const { formatDate } = useSettings();
+ const [deletingVehicleId, setDeletingVehicleId] = useState(null);
+
+ // Reset deleting state when delete action completes
+ useEffect(() => {
+ if (actionData?.success && actionData.action === "delete") {
+ setDeletingVehicleId(null);
+ }
+ }, [actionData]);
+
+ // Helper functions to get display labels
+ const getTransmissionLabel = (value: string) => {
+ return TRANSMISSION_TYPES.find(t => t.value === value)?.label || value;
+ };
+
+ const getFuelLabel = (value: string) => {
+ return FUEL_TYPES.find(f => f.value === value)?.label || value;
+ };
+
+ const getUseTypeLabel = (value: string) => {
+ return USE_TYPES.find(u => u.value === value)?.label || value;
+ };
+
+ const getBodyTypeLabel = (value: string) => {
+ return BODY_TYPES.find(b => b.value === value)?.label || value;
+ };
+
+ const columns = [
+ {
+ key: "plateNumber",
+ header: "رقم اللوحة",
+ render: (vehicle: VehicleWithOwner) => (
+
+
+ {vehicle.plateNumber}
+
+
+ المركبة رقم: {vehicle.id}
+
+
+ ),
+ },
+ {
+ key: "vehicle",
+ header: "تفاصيل المركبة",
+ render: (vehicle: VehicleWithOwner) => (
+
+
+ {vehicle.manufacturer} {vehicle.model}
+
+
+ {vehicle.year} • {getBodyTypeLabel(vehicle.bodyType)}
+
+ {vehicle.trim && (
+
+ فئة: {vehicle.trim}
+
+ )}
+
+ ),
+ },
+ {
+ key: "specifications",
+ header: "المواصفات",
+ render: (vehicle: VehicleWithOwner) => (
+
+
+ {getTransmissionLabel(vehicle.transmission)}
+
+
+ {getFuelLabel(vehicle.fuel)}
+
+ {vehicle.cylinders && (
+
+ {vehicle.cylinders} أسطوانة
+
+ )}
+ {vehicle.engineDisplacement && (
+
+ {vehicle.engineDisplacement}L
+
+ )}
+
+ ),
+ },
+ {
+ key: "owner",
+ header: "المالك",
+ render: (vehicle: VehicleWithOwner) => (
+
+
+ {vehicle.owner.name}
+
+ {vehicle.owner.phone && (
+
+ {vehicle.owner.phone}
+
+ )}
+
+ ),
+ },
+ {
+ key: "useType",
+ header: "نوع الاستخدام",
+ render: (vehicle: VehicleWithOwner) => (
+
+ {getUseTypeLabel(vehicle.useType)}
+
+ ),
+ },
+ {
+ key: "maintenance",
+ header: "الصيانة",
+ render: (vehicle: VehicleWithOwner) => (
+
+ {vehicle.lastVisitDate ? (
+
+ آخر زيارة: {formatDate(vehicle.lastVisitDate)}
+
+ ) : (
+
+ لا توجد زيارات
+
+ )}
+ {vehicle.suggestedNextVisitDate && (
+
+ الزيارة التالية: {formatDate(vehicle.suggestedNextVisitDate)}
+
+ )}
+
+ ),
+ },
+ {
+ key: "createdDate",
+ header: "تاريخ التسجيل",
+ render: (vehicle: VehicleWithOwner) => (
+
+ {formatDate(vehicle.createdDate)}
+
+ ),
+ },
+ {
+ key: "actions",
+ header: "الإجراءات",
+ render: (vehicle: VehicleWithOwner) => (
+
+ {onViewVehicle ? (
+ onViewVehicle(vehicle)}
+ disabled={isLoading}
+ >
+ عرض
+
+ ) : (
+
+
+ عرض
+
+
+ )}
+
+ onEditVehicle(vehicle)}
+ disabled={isLoading}
+ >
+ تعديل
+
+
+
+
+
+ {
+ e.preventDefault();
+ if (window.confirm("هل أنت متأكد من حذف هذه المركبة؟")) {
+ setDeletingVehicleId(vehicle.id);
+ (e.target as HTMLButtonElement).form?.submit();
+ }
+ }}
+ >
+ {deletingVehicleId === vehicle.id ? "جاري الحذف..." : "حذف"}
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ {vehicles.length === 0 ? (
+
+
🚗
+
+ لا توجد مركبات
+
+
+ لم يتم العثور على أي مركبات. قم بإضافة مركبة جديدة للبدء.
+
+
+ ) : (
+
+ )}
+
+
+ {totalPages > 1 && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/contexts/SettingsContext.tsx b/app/contexts/SettingsContext.tsx
new file mode 100644
index 0000000..ad54847
--- /dev/null
+++ b/app/contexts/SettingsContext.tsx
@@ -0,0 +1,163 @@
+import { createContext, useContext, type ReactNode } from 'react';
+import type { AppSettings } from '~/lib/settings-management.server';
+
+interface SettingsContextType {
+ settings: AppSettings;
+ formatNumber: (value: number) => string;
+ formatCurrency: (value: number) => string;
+ formatDate: (date: Date | string) => string;
+ formatDateTime: (date: Date | string) => string;
+}
+
+const SettingsContext = createContext(null);
+
+interface SettingsProviderProps {
+ children: ReactNode;
+ settings: AppSettings;
+}
+
+export function SettingsProvider({ children, settings }: SettingsProviderProps) {
+ // Helper function to convert Western numerals to Arabic numerals
+ const convertToArabicNumerals = (str: string): string => {
+ const arabicNumerals = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
+ return str.replace(/[0-9]/g, (digit) => arabicNumerals[parseInt(digit)]);
+ };
+
+ // Helper function to format date with custom pattern
+ const formatDateWithPattern = (date: Date, pattern: string): string => {
+ const day = date.getDate();
+ const month = date.getMonth() + 1;
+ const year = date.getFullYear();
+
+ // Format numbers according to locale
+ const formatNumber = (num: number, padLength: number = 2): string => {
+ const padded = num.toString().padStart(padLength, '0');
+ return settings.numberFormat === 'ar-SA'
+ ? convertToArabicNumerals(padded)
+ : padded;
+ };
+
+ return pattern
+ .replace(/yyyy/g, formatNumber(year, 4))
+ .replace(/yy/g, formatNumber(year % 100, 2))
+ .replace(/MM/g, formatNumber(month, 2))
+ .replace(/M/g, formatNumber(month, 1))
+ .replace(/dd/g, formatNumber(day, 2))
+ .replace(/d/g, formatNumber(day, 1));
+ };
+
+ const formatNumber = (value: number): string => {
+ return value.toLocaleString(settings.numberFormat);
+ };
+
+ const formatCurrency = (value: number): string => {
+ const formatted = value.toLocaleString(settings.numberFormat, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ return `${formatted} ${settings.currencySymbol}`;
+ };
+
+ const formatDate = (date: Date | string): string => {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ return formatDateWithPattern(dateObj, settings.dateDisplayFormat);
+ };
+
+ const formatDateTime = (date: Date | string): string => {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ const datePart = formatDateWithPattern(dateObj, settings.dateDisplayFormat);
+ const timePart = dateObj.toLocaleTimeString(settings.dateFormat, {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ return `${datePart} ${timePart}`;
+ };
+
+ const contextValue: SettingsContextType = {
+ settings,
+ formatNumber,
+ formatCurrency,
+ formatDate,
+ formatDateTime
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSettings(): SettingsContextType {
+ const context = useContext(SettingsContext);
+ if (!context) {
+ throw new Error('useSettings must be used within a SettingsProvider');
+ }
+ return context;
+}
+
+// Hook for formatting utilities without requiring context
+export function useFormatters(settings: AppSettings) {
+ // Helper function to convert Western numerals to Arabic numerals
+ const convertToArabicNumerals = (str: string): string => {
+ const arabicNumerals = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
+ return str.replace(/[0-9]/g, (digit) => arabicNumerals[parseInt(digit)]);
+ };
+
+ // Helper function to format date with custom pattern
+ const formatDateWithPattern = (date: Date, pattern: string): string => {
+ const day = date.getDate();
+ const month = date.getMonth() + 1;
+ const year = date.getFullYear();
+
+ // Format numbers according to locale
+ const formatNumber = (num: number, padLength: number = 2): string => {
+ const padded = num.toString().padStart(padLength, '0');
+ return settings.numberFormat === 'ar-SA'
+ ? convertToArabicNumerals(padded)
+ : padded;
+ };
+
+ return pattern
+ .replace(/yyyy/g, formatNumber(year, 4))
+ .replace(/yy/g, formatNumber(year % 100, 2))
+ .replace(/MM/g, formatNumber(month, 2))
+ .replace(/M/g, formatNumber(month, 1))
+ .replace(/dd/g, formatNumber(day, 2))
+ .replace(/d/g, formatNumber(day, 1));
+ };
+
+ const formatNumber = (value: number): string => {
+ return value.toLocaleString(settings.numberFormat);
+ };
+
+ const formatCurrency = (value: number): string => {
+ const formatted = value.toLocaleString(settings.numberFormat, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ return `${formatted} ${settings.currencySymbol}`;
+ };
+
+ const formatDate = (date: Date | string): string => {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ return formatDateWithPattern(dateObj, settings.dateDisplayFormat);
+ };
+
+ const formatDateTime = (date: Date | string): string => {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ const datePart = formatDateWithPattern(dateObj, settings.dateDisplayFormat);
+ const timePart = dateObj.toLocaleTimeString(settings.dateFormat, {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ return `${datePart} ${timePart}`;
+ };
+
+ return {
+ formatNumber,
+ formatCurrency,
+ formatDate,
+ formatDateTime
+ };
+}
\ No newline at end of file
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
new file mode 100644
index 0000000..94d5dc0
--- /dev/null
+++ b/app/entry.client.tsx
@@ -0,0 +1,18 @@
+/**
+ * By default, Remix will handle hydrating your app on the client for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.client
+ */
+
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+});
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
new file mode 100644
index 0000000..45db322
--- /dev/null
+++ b/app/entry.server.tsx
@@ -0,0 +1,140 @@
+/**
+ * By default, Remix will handle generating the HTTP Response for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.server
+ */
+
+import { PassThrough } from "node:stream";
+
+import type { AppLoadContext, EntryContext } from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import { isbot } from "isbot";
+import { renderToPipeableStream } from "react-dom/server";
+
+const ABORT_DELAY = 5_000;
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ // This is ignored so we can keep it in the template for visibility. Feel
+ // free to delete this parameter in your app if you're not using it!
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ loadContext: AppLoadContext
+) {
+ return isbot(request.headers.get("user-agent") || "")
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ );
+}
+
+function handleBotRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
+
+function handleBrowserRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/app/hooks/useDebounce.ts b/app/hooks/useDebounce.ts
new file mode 100644
index 0000000..2605ca6
--- /dev/null
+++ b/app/hooks/useDebounce.ts
@@ -0,0 +1,17 @@
+import { useState, useEffect } from 'react';
+
+export function useDebounce(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
\ No newline at end of file
diff --git a/app/hooks/useFormValidation.ts b/app/hooks/useFormValidation.ts
new file mode 100644
index 0000000..fbbeab7
--- /dev/null
+++ b/app/hooks/useFormValidation.ts
@@ -0,0 +1,219 @@
+import { useState, useCallback, useEffect } from 'react';
+import { z } from 'zod';
+
+interface UseFormValidationOptions {
+ schema: z.ZodSchema;
+ initialValues: Partial;
+ validateOnChange?: boolean;
+ validateOnBlur?: boolean;
+}
+
+interface FormState {
+ values: Partial;
+ errors: Record;
+ touched: Record;
+ isValid: boolean;
+ isSubmitting: boolean;
+}
+
+export function useFormValidation>({
+ schema,
+ initialValues,
+ validateOnChange = true,
+ validateOnBlur = true,
+}: UseFormValidationOptions) {
+ const [state, setState] = useState>({
+ values: initialValues,
+ errors: {},
+ touched: {},
+ isValid: false,
+ isSubmitting: false,
+ });
+
+ // Validate a single field
+ const validateField = useCallback((name: keyof T, value: any): string | null => {
+ try {
+ // Get the field schema
+ const fieldSchema = (schema as any).shape[name];
+ if (fieldSchema) {
+ fieldSchema.parse(value);
+ }
+ return null;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return error.errors[0]?.message || null;
+ }
+ return null;
+ }
+ }, [schema]);
+
+ // Validate all fields
+ const validateForm = useCallback((values: Partial): Record => {
+ try {
+ schema.parse(values);
+ return {};
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors: Record = {};
+ error.errors.forEach((err) => {
+ if (err.path.length > 0) {
+ errors[err.path[0] as string] = err.message;
+ }
+ });
+ return errors;
+ }
+ return {};
+ }
+ }, [schema]);
+
+ // Set field value
+ const setValue = useCallback((name: keyof T, value: any) => {
+ setState(prev => {
+ const newValues = { ...prev.values, [name]: value };
+ const fieldError = validateOnChange ? validateField(name, value) : null;
+ const newErrors = { ...prev.errors };
+
+ if (fieldError) {
+ newErrors[name as string] = fieldError;
+ } else {
+ delete newErrors[name as string];
+ }
+
+ const allErrors = validateOnChange ? validateForm(newValues) : newErrors;
+ const isValid = Object.keys(allErrors).length === 0;
+
+ return {
+ ...prev,
+ values: newValues,
+ errors: allErrors,
+ isValid,
+ };
+ });
+ }, [validateField, validateForm, validateOnChange]);
+
+ // Set field as touched
+ const setTouched = useCallback((name: keyof T, touched = true) => {
+ setState(prev => {
+ const newTouched = { ...prev.touched, [name]: touched };
+ let newErrors = { ...prev.errors };
+
+ if (touched && validateOnBlur) {
+ const fieldError = validateField(name, prev.values[name]);
+ if (fieldError) {
+ newErrors[name as string] = fieldError;
+ }
+ }
+
+ return {
+ ...prev,
+ touched: newTouched,
+ errors: newErrors,
+ };
+ });
+ }, [validateField, validateOnBlur]);
+
+ // Set multiple values
+ const setValues = useCallback((values: Partial) => {
+ setState(prev => {
+ const newValues = { ...prev.values, ...values };
+ const errors = validateForm(newValues);
+ const isValid = Object.keys(errors).length === 0;
+
+ return {
+ ...prev,
+ values: newValues,
+ errors,
+ isValid,
+ };
+ });
+ }, [validateForm]);
+
+ // Reset form
+ const reset = useCallback((newInitialValues?: Partial) => {
+ const resetValues = newInitialValues || initialValues;
+ setState({
+ values: resetValues,
+ errors: {},
+ touched: {},
+ isValid: false,
+ isSubmitting: false,
+ });
+ }, [initialValues]);
+
+ // Set submitting state
+ const setSubmitting = useCallback((isSubmitting: boolean) => {
+ setState(prev => ({ ...prev, isSubmitting }));
+ }, []);
+
+ // Validate entire form and return validation result
+ const validate = useCallback(() => {
+ const errors = validateForm(state.values);
+ const isValid = Object.keys(errors).length === 0;
+
+ setState(prev => ({
+ ...prev,
+ errors,
+ isValid,
+ touched: Object.keys(prev.values).reduce((acc, key) => {
+ acc[key] = true;
+ return acc;
+ }, {} as Record),
+ }));
+
+ return { isValid, errors };
+ }, [state.values, validateForm]);
+
+ // Get field props for easy integration with form components
+ const getFieldProps = useCallback((name: keyof T) => {
+ return {
+ name: name as string,
+ value: state.values[name] || '',
+ error: state.touched[name as string] ? state.errors[name as string] : undefined,
+ onChange: (e: React.ChangeEvent) => {
+ setValue(name, e.target.value);
+ },
+ onBlur: () => {
+ setTouched(name, true);
+ },
+ };
+ }, [state.values, state.errors, state.touched, setValue, setTouched]);
+
+ // Get field error
+ const getFieldError = useCallback((name: keyof T): string | undefined => {
+ return state.touched[name as string] ? state.errors[name as string] : undefined;
+ }, [state.errors, state.touched]);
+
+ // Check if field has error
+ const hasFieldError = useCallback((name: keyof T): boolean => {
+ return !!(state.touched[name as string] && state.errors[name as string]);
+ }, [state.errors, state.touched]);
+
+ // Update validation when schema or initial values change
+ useEffect(() => {
+ const errors = validateForm(state.values);
+ const isValid = Object.keys(errors).length === 0;
+
+ setState(prev => ({
+ ...prev,
+ errors,
+ isValid,
+ }));
+ }, [schema, validateForm]);
+
+ return {
+ values: state.values,
+ errors: state.errors,
+ touched: state.touched,
+ isValid: state.isValid,
+ isSubmitting: state.isSubmitting,
+ setValue,
+ setValues,
+ setTouched,
+ setSubmitting,
+ reset,
+ validate,
+ getFieldProps,
+ getFieldError,
+ hasFieldError,
+ };
+}
\ No newline at end of file
diff --git a/app/lib/__tests__/auth-integration.test.ts b/app/lib/__tests__/auth-integration.test.ts
new file mode 100644
index 0000000..144ecfa
--- /dev/null
+++ b/app/lib/__tests__/auth-integration.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from "vitest";
+import bcrypt from "bcryptjs";
+
+// Test the actual bcrypt functionality without importing our modules
+describe("Authentication Integration", () => {
+ describe("Password Hashing Integration", () => {
+ it("should hash and verify passwords using bcrypt", async () => {
+ const password = "testpassword123";
+
+ // Hash the password
+ const hashedPassword = await bcrypt.hash(password, 12);
+
+ expect(hashedPassword).toBeDefined();
+ expect(hashedPassword).not.toBe(password);
+ expect(hashedPassword.length).toBeGreaterThan(0);
+
+ // Verify correct password
+ const isValidPassword = await bcrypt.compare(password, hashedPassword);
+ expect(isValidPassword).toBe(true);
+
+ // Verify incorrect password
+ const isInvalidPassword = await bcrypt.compare("wrongpassword", hashedPassword);
+ expect(isInvalidPassword).toBe(false);
+ });
+
+ it("should generate different hashes for the same password", async () => {
+ const password = "testpassword123";
+
+ const hash1 = await bcrypt.hash(password, 12);
+ const hash2 = await bcrypt.hash(password, 12);
+
+ expect(hash1).not.toBe(hash2);
+
+ // But both should verify correctly
+ expect(await bcrypt.compare(password, hash1)).toBe(true);
+ expect(await bcrypt.compare(password, hash2)).toBe(true);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/auth.test.ts b/app/lib/__tests__/auth.test.ts
new file mode 100644
index 0000000..c5e7724
--- /dev/null
+++ b/app/lib/__tests__/auth.test.ts
@@ -0,0 +1,58 @@
+import { describe, it, expect, vi } from "vitest";
+import { hasPermission, canAccessUserManagement } from "../auth-helpers.server";
+import { AUTH_LEVELS } from "~/types/auth";
+
+// Mock the database
+vi.mock("../db.server", () => ({
+ prisma: {
+ user: {
+ findFirst: vi.fn(),
+ findUnique: vi.fn(),
+ create: vi.fn(),
+ count: vi.fn(),
+ },
+ },
+}));
+
+// Mock auth.server to avoid session secret requirement
+vi.mock("../auth.server", () => ({
+ hashPassword: vi.fn(),
+ verifyPassword: vi.fn(),
+ createUserSession: vi.fn(),
+ getUserSession: vi.fn(),
+ getUserId: vi.fn(),
+ requireUserId: vi.fn(),
+ getUser: vi.fn(),
+ requireUser: vi.fn(),
+ logout: vi.fn(),
+}));
+
+describe("Authentication System", () => {
+
+ describe("Authorization Helpers", () => {
+ it("should check permissions correctly", () => {
+ // Superadmin should have access to everything
+ expect(hasPermission(AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.SUPERADMIN)).toBe(true);
+ expect(hasPermission(AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.ADMIN)).toBe(true);
+ expect(hasPermission(AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.USER)).toBe(true);
+
+ // Admin should have access to admin and user levels
+ expect(hasPermission(AUTH_LEVELS.ADMIN, AUTH_LEVELS.SUPERADMIN)).toBe(false);
+ expect(hasPermission(AUTH_LEVELS.ADMIN, AUTH_LEVELS.ADMIN)).toBe(true);
+ expect(hasPermission(AUTH_LEVELS.ADMIN, AUTH_LEVELS.USER)).toBe(true);
+
+ // User should only have access to user level
+ expect(hasPermission(AUTH_LEVELS.USER, AUTH_LEVELS.SUPERADMIN)).toBe(false);
+ expect(hasPermission(AUTH_LEVELS.USER, AUTH_LEVELS.ADMIN)).toBe(false);
+ expect(hasPermission(AUTH_LEVELS.USER, AUTH_LEVELS.USER)).toBe(true);
+ });
+
+ it("should check user management access correctly", () => {
+ expect(canAccessUserManagement(AUTH_LEVELS.SUPERADMIN)).toBe(true);
+ expect(canAccessUserManagement(AUTH_LEVELS.ADMIN)).toBe(true);
+ expect(canAccessUserManagement(AUTH_LEVELS.USER)).toBe(false);
+ });
+ });
+
+
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/customer-management.test.ts b/app/lib/__tests__/customer-management.test.ts
new file mode 100644
index 0000000..780cd32
--- /dev/null
+++ b/app/lib/__tests__/customer-management.test.ts
@@ -0,0 +1,437 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+
+// Mock Prisma first
+vi.mock('../db.server', () => ({
+ prisma: {
+ customer: {
+ findMany: vi.fn(),
+ findUnique: vi.fn(),
+ findFirst: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ count: vi.fn(),
+ },
+ },
+}));
+
+import {
+ getCustomers,
+ createCustomer,
+ updateCustomer,
+ deleteCustomer,
+ getCustomerById,
+ getCustomersForSelect,
+ searchCustomers,
+ getCustomerStats
+} from '../customer-management.server';
+import { prisma } from '../db.server';
+
+const mockPrisma = prisma as any;
+
+describe('Customer Management Server', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('getCustomers', () => {
+ it('should return customers with pagination', async () => {
+ const mockCustomers = [
+ {
+ id: 1,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ createdDate: new Date(),
+ updateDate: new Date(),
+ vehicles: [],
+ maintenanceVisits: [],
+ },
+ ];
+
+ mockPrisma.customer.findMany.mockResolvedValue(mockCustomers);
+ mockPrisma.customer.count.mockResolvedValue(1);
+
+ const result = await getCustomers('', 1, 10);
+
+ expect(result).toEqual({
+ customers: mockCustomers,
+ total: 1,
+ totalPages: 1,
+ });
+ expect(mockPrisma.customer.findMany).toHaveBeenCalledWith({
+ where: {},
+ include: {
+ vehicles: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ lastVisitDate: true,
+ suggestedNextVisitDate: true,
+ },
+ },
+ maintenanceVisits: {
+ select: {
+ id: true,
+ visitDate: true,
+ cost: true,
+ maintenanceType: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ take: 5,
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ skip: 0,
+ take: 10,
+ });
+ });
+
+ it('should handle search query', async () => {
+ mockPrisma.customer.findMany.mockResolvedValue([]);
+ mockPrisma.customer.count.mockResolvedValue(0);
+
+ await getCustomers('أحمد', 1, 10);
+
+ expect(mockPrisma.customer.findMany).toHaveBeenCalledWith({
+ where: {
+ OR: [
+ { name: { contains: 'أحمد' } },
+ { phone: { contains: 'أحمد' } },
+ { email: { contains: 'أحمد' } },
+ { address: { contains: 'أحمد' } },
+ ],
+ },
+ include: expect.any(Object),
+ orderBy: { createdDate: 'desc' },
+ skip: 0,
+ take: 10,
+ });
+ });
+ });
+
+ describe('createCustomer', () => {
+ it('should create a new customer successfully', async () => {
+ const customerData = {
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ };
+
+ const mockCustomer = {
+ id: 1,
+ ...customerData,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ };
+
+ mockPrisma.customer.findFirst.mockResolvedValue(null);
+ mockPrisma.customer.create.mockResolvedValue(mockCustomer);
+
+ const result = await createCustomer(customerData);
+
+ expect(result).toEqual({
+ success: true,
+ customer: mockCustomer,
+ });
+ expect(mockPrisma.customer.create).toHaveBeenCalledWith({
+ data: {
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ },
+ });
+ });
+
+ it('should return error if phone already exists', async () => {
+ const customerData = {
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ };
+
+ const existingCustomer = {
+ id: 2,
+ name: 'محمد أحمد',
+ phone: '0501234567',
+ email: null,
+ address: null,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ };
+
+ mockPrisma.customer.findFirst.mockResolvedValue(existingCustomer);
+
+ const result = await createCustomer(customerData);
+
+ expect(result).toEqual({
+ success: false,
+ error: 'رقم الهاتف موجود بالفعل',
+ });
+ expect(mockPrisma.customer.create).not.toHaveBeenCalled();
+ });
+
+ it('should return error if email already exists', async () => {
+ const customerData = {
+ name: 'أحمد محمد',
+ email: 'ahmed@example.com',
+ };
+
+ const existingCustomer = {
+ id: 2,
+ name: 'محمد أحمد',
+ phone: null,
+ email: 'ahmed@example.com',
+ address: null,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ };
+
+ mockPrisma.customer.findFirst.mockResolvedValue(existingCustomer);
+
+ const result = await createCustomer(customerData);
+
+ expect(result).toEqual({
+ success: false,
+ error: 'البريد الإلكتروني موجود بالفعل',
+ });
+ expect(mockPrisma.customer.create).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('updateCustomer', () => {
+ it('should update customer successfully', async () => {
+ const customerId = 1;
+ const updateData = {
+ name: 'أحمد محمد المحدث',
+ phone: '0509876543',
+ };
+
+ const existingCustomer = {
+ id: customerId,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: null,
+ address: null,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ };
+
+ const updatedCustomer = {
+ ...existingCustomer,
+ ...updateData,
+ updateDate: new Date(),
+ };
+
+ mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
+ mockPrisma.customer.findFirst.mockResolvedValue(null);
+ mockPrisma.customer.update.mockResolvedValue(updatedCustomer);
+
+ const result = await updateCustomer(customerId, updateData);
+
+ expect(result).toEqual({
+ success: true,
+ customer: updatedCustomer,
+ });
+ expect(mockPrisma.customer.update).toHaveBeenCalledWith({
+ where: { id: customerId },
+ data: {
+ name: 'أحمد محمد المحدث',
+ phone: '0509876543',
+ },
+ });
+ });
+
+ it('should return error if customer not found', async () => {
+ const customerId = 999;
+ const updateData = { name: 'أحمد محمد' };
+
+ mockPrisma.customer.findUnique.mockResolvedValue(null);
+
+ const result = await updateCustomer(customerId, updateData);
+
+ expect(result).toEqual({
+ success: false,
+ error: 'العميل غير موجود',
+ });
+ expect(mockPrisma.customer.update).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('deleteCustomer', () => {
+ it('should delete customer successfully when no relationships exist', async () => {
+ const customerId = 1;
+ const existingCustomer = {
+ id: customerId,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: null,
+ address: null,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ vehicles: [],
+ maintenanceVisits: [],
+ };
+
+ mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
+ mockPrisma.customer.delete.mockResolvedValue(existingCustomer);
+
+ const result = await deleteCustomer(customerId);
+
+ expect(result).toEqual({
+ success: true,
+ });
+ expect(mockPrisma.customer.delete).toHaveBeenCalledWith({
+ where: { id: customerId },
+ });
+ });
+
+ it('should return error if customer has vehicles', async () => {
+ const customerId = 1;
+ const existingCustomer = {
+ id: customerId,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: null,
+ address: null,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ vehicles: [{ id: 1, plateNumber: 'ABC-123' }],
+ maintenanceVisits: [],
+ };
+
+ mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
+
+ const result = await deleteCustomer(customerId);
+
+ expect(result).toEqual({
+ success: false,
+ error: 'لا يمكن حذف العميل لأنه يملك 1 مركبة. يرجى حذف المركبات أولاً',
+ });
+ expect(mockPrisma.customer.delete).not.toHaveBeenCalled();
+ });
+
+ it('should return error if customer has maintenance visits', async () => {
+ const customerId = 1;
+ const existingCustomer = {
+ id: customerId,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: null,
+ address: null,
+ createdDate: new Date(),
+ updateDate: new Date(),
+ vehicles: [],
+ maintenanceVisits: [{ id: 1, visitDate: new Date() }],
+ };
+
+ mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
+
+ const result = await deleteCustomer(customerId);
+
+ expect(result).toEqual({
+ success: false,
+ error: 'لا يمكن حذف العميل لأنه لديه 1 زيارة صيانة. يرجى حذف الزيارات أولاً',
+ });
+ expect(mockPrisma.customer.delete).not.toHaveBeenCalled();
+ });
+
+ it('should return error if customer not found', async () => {
+ const customerId = 999;
+
+ mockPrisma.customer.findUnique.mockResolvedValue(null);
+
+ const result = await deleteCustomer(customerId);
+
+ expect(result).toEqual({
+ success: false,
+ error: 'العميل غير موجود',
+ });
+ expect(mockPrisma.customer.delete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('searchCustomers', () => {
+ it('should return empty array for short queries', async () => {
+ const result = await searchCustomers('a');
+ expect(result).toEqual([]);
+ expect(mockPrisma.customer.findMany).not.toHaveBeenCalled();
+ });
+
+ it('should search customers by name, phone, and email', async () => {
+ const mockCustomers = [
+ {
+ id: 1,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ },
+ ];
+
+ mockPrisma.customer.findMany.mockResolvedValue(mockCustomers);
+
+ const result = await searchCustomers('أحمد', 10);
+
+ expect(result).toEqual(mockCustomers);
+ expect(mockPrisma.customer.findMany).toHaveBeenCalledWith({
+ where: {
+ OR: [
+ { name: { contains: 'أحمد' } },
+ { phone: { contains: 'أحمد' } },
+ { email: { contains: 'أحمد' } },
+ ],
+ },
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ orderBy: { name: 'asc' },
+ take: 10,
+ });
+ });
+ });
+
+ describe('getCustomerStats', () => {
+ it('should return customer statistics', async () => {
+ const customerId = 1;
+ const mockCustomer = {
+ id: customerId,
+ name: 'أحمد محمد',
+ vehicles: [{ id: 1 }, { id: 2 }],
+ maintenanceVisits: [
+ { cost: 100, visitDate: new Date('2024-01-15') },
+ { cost: 200, visitDate: new Date('2024-01-10') },
+ { cost: 150, visitDate: new Date('2024-01-05') },
+ ],
+ };
+
+ mockPrisma.customer.findUnique.mockResolvedValue(mockCustomer);
+
+ const result = await getCustomerStats(customerId);
+
+ expect(result).toEqual({
+ totalVehicles: 2,
+ totalVisits: 3,
+ totalSpent: 450,
+ lastVisitDate: new Date('2024-01-15'),
+ });
+ });
+
+ it('should return null if customer not found', async () => {
+ const customerId = 999;
+
+ mockPrisma.customer.findUnique.mockResolvedValue(null);
+
+ const result = await getCustomerStats(customerId);
+
+ expect(result).toBeNull();
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/customer-routes-integration.test.ts b/app/lib/__tests__/customer-routes-integration.test.ts
new file mode 100644
index 0000000..28c21e2
--- /dev/null
+++ b/app/lib/__tests__/customer-routes-integration.test.ts
@@ -0,0 +1,293 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+// Mock the auth middleware
+vi.mock('../auth-middleware.server', () => ({
+ requireAuth: vi.fn().mockResolvedValue({
+ id: 1,
+ name: 'Test User',
+ username: 'testuser',
+ email: 'test@example.com',
+ authLevel: 1,
+ status: 'active',
+ }),
+}));
+
+// Mock the customer management functions
+vi.mock('../customer-management.server', () => ({
+ getCustomers: vi.fn(),
+ createCustomer: vi.fn(),
+ updateCustomer: vi.fn(),
+ deleteCustomer: vi.fn(),
+ getCustomerById: vi.fn(),
+}));
+
+// Mock validation
+vi.mock('../validation', () => ({
+ validateCustomer: vi.fn(),
+}));
+
+import { loader, action } from '../../routes/customers';
+import { getCustomers, createCustomer, updateCustomer, deleteCustomer } from '../customer-management.server';
+import { validateCustomer } from '../validation';
+
+const mockGetCustomers = getCustomers as any;
+const mockCreateCustomer = createCustomer as any;
+const mockUpdateCustomer = updateCustomer as any;
+const mockDeleteCustomer = deleteCustomer as any;
+const mockValidateCustomer = validateCustomer as any;
+
+describe('Customer Routes Integration', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('loader', () => {
+ it('should load customers with default pagination', async () => {
+ const mockCustomers = [
+ {
+ id: 1,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ createdDate: new Date(),
+ updateDate: new Date(),
+ vehicles: [],
+ maintenanceVisits: [],
+ },
+ ];
+
+ mockGetCustomers.mockResolvedValue({
+ customers: mockCustomers,
+ total: 1,
+ totalPages: 1,
+ });
+
+ const request = new Request('http://localhost:3000/customers');
+ const response = await loader({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(mockGetCustomers).toHaveBeenCalledWith('', 1, 10);
+ expect(data.customers).toEqual(mockCustomers);
+ expect(data.total).toBe(1);
+ expect(data.totalPages).toBe(1);
+ expect(data.currentPage).toBe(1);
+ });
+
+ it('should handle search and pagination parameters', async () => {
+ mockGetCustomers.mockResolvedValue({
+ customers: [],
+ total: 0,
+ totalPages: 0,
+ });
+
+ const request = new Request('http://localhost:3000/customers?search=أحمد&page=2&limit=20');
+ await loader({ request, params: {}, context: {} });
+
+ expect(mockGetCustomers).toHaveBeenCalledWith('أحمد', 2, 20);
+ });
+ });
+
+ describe('action', () => {
+ it('should create customer successfully', async () => {
+ const mockCustomer = {
+ id: 1,
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ createdDate: new Date(),
+ updateDate: new Date(),
+ };
+
+ mockValidateCustomer.mockReturnValue({
+ isValid: true,
+ errors: {},
+ });
+
+ mockCreateCustomer.mockResolvedValue({
+ success: true,
+ customer: mockCustomer,
+ });
+
+ const formData = new FormData();
+ formData.append('_action', 'create');
+ formData.append('name', 'أحمد محمد');
+ formData.append('phone', '0501234567');
+ formData.append('email', 'ahmed@example.com');
+ formData.append('address', 'الرياض');
+
+ const request = new Request('http://localhost:3000/customers', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const response = await action({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(mockValidateCustomer).toHaveBeenCalledWith({
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ });
+
+ expect(mockCreateCustomer).toHaveBeenCalledWith({
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض',
+ });
+
+ expect(data.success).toBe(true);
+ expect(data.customer).toEqual(mockCustomer);
+ expect(data.action).toBe('create');
+ expect(data.message).toBe('تم إنشاء العميل بنجاح');
+ });
+
+ it('should return validation errors for invalid data', async () => {
+ mockValidateCustomer.mockReturnValue({
+ isValid: false,
+ errors: {
+ name: 'اسم العميل مطلوب',
+ email: 'البريد الإلكتروني غير صحيح',
+ },
+ });
+
+ const formData = new FormData();
+ formData.append('_action', 'create');
+ formData.append('name', '');
+ formData.append('email', 'invalid-email');
+
+ const request = new Request('http://localhost:3000/customers', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const response = await action({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(response.status).toBe(400);
+ expect(data.success).toBe(false);
+ expect(data.errors).toEqual({
+ name: 'اسم العميل مطلوب',
+ email: 'البريد الإلكتروني غير صحيح',
+ });
+ expect(mockCreateCustomer).not.toHaveBeenCalled();
+ });
+
+ it('should update customer successfully', async () => {
+ const mockCustomer = {
+ id: 1,
+ name: 'أحمد محمد المحدث',
+ phone: '0509876543',
+ email: 'ahmed.updated@example.com',
+ address: 'جدة',
+ createdDate: new Date(),
+ updateDate: new Date(),
+ };
+
+ mockValidateCustomer.mockReturnValue({
+ isValid: true,
+ errors: {},
+ });
+
+ mockUpdateCustomer.mockResolvedValue({
+ success: true,
+ customer: mockCustomer,
+ });
+
+ const formData = new FormData();
+ formData.append('_action', 'update');
+ formData.append('id', '1');
+ formData.append('name', 'أحمد محمد المحدث');
+ formData.append('phone', '0509876543');
+ formData.append('email', 'ahmed.updated@example.com');
+ formData.append('address', 'جدة');
+
+ const request = new Request('http://localhost:3000/customers', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const response = await action({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(mockUpdateCustomer).toHaveBeenCalledWith(1, {
+ name: 'أحمد محمد المحدث',
+ phone: '0509876543',
+ email: 'ahmed.updated@example.com',
+ address: 'جدة',
+ });
+
+ expect(data.success).toBe(true);
+ expect(data.customer).toEqual(mockCustomer);
+ expect(data.action).toBe('update');
+ expect(data.message).toBe('تم تحديث العميل بنجاح');
+ });
+
+ it('should delete customer successfully', async () => {
+ mockDeleteCustomer.mockResolvedValue({
+ success: true,
+ });
+
+ const formData = new FormData();
+ formData.append('_action', 'delete');
+ formData.append('id', '1');
+
+ const request = new Request('http://localhost:3000/customers', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const response = await action({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(mockDeleteCustomer).toHaveBeenCalledWith(1);
+ expect(data.success).toBe(true);
+ expect(data.action).toBe('delete');
+ expect(data.message).toBe('تم حذف العميل بنجاح');
+ });
+
+ it('should handle delete errors', async () => {
+ mockDeleteCustomer.mockResolvedValue({
+ success: false,
+ error: 'لا يمكن حذف العميل لأنه يملك مركبات',
+ });
+
+ const formData = new FormData();
+ formData.append('_action', 'delete');
+ formData.append('id', '1');
+
+ const request = new Request('http://localhost:3000/customers', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const response = await action({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(response.status).toBe(400);
+ expect(data.success).toBe(false);
+ expect(data.error).toBe('لا يمكن حذف العميل لأنه يملك مركبات');
+ });
+
+ it('should handle unknown action', async () => {
+ const formData = new FormData();
+ formData.append('_action', 'unknown');
+
+ const request = new Request('http://localhost:3000/customers', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const response = await action({ request, params: {}, context: {} });
+ const data = await response.json();
+
+ expect(response.status).toBe(400);
+ expect(data.success).toBe(false);
+ expect(data.error).toBe('إجراء غير صحيح');
+ expect(data.action).toBe('unknown');
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/customer-validation.test.ts b/app/lib/__tests__/customer-validation.test.ts
new file mode 100644
index 0000000..f8faf10
--- /dev/null
+++ b/app/lib/__tests__/customer-validation.test.ts
@@ -0,0 +1,109 @@
+import { describe, it, expect } from 'vitest';
+import { validateCustomer } from '../validation';
+
+describe('Customer Validation', () => {
+ describe('validateCustomer', () => {
+ it('should validate required name field', () => {
+ const result = validateCustomer({ name: '' });
+ expect(result.isValid).toBe(false);
+ expect(result.errors.name).toBe('اسم العميل مطلوب');
+ });
+
+ it('should validate name length', () => {
+ const longName = 'أ'.repeat(101);
+ const result = validateCustomer({ name: longName });
+ expect(result.isValid).toBe(false);
+ expect(result.errors.name).toBe('الاسم يجب أن يكون أقل من 100 حرف');
+ });
+
+ it('should validate phone format', () => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ phone: 'invalid-phone'
+ });
+ expect(result.isValid).toBe(false);
+ expect(result.errors.phone).toBe('رقم الهاتف غير صحيح');
+ });
+
+ it('should accept valid phone formats', () => {
+ const validPhones = [
+ '0501234567',
+ '+966501234567',
+ '050 123 4567',
+ '050-123-4567',
+ '(050) 123-4567',
+ ];
+
+ validPhones.forEach(phone => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ phone
+ });
+ expect(result.isValid).toBe(true);
+ expect(result.errors.phone).toBeUndefined();
+ });
+ });
+
+ it('should validate email format', () => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ email: 'invalid-email'
+ });
+ expect(result.isValid).toBe(false);
+ expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
+ });
+
+ it('should accept valid email format', () => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ email: 'ahmed@example.com'
+ });
+ expect(result.isValid).toBe(true);
+ expect(result.errors.email).toBeUndefined();
+ });
+
+ it('should allow empty optional fields', () => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ phone: '',
+ email: '',
+ address: ''
+ });
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should validate complete customer data', () => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض، المملكة العربية السعودية'
+ });
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should handle undefined fields gracefully', () => {
+ const result = validateCustomer({
+ name: 'أحمد محمد',
+ phone: undefined,
+ email: undefined,
+ address: undefined
+ });
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should trim whitespace from name', () => {
+ const result = validateCustomer({ name: ' أحمد محمد ' });
+ expect(result.isValid).toBe(true);
+ });
+
+ it('should reject name with only whitespace', () => {
+ const result = validateCustomer({ name: ' ' });
+ expect(result.isValid).toBe(false);
+ expect(result.errors.name).toBe('اسم العميل مطلوب');
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/expense-management.test.ts b/app/lib/__tests__/expense-management.test.ts
new file mode 100644
index 0000000..13db575
--- /dev/null
+++ b/app/lib/__tests__/expense-management.test.ts
@@ -0,0 +1,278 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import {
+ createExpense,
+ getExpenses,
+ getExpenseById,
+ updateExpense,
+ deleteExpense,
+ getExpenseCategories,
+ getExpensesByCategory,
+ getTotalExpenses
+} from '../expense-management.server';
+import { prisma } from '../db.server';
+
+describe('Expense Management', () => {
+ beforeEach(async () => {
+ // Clean up test data
+ await prisma.expense.deleteMany();
+ });
+
+ afterEach(async () => {
+ // Clean up test data
+ await prisma.expense.deleteMany();
+ });
+
+ describe('createExpense', () => {
+ it('should create a new expense', async () => {
+ const expenseData = {
+ description: 'قطع غيار للمحرك',
+ category: 'قطع غيار',
+ amount: 500.00,
+ expenseDate: new Date('2024-01-15'),
+ };
+
+ const expense = await createExpense(expenseData);
+
+ expect(expense).toBeDefined();
+ expect(expense.description).toBe(expenseData.description);
+ expect(expense.category).toBe(expenseData.category);
+ expect(expense.amount).toBe(expenseData.amount);
+ expect(expense.expenseDate).toEqual(expenseData.expenseDate);
+ });
+
+ it('should create expense with current date if no date provided', async () => {
+ const expenseData = {
+ description: 'مصروف عام',
+ category: 'أخرى',
+ amount: 100.00,
+ };
+
+ const expense = await createExpense(expenseData);
+
+ expect(expense).toBeDefined();
+ expect(expense.expenseDate).toBeInstanceOf(Date);
+ });
+ });
+
+ describe('getExpenses', () => {
+ beforeEach(async () => {
+ // Create test expenses
+ await prisma.expense.createMany({
+ data: [
+ {
+ description: 'قطع غيار',
+ category: 'قطع غيار',
+ amount: 500.00,
+ expenseDate: new Date('2024-01-15'),
+ },
+ {
+ description: 'أدوات صيانة',
+ category: 'أدوات',
+ amount: 200.00,
+ expenseDate: new Date('2024-01-10'),
+ },
+ {
+ description: 'إيجار المحل',
+ category: 'إيجار',
+ amount: 3000.00,
+ expenseDate: new Date('2024-01-01'),
+ },
+ ],
+ });
+ });
+
+ it('should get all expenses with pagination', async () => {
+ const result = await getExpenses();
+
+ expect(result.expenses).toHaveLength(3);
+ expect(result.total).toBe(3);
+ expect(result.totalPages).toBe(1);
+ });
+
+ it('should filter expenses by search query', async () => {
+ const result = await getExpenses('قطع غيار');
+
+ expect(result.expenses).toHaveLength(1);
+ expect(result.expenses[0].description).toBe('قطع غيار');
+ });
+
+ it('should filter expenses by category', async () => {
+ const result = await getExpenses(undefined, 1, 10, 'أدوات');
+
+ expect(result.expenses).toHaveLength(1);
+ expect(result.expenses[0].category).toBe('أدوات');
+ });
+
+ it('should filter expenses by date range', async () => {
+ const dateFrom = new Date('2024-01-10');
+ const dateTo = new Date('2024-01-20');
+
+ const result = await getExpenses(undefined, 1, 10, undefined, dateFrom, dateTo);
+
+ expect(result.expenses).toHaveLength(2);
+ });
+
+ it('should handle pagination correctly', async () => {
+ const result = await getExpenses(undefined, 1, 2);
+
+ expect(result.expenses).toHaveLength(2);
+ expect(result.totalPages).toBe(2);
+ });
+ });
+
+ describe('getExpenseById', () => {
+ it('should get expense by ID', async () => {
+ const createdExpense = await prisma.expense.create({
+ data: {
+ description: 'تست مصروف',
+ category: 'أخرى',
+ amount: 100.00,
+ },
+ });
+
+ const expense = await getExpenseById(createdExpense.id);
+
+ expect(expense).toBeDefined();
+ expect(expense?.id).toBe(createdExpense.id);
+ expect(expense?.description).toBe('تست مصروف');
+ });
+
+ it('should return null for non-existent expense', async () => {
+ const expense = await getExpenseById(999);
+
+ expect(expense).toBeNull();
+ });
+ });
+
+ describe('updateExpense', () => {
+ it('should update expense successfully', async () => {
+ const createdExpense = await prisma.expense.create({
+ data: {
+ description: 'مصروف قديم',
+ category: 'أخرى',
+ amount: 100.00,
+ },
+ });
+
+ const updateData = {
+ description: 'مصروف محدث',
+ category: 'قطع غيار',
+ amount: 200.00,
+ };
+
+ const updatedExpense = await updateExpense(createdExpense.id, updateData);
+
+ expect(updatedExpense.description).toBe(updateData.description);
+ expect(updatedExpense.category).toBe(updateData.category);
+ expect(updatedExpense.amount).toBe(updateData.amount);
+ });
+ });
+
+ describe('deleteExpense', () => {
+ it('should delete expense successfully', async () => {
+ const createdExpense = await prisma.expense.create({
+ data: {
+ description: 'مصروف للحذف',
+ category: 'أخرى',
+ amount: 100.00,
+ },
+ });
+
+ await deleteExpense(createdExpense.id);
+
+ const deletedExpense = await prisma.expense.findUnique({
+ where: { id: createdExpense.id },
+ });
+
+ expect(deletedExpense).toBeNull();
+ });
+ });
+
+ describe('getExpenseCategories', () => {
+ beforeEach(async () => {
+ await prisma.expense.createMany({
+ data: [
+ { description: 'مصروف 1', category: 'قطع غيار', amount: 100 },
+ { description: 'مصروف 2', category: 'أدوات', amount: 200 },
+ { description: 'مصروف 3', category: 'قطع غيار', amount: 150 },
+ ],
+ });
+ });
+
+ it('should get unique expense categories', async () => {
+ const categories = await getExpenseCategories();
+
+ expect(categories).toHaveLength(2);
+ expect(categories).toContain('قطع غيار');
+ expect(categories).toContain('أدوات');
+ });
+ });
+
+ describe('getExpensesByCategory', () => {
+ beforeEach(async () => {
+ await prisma.expense.createMany({
+ data: [
+ { description: 'مصروف 1', category: 'قطع غيار', amount: 100 },
+ { description: 'مصروف 2', category: 'أدوات', amount: 200 },
+ { description: 'مصروف 3', category: 'قطع غيار', amount: 150 },
+ ],
+ });
+ });
+
+ it('should group expenses by category', async () => {
+ const result = await getExpensesByCategory();
+
+ expect(result).toHaveLength(2);
+
+ const spareParts = result.find(r => r.category === 'قطع غيار');
+ expect(spareParts?.total).toBe(250);
+ expect(spareParts?.count).toBe(2);
+
+ const tools = result.find(r => r.category === 'أدوات');
+ expect(tools?.total).toBe(200);
+ expect(tools?.count).toBe(1);
+ });
+ });
+
+ describe('getTotalExpenses', () => {
+ beforeEach(async () => {
+ await prisma.expense.createMany({
+ data: [
+ {
+ description: 'مصروف 1',
+ category: 'قطع غيار',
+ amount: 100,
+ expenseDate: new Date('2024-01-15'),
+ },
+ {
+ description: 'مصروف 2',
+ category: 'أدوات',
+ amount: 200,
+ expenseDate: new Date('2024-01-10'),
+ },
+ {
+ description: 'مصروف 3',
+ category: 'إيجار',
+ amount: 3000,
+ expenseDate: new Date('2023-12-15'),
+ },
+ ],
+ });
+ });
+
+ it('should calculate total expenses', async () => {
+ const total = await getTotalExpenses();
+
+ expect(total).toBe(3300);
+ });
+
+ it('should calculate total expenses for date range', async () => {
+ const dateFrom = new Date('2024-01-01');
+ const dateTo = new Date('2024-01-31');
+
+ const total = await getTotalExpenses(dateFrom, dateTo);
+
+ expect(total).toBe(300); // Only expenses from January 2024
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/financial-reporting.test.ts b/app/lib/__tests__/financial-reporting.test.ts
new file mode 100644
index 0000000..234df08
--- /dev/null
+++ b/app/lib/__tests__/financial-reporting.test.ts
@@ -0,0 +1,458 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import {
+ getFinancialSummary,
+ getMonthlyFinancialData,
+ getIncomeByMaintenanceType,
+ getExpenseBreakdown,
+ getTopCustomersByRevenue,
+ getFinancialTrends
+} from '../financial-reporting.server';
+import { prisma } from '../db.server';
+
+describe('Financial Reporting', () => {
+ beforeEach(async () => {
+ // Clean up test data
+ await prisma.income.deleteMany();
+ await prisma.expense.deleteMany();
+ await prisma.maintenanceVisit.deleteMany();
+ await prisma.vehicle.deleteMany();
+ await prisma.customer.deleteMany();
+ });
+
+ afterEach(async () => {
+ // Clean up test data
+ await prisma.income.deleteMany();
+ await prisma.expense.deleteMany();
+ await prisma.maintenanceVisit.deleteMany();
+ await prisma.vehicle.deleteMany();
+ await prisma.customer.deleteMany();
+ });
+
+ describe('getFinancialSummary', () => {
+ beforeEach(async () => {
+ // Create test customer
+ const customer = await prisma.customer.create({
+ data: {
+ name: 'عميل تجريبي',
+ phone: '0501234567',
+ },
+ });
+
+ // Create test vehicle
+ const vehicle = await prisma.vehicle.create({
+ data: {
+ plateNumber: 'ABC-123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: customer.id,
+ },
+ });
+
+ // Create test maintenance visit
+ const visit = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle.id,
+ customerId: customer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 200.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ // Create test income
+ await prisma.income.create({
+ data: {
+ maintenanceVisitId: visit.id,
+ amount: 200.00,
+ incomeDate: new Date('2024-01-15'),
+ },
+ });
+
+ // Create test expenses
+ await prisma.expense.createMany({
+ data: [
+ {
+ description: 'قطع غيار',
+ category: 'قطع غيار',
+ amount: 50.00,
+ expenseDate: new Date('2024-01-10'),
+ },
+ {
+ description: 'أدوات',
+ category: 'أدوات',
+ amount: 30.00,
+ expenseDate: new Date('2024-01-12'),
+ },
+ ],
+ });
+ });
+
+ it('should calculate financial summary correctly', async () => {
+ const summary = await getFinancialSummary();
+
+ expect(summary.totalIncome).toBe(200.00);
+ expect(summary.totalExpenses).toBe(80.00);
+ expect(summary.netProfit).toBe(120.00);
+ expect(summary.incomeCount).toBe(1);
+ expect(summary.expenseCount).toBe(2);
+ expect(summary.profitMargin).toBe(60.0); // (120/200) * 100
+ });
+
+ it('should filter by date range', async () => {
+ const dateFrom = new Date('2024-01-11');
+ const dateTo = new Date('2024-01-20');
+
+ const summary = await getFinancialSummary(dateFrom, dateTo);
+
+ expect(summary.totalIncome).toBe(200.00);
+ expect(summary.totalExpenses).toBe(30.00); // Only one expense in range
+ expect(summary.netProfit).toBe(170.00);
+ });
+ });
+
+ describe('getIncomeByMaintenanceType', () => {
+ beforeEach(async () => {
+ // Create test data
+ const customer = await prisma.customer.create({
+ data: { name: 'عميل تجريبي', phone: '0501234567' },
+ });
+
+ const vehicle = await prisma.vehicle.create({
+ data: {
+ plateNumber: 'ABC-123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: customer.id,
+ },
+ });
+
+ // Create maintenance visits with different types
+ const visit1 = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle.id,
+ customerId: customer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 200.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ const visit2 = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle.id,
+ customerId: customer.id,
+ maintenanceType: 'فحص دوري',
+ description: 'فحص دوري شامل',
+ cost: 150.00,
+ kilometers: 51000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ const visit3 = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle.id,
+ customerId: customer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت مرة أخرى',
+ cost: 180.00,
+ kilometers: 52000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ // Create corresponding income records
+ await prisma.income.createMany({
+ data: [
+ { maintenanceVisitId: visit1.id, amount: 200.00 },
+ { maintenanceVisitId: visit2.id, amount: 150.00 },
+ { maintenanceVisitId: visit3.id, amount: 180.00 },
+ ],
+ });
+ });
+
+ it('should group income by maintenance type', async () => {
+ const result = await getIncomeByMaintenanceType();
+
+ expect(result).toHaveLength(2);
+
+ const oilChange = result.find(r => r.category === 'تغيير زيت');
+ expect(oilChange?.amount).toBe(380.00);
+ expect(oilChange?.count).toBe(2);
+ expect(oilChange?.percentage).toBeCloseTo(71.7, 1); // 380/530 * 100
+
+ const inspection = result.find(r => r.category === 'فحص دوري');
+ expect(inspection?.amount).toBe(150.00);
+ expect(inspection?.count).toBe(1);
+ expect(inspection?.percentage).toBeCloseTo(28.3, 1); // 150/530 * 100
+ });
+ });
+
+ describe('getExpenseBreakdown', () => {
+ beforeEach(async () => {
+ await prisma.expense.createMany({
+ data: [
+ { description: 'قطع غيار 1', category: 'قطع غيار', amount: 100 },
+ { description: 'قطع غيار 2', category: 'قطع غيار', amount: 150 },
+ { description: 'أدوات 1', category: 'أدوات', amount: 200 },
+ { description: 'إيجار', category: 'إيجار', amount: 3000 },
+ ],
+ });
+ });
+
+ it('should group expenses by category with percentages', async () => {
+ const result = await getExpenseBreakdown();
+
+ expect(result).toHaveLength(3);
+
+ const rent = result.find(r => r.category === 'إيجار');
+ expect(rent?.amount).toBe(3000);
+ expect(rent?.count).toBe(1);
+ expect(rent?.percentage).toBeCloseTo(86.96, 1); // 3000/3450 * 100
+
+ const spareParts = result.find(r => r.category === 'قطع غيار');
+ expect(spareParts?.amount).toBe(250);
+ expect(spareParts?.count).toBe(2);
+ expect(spareParts?.percentage).toBeCloseTo(7.25, 1); // 250/3450 * 100
+
+ const tools = result.find(r => r.category === 'أدوات');
+ expect(tools?.amount).toBe(200);
+ expect(tools?.count).toBe(1);
+ expect(tools?.percentage).toBeCloseTo(5.80, 1); // 200/3450 * 100
+ });
+ });
+
+ describe('getTopCustomersByRevenue', () => {
+ beforeEach(async () => {
+ // Create test customers
+ const customer1 = await prisma.customer.create({
+ data: { name: 'عميل أول', phone: '0501111111' },
+ });
+
+ const customer2 = await prisma.customer.create({
+ data: { name: 'عميل ثاني', phone: '0502222222' },
+ });
+
+ // Create vehicles
+ const vehicle1 = await prisma.vehicle.create({
+ data: {
+ plateNumber: 'ABC-111',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: customer1.id,
+ },
+ });
+
+ const vehicle2 = await prisma.vehicle.create({
+ data: {
+ plateNumber: 'ABC-222',
+ bodyType: 'SUV',
+ manufacturer: 'هيونداي',
+ model: 'توسان',
+ year: 2021,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: customer2.id,
+ },
+ });
+
+ // Create maintenance visits
+ const visit1 = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle1.id,
+ customerId: customer1.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت',
+ cost: 300.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ const visit2 = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle1.id,
+ customerId: customer1.id,
+ maintenanceType: 'فحص دوري',
+ description: 'فحص دوري',
+ cost: 200.00,
+ kilometers: 51000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ const visit3 = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle2.id,
+ customerId: customer2.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت',
+ cost: 150.00,
+ kilometers: 30000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ // Create income records
+ await prisma.income.createMany({
+ data: [
+ { maintenanceVisitId: visit1.id, amount: 300.00 },
+ { maintenanceVisitId: visit2.id, amount: 200.00 },
+ { maintenanceVisitId: visit3.id, amount: 150.00 },
+ ],
+ });
+ });
+
+ it('should return top customers by revenue', async () => {
+ const result = await getTopCustomersByRevenue(10);
+
+ expect(result).toHaveLength(2);
+
+ // Should be sorted by revenue descending
+ expect(result[0].customerName).toBe('عميل أول');
+ expect(result[0].totalRevenue).toBe(500.00);
+ expect(result[0].visitCount).toBe(2);
+
+ expect(result[1].customerName).toBe('عميل ثاني');
+ expect(result[1].totalRevenue).toBe(150.00);
+ expect(result[1].visitCount).toBe(1);
+ });
+
+ it('should limit results correctly', async () => {
+ const result = await getTopCustomersByRevenue(1);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].customerName).toBe('عميل أول');
+ });
+ });
+
+ describe('getFinancialTrends', () => {
+ beforeEach(async () => {
+ // Create test customer and vehicle
+ const customer = await prisma.customer.create({
+ data: { name: 'عميل تجريبي', phone: '0501234567' },
+ });
+
+ const vehicle = await prisma.vehicle.create({
+ data: {
+ plateNumber: 'ABC-123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: customer.id,
+ },
+ });
+
+ // Create maintenance visits for different periods
+ const currentVisit = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle.id,
+ customerId: customer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت حالي',
+ cost: 300.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ const previousVisit = await prisma.maintenanceVisit.create({
+ data: {
+ vehicleId: vehicle.id,
+ customerId: customer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت سابق',
+ cost: 200.00,
+ kilometers: 45000,
+ nextVisitDelay: 3,
+ },
+ });
+
+ // Create income for current period (Jan 2024)
+ await prisma.income.create({
+ data: {
+ maintenanceVisitId: currentVisit.id,
+ amount: 300.00,
+ incomeDate: new Date('2024-01-15'),
+ },
+ });
+
+ // Create income for previous period (Dec 2023)
+ await prisma.income.create({
+ data: {
+ maintenanceVisitId: previousVisit.id,
+ amount: 200.00,
+ incomeDate: new Date('2023-12-15'),
+ },
+ });
+
+ // Create expenses for current period
+ await prisma.expense.create({
+ data: {
+ description: 'مصروف حالي',
+ category: 'قطع غيار',
+ amount: 100.00,
+ expenseDate: new Date('2024-01-10'),
+ },
+ });
+
+ // Create expenses for previous period
+ await prisma.expense.create({
+ data: {
+ description: 'مصروف سابق',
+ category: 'قطع غيار',
+ amount: 80.00,
+ expenseDate: new Date('2023-12-10'),
+ },
+ });
+ });
+
+ it('should calculate financial trends correctly', async () => {
+ const dateFrom = new Date('2024-01-01');
+ const dateTo = new Date('2024-01-31');
+
+ const result = await getFinancialTrends(dateFrom, dateTo);
+
+ expect(result.currentPeriod.totalIncome).toBe(300.00);
+ expect(result.currentPeriod.totalExpenses).toBe(100.00);
+ expect(result.currentPeriod.netProfit).toBe(200.00);
+
+ expect(result.previousPeriod.totalIncome).toBe(200.00);
+ expect(result.previousPeriod.totalExpenses).toBe(80.00);
+ expect(result.previousPeriod.netProfit).toBe(120.00);
+
+ // Income growth: (300-200)/200 * 100 = 50%
+ expect(result.trends.incomeGrowth).toBeCloseTo(50.0, 1);
+
+ // Expense growth: (100-80)/80 * 100 = 25%
+ expect(result.trends.expenseGrowth).toBeCloseTo(25.0, 1);
+
+ // Profit growth: (200-120)/120 * 100 = 66.67%
+ expect(result.trends.profitGrowth).toBeCloseTo(66.67, 1);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/form-validation.test.ts b/app/lib/__tests__/form-validation.test.ts
new file mode 100644
index 0000000..2172f0a
--- /dev/null
+++ b/app/lib/__tests__/form-validation.test.ts
@@ -0,0 +1,201 @@
+import { describe, it, expect } from 'vitest';
+import {
+ validateUserData,
+ validateCustomerData,
+ validateVehicleData,
+ validateMaintenanceVisitData,
+ validateExpenseData
+} from '../form-validation';
+
+describe('Form Validation', () => {
+ describe('validateUserData', () => {
+ it('should validate valid user data', () => {
+ const validData = {
+ name: 'أحمد محمد',
+ username: 'ahmed123',
+ email: 'ahmed@example.com',
+ password: 'password123',
+ authLevel: 3,
+ status: 'active',
+ };
+
+ const result = validateUserData(validData);
+ expect(result.success).toBe(true);
+ expect(result.errors).toEqual({});
+ });
+
+ it('should return errors for invalid user data', () => {
+ const invalidData = {
+ name: '',
+ username: 'ab',
+ email: 'invalid-email',
+ password: '123',
+ authLevel: 5,
+ status: 'invalid',
+ };
+
+ const result = validateUserData(invalidData);
+ expect(result.success).toBe(false);
+ expect(result.errors.name).toBe('الاسم مطلوب');
+ expect(result.errors.username).toBe('اسم المستخدم يجب أن يكون على الأقل 3 أحرف');
+ expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
+ expect(result.errors.password).toBe('كلمة المرور يجب أن تكون على الأقل 6 أحرف');
+ });
+ });
+
+ describe('validateCustomerData', () => {
+ it('should validate valid customer data', () => {
+ const validData = {
+ name: 'محمد أحمد',
+ phone: '+966501234567',
+ email: 'mohammed@example.com',
+ address: 'الرياض، المملكة العربية السعودية',
+ };
+
+ const result = validateCustomerData(validData);
+ expect(result.success).toBe(true);
+ expect(result.errors).toEqual({});
+ });
+
+ it('should validate customer with minimal data', () => {
+ const validData = {
+ name: 'سارة علي',
+ phone: '',
+ email: '',
+ address: '',
+ };
+
+ const result = validateCustomerData(validData);
+ expect(result.success).toBe(true);
+ expect(result.errors).toEqual({});
+ });
+
+ it('should return errors for invalid customer data', () => {
+ const invalidData = {
+ name: '',
+ phone: 'invalid-phone',
+ email: 'invalid-email',
+ address: 'Valid address',
+ };
+
+ const result = validateCustomerData(invalidData);
+ expect(result.success).toBe(false);
+ expect(result.errors.name).toBe('اسم العميل مطلوب');
+ expect(result.errors.phone).toBe('رقم الهاتف غير صحيح');
+ expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
+ });
+ });
+
+ describe('validateVehicleData', () => {
+ it('should validate valid vehicle data', () => {
+ const validData = {
+ plateNumber: 'ABC-1234',
+ bodyType: 'Sedan',
+ manufacturer: 'Toyota',
+ model: 'Camry',
+ trim: 'LE',
+ year: '2023',
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ cylinders: '4',
+ engineDisplacement: '2.5',
+ useType: 'personal',
+ ownerId: '1',
+ };
+
+ const result = validateVehicleData(validData);
+ expect(result.success).toBe(true);
+ expect(result.errors).toEqual({});
+ });
+
+ it('should return errors for invalid vehicle data', () => {
+ const invalidData = {
+ plateNumber: '',
+ bodyType: '',
+ manufacturer: '',
+ model: '',
+ year: '1800',
+ transmission: 'invalid',
+ fuel: 'invalid',
+ cylinders: '20',
+ engineDisplacement: '50',
+ useType: 'invalid',
+ ownerId: '0',
+ };
+
+ const result = validateVehicleData(invalidData);
+ expect(result.success).toBe(false);
+ expect(result.errors.plateNumber).toBe('رقم اللوحة مطلوب');
+ expect(result.errors.bodyType).toBe('نوع الهيكل مطلوب');
+ expect(result.errors.manufacturer).toBe('الشركة المصنعة مطلوبة');
+ expect(result.errors.model).toBe('الموديل مطلوب');
+ expect(result.errors.ownerId).toBe('مالك المركبة مطلوب');
+ });
+ });
+
+ describe('validateMaintenanceVisitData', () => {
+ it('should validate valid maintenance visit data', () => {
+ const validData = {
+ vehicleId: '1',
+ customerId: '1',
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك والفلتر',
+ cost: '150.50',
+ paymentStatus: 'paid',
+ kilometers: '50000',
+ nextVisitDelay: '3',
+ };
+
+ const result = validateMaintenanceVisitData(validData);
+ expect(result.success).toBe(true);
+ expect(result.errors).toEqual({});
+ });
+
+ it('should return errors for invalid maintenance visit data', () => {
+ const invalidData = {
+ vehicleId: '0',
+ customerId: '0',
+ maintenanceType: '',
+ description: '',
+ cost: '-10',
+ paymentStatus: 'invalid',
+ kilometers: '-100',
+ nextVisitDelay: '5',
+ };
+
+ const result = validateMaintenanceVisitData(invalidData);
+ expect(result.success).toBe(false);
+ expect(result.errors.vehicleId).toBe('المركبة مطلوبة');
+ expect(result.errors.customerId).toBe('العميل مطلوب');
+ expect(result.errors.maintenanceType).toBe('نوع الصيانة مطلوب');
+ expect(result.errors.description).toBe('وصف الصيانة مطلوب');
+ });
+ });
+
+ describe('validateExpenseData', () => {
+ it('should validate valid expense data', () => {
+ const validData = {
+ description: 'شراء قطع غيار',
+ category: 'قطع غيار',
+ amount: '250.75',
+ };
+
+ const result = validateExpenseData(validData);
+ expect(result.success).toBe(true);
+ expect(result.errors).toEqual({});
+ });
+
+ it('should return errors for invalid expense data', () => {
+ const invalidData = {
+ description: '',
+ category: '',
+ amount: '0',
+ };
+
+ const result = validateExpenseData(invalidData);
+ expect(result.success).toBe(false);
+ expect(result.errors.description).toBe('وصف المصروف مطلوب');
+ expect(result.errors.category).toBe('فئة المصروف مطلوبة');
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/maintenance-visit-management.test.ts b/app/lib/__tests__/maintenance-visit-management.test.ts
new file mode 100644
index 0000000..a34d896
--- /dev/null
+++ b/app/lib/__tests__/maintenance-visit-management.test.ts
@@ -0,0 +1,434 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import {
+ createMaintenanceVisit,
+ getMaintenanceVisits,
+ getMaintenanceVisitById,
+ updateMaintenanceVisit,
+ deleteMaintenanceVisit,
+ getVehicleMaintenanceHistory,
+ getCustomerMaintenanceHistory
+} from '../maintenance-visit-management.server';
+import { prisma } from '../db.server';
+import type { Customer, Vehicle } from '@prisma/client';
+
+describe('Maintenance Visit Management', () => {
+ let testCustomer: Customer;
+ let testVehicle: Vehicle;
+
+ beforeEach(async () => {
+ // Create test customer
+ testCustomer = await prisma.customer.create({
+ data: {
+ name: 'أحمد محمد',
+ phone: '0501234567',
+ email: 'ahmed@example.com',
+ },
+ });
+
+ // Create test vehicle
+ testVehicle = await prisma.vehicle.create({
+ data: {
+ plateNumber: 'أ ب ج 123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: testCustomer.id,
+ },
+ });
+ });
+
+ afterEach(async () => {
+ // Clean up test data
+ await prisma.income.deleteMany();
+ await prisma.maintenanceVisit.deleteMany();
+ await prisma.vehicle.deleteMany();
+ await prisma.customer.deleteMany();
+ });
+
+ describe('createMaintenanceVisit', () => {
+ it('should create a maintenance visit successfully', async () => {
+ const visitData = {
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك والفلتر',
+ cost: 150.00,
+ paymentStatus: 'paid',
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const visit = await createMaintenanceVisit(visitData);
+
+ expect(visit).toBeDefined();
+ expect(visit.vehicleId).toBe(testVehicle.id);
+ expect(visit.customerId).toBe(testCustomer.id);
+ expect(visit.maintenanceType).toBe('تغيير زيت');
+ expect(visit.cost).toBe(150.00);
+ expect(visit.nextVisitDelay).toBe(3);
+
+ // Check that vehicle was updated
+ const updatedVehicle = await prisma.vehicle.findUnique({
+ where: { id: testVehicle.id },
+ });
+ expect(updatedVehicle?.lastVisitDate).toBeDefined();
+ expect(updatedVehicle?.suggestedNextVisitDate).toBeDefined();
+
+ // Check that income was created
+ const income = await prisma.income.findFirst({
+ where: { maintenanceVisitId: visit.id },
+ });
+ expect(income).toBeDefined();
+ expect(income?.amount).toBe(150.00);
+ });
+
+ it('should calculate next visit date correctly', async () => {
+ const visitData = {
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'فحص دوري',
+ description: 'فحص دوري شامل',
+ cost: 200.00,
+ kilometers: 45000,
+ nextVisitDelay: 2, // 2 months
+ };
+
+ await createMaintenanceVisit(visitData);
+
+ const updatedVehicle = await prisma.vehicle.findUnique({
+ where: { id: testVehicle.id },
+ });
+
+ expect(updatedVehicle?.suggestedNextVisitDate).toBeDefined();
+
+ // Check that the suggested date is approximately 2 months from now
+ const now = new Date();
+ const expectedDate = new Date();
+ expectedDate.setMonth(expectedDate.getMonth() + 2);
+
+ const actualDate = updatedVehicle!.suggestedNextVisitDate!;
+ const timeDiff = Math.abs(actualDate.getTime() - expectedDate.getTime());
+ const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
+
+ expect(daysDiff).toBeLessThan(2); // Allow 2 days difference
+ });
+ });
+
+ describe('getMaintenanceVisits', () => {
+ beforeEach(async () => {
+ // Create test visits
+ await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ });
+
+ await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'فحص فرامل',
+ description: 'فحص وتنظيف الفرامل',
+ cost: 100.00,
+ kilometers: 52000,
+ nextVisitDelay: 2,
+ });
+ });
+
+ it('should get all maintenance visits', async () => {
+ const result = await getMaintenanceVisits();
+
+ expect(result.visits).toHaveLength(2);
+ expect(result.total).toBe(2);
+ expect(result.totalPages).toBe(1);
+ });
+
+ it('should search maintenance visits by maintenance type', async () => {
+ const result = await getMaintenanceVisits('تغيير زيت');
+
+ expect(result.visits).toHaveLength(1);
+ expect(result.visits[0].maintenanceType).toBe('تغيير زيت');
+ });
+
+ it('should filter by vehicle ID', async () => {
+ const result = await getMaintenanceVisits('', 1, 10, testVehicle.id);
+
+ expect(result.visits).toHaveLength(2);
+ result.visits.forEach(visit => {
+ expect(visit.vehicleId).toBe(testVehicle.id);
+ });
+ });
+
+ it('should filter by customer ID', async () => {
+ const result = await getMaintenanceVisits('', 1, 10, undefined, testCustomer.id);
+
+ expect(result.visits).toHaveLength(2);
+ result.visits.forEach(visit => {
+ expect(visit.customerId).toBe(testCustomer.id);
+ });
+ });
+
+ it('should paginate results', async () => {
+ const result = await getMaintenanceVisits('', 1, 1);
+
+ expect(result.visits).toHaveLength(1);
+ expect(result.total).toBe(2);
+ expect(result.totalPages).toBe(2);
+ });
+ });
+
+ describe('getMaintenanceVisitById', () => {
+ it('should get a maintenance visit by ID', async () => {
+ const createdVisit = await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'تغيير إطارات',
+ description: 'تغيير الإطارات الأمامية',
+ cost: 800.00,
+ kilometers: 55000,
+ nextVisitDelay: 4,
+ });
+
+ const visit = await getMaintenanceVisitById(createdVisit.id);
+
+ expect(visit).toBeDefined();
+ expect(visit?.id).toBe(createdVisit.id);
+ expect(visit?.vehicle).toBeDefined();
+ expect(visit?.customer).toBeDefined();
+ expect(visit?.income).toBeDefined();
+ });
+
+ it('should return null for non-existent visit', async () => {
+ const visit = await getMaintenanceVisitById(99999);
+ expect(visit).toBeNull();
+ });
+ });
+
+ describe('updateMaintenanceVisit', () => {
+ it('should update a maintenance visit successfully', async () => {
+ const createdVisit = await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'فحص دوري',
+ description: 'فحص دوري أولي',
+ cost: 200.00,
+ kilometers: 48000,
+ nextVisitDelay: 3,
+ });
+
+ const updateData = {
+ maintenanceType: 'فحص شامل',
+ description: 'فحص شامل محدث',
+ cost: 250.00,
+ paymentStatus: 'paid',
+ kilometers: 48500,
+ nextVisitDelay: 2,
+ };
+
+ const updatedVisit = await updateMaintenanceVisit(createdVisit.id, updateData);
+
+ expect(updatedVisit.maintenanceType).toBe('فحص شامل');
+ expect(updatedVisit.description).toBe('فحص شامل محدث');
+ expect(updatedVisit.cost).toBe(250.00);
+ expect(updatedVisit.paymentStatus).toBe('paid');
+ expect(updatedVisit.kilometers).toBe(48500);
+ expect(updatedVisit.nextVisitDelay).toBe(2);
+
+ // Check that income was updated
+ const income = await prisma.income.findFirst({
+ where: { maintenanceVisitId: createdVisit.id },
+ });
+ expect(income?.amount).toBe(250.00);
+ });
+
+ it('should update vehicle dates when visit date changes', async () => {
+ const createdVisit = await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'صيانة عامة',
+ description: 'صيانة عامة',
+ cost: 300.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ });
+
+ const newVisitDate = new Date();
+ newVisitDate.setDate(newVisitDate.getDate() - 5); // 5 days ago
+
+ await updateMaintenanceVisit(createdVisit.id, {
+ visitDate: newVisitDate,
+ nextVisitDelay: 4,
+ });
+
+ const updatedVehicle = await prisma.vehicle.findUnique({
+ where: { id: testVehicle.id },
+ });
+
+ expect(updatedVehicle?.lastVisitDate?.getTime()).toBe(newVisitDate.getTime());
+
+ // Check that suggested next visit date was recalculated
+ const expectedNextDate = new Date(newVisitDate);
+ expectedNextDate.setMonth(expectedNextDate.getMonth() + 4);
+
+ const actualNextDate = updatedVehicle!.suggestedNextVisitDate!;
+ const timeDiff = Math.abs(actualNextDate.getTime() - expectedNextDate.getTime());
+ const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
+
+ expect(daysDiff).toBeLessThan(2);
+ });
+ });
+
+ describe('deleteMaintenanceVisit', () => {
+ it('should delete a maintenance visit successfully', async () => {
+ const createdVisit = await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'تنظيف مكيف',
+ description: 'تنظيف وصيانة المكيف',
+ cost: 120.00,
+ kilometers: 47000,
+ nextVisitDelay: 2,
+ });
+
+ await deleteMaintenanceVisit(createdVisit.id);
+
+ // Check that visit was deleted
+ const deletedVisit = await prisma.maintenanceVisit.findUnique({
+ where: { id: createdVisit.id },
+ });
+ expect(deletedVisit).toBeNull();
+
+ // Check that income was deleted (cascade)
+ const income = await prisma.income.findFirst({
+ where: { maintenanceVisitId: createdVisit.id },
+ });
+ expect(income).toBeNull();
+
+ // Check that vehicle dates were cleared
+ const updatedVehicle = await prisma.vehicle.findUnique({
+ where: { id: testVehicle.id },
+ });
+ expect(updatedVehicle?.lastVisitDate).toBeNull();
+ expect(updatedVehicle?.suggestedNextVisitDate).toBeNull();
+ });
+
+ it('should update vehicle dates to most recent remaining visit after deletion', async () => {
+ // Create two visits
+ const firstVisit = await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'زيت قديم',
+ description: 'تغيير زيت قديم',
+ cost: 100.00,
+ kilometers: 45000,
+ nextVisitDelay: 3,
+ visitDate: new Date('2023-01-01'),
+ });
+
+ const secondVisit = await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'زيت جديد',
+ description: 'تغيير زيت جديد',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 2,
+ visitDate: new Date('2023-02-01'),
+ });
+
+ // Delete the more recent visit
+ await deleteMaintenanceVisit(secondVisit.id);
+
+ // Check that vehicle dates were updated to the remaining visit
+ const updatedVehicle = await prisma.vehicle.findUnique({
+ where: { id: testVehicle.id },
+ });
+
+ expect(updatedVehicle?.lastVisitDate?.getTime()).toBe(new Date('2023-01-01').getTime());
+
+ // Check that suggested next visit date was recalculated based on first visit
+ const expectedNextDate = new Date('2023-01-01');
+ expectedNextDate.setMonth(expectedNextDate.getMonth() + 3);
+
+ const actualNextDate = updatedVehicle!.suggestedNextVisitDate!;
+ expect(actualNextDate.getTime()).toBe(expectedNextDate.getTime());
+ });
+ });
+
+ describe('getVehicleMaintenanceHistory', () => {
+ it('should get maintenance history for a vehicle', async () => {
+ await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'زيت أول',
+ description: 'تغيير زيت أول',
+ cost: 100.00,
+ kilometers: 45000,
+ nextVisitDelay: 3,
+ });
+
+ await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'زيت ثاني',
+ description: 'تغيير زيت ثاني',
+ cost: 120.00,
+ kilometers: 48000,
+ nextVisitDelay: 3,
+ });
+
+ const history = await getVehicleMaintenanceHistory(testVehicle.id);
+
+ expect(history).toHaveLength(2);
+ expect(history[0].vehicleId).toBe(testVehicle.id);
+ expect(history[1].vehicleId).toBe(testVehicle.id);
+
+ // Should be ordered by visit date descending
+ expect(new Date(history[0].visitDate).getTime()).toBeGreaterThanOrEqual(
+ new Date(history[1].visitDate).getTime()
+ );
+ });
+ });
+
+ describe('getCustomerMaintenanceHistory', () => {
+ it('should get maintenance history for a customer', async () => {
+ await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'صيانة أولى',
+ description: 'صيانة أولى',
+ cost: 200.00,
+ kilometers: 45000,
+ nextVisitDelay: 3,
+ });
+
+ await createMaintenanceVisit({
+ vehicleId: testVehicle.id,
+ customerId: testCustomer.id,
+ maintenanceType: 'صيانة ثانية',
+ description: 'صيانة ثانية',
+ cost: 250.00,
+ kilometers: 48000,
+ nextVisitDelay: 2,
+ });
+
+ const history = await getCustomerMaintenanceHistory(testCustomer.id);
+
+ expect(history).toHaveLength(2);
+ expect(history[0].customerId).toBe(testCustomer.id);
+ expect(history[1].customerId).toBe(testCustomer.id);
+
+ // Should be ordered by visit date descending
+ expect(new Date(history[0].visitDate).getTime()).toBeGreaterThanOrEqual(
+ new Date(history[1].visitDate).getTime()
+ );
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/maintenance-visit-validation.test.ts b/app/lib/__tests__/maintenance-visit-validation.test.ts
new file mode 100644
index 0000000..427286d
--- /dev/null
+++ b/app/lib/__tests__/maintenance-visit-validation.test.ts
@@ -0,0 +1,425 @@
+import { describe, it, expect } from 'vitest';
+import { validateMaintenanceVisit } from '../validation';
+
+describe('Maintenance Visit Validation', () => {
+ describe('validateMaintenanceVisit', () => {
+ it('should validate a complete maintenance visit successfully', () => {
+ const validData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك والفلتر',
+ cost: 150.50,
+ paymentStatus: 'paid',
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(validData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should skip validation for missing vehicleId (partial validation)', () => {
+ const partialData = {
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should reject invalid vehicleId', () => {
+ const invalidData = {
+ vehicleId: 0,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.vehicleId).toBe('المركبة مطلوبة');
+ });
+
+ it('should skip validation for missing customerId (partial validation)', () => {
+ const partialData = {
+ vehicleId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should reject invalid customerId', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: -1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.customerId).toBe('العميل مطلوب');
+ });
+
+ it('should skip validation for missing maintenanceType (partial validation)', () => {
+ const partialData = {
+ vehicleId: 1,
+ customerId: 1,
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should reject empty maintenanceType', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: ' ',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.maintenanceType).toBe('نوع الصيانة مطلوب');
+ });
+
+ it('should skip validation for missing description (partial validation)', () => {
+ const partialData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should reject empty description', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: '',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.description).toBe('وصف الصيانة مطلوب');
+ });
+
+ it('should reject description that is too long', () => {
+ const longDescription = 'أ'.repeat(501); // 501 characters
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: longDescription,
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.description).toBe('الوصف يجب أن يكون أقل من 500 حرف');
+ });
+
+ it('should skip validation for missing cost (partial validation)', () => {
+ const partialData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should reject negative cost', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: -10.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.cost).toBe('التكلفة يجب أن تكون بين 0 و 999999.99');
+ });
+
+ it('should reject cost that is too high', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 1000000.00,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.cost).toBe('التكلفة يجب أن تكون بين 0 و 999999.99');
+ });
+
+ it('should validate payment status', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ paymentStatus: 'invalid_status',
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.paymentStatus).toBe('حالة الدفع غير صحيحة');
+ });
+
+ it('should accept valid payment statuses', () => {
+ const validStatuses = ['pending', 'paid', 'partial', 'cancelled'];
+
+ validStatuses.forEach(status => {
+ const validData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ paymentStatus: status,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(validData);
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ it('should skip validation for missing kilometers (partial validation)', () => {
+ const partialData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should reject negative kilometers', () => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: -1000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.kilometers).toBe('عدد الكيلومترات يجب أن يكون رقم موجب');
+ });
+
+ it('should accept zero kilometers', () => {
+ const validData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 0,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(validData);
+
+ expect(result.isValid).toBe(true);
+ });
+
+ it('should skip validation for missing nextVisitDelay (partial validation)', () => {
+ const partialData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should accept valid nextVisitDelay values', () => {
+ const validDelays = [1, 2, 3, 4];
+
+ validDelays.forEach(delay => {
+ const validData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: delay,
+ };
+
+ const result = validateMaintenanceVisit(validData);
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ it('should reject invalid nextVisitDelay values', () => {
+ const invalidDelays = [0, 5, 6, -1];
+
+ invalidDelays.forEach(delay => {
+ const invalidData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.00,
+ kilometers: 50000,
+ nextVisitDelay: delay,
+ };
+
+ const result = validateMaintenanceVisit(invalidData);
+ expect(result.isValid).toBe(false);
+ expect(result.errors.nextVisitDelay).toBe('فترة الزيارة التالية يجب أن تكون 1، 2، 3، أو 4 أشهر');
+ });
+ });
+
+ it('should handle partial validation for updates', () => {
+ const partialData = {
+ maintenanceType: 'فحص دوري',
+ cost: 200.00,
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should validate partial data with errors', () => {
+ const partialData = {
+ maintenanceType: '',
+ cost: -50.00,
+ paymentStatus: 'invalid',
+ };
+
+ const result = validateMaintenanceVisit(partialData);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.maintenanceType).toBe('نوع الصيانة مطلوب');
+ expect(result.errors.cost).toBe('التكلفة يجب أن تكون بين 0 و 999999.99');
+ expect(result.errors.paymentStatus).toBe('حالة الدفع غير صحيحة');
+ });
+
+ it('should accept zero cost', () => {
+ const validData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'فحص مجاني',
+ description: 'فحص مجاني للعميل',
+ cost: 0,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(validData);
+
+ expect(result.isValid).toBe(true);
+ });
+
+ it('should accept decimal cost values', () => {
+ const validData = {
+ vehicleId: 1,
+ customerId: 1,
+ maintenanceType: 'تغيير زيت',
+ description: 'تغيير زيت المحرك',
+ cost: 150.75,
+ kilometers: 50000,
+ nextVisitDelay: 3,
+ };
+
+ const result = validateMaintenanceVisit(validData);
+
+ expect(result.isValid).toBe(true);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/route-protection-integration.test.ts b/app/lib/__tests__/route-protection-integration.test.ts
new file mode 100644
index 0000000..c443292
--- /dev/null
+++ b/app/lib/__tests__/route-protection-integration.test.ts
@@ -0,0 +1,107 @@
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { checkPermission, createUnauthorizedResponse } from "../auth-middleware.server";
+import { AUTH_LEVELS, USER_STATUS } from "~/types/auth";
+import type { SafeUser } from "~/types/auth";
+
+// Mock user data for testing permissions
+const mockSuperAdmin: SafeUser = {
+ id: 1,
+ name: "Super Admin",
+ username: "superadmin",
+ email: "super@example.com",
+ status: USER_STATUS.ACTIVE,
+ authLevel: AUTH_LEVELS.SUPERADMIN,
+ createdDate: new Date(),
+ editDate: new Date(),
+};
+
+const mockAdmin: SafeUser = {
+ id: 2,
+ name: "Admin User",
+ username: "admin",
+ email: "admin@example.com",
+ status: USER_STATUS.ACTIVE,
+ authLevel: AUTH_LEVELS.ADMIN,
+ createdDate: new Date(),
+ editDate: new Date(),
+};
+
+const mockUser: SafeUser = {
+ id: 3,
+ name: "Regular User",
+ username: "user",
+ email: "user@example.com",
+ status: USER_STATUS.ACTIVE,
+ authLevel: AUTH_LEVELS.USER,
+ createdDate: new Date(),
+ editDate: new Date(),
+};
+
+describe("Route Protection Integration Tests", () => {
+ describe("checkPermission", () => {
+ it("should correctly check view_all_users permission", () => {
+ expect(checkPermission(mockSuperAdmin, "view_all_users")).toBe(true);
+ expect(checkPermission(mockAdmin, "view_all_users")).toBe(false);
+ expect(checkPermission(mockUser, "view_all_users")).toBe(false);
+ });
+
+ it("should correctly check create_users permission", () => {
+ expect(checkPermission(mockSuperAdmin, "create_users")).toBe(true);
+ expect(checkPermission(mockAdmin, "create_users")).toBe(true);
+ expect(checkPermission(mockUser, "create_users")).toBe(false);
+ });
+
+ it("should correctly check manage_finances permission", () => {
+ expect(checkPermission(mockSuperAdmin, "manage_finances")).toBe(true);
+ expect(checkPermission(mockAdmin, "manage_finances")).toBe(true);
+ expect(checkPermission(mockUser, "manage_finances")).toBe(false);
+ });
+
+ it("should correctly check view_reports permission", () => {
+ expect(checkPermission(mockSuperAdmin, "view_reports")).toBe(true);
+ expect(checkPermission(mockAdmin, "view_reports")).toBe(true);
+ expect(checkPermission(mockUser, "view_reports")).toBe(false);
+ });
+
+ it("should return false for unknown permission", () => {
+ expect(checkPermission(mockUser, "unknown_permission" as any)).toBe(false);
+ expect(checkPermission(mockAdmin, "unknown_permission" as any)).toBe(false);
+ expect(checkPermission(mockSuperAdmin, "unknown_permission" as any)).toBe(false);
+ });
+ });
+
+ describe("createUnauthorizedResponse", () => {
+ it("should create response with default message", () => {
+ const response = createUnauthorizedResponse();
+ expect(response.status).toBe(403);
+ expect(response.headers.get("Content-Type")).toBe("text/plain; charset=utf-8");
+ });
+
+ it("should create response with custom message", () => {
+ const customMessage = "Custom error message";
+ const response = createUnauthorizedResponse(customMessage);
+ expect(response.status).toBe(403);
+ expect(response.headers.get("Content-Type")).toBe("text/plain; charset=utf-8");
+ });
+ });
+
+ describe("Auth Level Hierarchy", () => {
+ it("should have correct auth level values", () => {
+ expect(AUTH_LEVELS.SUPERADMIN).toBe(1);
+ expect(AUTH_LEVELS.ADMIN).toBe(2);
+ expect(AUTH_LEVELS.USER).toBe(3);
+ });
+
+ it("should enforce correct hierarchy (lower number = higher privilege)", () => {
+ expect(AUTH_LEVELS.SUPERADMIN < AUTH_LEVELS.ADMIN).toBe(true);
+ expect(AUTH_LEVELS.ADMIN < AUTH_LEVELS.USER).toBe(true);
+ });
+ });
+
+ describe("User Status", () => {
+ it("should have correct status values", () => {
+ expect(USER_STATUS.ACTIVE).toBe("active");
+ expect(USER_STATUS.INACTIVE).toBe("inactive");
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/user-management.test.ts b/app/lib/__tests__/user-management.test.ts
new file mode 100644
index 0000000..cde2d36
--- /dev/null
+++ b/app/lib/__tests__/user-management.test.ts
@@ -0,0 +1,299 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { prisma } from '../db.server';
+import { getUsers, createUser, updateUser, deleteUser, toggleUserStatus } from '../user-management.server';
+import { hashPassword } from '../auth.server';
+import { AUTH_LEVELS, USER_STATUS } from '~/types/auth';
+
+describe('User Management', () => {
+ beforeEach(async () => {
+ // Clean up test data
+ await prisma.user.deleteMany({
+ where: {
+ email: {
+ contains: 'test'
+ }
+ }
+ });
+
+ // Ensure the main superadmin exists for login functionality
+ const existingSuperadmin = await prisma.user.findUnique({
+ where: { username: 'superadmin' }
+ });
+
+ if (!existingSuperadmin) {
+ const hashedPassword = await hashPassword('admin123');
+ await prisma.user.create({
+ data: {
+ name: 'Super Administrator',
+ username: 'superadmin',
+ email: 'admin@carmaintenance.com',
+ password: hashedPassword,
+ status: USER_STATUS.ACTIVE,
+ authLevel: AUTH_LEVELS.SUPERADMIN,
+ },
+ });
+ }
+ });
+
+ afterEach(async () => {
+ // Clean up test data but preserve the main superadmin
+ await prisma.user.deleteMany({
+ where: {
+ AND: [
+ {
+ email: {
+ contains: 'test'
+ }
+ },
+ {
+ username: {
+ not: 'superadmin'
+ }
+ }
+ ]
+ }
+ });
+ });
+
+ describe('getUsers', () => {
+ it('should return users with role-based filtering', async () => {
+ // Create test users with properly hashed passwords
+ const hashedPassword = await hashPassword('testpassword123');
+
+ const superadmin = await prisma.user.create({
+ data: {
+ name: 'Test Superadmin',
+ username: 'testsuperadmin',
+ email: 'testsuperadmin@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.SUPERADMIN,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ const admin = await prisma.user.create({
+ data: {
+ name: 'Test Admin',
+ username: 'testadmin',
+ email: 'testadmin@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.ADMIN,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ // Superadmin should see all users
+ const superadminResult = await getUsers(AUTH_LEVELS.SUPERADMIN);
+ expect(superadminResult.users.length).toBeGreaterThanOrEqual(2);
+
+ // Admin should not see superadmin users
+ const adminResult = await getUsers(AUTH_LEVELS.ADMIN);
+ const adminVisibleUsers = adminResult.users.filter(u => u.authLevel === AUTH_LEVELS.SUPERADMIN);
+ expect(adminVisibleUsers.length).toBe(0);
+ });
+
+ it('should support search functionality', async () => {
+ const hashedPassword = await hashPassword('testpassword123');
+
+ await prisma.user.create({
+ data: {
+ name: 'John Test User',
+ username: 'johntestuser',
+ email: 'johntest@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ const result = await getUsers(AUTH_LEVELS.SUPERADMIN, 'John');
+ expect(result.users.some(u => u.name.includes('John'))).toBe(true);
+ });
+ });
+
+ describe('createUser', () => {
+ it('should create a new user successfully', async () => {
+ const userData = {
+ name: 'New Test User',
+ username: 'newtestuser',
+ email: 'newtest@example.com',
+ password: 'password123',
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ };
+
+ const result = await createUser(userData, AUTH_LEVELS.SUPERADMIN);
+
+ expect(result.success).toBe(true);
+ expect(result.user).toBeDefined();
+ expect(result.user?.name).toBe(userData.name);
+ expect(result.user?.username).toBe(userData.username);
+ expect(result.user?.email).toBe(userData.email);
+ });
+
+ it('should prevent admin from creating superadmin', async () => {
+ const userData = {
+ name: 'Test Superadmin',
+ username: 'testsuperadmin2',
+ email: 'testsuperadmin2@example.com',
+ password: 'password123',
+ authLevel: AUTH_LEVELS.SUPERADMIN,
+ status: USER_STATUS.ACTIVE,
+ };
+
+ const result = await createUser(userData, AUTH_LEVELS.ADMIN);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toContain('لا يمكن للمدير إنشاء حساب مدير عام');
+ });
+
+ it('should prevent duplicate username', async () => {
+ const userData1 = {
+ name: 'Test User 1',
+ username: 'duplicatetest',
+ email: 'test1@example.com',
+ password: 'password123',
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ };
+
+ const userData2 = {
+ name: 'Test User 2',
+ username: 'duplicatetest', // Same username
+ email: 'test2@example.com',
+ password: 'password123',
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ };
+
+ await createUser(userData1, AUTH_LEVELS.SUPERADMIN);
+ const result = await createUser(userData2, AUTH_LEVELS.SUPERADMIN);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toContain('اسم المستخدم موجود بالفعل');
+ });
+ });
+
+ describe('updateUser', () => {
+ it('should update user successfully', async () => {
+ const hashedPassword = await hashPassword('testpassword123');
+
+ const user = await prisma.user.create({
+ data: {
+ name: 'Original Name',
+ username: 'originaluser' + Date.now(),
+ email: 'original' + Date.now() + '@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ const updateData = {
+ name: 'Updated Name',
+ email: 'updated' + Date.now() + '@example.com',
+ };
+
+ const result = await updateUser(user.id, updateData, AUTH_LEVELS.SUPERADMIN);
+
+ if (!result.success) {
+ console.log('Update failed:', result.error);
+ }
+
+ expect(result.success).toBe(true);
+ expect(result.user?.name).toBe('Updated Name');
+ expect(result.user?.email).toBe(updateData.email);
+ });
+ });
+
+ describe('toggleUserStatus', () => {
+ it('should toggle user status', async () => {
+ const hashedPassword = await hashPassword('testpassword123');
+
+ const user = await prisma.user.create({
+ data: {
+ name: 'Test User',
+ username: 'teststatususer' + Date.now(),
+ email: 'teststatus' + Date.now() + '@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ const result = await toggleUserStatus(user.id, AUTH_LEVELS.SUPERADMIN);
+
+ expect(result.success).toBe(true);
+ expect(result.user?.status).toBe(USER_STATUS.INACTIVE);
+ });
+ });
+
+ describe('deleteUser', () => {
+ it('should delete user successfully', async () => {
+ const hashedPassword = await hashPassword('testpassword123');
+
+ const user = await prisma.user.create({
+ data: {
+ name: 'Delete Test User',
+ username: 'deletetestuser' + Date.now(),
+ email: 'deletetest' + Date.now() + '@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.USER,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ const result = await deleteUser(user.id, AUTH_LEVELS.SUPERADMIN);
+
+ expect(result.success).toBe(true);
+
+ // Verify user is deleted
+ const deletedUser = await prisma.user.findUnique({
+ where: { id: user.id }
+ });
+ expect(deletedUser).toBeNull();
+ });
+
+ it('should prevent deletion of last superadmin', async () => {
+ // Delete any test superadmins but keep the main superadmin
+ await prisma.user.deleteMany({
+ where: {
+ AND: [
+ { authLevel: AUTH_LEVELS.SUPERADMIN },
+ { username: { not: 'superadmin' } }
+ ]
+ }
+ });
+
+ // Try to delete the main superadmin (should fail as it's the last one)
+ const mainSuperadmin = await prisma.user.findUnique({
+ where: { username: 'superadmin' }
+ });
+
+ if (mainSuperadmin) {
+ const result = await deleteUser(mainSuperadmin.id, AUTH_LEVELS.SUPERADMIN);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toContain('لا يمكن حذف آخر مدير عام في النظام');
+ } else {
+ // If main superadmin doesn't exist, create one and test
+ const hashedPassword = await hashPassword('admin123');
+ const superadmin = await prisma.user.create({
+ data: {
+ name: 'Last Superadmin',
+ username: 'lastsuperadmin' + Date.now(),
+ email: 'lastsuperadmin' + Date.now() + '@example.com',
+ password: hashedPassword,
+ authLevel: AUTH_LEVELS.SUPERADMIN,
+ status: USER_STATUS.ACTIVE,
+ }
+ });
+
+ const result = await deleteUser(superadmin.id, AUTH_LEVELS.SUPERADMIN);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toContain('لا يمكن حذف آخر مدير عام في النظام');
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/validation-utils.test.ts b/app/lib/__tests__/validation-utils.test.ts
new file mode 100644
index 0000000..9107980
--- /dev/null
+++ b/app/lib/__tests__/validation-utils.test.ts
@@ -0,0 +1,222 @@
+import { describe, it, expect } from 'vitest';
+import {
+ validateField,
+ validateFields,
+ validateEmail,
+ validatePhone,
+ validatePassword,
+ validateUsername,
+ validatePlateNumber,
+ validateYear,
+ validateCurrency,
+ sanitizeString,
+ sanitizeNumber,
+ sanitizeInteger,
+ sanitizeFormData,
+ PATTERNS
+} from '../validation-utils';
+
+describe('Validation Utils', () => {
+ describe('validateField', () => {
+ it('should validate required fields', () => {
+ const result1 = validateField('', { required: true });
+ expect(result1.isValid).toBe(false);
+ expect(result1.error).toBe('هذا الحقل مطلوب');
+
+ const result2 = validateField('value', { required: true });
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate email fields', () => {
+ const result1 = validateField('invalid-email', { email: true });
+ expect(result1.isValid).toBe(false);
+ expect(result1.error).toBe('البريد الإلكتروني غير صحيح');
+
+ const result2 = validateField('test@example.com', { email: true });
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate phone fields', () => {
+ const result1 = validateField('invalid-phone', { phone: true });
+ expect(result1.isValid).toBe(false);
+ expect(result1.error).toBe('رقم الهاتف غير صحيح');
+
+ const result2 = validateField('+966501234567', { phone: true });
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate length constraints', () => {
+ const result1 = validateField('ab', { minLength: 3 });
+ expect(result1.isValid).toBe(false);
+ expect(result1.error).toBe('يجب أن يكون على الأقل 3 أحرف');
+
+ const result2 = validateField('a'.repeat(101), { maxLength: 100 });
+ expect(result2.isValid).toBe(false);
+ expect(result2.error).toBe('يجب أن يكون أقل من 100 حرف');
+ });
+
+ it('should validate numeric constraints', () => {
+ const result1 = validateField(5, { min: 10 });
+ expect(result1.isValid).toBe(false);
+ expect(result1.error).toBe('يجب أن يكون على الأقل 10');
+
+ const result2 = validateField(15, { max: 10 });
+ expect(result2.isValid).toBe(false);
+ expect(result2.error).toBe('يجب أن يكون أقل من 10');
+ });
+
+ it('should validate custom rules', () => {
+ const customRule = (value: any) => {
+ return value === 'forbidden' ? 'هذه القيمة غير مسموحة' : null;
+ };
+
+ const result1 = validateField('forbidden', { custom: customRule });
+ expect(result1.isValid).toBe(false);
+ expect(result1.error).toBe('هذه القيمة غير مسموحة');
+
+ const result2 = validateField('allowed', { custom: customRule });
+ expect(result2.isValid).toBe(true);
+ });
+ });
+
+ describe('validateFields', () => {
+ it('should validate multiple fields', () => {
+ const data = {
+ name: '',
+ email: 'invalid-email',
+ age: 5,
+ };
+
+ const rules = {
+ name: { required: true },
+ email: { email: true },
+ age: { min: 18 },
+ };
+
+ const result = validateFields(data, rules);
+ expect(result.isValid).toBe(false);
+ expect(result.errors.name).toBe('هذا الحقل مطلوب');
+ expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
+ expect(result.errors.age).toBe('يجب أن يكون على الأقل 18');
+ });
+ });
+
+ describe('specific validation functions', () => {
+ it('should validate email', () => {
+ const result1 = validateEmail('');
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validateEmail('test@example.com');
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate phone', () => {
+ const result1 = validatePhone('invalid');
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validatePhone('+966501234567');
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate password', () => {
+ const result1 = validatePassword('123');
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validatePassword('password123');
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate username', () => {
+ const result1 = validateUsername('ab');
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validateUsername('user123');
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate plate number', () => {
+ const result1 = validatePlateNumber('');
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validatePlateNumber('ABC-1234');
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate year', () => {
+ const result1 = validateYear(1800);
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validateYear(2023);
+ expect(result2.isValid).toBe(true);
+ });
+
+ it('should validate currency', () => {
+ const result1 = validateCurrency(-10);
+ expect(result1.isValid).toBe(false);
+
+ const result2 = validateCurrency(100.50);
+ expect(result2.isValid).toBe(true);
+ });
+ });
+
+ describe('sanitization functions', () => {
+ it('should sanitize strings', () => {
+ expect(sanitizeString(' hello world ')).toBe('hello world');
+ expect(sanitizeString('\t\n test \n\t')).toBe('test');
+ });
+
+ it('should sanitize numbers', () => {
+ expect(sanitizeNumber('123.45')).toBe(123.45);
+ expect(sanitizeNumber('invalid')).toBe(null);
+ expect(sanitizeNumber(456)).toBe(456);
+ });
+
+ it('should sanitize integers', () => {
+ expect(sanitizeInteger('123')).toBe(123);
+ expect(sanitizeInteger('123.45')).toBe(123);
+ expect(sanitizeInteger('invalid')).toBe(null);
+ });
+
+ it('should sanitize form data', () => {
+ const data = {
+ name: ' John Doe ',
+ age: 25,
+ description: '\t\n Some text \n\t',
+ };
+
+ const sanitized = sanitizeFormData(data);
+ expect(sanitized.name).toBe('John Doe');
+ expect(sanitized.age).toBe(25);
+ expect(sanitized.description).toBe('Some text');
+ });
+ });
+
+ describe('patterns', () => {
+ it('should match email pattern', () => {
+ expect(PATTERNS.email.test('test@example.com')).toBe(true);
+ expect(PATTERNS.email.test('invalid-email')).toBe(false);
+ });
+
+ it('should match phone pattern', () => {
+ expect(PATTERNS.phone.test('+966501234567')).toBe(true);
+ expect(PATTERNS.phone.test('0501234567')).toBe(true);
+ expect(PATTERNS.phone.test('invalid-phone')).toBe(false);
+ });
+
+ it('should match username pattern', () => {
+ expect(PATTERNS.username.test('user123')).toBe(true);
+ expect(PATTERNS.username.test('user_name')).toBe(true);
+ expect(PATTERNS.username.test('user-name')).toBe(false);
+ expect(PATTERNS.username.test('user name')).toBe(false);
+ });
+
+ it('should match numeric patterns', () => {
+ expect(PATTERNS.numeric.test('12345')).toBe(true);
+ expect(PATTERNS.numeric.test('123abc')).toBe(false);
+
+ expect(PATTERNS.decimal.test('123.45')).toBe(true);
+ expect(PATTERNS.decimal.test('123')).toBe(true);
+ expect(PATTERNS.decimal.test('123.45.67')).toBe(false);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/vehicle-management.test.ts b/app/lib/__tests__/vehicle-management.test.ts
new file mode 100644
index 0000000..91c08c7
--- /dev/null
+++ b/app/lib/__tests__/vehicle-management.test.ts
@@ -0,0 +1,443 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import {
+ createVehicle,
+ updateVehicle,
+ deleteVehicle,
+ getVehicles,
+ getVehicleById,
+ getVehiclesForSelect,
+ searchVehicles,
+ getVehicleStats
+} from '../vehicle-management.server';
+import { prisma } from '../db.server';
+
+// Mock Prisma
+vi.mock('../db.server', () => ({
+ prisma: {
+ vehicle: {
+ create: vi.fn(),
+ findMany: vi.fn(),
+ findUnique: vi.fn(),
+ findFirst: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ count: vi.fn(),
+ },
+ customer: {
+ findUnique: vi.fn(),
+ },
+ },
+}));
+
+describe('Vehicle Management', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ describe('createVehicle', () => {
+ const validVehicleData = {
+ plateNumber: 'ABC-123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: 1,
+ };
+
+ it('should create a vehicle successfully', async () => {
+ const mockVehicle = { id: 1, ...validVehicleData };
+ const mockCustomer = { id: 1, name: 'أحمد محمد' };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+ (prisma.customer.findUnique as any).mockResolvedValue(mockCustomer);
+ (prisma.vehicle.create as any).mockResolvedValue(mockVehicle);
+
+ const result = await createVehicle(validVehicleData);
+
+ expect(result.success).toBe(true);
+ expect(result.vehicle).toEqual(mockVehicle);
+ expect(prisma.vehicle.create).toHaveBeenCalledWith({
+ data: expect.objectContaining({
+ plateNumber: 'ABC-123',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ ownerId: 1,
+ }),
+ });
+ });
+
+ it('should fail if plate number already exists', async () => {
+ const existingVehicle = { id: 2, plateNumber: 'ABC-123' };
+ (prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
+
+ const result = await createVehicle(validVehicleData);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('رقم اللوحة موجود بالفعل');
+ expect(prisma.vehicle.create).not.toHaveBeenCalled();
+ });
+
+ it('should fail if owner does not exist', async () => {
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+ (prisma.customer.findUnique as any).mockResolvedValue(null);
+
+ const result = await createVehicle(validVehicleData);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('المالك غير موجود');
+ expect(prisma.vehicle.create).not.toHaveBeenCalled();
+ });
+
+ it('should handle database errors', async () => {
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+ (prisma.customer.findUnique as any).mockResolvedValue({ id: 1 });
+ (prisma.vehicle.create as any).mockRejectedValue(new Error('Database error'));
+
+ const result = await createVehicle(validVehicleData);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('حدث خطأ أثناء إنشاء المركبة');
+ });
+ });
+
+ describe('updateVehicle', () => {
+ const updateData = {
+ plateNumber: 'XYZ-789',
+ manufacturer: 'هوندا',
+ model: 'أكورد',
+ };
+
+ it('should update a vehicle successfully', async () => {
+ const existingVehicle = { id: 1, plateNumber: 'ABC-123', ownerId: 1 };
+ const updatedVehicle = { ...existingVehicle, ...updateData };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
+ (prisma.vehicle.findFirst as any).mockResolvedValue(null);
+ (prisma.vehicle.update as any).mockResolvedValue(updatedVehicle);
+
+ const result = await updateVehicle(1, updateData);
+
+ expect(result.success).toBe(true);
+ expect(result.vehicle).toEqual(updatedVehicle);
+ expect(prisma.vehicle.update).toHaveBeenCalledWith({
+ where: { id: 1 },
+ data: expect.objectContaining({
+ plateNumber: 'XYZ-789',
+ manufacturer: 'هوندا',
+ model: 'أكورد',
+ }),
+ });
+ });
+
+ it('should fail if vehicle does not exist', async () => {
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+
+ const result = await updateVehicle(999, updateData);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('المركبة غير موجودة');
+ expect(prisma.vehicle.update).not.toHaveBeenCalled();
+ });
+
+ it('should fail if new plate number conflicts', async () => {
+ const existingVehicle = { id: 1, plateNumber: 'ABC-123' };
+ const conflictVehicle = { id: 2, plateNumber: 'XYZ-789' };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
+ (prisma.vehicle.findFirst as any).mockResolvedValue(conflictVehicle);
+
+ const result = await updateVehicle(1, updateData);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('رقم اللوحة موجود بالفعل');
+ expect(prisma.vehicle.update).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('deleteVehicle', () => {
+ it('should delete a vehicle successfully', async () => {
+ const existingVehicle = {
+ id: 1,
+ plateNumber: 'ABC-123',
+ maintenanceVisits: []
+ };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
+ (prisma.vehicle.delete as any).mockResolvedValue(existingVehicle);
+
+ const result = await deleteVehicle(1);
+
+ expect(result.success).toBe(true);
+ expect(prisma.vehicle.delete).toHaveBeenCalledWith({
+ where: { id: 1 },
+ });
+ });
+
+ it('should fail if vehicle does not exist', async () => {
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+
+ const result = await deleteVehicle(999);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('المركبة غير موجودة');
+ expect(prisma.vehicle.delete).not.toHaveBeenCalled();
+ });
+
+ it('should fail if vehicle has maintenance visits', async () => {
+ const existingVehicle = {
+ id: 1,
+ plateNumber: 'ABC-123',
+ maintenanceVisits: [{ id: 1 }, { id: 2 }]
+ };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
+
+ const result = await deleteVehicle(1);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toContain('لا يمكن حذف المركبة لأنها تحتوي على 2 زيارة صيانة');
+ expect(prisma.vehicle.delete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('getVehicles', () => {
+ it('should return vehicles with pagination', async () => {
+ const mockVehicles = [
+ {
+ id: 1,
+ plateNumber: 'ABC-123',
+ manufacturer: 'تويوتا',
+ owner: { id: 1, name: 'أحمد محمد' }
+ },
+ {
+ id: 2,
+ plateNumber: 'XYZ-789',
+ manufacturer: 'هوندا',
+ owner: { id: 2, name: 'محمد علي' }
+ }
+ ];
+
+ (prisma.vehicle.findMany as any).mockResolvedValue(mockVehicles);
+ (prisma.vehicle.count as any).mockResolvedValue(2);
+
+ const result = await getVehicles('', 1, 10);
+
+ expect(result.vehicles).toEqual(mockVehicles);
+ expect(result.total).toBe(2);
+ expect(result.totalPages).toBe(1);
+ expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
+ where: {},
+ include: {
+ owner: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ skip: 0,
+ take: 10,
+ });
+ });
+
+ it('should filter by search query', async () => {
+ (prisma.vehicle.findMany as any).mockResolvedValue([]);
+ (prisma.vehicle.count as any).mockResolvedValue(0);
+
+ await getVehicles('تويوتا', 1, 10);
+
+ expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
+ where: {
+ OR: [
+ { plateNumber: { contains: 'تويوتا' } },
+ { manufacturer: { contains: 'تويوتا' } },
+ { model: { contains: 'تويوتا' } },
+ { bodyType: { contains: 'تويوتا' } },
+ { owner: { name: { contains: 'تويوتا' } } },
+ ],
+ },
+ include: expect.any(Object),
+ orderBy: { createdDate: 'desc' },
+ skip: 0,
+ take: 10,
+ });
+ });
+
+ it('should filter by owner ID', async () => {
+ (prisma.vehicle.findMany as any).mockResolvedValue([]);
+ (prisma.vehicle.count as any).mockResolvedValue(0);
+
+ await getVehicles('', 1, 10, 5);
+
+ expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
+ where: { ownerId: 5 },
+ include: expect.any(Object),
+ orderBy: { createdDate: 'desc' },
+ skip: 0,
+ take: 10,
+ });
+ });
+ });
+
+ describe('getVehicleById', () => {
+ it('should return vehicle with full relationships', async () => {
+ const mockVehicle = {
+ id: 1,
+ plateNumber: 'ABC-123',
+ owner: { id: 1, name: 'أحمد محمد' },
+ maintenanceVisits: [
+ { id: 1, visitDate: new Date(), cost: 100 }
+ ]
+ };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(mockVehicle);
+
+ const result = await getVehicleById(1);
+
+ expect(result).toEqual(mockVehicle);
+ expect(prisma.vehicle.findUnique).toHaveBeenCalledWith({
+ where: { id: 1 },
+ include: {
+ owner: true,
+ maintenanceVisits: {
+ include: {
+ customer: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ },
+ },
+ },
+ orderBy: { visitDate: 'desc' },
+ take: 10,
+ },
+ },
+ });
+ });
+
+ it('should return null if vehicle not found', async () => {
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+
+ const result = await getVehicleById(999);
+
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('getVehicleStats', () => {
+ it('should calculate vehicle statistics', async () => {
+ const mockVehicle = {
+ id: 1,
+ suggestedNextVisitDate: new Date('2024-06-01'),
+ maintenanceVisits: [
+ { cost: 150, visitDate: new Date('2024-03-01') }, // Most recent first (desc order)
+ { cost: 200, visitDate: new Date('2024-02-01') },
+ { cost: 100, visitDate: new Date('2024-01-01') },
+ ]
+ };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(mockVehicle);
+
+ const result = await getVehicleStats(1);
+
+ expect(result).toEqual({
+ totalVisits: 3,
+ totalSpent: 450,
+ lastVisitDate: new Date('2024-03-01'),
+ nextSuggestedVisitDate: new Date('2024-06-01'),
+ averageVisitCost: 150,
+ });
+ });
+
+ it('should return null if vehicle not found', async () => {
+ (prisma.vehicle.findUnique as any).mockResolvedValue(null);
+
+ const result = await getVehicleStats(999);
+
+ expect(result).toBeNull();
+ });
+
+ it('should handle vehicle with no visits', async () => {
+ const mockVehicle = {
+ id: 1,
+ suggestedNextVisitDate: null,
+ maintenanceVisits: []
+ };
+
+ (prisma.vehicle.findUnique as any).mockResolvedValue(mockVehicle);
+
+ const result = await getVehicleStats(1);
+
+ expect(result).toEqual({
+ totalVisits: 0,
+ totalSpent: 0,
+ lastVisitDate: undefined,
+ nextSuggestedVisitDate: undefined,
+ averageVisitCost: 0,
+ });
+ });
+ });
+
+ describe('searchVehicles', () => {
+ it('should search vehicles by query', async () => {
+ const mockVehicles = [
+ {
+ id: 1,
+ plateNumber: 'ABC-123',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ owner: { id: 1, name: 'أحمد محمد' }
+ }
+ ];
+
+ (prisma.vehicle.findMany as any).mockResolvedValue(mockVehicles);
+
+ const result = await searchVehicles('تويوتا');
+
+ expect(result).toEqual(mockVehicles);
+ expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
+ where: {
+ OR: [
+ { plateNumber: { contains: 'تويوتا' } },
+ { manufacturer: { contains: 'تويوتا' } },
+ { model: { contains: 'تويوتا' } },
+ ],
+ },
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ owner: {
+ select: {
+ id: true,
+ name: true,
+ },
+ },
+ },
+ orderBy: { plateNumber: 'asc' },
+ take: 10,
+ });
+ });
+
+ it('should return empty array for short queries', async () => {
+ const result = await searchVehicles('a');
+
+ expect(result).toEqual([]);
+ expect(prisma.vehicle.findMany).not.toHaveBeenCalled();
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/__tests__/vehicle-validation.test.ts b/app/lib/__tests__/vehicle-validation.test.ts
new file mode 100644
index 0000000..94e322c
--- /dev/null
+++ b/app/lib/__tests__/vehicle-validation.test.ts
@@ -0,0 +1,379 @@
+import { describe, it, expect } from 'vitest';
+import { validateVehicle } from '../validation';
+
+describe('Vehicle Validation', () => {
+ describe('validateVehicle', () => {
+ const validVehicleData = {
+ plateNumber: 'ABC-123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: 1,
+ };
+
+ it('should validate a complete valid vehicle', () => {
+ const result = validateVehicle(validVehicleData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should validate a minimal valid vehicle', () => {
+ const minimalData = {
+ plateNumber: 'ABC-123',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ year: 2020,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ useType: 'personal',
+ ownerId: 1,
+ };
+
+ const result = validateVehicle(minimalData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ describe('plateNumber validation', () => {
+ it('should require plate number', () => {
+ const result = validateVehicle({ ...validVehicleData, plateNumber: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.plateNumber).toBe('رقم اللوحة مطلوب');
+ });
+
+ it('should require plate number to not be just whitespace', () => {
+ const result = validateVehicle({ ...validVehicleData, plateNumber: ' ' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.plateNumber).toBe('رقم اللوحة مطلوب');
+ });
+ });
+
+ describe('bodyType validation', () => {
+ it('should require body type', () => {
+ const result = validateVehicle({ ...validVehicleData, bodyType: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.bodyType).toBe('نوع الهيكل مطلوب');
+ });
+
+ it('should require body type to not be just whitespace', () => {
+ const result = validateVehicle({ ...validVehicleData, bodyType: ' ' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.bodyType).toBe('نوع الهيكل مطلوب');
+ });
+ });
+
+ describe('manufacturer validation', () => {
+ it('should require manufacturer', () => {
+ const result = validateVehicle({ ...validVehicleData, manufacturer: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.manufacturer).toBe('الشركة المصنعة مطلوبة');
+ });
+
+ it('should require manufacturer to not be just whitespace', () => {
+ const result = validateVehicle({ ...validVehicleData, manufacturer: ' ' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.manufacturer).toBe('الشركة المصنعة مطلوبة');
+ });
+ });
+
+ describe('model validation', () => {
+ it('should require model', () => {
+ const result = validateVehicle({ ...validVehicleData, model: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.model).toBe('الموديل مطلوب');
+ });
+
+ it('should require model to not be just whitespace', () => {
+ const result = validateVehicle({ ...validVehicleData, model: ' ' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.model).toBe('الموديل مطلوب');
+ });
+ });
+
+ describe('year validation', () => {
+ it('should not validate undefined year (partial validation)', () => {
+ const result = validateVehicle({ ...validVehicleData, year: undefined as any });
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors.year).toBeUndefined();
+ });
+
+ it('should reject year below minimum', () => {
+ const result = validateVehicle({ ...validVehicleData, year: 1989 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.year).toContain('السنة يجب أن تكون بين 1990');
+ });
+
+ it('should reject year above maximum', () => {
+ const currentYear = new Date().getFullYear();
+ const result = validateVehicle({ ...validVehicleData, year: currentYear + 2 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.year).toContain('السنة يجب أن تكون بين');
+ });
+
+ it('should accept current year', () => {
+ const currentYear = new Date().getFullYear();
+ const result = validateVehicle({ ...validVehicleData, year: currentYear });
+
+ expect(result.isValid).toBe(true);
+ });
+
+ it('should accept next year', () => {
+ const nextYear = new Date().getFullYear() + 1;
+ const result = validateVehicle({ ...validVehicleData, year: nextYear });
+
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ describe('transmission validation', () => {
+ it('should require transmission', () => {
+ const result = validateVehicle({ ...validVehicleData, transmission: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.transmission).toBe('نوع ناقل الحركة غير صحيح');
+ });
+
+ it('should accept valid transmission types', () => {
+ const automaticResult = validateVehicle({ ...validVehicleData, transmission: 'Automatic' });
+ const manualResult = validateVehicle({ ...validVehicleData, transmission: 'Manual' });
+
+ expect(automaticResult.isValid).toBe(true);
+ expect(manualResult.isValid).toBe(true);
+ });
+
+ it('should reject invalid transmission types', () => {
+ const result = validateVehicle({ ...validVehicleData, transmission: 'CVT' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.transmission).toBe('نوع ناقل الحركة غير صحيح');
+ });
+ });
+
+ describe('fuel validation', () => {
+ it('should require fuel type', () => {
+ const result = validateVehicle({ ...validVehicleData, fuel: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.fuel).toBe('نوع الوقود غير صحيح');
+ });
+
+ it('should accept valid fuel types', () => {
+ const validFuels = ['Gasoline', 'Diesel', 'Hybrid', 'Mild Hybrid', 'Electric'];
+
+ validFuels.forEach(fuel => {
+ const result = validateVehicle({ ...validVehicleData, fuel });
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ it('should reject invalid fuel types', () => {
+ const result = validateVehicle({ ...validVehicleData, fuel: 'Nuclear' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.fuel).toBe('نوع الوقود غير صحيح');
+ });
+ });
+
+ describe('cylinders validation', () => {
+ it('should accept valid cylinder counts', () => {
+ const validCylinders = [1, 2, 3, 4, 6, 8, 10, 12];
+
+ validCylinders.forEach(cylinders => {
+ const result = validateVehicle({ ...validVehicleData, cylinders });
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ it('should reject cylinder count below minimum', () => {
+ const result = validateVehicle({ ...validVehicleData, cylinders: 0 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.cylinders).toContain('عدد الأسطوانات يجب أن يكون بين 1');
+ });
+
+ it('should reject cylinder count above maximum', () => {
+ const result = validateVehicle({ ...validVehicleData, cylinders: 16 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.cylinders).toContain('عدد الأسطوانات يجب أن يكون بين 1 و 12');
+ });
+
+ it('should accept null/undefined cylinders', () => {
+ const result1 = validateVehicle({ ...validVehicleData, cylinders: undefined });
+ const result2 = validateVehicle({ ...validVehicleData, cylinders: null as any });
+
+ expect(result1.isValid).toBe(true);
+ expect(result2.isValid).toBe(true);
+ });
+ });
+
+ describe('engineDisplacement validation', () => {
+ it('should accept valid engine displacements', () => {
+ const validDisplacements = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0];
+
+ validDisplacements.forEach(engineDisplacement => {
+ const result = validateVehicle({ ...validVehicleData, engineDisplacement });
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ it('should reject engine displacement at zero', () => {
+ const result = validateVehicle({ ...validVehicleData, engineDisplacement: 0 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.engineDisplacement).toContain('سعة المحرك يجب أن تكون بين 0.1');
+ });
+
+ it('should reject engine displacement above maximum', () => {
+ const result = validateVehicle({ ...validVehicleData, engineDisplacement: 15.0 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.engineDisplacement).toContain('سعة المحرك يجب أن تكون بين 0.1 و 10');
+ });
+
+ it('should accept null/undefined engine displacement', () => {
+ const result1 = validateVehicle({ ...validVehicleData, engineDisplacement: undefined });
+ const result2 = validateVehicle({ ...validVehicleData, engineDisplacement: null as any });
+
+ expect(result1.isValid).toBe(true);
+ expect(result2.isValid).toBe(true);
+ });
+ });
+
+ describe('useType validation', () => {
+ it('should require use type', () => {
+ const result = validateVehicle({ ...validVehicleData, useType: '' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.useType).toBe('نوع الاستخدام غير صحيح');
+ });
+
+ it('should accept valid use types', () => {
+ const validUseTypes = ['personal', 'taxi', 'apps', 'loading', 'travel'];
+
+ validUseTypes.forEach(useType => {
+ const result = validateVehicle({ ...validVehicleData, useType });
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ it('should reject invalid use types', () => {
+ const result = validateVehicle({ ...validVehicleData, useType: 'military' });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.useType).toBe('نوع الاستخدام غير صحيح');
+ });
+ });
+
+ describe('ownerId validation', () => {
+ it('should not validate undefined owner ID (partial validation)', () => {
+ const result = validateVehicle({ ...validVehicleData, ownerId: undefined as any });
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors.ownerId).toBeUndefined();
+ });
+
+ it('should reject zero owner ID', () => {
+ const result = validateVehicle({ ...validVehicleData, ownerId: 0 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.ownerId).toBe('مالك المركبة مطلوب');
+ });
+
+ it('should reject negative owner ID', () => {
+ const result = validateVehicle({ ...validVehicleData, ownerId: -1 });
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors.ownerId).toBe('مالك المركبة مطلوب');
+ });
+
+ it('should accept positive owner ID', () => {
+ const result = validateVehicle({ ...validVehicleData, ownerId: 5 });
+
+ expect(result.isValid).toBe(true);
+ });
+ });
+
+ describe('multiple validation errors', () => {
+ it('should return all validation errors', () => {
+ const invalidData = {
+ plateNumber: '',
+ bodyType: '',
+ manufacturer: '',
+ model: '',
+ year: 1980,
+ transmission: 'Invalid',
+ fuel: 'Invalid',
+ cylinders: 20,
+ engineDisplacement: 15.0,
+ useType: 'Invalid',
+ ownerId: 0,
+ };
+
+ const result = validateVehicle(invalidData);
+
+ expect(result.isValid).toBe(false);
+ expect(Object.keys(result.errors)).toContain('plateNumber');
+ expect(Object.keys(result.errors)).toContain('bodyType');
+ expect(Object.keys(result.errors)).toContain('manufacturer');
+ expect(Object.keys(result.errors)).toContain('model');
+ expect(Object.keys(result.errors)).toContain('year');
+ expect(Object.keys(result.errors)).toContain('transmission');
+ expect(Object.keys(result.errors)).toContain('fuel');
+ expect(Object.keys(result.errors)).toContain('cylinders');
+ expect(Object.keys(result.errors)).toContain('engineDisplacement');
+ expect(Object.keys(result.errors)).toContain('useType');
+ expect(Object.keys(result.errors)).toContain('ownerId');
+ });
+ });
+
+ describe('partial validation', () => {
+ it('should validate only provided fields', () => {
+ const partialData = {
+ plateNumber: 'XYZ-789',
+ year: 2021,
+ };
+
+ const result = validateVehicle(partialData);
+
+ expect(result.isValid).toBe(true);
+ expect(Object.keys(result.errors)).toHaveLength(0);
+ });
+
+ it('should validate only provided fields with errors', () => {
+ const partialData = {
+ plateNumber: '',
+ year: 1980,
+ cylinders: 20,
+ };
+
+ const result = validateVehicle(partialData);
+
+ expect(result.isValid).toBe(false);
+ expect(Object.keys(result.errors)).toContain('plateNumber');
+ expect(Object.keys(result.errors)).toContain('year');
+ expect(Object.keys(result.errors)).toContain('cylinders');
+ expect(Object.keys(result.errors)).not.toContain('manufacturer');
+ expect(Object.keys(result.errors)).not.toContain('model');
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/lib/auth-constants.ts b/app/lib/auth-constants.ts
new file mode 100644
index 0000000..ff9a83f
--- /dev/null
+++ b/app/lib/auth-constants.ts
@@ -0,0 +1,51 @@
+// Authentication configuration constants
+export const AUTH_CONFIG = {
+ // Password requirements
+ MIN_PASSWORD_LENGTH: 6,
+ MAX_PASSWORD_LENGTH: 128,
+
+ // Session configuration
+ SESSION_MAX_AGE: 60 * 60 * 24 * 30, // 30 days in seconds
+
+ // Rate limiting (for future implementation)
+ MAX_LOGIN_ATTEMPTS: 5,
+ LOGIN_ATTEMPT_WINDOW: 15 * 60 * 1000, // 15 minutes in milliseconds
+
+ // Cookie configuration
+ COOKIE_NAME: "car_maintenance_session",
+} as const;
+
+// Authentication error messages in Arabic
+export const AUTH_ERRORS = {
+ INVALID_CREDENTIALS: "اسم المستخدم أو كلمة المرور غير صحيحة",
+ ACCOUNT_INACTIVE: "الحساب غير مفعل",
+ ACCOUNT_NOT_FOUND: "الحساب غير موجود",
+ USERNAME_REQUIRED: "اسم المستخدم مطلوب",
+ EMAIL_REQUIRED: "البريد الإلكتروني مطلوب",
+ PASSWORD_REQUIRED: "كلمة المرور مطلوبة",
+ NAME_REQUIRED: "الاسم مطلوب",
+ PASSWORD_TOO_SHORT: "كلمة المرور يجب أن تكون 6 أحرف على الأقل",
+ PASSWORD_MISMATCH: "كلمة المرور غير متطابقة",
+ INVALID_EMAIL: "صيغة البريد الإلكتروني غير صحيحة",
+ USERNAME_EXISTS: "اسم المستخدم موجود بالفعل",
+ EMAIL_EXISTS: "البريد الإلكتروني موجود بالفعل",
+ INSUFFICIENT_PERMISSIONS: "ليس لديك صلاحية للوصول إلى هذه الصفحة",
+ SESSION_EXPIRED: "انتهت صلاحية الجلسة، يرجى تسجيل الدخول مرة أخرى",
+ SIGNUP_DISABLED: "التسجيل غير متاح حالياً",
+} as const;
+
+// Success messages in Arabic
+export const AUTH_SUCCESS = {
+ LOGIN_SUCCESS: "تم تسجيل الدخول بنجاح",
+ LOGOUT_SUCCESS: "تم تسجيل الخروج بنجاح",
+ SIGNUP_SUCCESS: "تم إنشاء الحساب بنجاح",
+ PASSWORD_CHANGED: "تم تغيير كلمة المرور بنجاح",
+ PROFILE_UPDATED: "تم تحديث الملف الشخصي بنجاح",
+} as const;
+
+// Validation patterns
+export const VALIDATION_PATTERNS = {
+ EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ USERNAME: /^[a-zA-Z0-9_]{3,20}$/,
+ PHONE: /^[0-9+\-\s()]{10,15}$/,
+} as const;
\ No newline at end of file
diff --git a/app/lib/auth-helpers.server.ts b/app/lib/auth-helpers.server.ts
new file mode 100644
index 0000000..90968ea
--- /dev/null
+++ b/app/lib/auth-helpers.server.ts
@@ -0,0 +1,220 @@
+import { redirect } from "@remix-run/node";
+import { prisma } from "./db.server";
+import { verifyPassword, hashPassword } from "./auth.server";
+import type {
+ SignInFormData,
+ SignUpFormData,
+ AuthResult,
+ AuthLevel,
+ SafeUser,
+ RouteProtectionOptions
+} from "~/types/auth";
+import { AUTH_LEVELS, USER_STATUS } from "~/types/auth";
+import { AUTH_ERRORS, AUTH_CONFIG, VALIDATION_PATTERNS } from "./auth-constants";
+
+// Authentication validation functions
+export async function validateSignIn(formData: SignInFormData): Promise {
+ const { usernameOrEmail, password } = formData;
+
+ // Find user by username or email
+ const user = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { username: usernameOrEmail },
+ { email: usernameOrEmail },
+ ],
+ },
+ });
+
+ if (!user) {
+ return {
+ success: false,
+ errors: [{ message: AUTH_ERRORS.INVALID_CREDENTIALS }],
+ };
+ }
+
+ // Check if user is active
+ if (user.status !== USER_STATUS.ACTIVE) {
+ return {
+ success: false,
+ errors: [{ message: AUTH_ERRORS.ACCOUNT_INACTIVE }],
+ };
+ }
+
+ // Verify password
+ const isValidPassword = await verifyPassword(password, user.password);
+ if (!isValidPassword) {
+ return {
+ success: false,
+ errors: [{ message: AUTH_ERRORS.INVALID_CREDENTIALS }],
+ };
+ }
+
+ // Return success with safe user data
+ const { password: _, ...safeUser } = user;
+ return {
+ success: true,
+ user: safeUser,
+ };
+}
+
+export async function validateSignUp(formData: SignUpFormData): Promise {
+ const { name, username, email, password, confirmPassword } = formData;
+ const errors: { field?: string; message: string }[] = [];
+
+ // Validate required fields
+ if (!name.trim()) {
+ errors.push({ field: "name", message: AUTH_ERRORS.NAME_REQUIRED });
+ }
+
+ if (!username.trim()) {
+ errors.push({ field: "username", message: AUTH_ERRORS.USERNAME_REQUIRED });
+ }
+
+ if (!email.trim()) {
+ errors.push({ field: "email", message: AUTH_ERRORS.EMAIL_REQUIRED });
+ }
+
+ if (!password) {
+ errors.push({ field: "password", message: AUTH_ERRORS.PASSWORD_REQUIRED });
+ }
+
+ if (password !== confirmPassword) {
+ errors.push({ field: "confirmPassword", message: AUTH_ERRORS.PASSWORD_MISMATCH });
+ }
+
+ // Validate password strength
+ if (password && password.length < AUTH_CONFIG.MIN_PASSWORD_LENGTH) {
+ errors.push({ field: "password", message: AUTH_ERRORS.PASSWORD_TOO_SHORT });
+ }
+
+ // Validate email format
+ if (email && !VALIDATION_PATTERNS.EMAIL.test(email)) {
+ errors.push({ field: "email", message: AUTH_ERRORS.INVALID_EMAIL });
+ }
+
+ // Check for existing username
+ if (username) {
+ const existingUsername = await prisma.user.findUnique({
+ where: { username },
+ });
+ if (existingUsername) {
+ errors.push({ field: "username", message: AUTH_ERRORS.USERNAME_EXISTS });
+ }
+ }
+
+ // Check for existing email
+ if (email) {
+ const existingEmail = await prisma.user.findUnique({
+ where: { email },
+ });
+ if (existingEmail) {
+ errors.push({ field: "email", message: AUTH_ERRORS.EMAIL_EXISTS });
+ }
+ }
+
+ if (errors.length > 0) {
+ return {
+ success: false,
+ errors,
+ };
+ }
+
+ return { success: true };
+}
+
+// User creation function
+export async function createUser(formData: SignUpFormData): Promise {
+ const { name, username, email, password } = formData;
+
+ const hashedPassword = await hashPassword(password);
+
+ const user = await prisma.user.create({
+ data: {
+ name: name.trim(),
+ username: username.trim(),
+ email: email.trim(),
+ password: hashedPassword,
+ status: USER_STATUS.ACTIVE,
+ authLevel: AUTH_LEVELS.ADMIN, // First user becomes admin
+ createdDate: new Date(),
+ editDate: new Date(),
+ },
+ });
+
+ const { password: _, ...safeUser } = user;
+ return safeUser;
+}
+
+// Authorization helper functions
+export function hasPermission(userAuthLevel: AuthLevel, requiredAuthLevel: AuthLevel): boolean {
+ return userAuthLevel <= requiredAuthLevel;
+}
+
+export function canAccessUserManagement(userAuthLevel: AuthLevel): boolean {
+ return userAuthLevel <= AUTH_LEVELS.ADMIN;
+}
+
+export function canViewAllUsers(userAuthLevel: AuthLevel): boolean {
+ return userAuthLevel === AUTH_LEVELS.SUPERADMIN;
+}
+
+export function canCreateUsers(userAuthLevel: AuthLevel): boolean {
+ return userAuthLevel <= AUTH_LEVELS.ADMIN;
+}
+
+// Route protection middleware
+export async function requireAuthLevel(
+ request: Request,
+ requiredAuthLevel: AuthLevel,
+ options: RouteProtectionOptions = {}
+) {
+ const { allowInactive = false, redirectTo = "/signin" } = options;
+
+ // Get user from session
+ const userId = await getUserId(request);
+ if (!userId) {
+ throw redirect(redirectTo);
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ });
+
+ if (!user) {
+ throw redirect(redirectTo);
+ }
+
+ // Check if user is active (unless explicitly allowed)
+ if (!allowInactive && user.status !== USER_STATUS.ACTIVE) {
+ throw redirect("/signin?error=account_inactive");
+ }
+
+ // Check authorization level
+ if (!hasPermission(user.authLevel as AuthLevel, requiredAuthLevel)) {
+ throw redirect("/dashboard?error=insufficient_permissions");
+ }
+
+ const { password: _, ...safeUser } = user;
+ return safeUser;
+}
+
+// Check if signup should be allowed (only when no admin users exist)
+export async function isSignupAllowed(): Promise {
+ const adminCount = await prisma.user.count({
+ where: {
+ authLevel: {
+ in: [AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.ADMIN],
+ },
+ status: USER_STATUS.ACTIVE,
+ },
+ });
+
+ return adminCount === 0;
+}
+
+// Import getUserId function
+async function getUserId(request: Request): Promise {
+ const { getUserId: getSessionUserId } = await import("./auth.server");
+ return getSessionUserId(request);
+}
\ No newline at end of file
diff --git a/app/lib/auth-middleware.server.ts b/app/lib/auth-middleware.server.ts
new file mode 100644
index 0000000..f11795f
--- /dev/null
+++ b/app/lib/auth-middleware.server.ts
@@ -0,0 +1,173 @@
+import { redirect } from "@remix-run/node";
+import { getUser, requireUserId } from "./auth.server";
+import { requireAuthLevel } from "./auth-helpers.server";
+import type { AuthLevel, SafeUser, RouteProtectionOptions } from "~/types/auth";
+import { AUTH_LEVELS, USER_STATUS } from "~/types/auth";
+import { AUTH_ERRORS } from "./auth-constants";
+
+// Enhanced middleware for protecting routes that require authentication
+export async function requireAuthentication(
+ request: Request,
+ options: RouteProtectionOptions = {}
+): Promise {
+ const { allowInactive = false, redirectTo = "/signin" } = options;
+
+ await requireUserId(request, redirectTo);
+ const user = await getUser(request);
+
+ if (!user) {
+ throw redirect(redirectTo);
+ }
+
+ // Check if user is active (unless explicitly allowed)
+ if (!allowInactive && user.status !== USER_STATUS.ACTIVE) {
+ throw redirect("/signin?error=account_inactive");
+ }
+
+ return user;
+}
+
+// Middleware for protecting admin routes (admin and superadmin)
+export async function requireAdmin(
+ request: Request,
+ options: RouteProtectionOptions = {}
+): Promise {
+ return requireAuthLevel(request, AUTH_LEVELS.ADMIN, options);
+}
+
+// Middleware for protecting superadmin routes
+export async function requireSuperAdmin(
+ request: Request,
+ options: RouteProtectionOptions = {}
+): Promise {
+ return requireAuthLevel(request, AUTH_LEVELS.SUPERADMIN, options);
+}
+
+// Middleware for protecting routes with custom auth level
+export async function requireAuth(
+ request: Request,
+ authLevel: AuthLevel,
+ options: RouteProtectionOptions = {}
+): Promise {
+ return requireAuthLevel(request, authLevel, options);
+}
+
+// Middleware for redirecting authenticated users away from auth pages
+export async function redirectIfAuthenticated(
+ request: Request,
+ redirectTo: string = "/dashboard"
+) {
+ const user = await getUser(request);
+ if (user && user.status === USER_STATUS.ACTIVE) {
+ throw redirect(redirectTo);
+ }
+}
+
+// Middleware for optional authentication (user may or may not be logged in)
+export async function getOptionalUser(request: Request): Promise {
+ try {
+ const user = await getUser(request);
+ return user && user.status === USER_STATUS.ACTIVE ? user : null;
+ } catch {
+ return null;
+ }
+}
+
+// Session validation middleware
+export async function validateSession(request: Request): Promise<{
+ isValid: boolean;
+ user: SafeUser | null;
+ error?: string;
+}> {
+ try {
+ const user = await getUser(request);
+ if (!user) {
+ return {
+ isValid: false,
+ user: null,
+ error: "no_user",
+ };
+ }
+
+ if (user.status !== USER_STATUS.ACTIVE) {
+ return {
+ isValid: false,
+ user: null,
+ error: "inactive_user",
+ };
+ }
+
+ return {
+ isValid: true,
+ user,
+ };
+ } catch {
+ return {
+ isValid: false,
+ user: null,
+ error: "session_error",
+ };
+ }
+}
+
+// Route-specific protection functions
+export async function protectUserManagementRoute(request: Request): Promise {
+ const user = await requireAdmin(request);
+
+ // Additional business logic: ensure user can manage users
+ if (user.authLevel > AUTH_LEVELS.ADMIN) {
+ throw redirect("/dashboard?error=insufficient_permissions");
+ }
+
+ return user;
+}
+
+export async function protectFinancialRoute(request: Request): Promise {
+ // Financial routes require at least admin level
+ return requireAdmin(request);
+}
+
+export async function protectCustomerRoute(request: Request): Promise {
+ // Customer routes require authentication but any auth level can access
+ return requireAuthentication(request);
+}
+
+export async function protectVehicleRoute(request: Request): Promise {
+ // Vehicle routes require authentication but any auth level can access
+ return requireAuthentication(request);
+}
+
+export async function protectMaintenanceRoute(request: Request): Promise {
+ // Maintenance routes require authentication but any auth level can access
+ return requireAuthentication(request);
+}
+
+// Utility function to check specific permissions
+export function checkPermission(
+ user: SafeUser,
+ permission: 'view_all_users' | 'create_users' | 'manage_finances' | 'view_reports'
+): boolean {
+ switch (permission) {
+ case 'view_all_users':
+ return user.authLevel === AUTH_LEVELS.SUPERADMIN;
+ case 'create_users':
+ return user.authLevel <= AUTH_LEVELS.ADMIN;
+ case 'manage_finances':
+ return user.authLevel <= AUTH_LEVELS.ADMIN;
+ case 'view_reports':
+ return user.authLevel <= AUTH_LEVELS.ADMIN;
+ default:
+ return false;
+ }
+}
+
+// Error handling for unauthorized access
+export function createUnauthorizedResponse(message?: string) {
+ const errorMessage = message || AUTH_ERRORS.INSUFFICIENT_PERMISSIONS;
+ return new Response(errorMessage, {
+ status: 403,
+ headers: {
+ 'Content-Type': 'text/plain; charset=utf-8'
+ }
+ });
+}
\ No newline at end of file
diff --git a/app/lib/auth.server.ts b/app/lib/auth.server.ts
new file mode 100644
index 0000000..d8f1d65
--- /dev/null
+++ b/app/lib/auth.server.ts
@@ -0,0 +1,113 @@
+import bcrypt from "bcryptjs";
+import { createCookieSessionStorage, redirect } from "@remix-run/node";
+import { prisma } from "./db.server";
+import type { User } from "@prisma/client";
+
+// Session configuration
+const sessionSecret = process.env.SESSION_SECRET;
+if (!sessionSecret) {
+ throw new Error("SESSION_SECRET must be set");
+}
+
+// Create session storage
+const storage = createCookieSessionStorage({
+ cookie: {
+ name: "car_maintenance_session",
+ secure: process.env.NODE_ENV === "production",
+ secrets: [sessionSecret],
+ sameSite: "lax",
+ path: "/",
+ maxAge: 60 * 60 * 24 * 30, // 30 days
+ httpOnly: true,
+ },
+});
+
+// Password hashing utilities
+export async function hashPassword(password: string): Promise {
+ return bcrypt.hash(password, 12);
+}
+
+export async function verifyPassword(
+ password: string,
+ hashedPassword: string
+): Promise {
+ return bcrypt.compare(password, hashedPassword);
+}
+
+// Session management functions
+export async function createUserSession(
+ userId: number,
+ redirectTo: string = "/dashboard"
+) {
+ const session = await storage.getSession();
+ session.set("userId", userId);
+ return redirect(redirectTo, {
+ headers: {
+ "Set-Cookie": await storage.commitSession(session),
+ },
+ });
+}
+
+export async function getUserSession(request: Request) {
+ return storage.getSession(request.headers.get("Cookie"));
+}
+
+export async function getUserId(request: Request): Promise {
+ const session = await getUserSession(request);
+ const userId = session.get("userId");
+ if (!userId || typeof userId !== "number") return null;
+ return userId;
+}
+
+export async function requireUserId(
+ request: Request,
+ redirectTo: string = new URL(request.url).pathname
+) {
+ const userId = await getUserId(request);
+ if (!userId) {
+ const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
+ throw redirect(`/signin?${searchParams}`);
+ }
+ return userId;
+}
+
+export async function getUser(request: Request): Promise | null> {
+ const userId = await getUserId(request);
+ if (!userId) return null;
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ });
+ return user;
+ } catch {
+ throw logout(request);
+ }
+}
+
+export async function requireUser(request: Request) {
+ const user = await getUser(request);
+ if (!user) {
+ throw logout(request);
+ }
+ return user;
+}
+
+export async function logout(request: Request) {
+ const session = await getUserSession(request);
+ return redirect("/signin", {
+ headers: {
+ "Set-Cookie": await storage.destroySession(session),
+ },
+ });
+}
\ No newline at end of file
diff --git a/app/lib/car-dataset-management.server.ts b/app/lib/car-dataset-management.server.ts
new file mode 100644
index 0000000..0f88410
--- /dev/null
+++ b/app/lib/car-dataset-management.server.ts
@@ -0,0 +1,257 @@
+import { prisma } from "~/lib/db.server";
+import type { CreateCarDatasetData, UpdateCarDatasetData } from "~/types/database";
+
+// Get all unique manufacturers
+export async function getManufacturers() {
+ try {
+ const manufacturers = await prisma.carDataset.findMany({
+ where: { isActive: true },
+ select: { manufacturer: true },
+ distinct: ['manufacturer'],
+ orderBy: { manufacturer: 'asc' },
+ });
+
+ return manufacturers.map(item => item.manufacturer);
+ } catch (error) {
+ console.error("Error fetching manufacturers:", error);
+ throw new Error("فشل في جلب الشركات المصنعة");
+ }
+}
+
+// Get all models for a specific manufacturer
+export async function getModelsByManufacturer(manufacturer: string) {
+ try {
+ const models = await prisma.carDataset.findMany({
+ where: {
+ manufacturer,
+ isActive: true
+ },
+ select: { model: true, bodyType: true },
+ orderBy: { model: 'asc' },
+ });
+
+ return models;
+ } catch (error) {
+ console.error("Error fetching models:", error);
+ throw new Error("فشل في جلب الموديلات");
+ }
+}
+
+// Get body type for a specific manufacturer and model
+export async function getBodyType(manufacturer: string, model: string) {
+ try {
+ const carData = await prisma.carDataset.findFirst({
+ where: {
+ manufacturer,
+ model,
+ isActive: true
+ },
+ select: { bodyType: true },
+ });
+
+ return carData?.bodyType || null;
+ } catch (error) {
+ console.error("Error fetching body type:", error);
+ throw new Error("فشل في جلب نوع الهيكل");
+ }
+}
+
+// Get all car dataset entries with pagination
+export async function getCarDataset(
+ searchQuery: string = "",
+ page: number = 1,
+ limit: number = 50,
+ manufacturer?: string
+) {
+ try {
+ const offset = (page - 1) * limit;
+
+ const where = {
+ AND: [
+ manufacturer ? { manufacturer } : {},
+ searchQuery ? {
+ OR: [
+ { manufacturer: { contains: searchQuery, mode: 'insensitive' as const } },
+ { model: { contains: searchQuery, mode: 'insensitive' as const } },
+ { bodyType: { contains: searchQuery, mode: 'insensitive' as const } },
+ ]
+ } : {}
+ ]
+ };
+
+ const [carDataset, total] = await Promise.all([
+ prisma.carDataset.findMany({
+ where,
+ orderBy: [
+ { manufacturer: 'asc' },
+ { model: 'asc' }
+ ],
+ skip: offset,
+ take: limit,
+ }),
+ prisma.carDataset.count({ where })
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ return {
+ carDataset,
+ total,
+ totalPages,
+ };
+ } catch (error) {
+ console.error("Error fetching car dataset:", error);
+ throw new Error("فشل في جلب بيانات السيارات");
+ }
+}
+
+// Create a new car dataset entry
+export async function createCarDataset(data: CreateCarDatasetData) {
+ try {
+ const carDataset = await prisma.carDataset.create({
+ data: {
+ manufacturer: data.manufacturer.trim(),
+ model: data.model.trim(),
+ bodyType: data.bodyType.trim(),
+ isActive: data.isActive ?? true,
+ },
+ });
+
+ return { success: true, carDataset };
+ } catch (error: any) {
+ console.error("Error creating car dataset:", error);
+
+ if (error.code === 'P2002') {
+ return {
+ success: false,
+ error: "هذا الموديل موجود بالفعل لهذه الشركة المصنعة"
+ };
+ }
+
+ return {
+ success: false,
+ error: "فشل في إنشاء بيانات السيارة"
+ };
+ }
+}
+
+// Update a car dataset entry
+export async function updateCarDataset(id: number, data: UpdateCarDatasetData) {
+ try {
+ const carDataset = await prisma.carDataset.update({
+ where: { id },
+ data: {
+ ...(data.manufacturer && { manufacturer: data.manufacturer.trim() }),
+ ...(data.model && { model: data.model.trim() }),
+ ...(data.bodyType && { bodyType: data.bodyType.trim() }),
+ ...(data.isActive !== undefined && { isActive: data.isActive }),
+ },
+ });
+
+ return { success: true, carDataset };
+ } catch (error: any) {
+ console.error("Error updating car dataset:", error);
+
+ if (error.code === 'P2002') {
+ return {
+ success: false,
+ error: "هذا الموديل موجود بالفعل لهذه الشركة المصنعة"
+ };
+ }
+
+ if (error.code === 'P2025') {
+ return {
+ success: false,
+ error: "بيانات السيارة غير موجودة"
+ };
+ }
+
+ return {
+ success: false,
+ error: "فشل في تحديث بيانات السيارة"
+ };
+ }
+}
+
+// Delete a car dataset entry
+export async function deleteCarDataset(id: number) {
+ try {
+ await prisma.carDataset.delete({
+ where: { id },
+ });
+
+ return { success: true };
+ } catch (error: any) {
+ console.error("Error deleting car dataset:", error);
+
+ if (error.code === 'P2025') {
+ return {
+ success: false,
+ error: "بيانات السيارة غير موجودة"
+ };
+ }
+
+ return {
+ success: false,
+ error: "فشل في حذف بيانات السيارة"
+ };
+ }
+}
+
+// Get car dataset entry by ID
+export async function getCarDatasetById(id: number) {
+ try {
+ const carDataset = await prisma.carDataset.findUnique({
+ where: { id },
+ });
+
+ return carDataset;
+ } catch (error) {
+ console.error("Error fetching car dataset by ID:", error);
+ throw new Error("فشل في جلب بيانات السيارة");
+ }
+}
+
+// Bulk import car dataset
+export async function bulkImportCarDataset(data: CreateCarDatasetData[]) {
+ try {
+ const results = await prisma.$transaction(async (tx) => {
+ const created = [];
+ const errors = [];
+
+ for (const item of data) {
+ try {
+ const carDataset = await tx.carDataset.create({
+ data: {
+ manufacturer: item.manufacturer.trim(),
+ model: item.model.trim(),
+ bodyType: item.bodyType.trim(),
+ isActive: item.isActive ?? true,
+ },
+ });
+ created.push(carDataset);
+ } catch (error: any) {
+ if (error.code === 'P2002') {
+ errors.push(`${item.manufacturer} ${item.model} - موجود بالفعل`);
+ } else {
+ errors.push(`${item.manufacturer} ${item.model} - خطأ غير معروف`);
+ }
+ }
+ }
+
+ return { created, errors };
+ });
+
+ return {
+ success: true,
+ created: results.created.length,
+ errors: results.errors
+ };
+ } catch (error) {
+ console.error("Error bulk importing car dataset:", error);
+ return {
+ success: false,
+ error: "فشل في استيراد بيانات السيارات"
+ };
+ }
+}
\ No newline at end of file
diff --git a/app/lib/constants.ts b/app/lib/constants.ts
new file mode 100644
index 0000000..9b313bd
--- /dev/null
+++ b/app/lib/constants.ts
@@ -0,0 +1,173 @@
+// Authentication levels
+export const AUTH_LEVELS = {
+ SUPERADMIN: 1,
+ ADMIN: 2,
+ USER: 3,
+} as const;
+
+export const AUTH_LEVEL_NAMES = {
+ [AUTH_LEVELS.SUPERADMIN]: 'مدير عام',
+ [AUTH_LEVELS.ADMIN]: 'مدير',
+ [AUTH_LEVELS.USER]: 'مستخدم',
+} as const;
+
+// User status options
+export const USER_STATUS = {
+ ACTIVE: 'active',
+ INACTIVE: 'inactive',
+} as const;
+
+export const USER_STATUS_NAMES = {
+ [USER_STATUS.ACTIVE]: 'نشط',
+ [USER_STATUS.INACTIVE]: 'غير نشط',
+} as const;
+
+// Vehicle transmission options
+export const TRANSMISSION_TYPES = [
+ { value: 'Automatic', label: 'أوتوماتيك' },
+ { value: 'Manual', label: 'يدوي' },
+] as const;
+
+// Vehicle fuel types
+export const FUEL_TYPES = [
+ { value: 'Gasoline', label: 'بنزين' },
+ { value: 'Diesel', label: 'ديزل' },
+ { value: 'Hybrid', label: 'هجين' },
+ { value: 'Mild Hybrid', label: 'هجين خفيف' },
+ { value: 'Electric', label: 'كهربائي' },
+] as const;
+
+// Vehicle use types
+export const USE_TYPES = [
+ { value: 'personal', label: 'شخصي' },
+ { value: 'taxi', label: 'تاكسي' },
+ { value: 'apps', label: 'تطبيقات' },
+ { value: 'loading', label: 'نقل' },
+ { value: 'travel', label: 'سفر' },
+] as const;
+
+// Vehicle body types (common in Saudi Arabia)
+export const BODY_TYPES = [
+ { value: 'سيدان', label: 'سيدان' },
+ { value: 'هاتشباك', label: 'هاتشباك' },
+ { value: 'SUV', label: 'SUV' },
+ { value: 'كروس أوفر', label: 'كروس أوفر' },
+ { value: 'بيك أب', label: 'بيك أب' },
+ { value: 'كوبيه', label: 'كوبيه' },
+ { value: 'كونفرتيبل', label: 'كونفرتيبل' },
+ { value: 'فان', label: 'فان' },
+ { value: 'شاحنة', label: 'شاحنة' },
+] as const;
+
+// Popular car manufacturers in Saudi Arabia
+export const MANUFACTURERS = [
+ { value: 'تويوتا', label: 'تويوتا' },
+ { value: 'هيونداي', label: 'هيونداي' },
+ { value: 'نيسان', label: 'نيسان' },
+ { value: 'كيا', label: 'كيا' },
+ { value: 'هوندا', label: 'هوندا' },
+ { value: 'فورد', label: 'فورد' },
+ { value: 'شيفروليه', label: 'شيفروليه' },
+ { value: 'مازda', label: 'مازda' },
+ { value: 'ميتسوبيشي', label: 'ميتسوبيشي' },
+ { value: 'سوزوكي', label: 'سوزوكي' },
+ { value: 'لكزس', label: 'لكزس' },
+ { value: 'إنفينيتي', label: 'إنفينيتي' },
+ { value: 'جينيسيس', label: 'جينيسيس' },
+ { value: 'BMW', label: 'BMW' },
+ { value: 'مرسيدس بنز', label: 'مرسيدس بنز' },
+ { value: 'أودي', label: 'أودي' },
+ { value: 'فولكس واجن', label: 'فولكس واجن' },
+ { value: 'جيب', label: 'جيب' },
+ { value: 'لاند روفر', label: 'لاند روفر' },
+ { value: 'كاديلاك', label: 'كاديلاك' },
+ { value: 'لينكولن', label: 'لينكولن' },
+ { value: 'جاكوار', label: 'جاكوار' },
+ { value: 'بورش', label: 'بورش' },
+ { value: 'فيراري', label: 'فيراري' },
+ { value: 'لامبورغيني', label: 'لامبورغيني' },
+ { value: 'بنتلي', label: 'بنتلي' },
+ { value: 'رولز رويس', label: 'رولز رويس' },
+ { value: 'أخرى', label: 'أخرى' },
+] as const;
+
+// Payment status options
+export const PAYMENT_STATUS = {
+ PENDING: 'pending',
+ PAID: 'paid',
+ PARTIAL: 'partial',
+ CANCELLED: 'cancelled',
+} as const;
+
+export const PAYMENT_STATUS_NAMES = {
+ [PAYMENT_STATUS.PENDING]: 'معلق',
+ [PAYMENT_STATUS.PAID]: 'مدفوع',
+ [PAYMENT_STATUS.PARTIAL]: 'مدفوع جزئياً',
+ [PAYMENT_STATUS.CANCELLED]: 'ملغي',
+} as const;
+
+// Maintenance visit delay options (in months)
+export const VISIT_DELAY_OPTIONS = [
+ { value: 1, label: 'شهر واحد' },
+ { value: 2, label: 'شهرين' },
+ { value: 3, label: 'ثلاثة أشهر' },
+ { value: 4, label: 'أربعة أشهر' },
+] as const;
+
+// Common maintenance types
+export const MAINTENANCE_TYPES = [
+ { value: 'تغيير زيت', label: 'تغيير زيت' },
+ { value: 'فحص دوري', label: 'فحص دوري' },
+ { value: 'تغيير فلاتر', label: 'تغيير فلاتر' },
+ { value: 'فحص فرامل', label: 'فحص فرامل' },
+ { value: 'تغيير إطارات', label: 'تغيير إطارات' },
+ { value: 'فحص بطارية', label: 'فحص بطارية' },
+ { value: 'تنظيف مكيف', label: 'تنظيف مكيف' },
+ { value: 'فحص محرك', label: 'فحص محرك' },
+ { value: 'تغيير شمعات', label: 'تغيير شمعات' },
+ { value: 'فحص ناقل حركة', label: 'فحص ناقل حركة' },
+ { value: 'إصلاح عام', label: 'إصلاح عام' },
+ { value: 'أخرى', label: 'أخرى' },
+] as const;
+
+// Expense categories
+export const EXPENSE_CATEGORIES = [
+ { value: 'قطع غيار', label: 'قطع غيار' },
+ { value: 'أدوات', label: 'أدوات' },
+ { value: 'إيجار', label: 'إيجار' },
+ { value: 'كهرباء', label: 'كهرباء' },
+ { value: 'ماء', label: 'ماء' },
+ { value: 'رواتب', label: 'رواتب' },
+ { value: 'تأمين', label: 'تأمين' },
+ { value: 'وقود', label: 'وقود' },
+ { value: 'صيانة معدات', label: 'صيانة معدات' },
+ { value: 'تسويق', label: 'تسويق' },
+ { value: 'مصاريف إدارية', label: 'مصاريف إدارية' },
+ { value: 'أخرى', label: 'أخرى' },
+] as const;
+
+// Date format options
+export const DATE_FORMATS = {
+ SHORT: 'dd/MM/yyyy',
+ LONG: 'dd MMMM yyyy',
+ WITH_TIME: 'dd/MM/yyyy HH:mm',
+} as const;
+
+// Pagination defaults
+export const PAGINATION = {
+ DEFAULT_PAGE_SIZE: 10,
+ PAGE_SIZE_OPTIONS: [10, 25, 50, 100],
+} as const;
+
+// Validation constants
+export const VALIDATION = {
+ MIN_PASSWORD_LENGTH: 6,
+ MAX_NAME_LENGTH: 100,
+ MAX_DESCRIPTION_LENGTH: 500,
+ MIN_YEAR: 1990,
+ MAX_YEAR: new Date().getFullYear() + 1,
+ MAX_CYLINDERS: 12,
+ MAX_ENGINE_DISPLACEMENT: 10.0,
+ MIN_COST: 0,
+ MAX_COST: 999999.99,
+} as const;
\ No newline at end of file
diff --git a/app/lib/customer-management.server.ts b/app/lib/customer-management.server.ts
new file mode 100644
index 0000000..72464ec
--- /dev/null
+++ b/app/lib/customer-management.server.ts
@@ -0,0 +1,326 @@
+import { prisma } from "./db.server";
+import type { Customer } from "@prisma/client";
+import type { CreateCustomerData, UpdateCustomerData, CustomerWithVehicles } from "~/types/database";
+
+// Get all customers with search and pagination
+export async function getCustomers(
+ searchQuery?: string,
+ page: number = 1,
+ limit: number = 10
+): Promise<{
+ customers: CustomerWithVehicles[];
+ total: number;
+ totalPages: number;
+}> {
+ const offset = (page - 1) * limit;
+
+ // Build where clause for search
+ const whereClause: any = {};
+
+ if (searchQuery) {
+ const searchLower = searchQuery.toLowerCase();
+ whereClause.OR = [
+ { name: { contains: searchLower } },
+ { phone: { contains: searchLower } },
+ { email: { contains: searchLower } },
+ { address: { contains: searchLower } },
+ ];
+ }
+
+ const [customers, total] = await Promise.all([
+ prisma.customer.findMany({
+ where: whereClause,
+ include: {
+ vehicles: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ lastVisitDate: true,
+ suggestedNextVisitDate: true,
+ },
+ },
+ maintenanceVisits: {
+ select: {
+ id: true,
+ visitDate: true,
+ cost: true,
+ maintenanceJobs: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ take: 5, // Only get last 5 visits for performance
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ skip: offset,
+ take: limit,
+ }),
+ prisma.customer.count({ where: whereClause }),
+ ]);
+
+ return {
+ customers,
+ total,
+ totalPages: Math.ceil(total / limit),
+ };
+}
+
+// Get customer by ID with full relationships
+export async function getCustomerById(id: number): Promise {
+ return await prisma.customer.findUnique({
+ where: { id },
+ include: {
+ vehicles: {
+ orderBy: { createdDate: 'desc' },
+ },
+ maintenanceVisits: {
+ include: {
+ vehicle: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ },
+ },
+ },
+ orderBy: { visitDate: 'desc' },
+ take: 3, // Only get latest 3 visits for the enhanced view
+ },
+ },
+ });
+}
+
+// Create new customer
+export async function createCustomer(
+ customerData: CreateCustomerData
+): Promise<{ success: boolean; customer?: Customer; error?: string }> {
+ try {
+ // Check if customer with same phone or email already exists (if provided)
+ if (customerData.phone || customerData.email) {
+ const existingCustomer = await prisma.customer.findFirst({
+ where: {
+ OR: [
+ customerData.phone ? { phone: customerData.phone } : {},
+ customerData.email ? { email: customerData.email } : {},
+ ].filter(condition => Object.keys(condition).length > 0),
+ },
+ });
+
+ if (existingCustomer) {
+ if (existingCustomer.phone === customerData.phone) {
+ return { success: false, error: "رقم الهاتف موجود بالفعل" };
+ }
+ if (existingCustomer.email === customerData.email) {
+ return { success: false, error: "البريد الإلكتروني موجود بالفعل" };
+ }
+ }
+ }
+
+ // Create customer
+ const customer = await prisma.customer.create({
+ data: {
+ name: customerData.name.trim(),
+ phone: customerData.phone?.trim() || null,
+ email: customerData.email?.trim() || null,
+ address: customerData.address?.trim() || null,
+ },
+ });
+
+ return { success: true, customer };
+ } catch (error) {
+ console.error("Error creating customer:", error);
+ return { success: false, error: "حدث خطأ أثناء إنشاء العميل" };
+ }
+}
+
+// Update customer
+export async function updateCustomer(
+ id: number,
+ customerData: UpdateCustomerData
+): Promise<{ success: boolean; customer?: Customer; error?: string }> {
+ try {
+ // Check if customer exists
+ const existingCustomer = await prisma.customer.findUnique({
+ where: { id },
+ });
+
+ if (!existingCustomer) {
+ return { success: false, error: "العميل غير موجود" };
+ }
+
+ // Check for phone/email conflicts with other customers
+ if (customerData.phone || customerData.email) {
+ const conflictCustomer = await prisma.customer.findFirst({
+ where: {
+ AND: [
+ { id: { not: id } },
+ {
+ OR: [
+ customerData.phone ? { phone: customerData.phone } : {},
+ customerData.email ? { email: customerData.email } : {},
+ ].filter(condition => Object.keys(condition).length > 0),
+ },
+ ],
+ },
+ });
+
+ if (conflictCustomer) {
+ if (conflictCustomer.phone === customerData.phone) {
+ return { success: false, error: "رقم الهاتف موجود بالفعل" };
+ }
+ if (conflictCustomer.email === customerData.email) {
+ return { success: false, error: "البريد الإلكتروني موجود بالفعل" };
+ }
+ }
+ }
+
+ // Prepare update data
+ const updateData: any = {};
+ if (customerData.name !== undefined) updateData.name = customerData.name.trim();
+ if (customerData.phone !== undefined) updateData.phone = customerData.phone?.trim() || null;
+ if (customerData.email !== undefined) updateData.email = customerData.email?.trim() || null;
+ if (customerData.address !== undefined) updateData.address = customerData.address?.trim() || null;
+
+ // Update customer
+ const customer = await prisma.customer.update({
+ where: { id },
+ data: updateData,
+ });
+
+ return { success: true, customer };
+ } catch (error) {
+ console.error("Error updating customer:", error);
+ return { success: false, error: "حدث خطأ أثناء تحديث العميل" };
+ }
+}
+
+// Delete customer with relationship handling
+export async function deleteCustomer(
+ id: number
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ // Check if customer exists
+ const existingCustomer = await prisma.customer.findUnique({
+ where: { id },
+ include: {
+ vehicles: true,
+ maintenanceVisits: true,
+ },
+ });
+
+ if (!existingCustomer) {
+ return { success: false, error: "العميل غير موجود" };
+ }
+
+ // Check if customer has vehicles or maintenance visits
+ if (existingCustomer.vehicles.length > 0) {
+ return {
+ success: false,
+ error: `لا يمكن حذف العميل لأنه يملك ${existingCustomer.vehicles.length} مركبة. يرجى حذف المركبات أولاً`
+ };
+ }
+
+ if (existingCustomer.maintenanceVisits.length > 0) {
+ return {
+ success: false,
+ error: `لا يمكن حذف العميل لأنه لديه ${existingCustomer.maintenanceVisits.length} زيارة صيانة. يرجى حذف الزيارات أولاً`
+ };
+ }
+
+ // Delete customer
+ await prisma.customer.delete({
+ where: { id },
+ });
+
+ return { success: true };
+ } catch (error) {
+ console.error("Error deleting customer:", error);
+ return { success: false, error: "حدث خطأ أثناء حذف العميل" };
+ }
+}
+
+// Get customers for dropdown/select options
+export async function getCustomersForSelect(): Promise<{ id: number; name: string; phone?: string | null }[]> {
+ return await prisma.customer.findMany({
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ },
+ orderBy: { name: 'asc' },
+ });
+}
+
+// Get customer statistics
+export async function getCustomerStats(customerId: number): Promise<{
+ totalVehicles: number;
+ totalVisits: number;
+ totalSpent: number;
+ lastVisitDate?: Date;
+} | null> {
+ const customer = await prisma.customer.findUnique({
+ where: { id: customerId },
+ include: {
+ vehicles: {
+ select: { id: true },
+ },
+ maintenanceVisits: {
+ select: {
+ cost: true,
+ visitDate: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ },
+ },
+ });
+
+ if (!customer) return null;
+
+ const totalSpent = customer.maintenanceVisits.reduce((sum, visit) => sum + visit.cost, 0);
+ const lastVisitDate = customer.maintenanceVisits.length > 0
+ ? customer.maintenanceVisits[0].visitDate
+ : undefined;
+
+ return {
+ totalVehicles: customer.vehicles.length,
+ totalVisits: customer.maintenanceVisits.length,
+ totalSpent,
+ lastVisitDate,
+ };
+}
+
+// Search customers by name or phone (for autocomplete)
+export async function searchCustomers(query: string, limit: number = 10): Promise<{
+ id: number;
+ name: string;
+ phone?: string | null;
+ email?: string | null;
+}[]> {
+ if (!query || query.trim().length < 2) {
+ return [];
+ }
+
+ const searchLower = query.toLowerCase();
+
+ return await prisma.customer.findMany({
+ where: {
+ OR: [
+ { name: { contains: searchLower } },
+ { phone: { contains: searchLower } },
+ { email: { contains: searchLower } },
+ ],
+ },
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ orderBy: { name: 'asc' },
+ take: limit,
+ });
+}
\ No newline at end of file
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts
new file mode 100644
index 0000000..7cc9a3b
--- /dev/null
+++ b/app/lib/db.server.ts
@@ -0,0 +1,405 @@
+import { PrismaClient } from '@prisma/client';
+
+let prisma: PrismaClient;
+
+declare global {
+ var __db__: PrismaClient;
+}
+
+// This is needed because in development we don't want to restart
+// the server with every change, but we want to make sure we don't
+// create a new connection to the DB with every change either.
+// In production, we'll have a single connection to the DB.
+if (process.env.NODE_ENV === 'production') {
+ prisma = new PrismaClient();
+} else {
+ if (!global.__db__) {
+ global.__db__ = new PrismaClient();
+ }
+ prisma = global.__db__;
+ prisma.$connect();
+}
+
+export { prisma };
+
+// Database utility functions
+export async function createUser(data: {
+ name: string;
+ username: string;
+ email: string;
+ password: string;
+ authLevel: number;
+ status?: string;
+}) {
+ return prisma.user.create({
+ data: {
+ ...data,
+ status: data.status || 'active',
+ },
+ });
+}
+
+export async function getUserByUsername(username: string) {
+ return prisma.user.findUnique({
+ where: { username },
+ });
+}
+
+export async function getUserByEmail(email: string) {
+ return prisma.user.findUnique({
+ where: { email },
+ });
+}
+
+export async function getUserById(id: number) {
+ return prisma.user.findUnique({
+ where: { id },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ });
+}
+
+export async function updateUser(id: number, data: {
+ name?: string;
+ username?: string;
+ email?: string;
+ password?: string;
+ authLevel?: number;
+ status?: string;
+}) {
+ return prisma.user.update({
+ where: { id },
+ data,
+ });
+}
+
+export async function deleteUser(id: number) {
+ return prisma.user.delete({
+ where: { id },
+ });
+}
+
+export async function getUsers(authLevel?: number) {
+ const where = authLevel ? { authLevel: { gte: authLevel } } : {};
+
+ return prisma.user.findMany({
+ where,
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ orderBy: { createdDate: 'desc' },
+ });
+}
+
+// Customer operations
+export async function createCustomer(data: {
+ name: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+}) {
+ return prisma.customer.create({ data });
+}
+
+export async function getCustomers() {
+ return prisma.customer.findMany({
+ include: {
+ vehicles: true,
+ _count: {
+ select: {
+ vehicles: true,
+ maintenanceVisits: true,
+ },
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ });
+}
+
+export async function getCustomerById(id: number) {
+ return prisma.customer.findUnique({
+ where: { id },
+ include: {
+ vehicles: true,
+ maintenanceVisits: {
+ include: {
+ vehicle: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ },
+ },
+ });
+}
+
+export async function updateCustomer(id: number, data: {
+ name?: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+}) {
+ return prisma.customer.update({
+ where: { id },
+ data,
+ });
+}
+
+export async function deleteCustomer(id: number) {
+ return prisma.customer.delete({
+ where: { id },
+ });
+}
+
+// Vehicle operations
+export async function createVehicle(data: {
+ plateNumber: string;
+ bodyType: string;
+ manufacturer: string;
+ model: string;
+ trim?: string;
+ year: number;
+ transmission: string;
+ fuel: string;
+ cylinders?: number;
+ engineDisplacement?: number;
+ useType: string;
+ ownerId: number;
+}) {
+ return prisma.vehicle.create({ data });
+}
+
+export async function getVehicles() {
+ return prisma.vehicle.findMany({
+ include: {
+ owner: true,
+ _count: {
+ select: {
+ maintenanceVisits: true,
+ },
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ });
+}
+
+export async function getVehicleById(id: number) {
+ return prisma.vehicle.findUnique({
+ where: { id },
+ include: {
+ owner: true,
+ maintenanceVisits: {
+ include: {
+ income: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ },
+ },
+ });
+}
+
+export async function updateVehicle(id: number, data: {
+ plateNumber?: string;
+ bodyType?: string;
+ manufacturer?: string;
+ model?: string;
+ trim?: string;
+ year?: number;
+ transmission?: string;
+ fuel?: string;
+ cylinders?: number;
+ engineDisplacement?: number;
+ useType?: string;
+ ownerId?: number;
+ lastVisitDate?: Date;
+ suggestedNextVisitDate?: Date;
+}) {
+ return prisma.vehicle.update({
+ where: { id },
+ data,
+ });
+}
+
+export async function deleteVehicle(id: number) {
+ return prisma.vehicle.delete({
+ where: { id },
+ });
+}
+
+// Maintenance visit operations
+export async function createMaintenanceVisit(data: {
+ vehicleId: number;
+ customerId: number;
+ maintenanceType: string;
+ description: string;
+ cost: number;
+ paymentStatus?: string;
+ kilometers: number;
+ visitDate?: Date;
+ nextVisitDelay: number;
+}) {
+ return prisma.$transaction(async (tx) => {
+ // Create the maintenance visit
+ const visit = await tx.maintenanceVisit.create({
+ data: {
+ ...data,
+ paymentStatus: data.paymentStatus || 'pending',
+ visitDate: data.visitDate || new Date(),
+ },
+ });
+
+ // Update vehicle's last visit date and calculate next visit date
+ const nextVisitDate = new Date();
+ nextVisitDate.setMonth(nextVisitDate.getMonth() + data.nextVisitDelay);
+
+ await tx.vehicle.update({
+ where: { id: data.vehicleId },
+ data: {
+ lastVisitDate: visit.visitDate,
+ suggestedNextVisitDate: nextVisitDate,
+ },
+ });
+
+ // Create corresponding income record
+ await tx.income.create({
+ data: {
+ maintenanceVisitId: visit.id,
+ amount: data.cost,
+ incomeDate: visit.visitDate,
+ },
+ });
+
+ return visit;
+ });
+}
+
+export async function getMaintenanceVisits() {
+ return prisma.maintenanceVisit.findMany({
+ include: {
+ vehicle: {
+ include: {
+ owner: true,
+ },
+ },
+ customer: true,
+ income: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ });
+}
+
+export async function getMaintenanceVisitById(id: number) {
+ return prisma.maintenanceVisit.findUnique({
+ where: { id },
+ include: {
+ vehicle: {
+ include: {
+ owner: true,
+ },
+ },
+ customer: true,
+ income: true,
+ },
+ });
+}
+
+export async function updateMaintenanceVisit(id: number, data: {
+ vehicleId?: number;
+ customerId?: number;
+ maintenanceType?: string;
+ description?: string;
+ cost?: number;
+ paymentStatus?: string;
+ kilometers?: number;
+ visitDate?: Date;
+ nextVisitDelay?: number;
+}) {
+ return prisma.maintenanceVisit.update({
+ where: { id },
+ data,
+ });
+}
+
+export async function deleteMaintenanceVisit(id: number) {
+ return prisma.maintenanceVisit.delete({
+ where: { id },
+ });
+}
+
+// Financial operations
+export async function createExpense(data: {
+ description: string;
+ category: string;
+ amount: number;
+ expenseDate?: Date;
+}) {
+ return prisma.expense.create({
+ data: {
+ ...data,
+ expenseDate: data.expenseDate || new Date(),
+ },
+ });
+}
+
+export async function getExpenses() {
+ return prisma.expense.findMany({
+ orderBy: { expenseDate: 'desc' },
+ });
+}
+
+export async function getIncome() {
+ return prisma.income.findMany({
+ include: {
+ maintenanceVisit: {
+ include: {
+ vehicle: true,
+ customer: true,
+ },
+ },
+ },
+ orderBy: { incomeDate: 'desc' },
+ });
+}
+
+export async function getFinancialSummary(dateFrom?: Date, dateTo?: Date) {
+ const where = dateFrom && dateTo ? {
+ AND: [
+ { createdDate: { gte: dateFrom } },
+ { createdDate: { lte: dateTo } },
+ ],
+ } : {};
+
+ const [totalIncome, totalExpenses] = await Promise.all([
+ prisma.income.aggregate({
+ where: dateFrom && dateTo ? {
+ incomeDate: { gte: dateFrom, lte: dateTo },
+ } : {},
+ _sum: { amount: true },
+ }),
+ prisma.expense.aggregate({
+ where: dateFrom && dateTo ? {
+ expenseDate: { gte: dateFrom, lte: dateTo },
+ } : {},
+ _sum: { amount: true },
+ }),
+ ]);
+
+ return {
+ totalIncome: totalIncome._sum.amount || 0,
+ totalExpenses: totalExpenses._sum.amount || 0,
+ netProfit: (totalIncome._sum.amount || 0) - (totalExpenses._sum.amount || 0),
+ };
+}
\ No newline at end of file
diff --git a/app/lib/expense-management.server.ts b/app/lib/expense-management.server.ts
new file mode 100644
index 0000000..519eba6
--- /dev/null
+++ b/app/lib/expense-management.server.ts
@@ -0,0 +1,176 @@
+import { prisma } from "./db.server";
+import type { Expense } from "@prisma/client";
+import type { CreateExpenseData, UpdateExpenseData, FinancialSearchParams } from "~/types/database";
+
+// Get all expenses with search and pagination
+export async function getExpenses(
+ searchQuery?: string,
+ page: number = 1,
+ limit: number = 10,
+ category?: string,
+ dateFrom?: Date,
+ dateTo?: Date
+): Promise<{
+ expenses: Expense[];
+ total: number;
+ totalPages: number;
+}> {
+ const offset = (page - 1) * limit;
+
+ // Build where clause for search and filters
+ const whereClause: any = {};
+
+ if (category) {
+ whereClause.category = category;
+ }
+
+ if (dateFrom || dateTo) {
+ whereClause.expenseDate = {};
+ if (dateFrom) {
+ whereClause.expenseDate.gte = dateFrom;
+ }
+ if (dateTo) {
+ whereClause.expenseDate.lte = dateTo;
+ }
+ }
+
+ if (searchQuery) {
+ const searchLower = searchQuery.toLowerCase();
+ whereClause.OR = [
+ { description: { contains: searchLower } },
+ { category: { contains: searchLower } },
+ ];
+ }
+
+ const [expenses, total] = await Promise.all([
+ prisma.expense.findMany({
+ where: whereClause,
+ orderBy: { expenseDate: 'desc' },
+ skip: offset,
+ take: limit,
+ }),
+ prisma.expense.count({ where: whereClause }),
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ return {
+ expenses,
+ total,
+ totalPages,
+ };
+}
+
+// Get a single expense by ID
+export async function getExpenseById(id: number): Promise {
+ return prisma.expense.findUnique({
+ where: { id },
+ });
+}
+
+// Create a new expense
+export async function createExpense(data: CreateExpenseData): Promise {
+ return prisma.expense.create({
+ data: {
+ description: data.description,
+ category: data.category,
+ amount: data.amount,
+ expenseDate: data.expenseDate || new Date(),
+ },
+ });
+}
+
+// Update an existing expense
+export async function updateExpense(id: number, data: UpdateExpenseData): Promise {
+ return prisma.expense.update({
+ where: { id },
+ data: {
+ description: data.description,
+ category: data.category,
+ amount: data.amount,
+ expenseDate: data.expenseDate,
+ },
+ });
+}
+
+// Delete an expense
+export async function deleteExpense(id: number): Promise {
+ await prisma.expense.delete({
+ where: { id },
+ });
+}
+
+// Get expense categories for dropdown
+export async function getExpenseCategories(): Promise {
+ const categories = await prisma.expense.findMany({
+ select: { category: true },
+ distinct: ['category'],
+ orderBy: { category: 'asc' },
+ });
+
+ return categories.map(c => c.category);
+}
+
+// Get expenses by category for reporting
+export async function getExpensesByCategory(
+ dateFrom?: Date,
+ dateTo?: Date
+): Promise<{ category: string; total: number; count: number }[]> {
+ const whereClause: any = {};
+
+ if (dateFrom || dateTo) {
+ whereClause.expenseDate = {};
+ if (dateFrom) {
+ whereClause.expenseDate.gte = dateFrom;
+ }
+ if (dateTo) {
+ whereClause.expenseDate.lte = dateTo;
+ }
+ }
+
+ const result = await prisma.expense.groupBy({
+ by: ['category'],
+ where: whereClause,
+ _sum: {
+ amount: true,
+ },
+ _count: {
+ id: true,
+ },
+ orderBy: {
+ _sum: {
+ amount: 'desc',
+ },
+ },
+ });
+
+ return result.map(item => ({
+ category: item.category,
+ total: item._sum.amount || 0,
+ count: item._count.id,
+ }));
+}
+
+// Get total expenses for a date range
+export async function getTotalExpenses(dateFrom?: Date, dateTo?: Date): Promise {
+ const whereClause: any = {};
+
+ if (dateFrom || dateTo) {
+ whereClause.expenseDate = {};
+ if (dateFrom) {
+ whereClause.expenseDate.gte = dateFrom;
+ }
+ if (dateTo) {
+ whereClause.expenseDate.lte = dateTo;
+ }
+ }
+
+ const result = await prisma.expense.aggregate({
+ where: whereClause,
+ _sum: {
+ amount: true,
+ },
+ });
+
+ return result._sum.amount || 0;
+}
\ No newline at end of file
diff --git a/app/lib/financial-reporting.server.ts b/app/lib/financial-reporting.server.ts
new file mode 100644
index 0000000..8a7f713
--- /dev/null
+++ b/app/lib/financial-reporting.server.ts
@@ -0,0 +1,361 @@
+import { prisma } from "./db.server";
+import { getTotalExpenses, getExpensesByCategory } from "./expense-management.server";
+
+// Financial summary interface
+export interface FinancialSummary {
+ totalIncome: number;
+ totalExpenses: number;
+ netProfit: number;
+ incomeCount: number;
+ expenseCount: number;
+ profitMargin: number;
+}
+
+// Monthly financial data interface
+export interface MonthlyFinancialData {
+ month: string;
+ year: number;
+ income: number;
+ expenses: number;
+ profit: number;
+}
+
+// Category breakdown interface
+export interface CategoryBreakdown {
+ category: string;
+ amount: number;
+ count: number;
+ percentage: number;
+}
+
+// Get financial summary for a date range
+export async function getFinancialSummary(
+ dateFrom?: Date,
+ dateTo?: Date
+): Promise {
+ const whereClause: any = {};
+
+ if (dateFrom || dateTo) {
+ whereClause.incomeDate = {};
+ if (dateFrom) {
+ whereClause.incomeDate.gte = dateFrom;
+ }
+ if (dateTo) {
+ whereClause.incomeDate.lte = dateTo;
+ }
+ }
+
+ // Get income data
+ const incomeResult = await prisma.income.aggregate({
+ where: whereClause,
+ _sum: {
+ amount: true,
+ },
+ _count: {
+ id: true,
+ },
+ });
+
+ const totalIncome = incomeResult._sum.amount || 0;
+ const incomeCount = incomeResult._count.id;
+
+ // Get expense data
+ const totalExpenses = await getTotalExpenses(dateFrom, dateTo);
+ const expenseCount = await prisma.expense.count({
+ where: dateFrom || dateTo ? {
+ expenseDate: {
+ ...(dateFrom && { gte: dateFrom }),
+ ...(dateTo && { lte: dateTo }),
+ },
+ } : undefined,
+ });
+
+ // Calculate derived metrics
+ const netProfit = totalIncome - totalExpenses;
+ const profitMargin = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0;
+
+ return {
+ totalIncome,
+ totalExpenses,
+ netProfit,
+ incomeCount,
+ expenseCount,
+ profitMargin,
+ };
+}
+
+// Get monthly financial data for the last 12 months
+export async function getMonthlyFinancialData(): Promise {
+ const months: MonthlyFinancialData[] = [];
+ const currentDate = new Date();
+
+ for (let i = 11; i >= 0; i--) {
+ const date = new Date(currentDate.getFullYear(), currentDate.getMonth() - i, 1);
+ const nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1);
+
+ const monthNumber = date.getMonth() + 1; // 1-12
+ const year = date.getFullYear();
+
+ // Get income for this month
+ const incomeResult = await prisma.income.aggregate({
+ where: {
+ incomeDate: {
+ gte: date,
+ lt: nextMonth,
+ },
+ },
+ _sum: {
+ amount: true,
+ },
+ });
+
+ // Get expenses for this month
+ const expenseResult = await prisma.expense.aggregate({
+ where: {
+ expenseDate: {
+ gte: date,
+ lt: nextMonth,
+ },
+ },
+ _sum: {
+ amount: true,
+ },
+ });
+
+ const income = incomeResult._sum.amount || 0;
+ const expenses = expenseResult._sum.amount || 0;
+ const profit = income - expenses;
+
+ months.push({
+ month: monthNumber.toString(),
+ year,
+ income,
+ expenses,
+ profit,
+ });
+ }
+
+ return months;
+}
+
+// Get income breakdown by maintenance type
+export async function getIncomeByMaintenanceType(
+ dateFrom?: Date,
+ dateTo?: Date
+): Promise {
+ const whereClause: any = {};
+
+ if (dateFrom || dateTo) {
+ whereClause.incomeDate = {};
+ if (dateFrom) {
+ whereClause.incomeDate.gte = dateFrom;
+ }
+ if (dateTo) {
+ whereClause.incomeDate.lte = dateTo;
+ }
+ }
+
+ // Get income grouped by maintenance type
+ const result = await prisma.income.findMany({
+ where: whereClause,
+ include: {
+ maintenanceVisit: {
+ select: {
+ maintenanceJobs: true,
+ },
+ },
+ },
+ });
+
+ // Group by maintenance type
+ const grouped = result.reduce((acc, income) => {
+ try {
+ const jobs = JSON.parse(income.maintenanceVisit.maintenanceJobs);
+
+ // Calculate total cost from individual jobs to determine weights
+ const totalJobCost = jobs.reduce((sum: number, job: any) => sum + (job.cost || 0), 0);
+
+ if (totalJobCost > 0) {
+ // Weighted distribution based on specific costs
+ jobs.forEach((job: any) => {
+ const type = job.job || 'غير محدد';
+ const jobCost = job.cost || 0;
+ // Calculate weight: if total cost is 0 (shouldn't happen due to if check), avoid division by zero
+ const weight = jobCost / totalJobCost;
+ const attributedAmount = income.amount * weight;
+
+ if (!acc[type]) {
+ acc[type] = { amount: 0, count: 0 };
+ }
+ acc[type].amount += attributedAmount;
+ acc[type].count += 1;
+ });
+ } else {
+ // Fallback: Equal distribution (Legacy behavior)
+ const types = jobs.map((job: any) => job.job || 'غير محدد');
+ types.forEach((type: string) => {
+ if (!acc[type]) {
+ acc[type] = { amount: 0, count: 0 };
+ }
+ acc[type].amount += income.amount / types.length;
+ acc[type].count += 1;
+ });
+ }
+ } catch {
+ // Fallback for invalid JSON
+ const type = 'غير محدد';
+ if (!acc[type]) {
+ acc[type] = { amount: 0, count: 0 };
+ }
+ acc[type].amount += income.amount;
+ acc[type].count += 1;
+ }
+ return acc;
+ }, {} as Record);
+
+ // Calculate total for percentage calculation
+ const total = Object.values(grouped).reduce((sum, item) => sum + item.amount, 0);
+
+ // Convert to array with percentages
+ return Object.entries(grouped)
+ .map(([category, data]) => ({
+ category,
+ amount: data.amount,
+ count: data.count,
+ percentage: total > 0 ? (data.amount / total) * 100 : 0,
+ }))
+ .sort((a, b) => b.amount - a.amount);
+}
+
+// Get expense breakdown by category
+export async function getExpenseBreakdown(
+ dateFrom?: Date,
+ dateTo?: Date
+): Promise {
+ const categoryData = await getExpensesByCategory(dateFrom, dateTo);
+ const total = categoryData.reduce((sum, item) => sum + item.total, 0);
+
+ return categoryData.map(item => ({
+ category: item.category,
+ amount: item.total,
+ count: item.count,
+ percentage: total > 0 ? (item.total / total) * 100 : 0,
+ }));
+}
+
+// Get top customers by revenue
+export async function getTopCustomersByRevenue(
+ limit: number = 10,
+ dateFrom?: Date,
+ dateTo?: Date
+): Promise<{
+ customerId: number;
+ customerName: string;
+ totalRevenue: number;
+ visitCount: number;
+}[]> {
+ const whereClause: any = {};
+
+ if (dateFrom || dateTo) {
+ whereClause.incomeDate = {};
+ if (dateFrom) {
+ whereClause.incomeDate.gte = dateFrom;
+ }
+ if (dateTo) {
+ whereClause.incomeDate.lte = dateTo;
+ }
+ }
+
+ const result = await prisma.income.findMany({
+ where: whereClause,
+ include: {
+ maintenanceVisit: {
+ include: {
+ customer: {
+ select: {
+ id: true,
+ name: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ // Group by customer
+ const customerRevenue = result.reduce((acc, income) => {
+ const customer = income.maintenanceVisit.customer;
+ const customerId = customer.id;
+
+ if (!acc[customerId]) {
+ acc[customerId] = {
+ customerId,
+ customerName: customer.name,
+ totalRevenue: 0,
+ visitCount: 0,
+ };
+ }
+
+ acc[customerId].totalRevenue += income.amount;
+ acc[customerId].visitCount += 1;
+
+ return acc;
+ }, {} as Record);
+
+ return Object.values(customerRevenue)
+ .sort((a, b) => b.totalRevenue - a.totalRevenue)
+ .slice(0, limit);
+}
+
+// Get financial trends (comparing current period with previous period)
+export async function getFinancialTrends(
+ dateFrom: Date,
+ dateTo: Date
+): Promise<{
+ currentPeriod: FinancialSummary;
+ previousPeriod: FinancialSummary;
+ trends: {
+ incomeGrowth: number;
+ expenseGrowth: number;
+ profitGrowth: number;
+ };
+}> {
+ // Calculate previous period dates
+ const periodLength = dateTo.getTime() - dateFrom.getTime();
+ const previousDateTo = new Date(dateFrom.getTime() - 1);
+ const previousDateFrom = new Date(previousDateTo.getTime() - periodLength);
+
+ // Get current and previous period summaries
+ const [currentPeriod, previousPeriod] = await Promise.all([
+ getFinancialSummary(dateFrom, dateTo),
+ getFinancialSummary(previousDateFrom, previousDateTo),
+ ]);
+
+ // Calculate growth percentages
+ const incomeGrowth = previousPeriod.totalIncome > 0
+ ? ((currentPeriod.totalIncome - previousPeriod.totalIncome) / previousPeriod.totalIncome) * 100
+ : 0;
+
+ const expenseGrowth = previousPeriod.totalExpenses > 0
+ ? ((currentPeriod.totalExpenses - previousPeriod.totalExpenses) / previousPeriod.totalExpenses) * 100
+ : 0;
+
+ const profitGrowth = previousPeriod.netProfit !== 0
+ ? ((currentPeriod.netProfit - previousPeriod.netProfit) / Math.abs(previousPeriod.netProfit)) * 100
+ : 0;
+
+ return {
+ currentPeriod,
+ previousPeriod,
+ trends: {
+ incomeGrowth,
+ expenseGrowth,
+ profitGrowth,
+ },
+ };
+}
\ No newline at end of file
diff --git a/app/lib/form-validation.ts b/app/lib/form-validation.ts
new file mode 100644
index 0000000..3dd0be1
--- /dev/null
+++ b/app/lib/form-validation.ts
@@ -0,0 +1,274 @@
+import { z } from 'zod';
+import { VALIDATION, AUTH_LEVELS, USER_STATUS, TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, PAYMENT_STATUS } from './constants';
+
+// Zod schemas for server-side validation
+export const userSchema = z.object({
+ name: z.string()
+ .min(1, 'الاسم مطلوب')
+ .max(VALIDATION.MAX_NAME_LENGTH, `الاسم يجب أن يكون أقل من ${VALIDATION.MAX_NAME_LENGTH} حرف`)
+ .trim(),
+ username: z.string()
+ .min(3, 'اسم المستخدم يجب أن يكون على الأقل 3 أحرف')
+ .regex(/^[a-zA-Z0-9_]+$/, 'اسم المستخدم يجب أن يحتوي على أحرف وأرقام فقط')
+ .trim(),
+ email: z.string()
+ .email('البريد الإلكتروني غير صحيح')
+ .trim(),
+ password: z.string()
+ .min(VALIDATION.MIN_PASSWORD_LENGTH, `كلمة المرور يجب أن تكون على الأقل ${VALIDATION.MIN_PASSWORD_LENGTH} أحرف`),
+ authLevel: z.number()
+ .refine(val => Object.values(AUTH_LEVELS).includes(val as any), 'مستوى الصلاحية غير صحيح'),
+ status: z.enum([USER_STATUS.ACTIVE, USER_STATUS.INACTIVE], {
+ errorMap: () => ({ message: 'حالة المستخدم غير صحيحة' })
+ }),
+});
+
+export const customerSchema = z.object({
+ name: z.string()
+ .min(1, 'اسم العميل مطلوب')
+ .max(VALIDATION.MAX_NAME_LENGTH, `الاسم يجب أن يكون أقل من ${VALIDATION.MAX_NAME_LENGTH} حرف`)
+ .trim(),
+ phone: z.string()
+ .regex(/^[\+]?[0-9\s\-\(\)]*$/, 'رقم الهاتف غير صحيح')
+ .optional()
+ .or(z.literal('')),
+ email: z.string()
+ .email('البريد الإلكتروني غير صحيح')
+ .optional()
+ .or(z.literal('')),
+ address: z.string()
+ .optional()
+ .or(z.literal('')),
+});
+
+export const vehicleSchema = z.object({
+ plateNumber: z.string()
+ .min(1, 'رقم اللوحة مطلوب')
+ .trim(),
+ bodyType: z.string()
+ .min(1, 'نوع الهيكل مطلوب')
+ .trim(),
+ manufacturer: z.string()
+ .min(1, 'الشركة المصنعة مطلوبة')
+ .trim(),
+ model: z.string()
+ .min(1, 'الموديل مطلوب')
+ .trim(),
+ trim: z.string()
+ .optional()
+ .or(z.literal('')),
+ year: z.number()
+ .min(VALIDATION.MIN_YEAR, `السنة يجب أن تكون بين ${VALIDATION.MIN_YEAR} و ${VALIDATION.MAX_YEAR}`)
+ .max(VALIDATION.MAX_YEAR, `السنة يجب أن تكون بين ${VALIDATION.MIN_YEAR} و ${VALIDATION.MAX_YEAR}`),
+ transmission: z.enum(TRANSMISSION_TYPES.map(t => t.value) as [string, ...string[]], {
+ errorMap: () => ({ message: 'نوع ناقل الحركة غير صحيح' })
+ }),
+ fuel: z.enum(FUEL_TYPES.map(f => f.value) as [string, ...string[]], {
+ errorMap: () => ({ message: 'نوع الوقود غير صحيح' })
+ }),
+ cylinders: z.number()
+ .min(1, `عدد الأسطوانات يجب أن يكون بين 1 و ${VALIDATION.MAX_CYLINDERS}`)
+ .max(VALIDATION.MAX_CYLINDERS, `عدد الأسطوانات يجب أن يكون بين 1 و ${VALIDATION.MAX_CYLINDERS}`)
+ .optional()
+ .nullable(),
+ engineDisplacement: z.number()
+ .min(0.1, `سعة المحرك يجب أن تكون بين 0.1 و ${VALIDATION.MAX_ENGINE_DISPLACEMENT}`)
+ .max(VALIDATION.MAX_ENGINE_DISPLACEMENT, `سعة المحرك يجب أن تكون بين 0.1 و ${VALIDATION.MAX_ENGINE_DISPLACEMENT}`)
+ .optional()
+ .nullable(),
+ useType: z.enum(USE_TYPES.map(u => u.value) as [string, ...string[]], {
+ errorMap: () => ({ message: 'نوع الاستخدام غير صحيح' })
+ }),
+ ownerId: z.number()
+ .min(1, 'مالك المركبة مطلوب'),
+});
+
+export const maintenanceVisitSchema = z.object({
+ vehicleId: z.number()
+ .min(1, 'المركبة مطلوبة'),
+ customerId: z.number()
+ .min(1, 'العميل مطلوب'),
+ maintenanceType: z.string()
+ .min(1, 'نوع الصيانة مطلوب')
+ .trim(),
+ description: z.string()
+ .min(1, 'وصف الصيانة مطلوب')
+ .max(VALIDATION.MAX_DESCRIPTION_LENGTH, `الوصف يجب أن يكون أقل من ${VALIDATION.MAX_DESCRIPTION_LENGTH} حرف`)
+ .trim(),
+ cost: z.number()
+ .min(VALIDATION.MIN_COST, `التكلفة يجب أن تكون بين ${VALIDATION.MIN_COST} و ${VALIDATION.MAX_COST}`)
+ .max(VALIDATION.MAX_COST, `التكلفة يجب أن تكون بين ${VALIDATION.MIN_COST} و ${VALIDATION.MAX_COST}`),
+ paymentStatus: z.enum(Object.values(PAYMENT_STATUS) as [string, ...string[]], {
+ errorMap: () => ({ message: 'حالة الدفع غير صحيحة' })
+ }),
+ kilometers: z.number()
+ .min(0, 'عدد الكيلومترات يجب أن يكون رقم موجب'),
+ nextVisitDelay: z.number()
+ .refine(val => [1, 2, 3, 4].includes(val), 'فترة الزيارة التالية يجب أن تكون 1، 2، 3، أو 4 أشهر'),
+});
+
+export const expenseSchema = z.object({
+ description: z.string()
+ .min(1, 'وصف المصروف مطلوب')
+ .max(VALIDATION.MAX_DESCRIPTION_LENGTH, `الوصف يجب أن يكون أقل من ${VALIDATION.MAX_DESCRIPTION_LENGTH} حرف`)
+ .trim(),
+ category: z.string()
+ .min(1, 'فئة المصروف مطلوبة')
+ .trim(),
+ amount: z.number()
+ .min(VALIDATION.MIN_COST + 0.01, `المبلغ يجب أن يكون بين ${VALIDATION.MIN_COST + 0.01} و ${VALIDATION.MAX_COST}`)
+ .max(VALIDATION.MAX_COST, `المبلغ يجب أن يكون بين ${VALIDATION.MIN_COST + 0.01} و ${VALIDATION.MAX_COST}`),
+});
+
+// Validation result type
+export interface ValidationResult {
+ success: boolean;
+ errors: Record;
+ data?: any;
+}
+
+// Server-side validation functions
+export function validateUserData(data: any): ValidationResult {
+ try {
+ const validatedData = userSchema.parse(data);
+ return { success: true, errors: {}, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors: Record = {};
+ error.issues.forEach((issue) => {
+ if (issue.path.length > 0) {
+ errors[issue.path[0] as string] = issue.message;
+ }
+ });
+ return { success: false, errors };
+ }
+ return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
+ }
+}
+
+export function validateCustomerData(data: any): ValidationResult {
+ try {
+ const validatedData = customerSchema.parse(data);
+ return { success: true, errors: {}, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors: Record = {};
+ error.issues.forEach((issue) => {
+ if (issue.path.length > 0) {
+ errors[issue.path[0] as string] = issue.message;
+ }
+ });
+ return { success: false, errors };
+ }
+ return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
+ }
+}
+
+export function validateVehicleData(data: any): ValidationResult {
+ try {
+ // Convert string numbers to actual numbers
+ const processedData = {
+ ...data,
+ year: data.year ? parseInt(data.year) : undefined,
+ cylinders: data.cylinders ? parseInt(data.cylinders) : null,
+ engineDisplacement: data.engineDisplacement ? parseFloat(data.engineDisplacement) : null,
+ ownerId: data.ownerId ? parseInt(data.ownerId) : undefined,
+ };
+
+ const validatedData = vehicleSchema.parse(processedData);
+ return { success: true, errors: {}, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors: Record = {};
+ error.issues.forEach((issue) => {
+ if (issue.path.length > 0) {
+ errors[issue.path[0] as string] = issue.message;
+ }
+ });
+ return { success: false, errors };
+ }
+ return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
+ }
+}
+
+export function validateMaintenanceVisitData(data: any): ValidationResult {
+ try {
+ // Convert string numbers to actual numbers
+ const processedData = {
+ ...data,
+ vehicleId: data.vehicleId ? parseInt(data.vehicleId) : undefined,
+ customerId: data.customerId ? parseInt(data.customerId) : undefined,
+ cost: data.cost ? parseFloat(data.cost) : undefined,
+ kilometers: data.kilometers ? parseInt(data.kilometers) : undefined,
+ nextVisitDelay: data.nextVisitDelay ? parseInt(data.nextVisitDelay) : undefined,
+ };
+
+ const validatedData = maintenanceVisitSchema.parse(processedData);
+ return { success: true, errors: {}, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors: Record = {};
+ error.issues.forEach((issue) => {
+ if (issue.path.length > 0) {
+ errors[issue.path[0] as string] = issue.message;
+ }
+ });
+ return { success: false, errors };
+ }
+ return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
+ }
+}
+
+export function validateExpenseData(data: any): ValidationResult {
+ try {
+ // Convert string numbers to actual numbers
+ const processedData = {
+ ...data,
+ amount: data.amount ? parseFloat(data.amount) : undefined,
+ };
+
+ const validatedData = expenseSchema.parse(processedData);
+ return { success: true, errors: {}, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors: Record = {};
+ error.issues.forEach((issue) => {
+ if (issue.path.length > 0) {
+ errors[issue.path[0] as string] = issue.message;
+ }
+ });
+ return { success: false, errors };
+ }
+ return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
+ }
+}
+
+// Client-side validation helpers
+export function validateField(value: any, schema: z.ZodSchema, fieldName: string): string | null {
+ try {
+ schema.parse({ [fieldName]: value });
+ return null;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const fieldError = error.errors.find(err => err.path.includes(fieldName));
+ return fieldError?.message || null;
+ }
+ return null;
+ }
+}
+
+// Real-time validation for forms
+export function validateFormField(fieldName: string, value: any, schema: z.ZodSchema): string | null {
+ try {
+ const fieldSchema = (schema as any).shape[fieldName];
+ if (fieldSchema) {
+ fieldSchema.parse(value);
+ }
+ return null;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return error.issues[0]?.message || null;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/lib/layout-utils.ts b/app/lib/layout-utils.ts
new file mode 100644
index 0000000..0d24c71
--- /dev/null
+++ b/app/lib/layout-utils.ts
@@ -0,0 +1,146 @@
+/**
+ * Layout utilities for RTL support and responsive design
+ */
+
+export interface LayoutConfig {
+ direction: 'rtl' | 'ltr';
+ language: 'ar' | 'en';
+ sidebarCollapsed: boolean;
+ isMobile: boolean;
+}
+
+export const defaultLayoutConfig: LayoutConfig = {
+ direction: 'rtl',
+ language: 'ar',
+ sidebarCollapsed: false,
+ isMobile: false,
+};
+
+/**
+ * Get responsive classes based on screen size and RTL direction
+ */
+export function getResponsiveClasses(config: LayoutConfig) {
+ const { direction, sidebarCollapsed, isMobile } = config;
+
+ return {
+ container: `container-rtl ${direction === 'rtl' ? 'rtl' : 'ltr'}`,
+ flexRow: direction === 'rtl' ? 'flex-rtl-reverse' : 'flex-rtl',
+ textAlign: direction === 'rtl' ? 'text-right' : 'text-left',
+ marginStart: direction === 'rtl' ? 'mr-auto' : 'ml-auto',
+ marginEnd: direction === 'rtl' ? 'ml-auto' : 'mr-auto',
+ paddingStart: direction === 'rtl' ? 'pr-4' : 'pl-4',
+ paddingEnd: direction === 'rtl' ? 'pl-4' : 'pr-4',
+ borderStart: direction === 'rtl' ? 'border-r' : 'border-l',
+ borderEnd: direction === 'rtl' ? 'border-l' : 'border-r',
+ roundedStart: direction === 'rtl' ? 'rounded-r' : 'rounded-l',
+ roundedEnd: direction === 'rtl' ? 'rounded-l' : 'rounded-r',
+ sidebar: getSidebarClasses(config),
+ mainContent: getMainContentClasses(config),
+ };
+}
+
+function getSidebarClasses(config: LayoutConfig) {
+ const { direction, sidebarCollapsed, isMobile } = config;
+
+ let classes = 'sidebar-rtl';
+
+ if (isMobile) {
+ classes += ' mobile-sidebar-rtl';
+ if (sidebarCollapsed) {
+ classes += ' closed';
+ }
+ } else {
+ if (sidebarCollapsed) {
+ classes += ' collapsed';
+ }
+ }
+
+ return classes;
+}
+
+function getMainContentClasses(config: LayoutConfig) {
+ const { sidebarCollapsed, isMobile } = config;
+
+ let classes = 'main-content-rtl';
+
+ if (!isMobile && !sidebarCollapsed) {
+ classes += ' sidebar-open';
+ }
+
+ return classes;
+}
+
+/**
+ * Get Arabic text rendering classes
+ */
+export function getArabicTextClasses(size: 'sm' | 'base' | 'lg' | 'xl' | '2xl' = 'base') {
+ const sizeClasses = {
+ sm: 'text-sm',
+ base: 'text-base',
+ lg: 'text-lg',
+ xl: 'text-xl',
+ '2xl': 'text-2xl',
+ };
+
+ return `arabic-text font-arabic ${sizeClasses[size]}`;
+}
+
+/**
+ * Get form input classes with RTL support
+ */
+export function getFormInputClasses(error?: boolean) {
+ let classes = 'w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500';
+
+ if (error) {
+ classes += ' border-red-500 focus:ring-red-500 focus:border-red-500';
+ } else {
+ classes += ' border-gray-300';
+ }
+
+ return classes;
+}
+
+/**
+ * Get button classes with RTL support
+ */
+export function getButtonClasses(
+ variant: 'primary' | 'secondary' | 'danger' = 'primary',
+ size: 'sm' | 'md' | 'lg' = 'md'
+) {
+ const baseClasses = 'btn inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2';
+
+ const variantClasses = {
+ primary: 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500',
+ secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900 focus:ring-gray-500',
+ danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500',
+ };
+
+ const sizeClasses = {
+ sm: 'px-3 py-2 text-sm',
+ md: 'px-4 py-2 text-base',
+ lg: 'px-6 py-3 text-lg',
+ };
+
+ return `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`;
+}
+
+/**
+ * Breakpoint utilities for responsive design
+ */
+export const breakpoints = {
+ xs: 475,
+ sm: 640,
+ md: 768,
+ lg: 1024,
+ xl: 1280,
+ '2xl': 1536,
+};
+
+/**
+ * Check if current screen size matches breakpoint
+ */
+export function useBreakpoint(breakpoint: keyof typeof breakpoints) {
+ if (typeof window === 'undefined') return false;
+
+ return window.innerWidth >= breakpoints[breakpoint];
+}
\ No newline at end of file
diff --git a/app/lib/maintenance-type-management.server.ts b/app/lib/maintenance-type-management.server.ts
new file mode 100644
index 0000000..6e186d4
--- /dev/null
+++ b/app/lib/maintenance-type-management.server.ts
@@ -0,0 +1,220 @@
+import { prisma } from "./db.server";
+import type { MaintenanceType } from "@prisma/client";
+import type { CreateMaintenanceTypeData, UpdateMaintenanceTypeData } from "~/types/database";
+
+// Get all maintenance types
+export async function getMaintenanceTypes(
+ includeInactive: boolean = false
+): Promise {
+ const whereClause = includeInactive ? {} : { isActive: true };
+
+ return await prisma.maintenanceType.findMany({
+ where: whereClause,
+ orderBy: { name: 'asc' },
+ });
+}
+
+// Get maintenance types for select dropdown
+export async function getMaintenanceTypesForSelect(): Promise<{
+ id: number;
+ name: string;
+}[]> {
+ return await prisma.maintenanceType.findMany({
+ where: { isActive: true },
+ select: {
+ id: true,
+ name: true,
+ },
+ orderBy: { name: 'asc' },
+ });
+}
+
+// Get maintenance type by ID
+export async function getMaintenanceTypeById(id: number): Promise {
+ return await prisma.maintenanceType.findUnique({
+ where: { id },
+ });
+}
+
+// Create new maintenance type
+export async function createMaintenanceType(
+ data: CreateMaintenanceTypeData
+): Promise<{ success: boolean; maintenanceType?: MaintenanceType; error?: string }> {
+ try {
+ // Check if maintenance type with same name already exists
+ const existingType = await prisma.maintenanceType.findUnique({
+ where: { name: data.name.trim() },
+ });
+
+ if (existingType) {
+ return { success: false, error: "نوع الصيانة موجود بالفعل" };
+ }
+
+ // Create maintenance type
+ const maintenanceType = await prisma.maintenanceType.create({
+ data: {
+ name: data.name.trim(),
+ description: data.description?.trim() || null,
+ isActive: data.isActive ?? true,
+ },
+ });
+
+ return { success: true, maintenanceType };
+ } catch (error) {
+ console.error("Error creating maintenance type:", error);
+ return { success: false, error: "حدث خطأ أثناء إنشاء نوع الصيانة" };
+ }
+}
+
+// Update maintenance type
+export async function updateMaintenanceType(
+ id: number,
+ data: UpdateMaintenanceTypeData
+): Promise<{ success: boolean; maintenanceType?: MaintenanceType; error?: string }> {
+ try {
+ // Check if maintenance type exists
+ const existingType = await prisma.maintenanceType.findUnique({
+ where: { id },
+ });
+
+ if (!existingType) {
+ return { success: false, error: "نوع الصيانة غير موجود" };
+ }
+
+ // Check for name conflicts with other types
+ if (data.name) {
+ const conflictType = await prisma.maintenanceType.findFirst({
+ where: {
+ AND: [
+ { id: { not: id } },
+ { name: data.name.trim() },
+ ],
+ },
+ });
+
+ if (conflictType) {
+ return { success: false, error: "اسم نوع الصيانة موجود بالفعل" };
+ }
+ }
+
+ // Prepare update data
+ const updateData: any = {};
+ if (data.name !== undefined) updateData.name = data.name.trim();
+ if (data.description !== undefined) updateData.description = data.description?.trim() || null;
+ if (data.isActive !== undefined) updateData.isActive = data.isActive;
+
+ // Update maintenance type
+ const maintenanceType = await prisma.maintenanceType.update({
+ where: { id },
+ data: updateData,
+ });
+
+ return { success: true, maintenanceType };
+ } catch (error) {
+ console.error("Error updating maintenance type:", error);
+ return { success: false, error: "حدث خطأ أثناء تحديث نوع الصيانة" };
+ }
+}
+
+// Delete maintenance type (soft delete by setting isActive to false)
+export async function deleteMaintenanceType(
+ id: number
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ // Check if maintenance type exists
+ const existingType = await prisma.maintenanceType.findUnique({
+ where: { id },
+ include: {
+ maintenanceVisits: true,
+ },
+ });
+
+ if (!existingType) {
+ return { success: false, error: "نوع الصيانة غير موجود" };
+ }
+
+ // Check if maintenance type has visits
+ if (existingType.maintenanceVisits.length > 0) {
+ // Soft delete - set as inactive instead of deleting
+ await prisma.maintenanceType.update({
+ where: { id },
+ data: { isActive: false },
+ });
+
+ return { success: true };
+ } else {
+ // Hard delete if no visits are associated
+ await prisma.maintenanceType.delete({
+ where: { id },
+ });
+
+ return { success: true };
+ }
+ } catch (error) {
+ console.error("Error deleting maintenance type:", error);
+ return { success: false, error: "حدث خطأ أثناء حذف نوع الصيانة" };
+ }
+}
+
+// Toggle maintenance type active status
+export async function toggleMaintenanceTypeStatus(
+ id: number
+): Promise<{ success: boolean; maintenanceType?: MaintenanceType; error?: string }> {
+ try {
+ const existingType = await prisma.maintenanceType.findUnique({
+ where: { id },
+ });
+
+ if (!existingType) {
+ return { success: false, error: "نوع الصيانة غير موجود" };
+ }
+
+ const maintenanceType = await prisma.maintenanceType.update({
+ where: { id },
+ data: { isActive: !existingType.isActive },
+ });
+
+ return { success: true, maintenanceType };
+ } catch (error) {
+ console.error("Error toggling maintenance type status:", error);
+ return { success: false, error: "حدث خطأ أثناء تغيير حالة نوع الصيانة" };
+ }
+}
+
+// Get maintenance type statistics
+export async function getMaintenanceTypeStats(typeId: number): Promise<{
+ totalVisits: number;
+ totalRevenue: number;
+ averageCost: number;
+ lastVisitDate?: Date;
+} | null> {
+ const type = await prisma.maintenanceType.findUnique({
+ where: { id: typeId },
+ include: {
+ maintenanceVisits: {
+ select: {
+ cost: true,
+ visitDate: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ },
+ },
+ });
+
+ if (!type) return null;
+
+ const totalRevenue = type.maintenanceVisits.reduce((sum, visit) => sum + visit.cost, 0);
+ const averageCost = type.maintenanceVisits.length > 0
+ ? totalRevenue / type.maintenanceVisits.length
+ : 0;
+ const lastVisitDate = type.maintenanceVisits.length > 0
+ ? type.maintenanceVisits[0].visitDate
+ : undefined;
+
+ return {
+ totalVisits: type.maintenanceVisits.length,
+ totalRevenue,
+ averageCost,
+ lastVisitDate,
+ };
+}
\ No newline at end of file
diff --git a/app/lib/maintenance-visit-management.server.ts b/app/lib/maintenance-visit-management.server.ts
new file mode 100644
index 0000000..b355a9f
--- /dev/null
+++ b/app/lib/maintenance-visit-management.server.ts
@@ -0,0 +1,365 @@
+import { prisma } from "./db.server";
+import type { MaintenanceVisit } from "@prisma/client";
+import type {
+ CreateMaintenanceVisitData,
+ UpdateMaintenanceVisitData,
+ MaintenanceVisitWithRelations
+} from "~/types/database";
+
+// Get all maintenance visits with search and pagination
+export async function getMaintenanceVisits(
+ searchQuery?: string,
+ page: number = 1,
+ limit: number = 10,
+ vehicleId?: number,
+ customerId?: number
+): Promise<{
+ visits: MaintenanceVisitWithRelations[];
+ total: number;
+ totalPages: number;
+}> {
+ const offset = (page - 1) * limit;
+
+ // Build where clause for search and filters
+ const whereClause: any = {};
+
+ if (vehicleId) {
+ whereClause.vehicleId = vehicleId;
+ }
+
+ if (customerId) {
+ whereClause.customerId = customerId;
+ }
+
+ if (searchQuery) {
+ const searchLower = searchQuery.toLowerCase();
+ whereClause.OR = [
+ { maintenanceJobs: { contains: searchLower } },
+ { description: { contains: searchLower } },
+ { paymentStatus: { contains: searchLower } },
+ { vehicle: { plateNumber: { contains: searchLower } } },
+ { customer: { name: { contains: searchLower } } },
+ ];
+ }
+
+ const [visits, total] = await Promise.all([
+ prisma.maintenanceVisit.findMany({
+ where: whereClause,
+ include: {
+ vehicle: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ },
+ },
+ customer: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ income: {
+ select: {
+ id: true,
+ amount: true,
+ incomeDate: true,
+ },
+ },
+ },
+ orderBy: { visitDate: 'desc' },
+ skip: offset,
+ take: limit,
+ }),
+ prisma.maintenanceVisit.count({ where: whereClause }),
+ ]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ return {
+ visits,
+ total,
+ totalPages,
+ };
+}
+
+// Get a single maintenance visit by ID
+export async function getMaintenanceVisitById(id: number): Promise {
+ return prisma.maintenanceVisit.findUnique({
+ where: { id },
+ include: {
+ vehicle: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ ownerId: true,
+ },
+ },
+ customer: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ income: {
+ select: {
+ id: true,
+ amount: true,
+ incomeDate: true,
+ },
+ },
+ },
+ });
+}
+
+// Create a new maintenance visit
+export async function createMaintenanceVisit(data: CreateMaintenanceVisitData): Promise {
+ // Calculate next visit date based on delay
+ const nextVisitDate = new Date();
+ nextVisitDate.setMonth(nextVisitDate.getMonth() + data.nextVisitDelay);
+
+ // Start a transaction to create visit, update vehicle, and create income
+ const result = await prisma.$transaction(async (tx) => {
+ // Create the maintenance visit
+ const visit = await tx.maintenanceVisit.create({
+ data: {
+ vehicleId: data.vehicleId,
+ customerId: data.customerId,
+ maintenanceJobs: JSON.stringify(data.maintenanceJobs),
+ description: data.description,
+ cost: data.cost,
+ paymentStatus: data.paymentStatus || "pending",
+ kilometers: data.kilometers,
+ visitDate: data.visitDate || new Date(),
+ nextVisitDelay: data.nextVisitDelay,
+ },
+ });
+
+ // Update vehicle's last visit date and suggested next visit date
+ await tx.vehicle.update({
+ where: { id: data.vehicleId },
+ data: {
+ lastVisitDate: visit.visitDate,
+ suggestedNextVisitDate: nextVisitDate,
+ },
+ });
+
+ // Create income record
+ await tx.income.create({
+ data: {
+ maintenanceVisitId: visit.id,
+ amount: data.cost,
+ incomeDate: visit.visitDate,
+ },
+ });
+
+ return visit;
+ });
+
+ return result;
+}
+
+// Update an existing maintenance visit
+export async function updateMaintenanceVisit(
+ id: number,
+ data: UpdateMaintenanceVisitData
+): Promise {
+ // Calculate next visit date if delay is updated
+ let nextVisitDate: Date | undefined;
+ if (data.nextVisitDelay) {
+ nextVisitDate = new Date(data.visitDate || new Date());
+ nextVisitDate.setMonth(nextVisitDate.getMonth() + data.nextVisitDelay);
+ }
+
+ // Start a transaction to update visit, vehicle, and income
+ const result = await prisma.$transaction(async (tx) => {
+ // Get the current visit to check for changes
+ const currentVisit = await tx.maintenanceVisit.findUnique({
+ where: { id },
+ select: { vehicleId: true, cost: true, visitDate: true },
+ });
+
+ if (!currentVisit) {
+ throw new Error("Maintenance visit not found");
+ }
+
+ // Update the maintenance visit
+ const visit = await tx.maintenanceVisit.update({
+ where: { id },
+ data: {
+ maintenanceJobs: data.maintenanceJobs ? JSON.stringify(data.maintenanceJobs) : undefined,
+ description: data.description,
+ cost: data.cost,
+ paymentStatus: data.paymentStatus,
+ kilometers: data.kilometers,
+ visitDate: data.visitDate,
+ nextVisitDelay: data.nextVisitDelay,
+ },
+ });
+
+ // Update vehicle if visit date or delay changed
+ if (data.visitDate || data.nextVisitDelay) {
+ const updateData: any = {};
+
+ if (data.visitDate) {
+ updateData.lastVisitDate = data.visitDate;
+ }
+
+ if (nextVisitDate) {
+ updateData.suggestedNextVisitDate = nextVisitDate;
+ }
+
+ if (Object.keys(updateData).length > 0) {
+ await tx.vehicle.update({
+ where: { id: currentVisit.vehicleId },
+ data: updateData,
+ });
+ }
+ }
+
+ // Update income record if cost or date changed
+ if (data.cost !== undefined || data.visitDate) {
+ await tx.income.updateMany({
+ where: { maintenanceVisitId: id },
+ data: {
+ amount: data.cost || currentVisit.cost,
+ incomeDate: data.visitDate || currentVisit.visitDate,
+ },
+ });
+ }
+
+ return visit;
+ });
+
+ return result;
+}
+
+// Delete a maintenance visit
+export async function deleteMaintenanceVisit(id: number): Promise {
+ await prisma.$transaction(async (tx) => {
+ // Get visit details before deletion
+ const visit = await tx.maintenanceVisit.findUnique({
+ where: { id },
+ select: { vehicleId: true },
+ });
+
+ if (!visit) {
+ throw new Error("Maintenance visit not found");
+ }
+
+ // Delete the maintenance visit (income will be deleted by cascade)
+ await tx.maintenanceVisit.delete({
+ where: { id },
+ });
+
+ // Update vehicle's last visit date to the most recent remaining visit
+ const lastVisit = await tx.maintenanceVisit.findFirst({
+ where: { vehicleId: visit.vehicleId },
+ orderBy: { visitDate: 'desc' },
+ select: { visitDate: true, nextVisitDelay: true },
+ });
+
+ let updateData: any = {};
+
+ if (lastVisit) {
+ updateData.lastVisitDate = lastVisit.visitDate;
+ // Recalculate next visit date
+ const nextDate = new Date(lastVisit.visitDate);
+ nextDate.setMonth(nextDate.getMonth() + lastVisit.nextVisitDelay);
+ updateData.suggestedNextVisitDate = nextDate;
+ } else {
+ // No visits left, clear the dates
+ updateData.lastVisitDate = null;
+ updateData.suggestedNextVisitDate = null;
+ }
+
+ await tx.vehicle.update({
+ where: { id: visit.vehicleId },
+ data: updateData,
+ });
+ });
+}
+
+// Get maintenance visits for a specific vehicle
+export async function getVehicleMaintenanceHistory(vehicleId: number): Promise {
+ return prisma.maintenanceVisit.findMany({
+ where: { vehicleId },
+ include: {
+ vehicle: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ },
+ },
+ customer: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ income: {
+ select: {
+ id: true,
+ amount: true,
+ incomeDate: true,
+ },
+ },
+ },
+ orderBy: { visitDate: 'desc' },
+ });
+}
+
+// Get maintenance visits for a specific customer
+export async function getCustomerMaintenanceHistory(customerId: number): Promise {
+ return prisma.maintenanceVisit.findMany({
+ where: { customerId },
+ include: {
+ vehicle: {
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ },
+ },
+ customer: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ maintenanceType: {
+ select: {
+ id: true,
+ name: true,
+ description: true,
+ },
+ },
+ income: {
+ select: {
+ id: true,
+ amount: true,
+ incomeDate: true,
+ },
+ },
+ },
+ orderBy: { visitDate: 'desc' },
+ });
+}
\ No newline at end of file
diff --git a/app/lib/settings-management.server.ts b/app/lib/settings-management.server.ts
new file mode 100644
index 0000000..730170c
--- /dev/null
+++ b/app/lib/settings-management.server.ts
@@ -0,0 +1,209 @@
+import { prisma as db } from './db.server';
+
+export interface AppSettings {
+ dateFormat: 'ar-SA' | 'en-US';
+ currency: string;
+ numberFormat: 'ar-SA' | 'en-US';
+ currencySymbol: string;
+ dateDisplayFormat: string;
+}
+
+export interface SettingItem {
+ id: number;
+ key: string;
+ value: string;
+ description?: string;
+ createdDate: Date;
+ updateDate: Date;
+}
+
+// Default settings
+const DEFAULT_SETTINGS: AppSettings = {
+ dateFormat: 'ar-SA',
+ currency: 'JOD',
+ numberFormat: 'ar-SA',
+ currencySymbol: 'د.أ',
+ dateDisplayFormat: 'dd/MM/yyyy'
+};
+
+// Get all settings as a typed object
+export async function getAppSettings(): Promise {
+ try {
+ const settings = await db.settings.findMany();
+
+ const settingsMap = settings.reduce((acc, setting) => {
+ acc[setting.key] = setting.value;
+ return acc;
+ }, {} as Record);
+
+ return {
+ dateFormat: (settingsMap.dateFormat as 'ar-SA' | 'en-US') || DEFAULT_SETTINGS.dateFormat,
+ currency: settingsMap.currency || DEFAULT_SETTINGS.currency,
+ numberFormat: (settingsMap.numberFormat as 'ar-SA' | 'en-US') || DEFAULT_SETTINGS.numberFormat,
+ currencySymbol: settingsMap.currencySymbol || DEFAULT_SETTINGS.currencySymbol,
+ dateDisplayFormat: settingsMap.dateDisplayFormat || DEFAULT_SETTINGS.dateDisplayFormat
+ };
+ } catch (error) {
+ console.error('Error fetching settings:', error);
+ return DEFAULT_SETTINGS;
+ }
+}
+
+// Get a specific setting by key
+export async function getSetting(key: string): Promise {
+ try {
+ const setting = await db.settings.findUnique({
+ where: { key }
+ });
+ return setting?.value || null;
+ } catch (error) {
+ console.error(`Error fetching setting ${key}:`, error);
+ return null;
+ }
+}
+
+// Update or create a setting
+export async function updateSetting(key: string, value: string, description?: string): Promise {
+ try {
+ return await db.settings.upsert({
+ where: { key },
+ update: {
+ value,
+ description: description || undefined,
+ updateDate: new Date()
+ },
+ create: {
+ key,
+ value,
+ description: description || undefined
+ }
+ });
+ } catch (error) {
+ console.error(`Error updating setting ${key}:`, error);
+ throw new Error(`Failed to update setting: ${key}`);
+ }
+}
+
+// Update multiple settings at once
+export async function updateSettings(settings: Partial): Promise {
+ try {
+ const updates = Object.entries(settings).map(([key, value]) =>
+ updateSetting(key, value as string)
+ );
+
+ await Promise.all(updates);
+ } catch (error) {
+ console.error('Error updating settings:', error);
+ throw new Error('Failed to update settings');
+ }
+}
+
+// Get all settings for admin management
+export async function getAllSettings(): Promise {
+ try {
+ return await db.settings.findMany({
+ orderBy: { key: 'asc' }
+ });
+ } catch (error) {
+ console.error('Error fetching all settings:', error);
+ throw new Error('Failed to fetch settings');
+ }
+}
+
+// Initialize default settings if they don't exist
+export async function initializeDefaultSettings(): Promise {
+ try {
+ const existingSettings = await db.settings.findMany();
+ const existingKeys = new Set(existingSettings.map(s => s.key));
+
+ const defaultEntries = [
+ { key: 'dateFormat', value: 'ar-SA', description: 'Date format locale (ar-SA or en-US)' },
+ { key: 'currency', value: 'JOD', description: 'Currency code (JOD, USD, EUR, etc.)' },
+ { key: 'numberFormat', value: 'ar-SA', description: 'Number format locale (ar-SA or en-US)' },
+ { key: 'currencySymbol', value: 'د.أ', description: 'Currency symbol display' },
+ { key: 'dateDisplayFormat', value: 'dd/MM/yyyy', description: 'Date display format pattern' }
+ ];
+
+ const newSettings = defaultEntries.filter(entry => !existingKeys.has(entry.key));
+
+ if (newSettings.length > 0) {
+ await db.settings.createMany({
+ data: newSettings
+ });
+ }
+ } catch (error) {
+ console.error('Error initializing default settings:', error);
+ // Don't throw error, just log it and continue with defaults
+ }
+}
+
+// Formatting utilities using settings
+export class SettingsFormatter {
+ constructor(private settings: AppSettings) {}
+
+ // Format number according to settings
+ formatNumber(value: number): string {
+ return value.toLocaleString(this.settings.numberFormat);
+ }
+
+ // Format currency according to settings
+ formatCurrency(value: number): string {
+ const formatted = value.toLocaleString(this.settings.numberFormat, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ return `${formatted} ${this.settings.currencySymbol}`;
+ }
+
+ // Format date according to settings
+ formatDate(date: Date | string): string {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ return this.formatDateWithPattern(dateObj, this.settings.dateDisplayFormat);
+ }
+
+ // Format datetime according to settings
+ formatDateTime(date: Date | string): string {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ const datePart = this.formatDateWithPattern(dateObj, this.settings.dateDisplayFormat);
+ const timePart = dateObj.toLocaleTimeString(this.settings.dateFormat, {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ return `${datePart} ${timePart}`;
+ }
+
+ // Helper method to format date with custom pattern
+ private formatDateWithPattern(date: Date, pattern: string): string {
+ const day = date.getDate();
+ const month = date.getMonth() + 1;
+ const year = date.getFullYear();
+
+ // Format numbers according to locale
+ const formatNumber = (num: number, padLength: number = 2): string => {
+ const padded = num.toString().padStart(padLength, '0');
+ return this.settings.numberFormat === 'ar-SA'
+ ? this.convertToArabicNumerals(padded)
+ : padded;
+ };
+
+ return pattern
+ .replace(/yyyy/g, formatNumber(year, 4))
+ .replace(/yy/g, formatNumber(year % 100, 2))
+ .replace(/MM/g, formatNumber(month, 2))
+ .replace(/M/g, formatNumber(month, 1))
+ .replace(/dd/g, formatNumber(day, 2))
+ .replace(/d/g, formatNumber(day, 1));
+ }
+
+ // Helper method to convert Western numerals to Arabic numerals
+ private convertToArabicNumerals(str: string): string {
+ const arabicNumerals = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
+ return str.replace(/[0-9]/g, (digit) => arabicNumerals[parseInt(digit)]);
+ }
+}
+
+// Create formatter instance with current settings
+export async function createFormatter(): Promise {
+ const settings = await getAppSettings();
+ return new SettingsFormatter(settings);
+}
\ No newline at end of file
diff --git a/app/lib/table-utils.ts b/app/lib/table-utils.ts
new file mode 100644
index 0000000..54bd327
--- /dev/null
+++ b/app/lib/table-utils.ts
@@ -0,0 +1,309 @@
+// Table utilities for searching, filtering, sorting, and pagination
+
+export interface SortConfig {
+ key: string;
+ direction: 'asc' | 'desc';
+}
+
+export interface FilterConfig {
+ [key: string]: string | string[] | number | boolean;
+}
+
+export interface PaginationConfig {
+ page: number;
+ pageSize: number;
+}
+
+export interface TableState {
+ search: string;
+ filters: FilterConfig;
+ sort: SortConfig | null;
+ pagination: PaginationConfig;
+}
+
+// Search function with Arabic text support
+export function searchData>(
+ data: T[],
+ searchTerm: string,
+ searchableFields: (keyof T)[]
+): T[] {
+ if (!searchTerm.trim()) return data;
+
+ const normalizedSearch = normalizeArabicText(searchTerm.toLowerCase());
+
+ return data.filter(item => {
+ return searchableFields.some(field => {
+ const value = item[field];
+ if (value == null) return false;
+
+ const normalizedValue = normalizeArabicText(String(value).toLowerCase());
+ return normalizedValue.includes(normalizedSearch);
+ });
+ });
+}
+
+// Filter data based on multiple criteria
+export function filterData>(
+ data: T[],
+ filters: FilterConfig
+): T[] {
+ return data.filter(item => {
+ return Object.entries(filters).every(([key, filterValue]) => {
+ if (!filterValue || filterValue === '' || (Array.isArray(filterValue) && filterValue.length === 0)) {
+ return true;
+ }
+
+ const itemValue = item[key];
+
+ if (Array.isArray(filterValue)) {
+ // Multi-select filter
+ return filterValue.includes(String(itemValue));
+ }
+
+ if (typeof filterValue === 'string') {
+ // Text filter
+ if (itemValue == null) return false;
+ const normalizedItemValue = normalizeArabicText(String(itemValue).toLowerCase());
+ const normalizedFilterValue = normalizeArabicText(filterValue.toLowerCase());
+ return normalizedItemValue.includes(normalizedFilterValue);
+ }
+
+ if (typeof filterValue === 'number') {
+ // Numeric filter
+ return Number(itemValue) === filterValue;
+ }
+
+ if (typeof filterValue === 'boolean') {
+ // Boolean filter
+ return Boolean(itemValue) === filterValue;
+ }
+
+ return true;
+ });
+ });
+}
+
+// Sort data
+export function sortData>(
+ data: T[],
+ sortConfig: SortConfig | null
+): T[] {
+ if (!sortConfig) return data;
+
+ return [...data].sort((a, b) => {
+ const aValue = a[sortConfig.key];
+ const bValue = b[sortConfig.key];
+
+ // Handle null/undefined values
+ if (aValue == null && bValue == null) return 0;
+ if (aValue == null) return sortConfig.direction === 'asc' ? 1 : -1;
+ if (bValue == null) return sortConfig.direction === 'asc' ? -1 : 1;
+
+ // Handle different data types
+ let comparison = 0;
+
+ if (typeof aValue === 'string' && typeof bValue === 'string') {
+ // String comparison with Arabic support
+ comparison = normalizeArabicText(aValue).localeCompare(normalizeArabicText(bValue), 'ar');
+ } else if (typeof aValue === 'number' && typeof bValue === 'number') {
+ // Numeric comparison
+ comparison = aValue - bValue;
+ } else if (aValue instanceof Date && bValue instanceof Date) {
+ // Date comparison
+ comparison = aValue.getTime() - bValue.getTime();
+ } else {
+ // Fallback to string comparison
+ comparison = String(aValue).localeCompare(String(bValue), 'ar');
+ }
+
+ return sortConfig.direction === 'asc' ? comparison : -comparison;
+ });
+}
+
+// Paginate data
+export function paginateData(
+ data: T[],
+ pagination: PaginationConfig
+): {
+ data: T[];
+ totalItems: number;
+ totalPages: number;
+ currentPage: number;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+} {
+ const { page, pageSize } = pagination;
+ const totalItems = data.length;
+ const totalPages = Math.ceil(totalItems / pageSize);
+ const startIndex = (page - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedData = data.slice(startIndex, endIndex);
+
+ return {
+ data: paginatedData,
+ totalItems,
+ totalPages,
+ currentPage: page,
+ hasNextPage: page < totalPages,
+ hasPreviousPage: page > 1,
+ };
+}
+
+// Process table data with all operations
+export function processTableData>(
+ data: T[],
+ state: TableState,
+ searchableFields: (keyof T)[]
+) {
+ let processedData = [...data];
+
+ // Apply search
+ if (state.search) {
+ processedData = searchData(processedData, state.search, searchableFields);
+ }
+
+ // Apply filters
+ if (Object.keys(state.filters).length > 0) {
+ processedData = filterData(processedData, state.filters);
+ }
+
+ // Apply sorting
+ if (state.sort) {
+ processedData = sortData(processedData, state.sort);
+ }
+
+ // Apply pagination
+ const paginationResult = paginateData(processedData, state.pagination);
+
+ return {
+ ...paginationResult,
+ filteredCount: processedData.length,
+ originalCount: data.length,
+ };
+}
+
+// Normalize Arabic text for better searching
+function normalizeArabicText(text: string): string {
+ return text
+ // Normalize Arabic characters
+ .replace(/[أإآ]/g, 'ا')
+ .replace(/[ة]/g, 'ه')
+ .replace(/[ي]/g, 'ى')
+ // Remove diacritics
+ .replace(/[\u064B-\u065F\u0670\u06D6-\u06ED]/g, '')
+ // Normalize whitespace
+ .replace(/\s+/g, ' ')
+ .trim();
+}
+
+// Generate filter options from data
+export function generateFilterOptions>(
+ data: T[],
+ field: keyof T,
+ labelFormatter?: (value: any) => string
+): { value: string; label: string }[] {
+ const uniqueValues = Array.from(new Set(
+ data
+ .map(item => item[field])
+ .filter(value => value != null && value !== '')
+ ));
+
+ return uniqueValues
+ .sort((a, b) => {
+ if (typeof a === 'string' && typeof b === 'string') {
+ return normalizeArabicText(a).localeCompare(normalizeArabicText(b), 'ar');
+ }
+ return String(a).localeCompare(String(b));
+ })
+ .map(value => ({
+ value: String(value),
+ label: labelFormatter ? labelFormatter(value) : String(value),
+ }));
+}
+
+// Create table state from URL search params
+export function createTableStateFromParams(
+ searchParams: URLSearchParams,
+ defaultPageSize = 10
+): TableState {
+ return {
+ search: searchParams.get('search') || '',
+ filters: Object.fromEntries(
+ Array.from(searchParams.entries())
+ .filter(([key]) => key.startsWith('filter_'))
+ .map(([key, value]) => [key.replace('filter_', ''), value])
+ ),
+ sort: searchParams.get('sort') && searchParams.get('sortDir') ? {
+ key: searchParams.get('sort')!,
+ direction: searchParams.get('sortDir') as 'asc' | 'desc',
+ } : null,
+ pagination: {
+ page: parseInt(searchParams.get('page') || '1'),
+ pageSize: parseInt(searchParams.get('pageSize') || String(defaultPageSize)),
+ },
+ };
+}
+
+// Convert table state to URL search params
+export function tableStateToParams(state: TableState): URLSearchParams {
+ const params = new URLSearchParams();
+
+ if (state.search) {
+ params.set('search', state.search);
+ }
+
+ Object.entries(state.filters).forEach(([key, value]) => {
+ if (value && value !== '') {
+ params.set(`filter_${key}`, String(value));
+ }
+ });
+
+ if (state.sort) {
+ params.set('sort', state.sort.key);
+ params.set('sortDir', state.sort.direction);
+ }
+
+ if (state.pagination.page > 1) {
+ params.set('page', String(state.pagination.page));
+ }
+
+ if (state.pagination.pageSize !== 10) {
+ params.set('pageSize', String(state.pagination.pageSize));
+ }
+
+ return params;
+}
+
+// Debounce function for search input
+export function debounce any>(
+ func: T,
+ delay: number
+): (...args: Parameters) => void {
+ let timeoutId: NodeJS.Timeout;
+
+ return (...args: Parameters) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func(...args), delay);
+ };
+}
+
+// Export utility types
+export type TableColumn = {
+ key: keyof T;
+ header: string;
+ sortable?: boolean;
+ filterable?: boolean;
+ searchable?: boolean;
+ render?: (value: any, item: T) => React.ReactNode;
+ width?: string;
+ align?: 'left' | 'center' | 'right';
+};
+
+export type TableAction = {
+ label: string;
+ icon?: React.ReactNode;
+ onClick: (item: T) => void;
+ variant?: 'primary' | 'secondary' | 'danger';
+ disabled?: (item: T) => boolean;
+ hidden?: (item: T) => boolean;
+};
\ No newline at end of file
diff --git a/app/lib/user-management.server.ts b/app/lib/user-management.server.ts
new file mode 100644
index 0000000..ec6e08d
--- /dev/null
+++ b/app/lib/user-management.server.ts
@@ -0,0 +1,341 @@
+import { prisma } from "./db.server";
+import { hashPassword } from "./auth.server";
+import type { User } from "@prisma/client";
+import type { CreateUserData, UpdateUserData, UserWithoutPassword } from "~/types/database";
+import { AUTH_LEVELS, USER_STATUS } from "~/types/auth";
+
+// Get all users with role-based filtering
+export async function getUsers(
+ currentUserAuthLevel: number,
+ searchQuery?: string,
+ page: number = 1,
+ limit: number = 10
+): Promise<{
+ users: UserWithoutPassword[];
+ total: number;
+ totalPages: number;
+}> {
+ const offset = (page - 1) * limit;
+
+ // Build where clause based on current user's auth level
+ const whereClause: any = {};
+
+ // Admins cannot see superadmin accounts
+ if (currentUserAuthLevel === AUTH_LEVELS.ADMIN) {
+ whereClause.authLevel = {
+ gt: AUTH_LEVELS.SUPERADMIN,
+ };
+ }
+
+ // Add search functionality
+ if (searchQuery) {
+ const searchLower = searchQuery.toLowerCase();
+ whereClause.OR = [
+ { name: { contains: searchLower } },
+ { username: { contains: searchLower } },
+ { email: { contains: searchLower } },
+ ];
+ }
+
+ const [users, total] = await Promise.all([
+ prisma.user.findMany({
+ where: whereClause,
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ orderBy: { createdDate: 'desc' },
+ skip: offset,
+ take: limit,
+ }),
+ prisma.user.count({ where: whereClause }),
+ ]);
+
+ return {
+ users,
+ total,
+ totalPages: Math.ceil(total / limit),
+ };
+}
+
+// Get user by ID with role-based access control
+export async function getUserById(
+ id: number,
+ currentUserAuthLevel: number
+): Promise {
+ const user = await prisma.user.findUnique({
+ where: { id },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ });
+
+ if (!user) return null;
+
+ // Admins cannot access superadmin accounts
+ if (currentUserAuthLevel === AUTH_LEVELS.ADMIN && user.authLevel === AUTH_LEVELS.SUPERADMIN) {
+ return null;
+ }
+
+ return user;
+}
+
+// Create new user
+export async function createUser(
+ userData: CreateUserData,
+ currentUserAuthLevel: number
+): Promise<{ success: boolean; user?: UserWithoutPassword; error?: string }> {
+ try {
+ // Validate that current user can create users with the specified auth level
+ if (currentUserAuthLevel === AUTH_LEVELS.ADMIN && userData.authLevel === AUTH_LEVELS.SUPERADMIN) {
+ return { success: false, error: "لا يمكن للمدير إنشاء حساب مدير عام" };
+ }
+
+ // Check if username or email already exists
+ const existingUser = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { username: userData.username },
+ { email: userData.email },
+ ],
+ },
+ });
+
+ if (existingUser) {
+ if (existingUser.username === userData.username) {
+ return { success: false, error: "اسم المستخدم موجود بالفعل" };
+ }
+ if (existingUser.email === userData.email) {
+ return { success: false, error: "البريد الإلكتروني موجود بالفعل" };
+ }
+ }
+
+ // Hash password
+ const hashedPassword = await hashPassword(userData.password);
+
+ // Create user
+ const user = await prisma.user.create({
+ data: {
+ name: userData.name,
+ username: userData.username,
+ email: userData.email,
+ password: hashedPassword,
+ authLevel: userData.authLevel,
+ status: userData.status || USER_STATUS.ACTIVE,
+ },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ });
+
+ return { success: true, user };
+ } catch (error) {
+ console.error("Error creating user:", error);
+ return { success: false, error: "حدث خطأ أثناء إنشاء المستخدم" };
+ }
+}
+
+// Update user
+export async function updateUser(
+ id: number,
+ userData: UpdateUserData,
+ currentUserAuthLevel: number
+): Promise<{ success: boolean; user?: UserWithoutPassword; error?: string }> {
+ try {
+ // Get existing user to check permissions
+ const existingUser = await getUserById(id, currentUserAuthLevel);
+ if (!existingUser) {
+ return { success: false, error: "المستخدم غير موجود أو لا يمكن الوصول إليه" };
+ }
+
+ // Validate auth level changes
+ if (userData.authLevel !== undefined) {
+ if (currentUserAuthLevel === AUTH_LEVELS.ADMIN && userData.authLevel === AUTH_LEVELS.SUPERADMIN) {
+ return { success: false, error: "لا يمكن للمدير تعيين مستوى مدير عام" };
+ }
+ }
+
+ // Check for username/email conflicts
+ if (userData.username || userData.email) {
+ const conflictUser = await prisma.user.findFirst({
+ where: {
+ AND: [
+ { id: { not: id } },
+ {
+ OR: [
+ userData.username ? { username: userData.username } : {},
+ userData.email ? { email: userData.email } : {},
+ ].filter(condition => Object.keys(condition).length > 0),
+ },
+ ],
+ },
+ });
+
+ if (conflictUser) {
+ if (conflictUser.username === userData.username) {
+ return { success: false, error: "اسم المستخدم موجود بالفعل" };
+ }
+ if (conflictUser.email === userData.email) {
+ return { success: false, error: "البريد الإلكتروني موجود بالفعل" };
+ }
+ }
+ }
+
+ // Prepare update data
+ const updateData: any = {};
+ if (userData.name !== undefined) updateData.name = userData.name;
+ if (userData.username !== undefined) updateData.username = userData.username;
+ if (userData.email !== undefined) updateData.email = userData.email;
+ if (userData.authLevel !== undefined) updateData.authLevel = userData.authLevel;
+ if (userData.status !== undefined) updateData.status = userData.status;
+
+ // Hash new password if provided
+ if (userData.password) {
+ updateData.password = await hashPassword(userData.password);
+ }
+
+ // Update user
+ const user = await prisma.user.update({
+ where: { id },
+ data: updateData,
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ });
+
+ return { success: true, user };
+ } catch (error) {
+ console.error("Error updating user:", error);
+ return { success: false, error: "حدث خطأ أثناء تحديث المستخدم" };
+ }
+}
+
+// Delete user
+export async function deleteUser(
+ id: number,
+ currentUserAuthLevel: number
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ // Get existing user to check permissions
+ const existingUser = await getUserById(id, currentUserAuthLevel);
+ if (!existingUser) {
+ return { success: false, error: "المستخدم غير موجود أو لا يمكن الوصول إليه" };
+ }
+
+ // Prevent deletion of superadmin by admin
+ if (currentUserAuthLevel === AUTH_LEVELS.ADMIN && existingUser.authLevel === AUTH_LEVELS.SUPERADMIN) {
+ return { success: false, error: "لا يمكن للمدير حذف حساب مدير عام" };
+ }
+
+ // Check if this is the last superadmin
+ if (existingUser.authLevel === AUTH_LEVELS.SUPERADMIN) {
+ const superadminCount = await prisma.user.count({
+ where: { authLevel: AUTH_LEVELS.SUPERADMIN },
+ });
+
+ if (superadminCount <= 1) {
+ return { success: false, error: "لا يمكن حذف آخر مدير عام في النظام" };
+ }
+ }
+
+ // Delete user
+ await prisma.user.delete({
+ where: { id },
+ });
+
+ return { success: true };
+ } catch (error) {
+ console.error("Error deleting user:", error);
+ return { success: false, error: "حدث خطأ أثناء حذف المستخدم" };
+ }
+}
+
+// Toggle user status (active/inactive)
+export async function toggleUserStatus(
+ id: number,
+ currentUserAuthLevel: number
+): Promise<{ success: boolean; user?: UserWithoutPassword; error?: string }> {
+ try {
+ const existingUser = await getUserById(id, currentUserAuthLevel);
+ if (!existingUser) {
+ return { success: false, error: "المستخدم غير موجود أو لا يمكن الوصول إليه" };
+ }
+
+ const newStatus = existingUser.status === USER_STATUS.ACTIVE
+ ? USER_STATUS.INACTIVE
+ : USER_STATUS.ACTIVE;
+
+ const user = await prisma.user.update({
+ where: { id },
+ data: { status: newStatus },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ status: true,
+ authLevel: true,
+ createdDate: true,
+ editDate: true,
+ },
+ });
+
+ return { success: true, user };
+ } catch (error) {
+ console.error("Error toggling user status:", error);
+ return { success: false, error: "حدث خطأ أثناء تغيير حالة المستخدم" };
+ }
+}
+
+// Get auth level display name
+export function getAuthLevelName(authLevel: number): string {
+ switch (authLevel) {
+ case AUTH_LEVELS.SUPERADMIN:
+ return "مدير عام";
+ case AUTH_LEVELS.ADMIN:
+ return "مدير";
+ case AUTH_LEVELS.USER:
+ return "مستخدم";
+ default:
+ return "غير محدد";
+ }
+}
+
+// Get status display name
+export function getStatusName(status: string): string {
+ switch (status) {
+ case USER_STATUS.ACTIVE:
+ return "نشط";
+ case USER_STATUS.INACTIVE:
+ return "غير نشط";
+ default:
+ return "غير محدد";
+ }
+}
\ No newline at end of file
diff --git a/app/lib/user-utils.ts b/app/lib/user-utils.ts
new file mode 100644
index 0000000..af39967
--- /dev/null
+++ b/app/lib/user-utils.ts
@@ -0,0 +1,27 @@
+import { AUTH_LEVELS, USER_STATUS } from '~/types/auth';
+
+// Get auth level display name (client-side version)
+export function getAuthLevelName(authLevel: number): string {
+ switch (authLevel) {
+ case AUTH_LEVELS.SUPERADMIN:
+ return "مدير عام";
+ case AUTH_LEVELS.ADMIN:
+ return "مدير";
+ case AUTH_LEVELS.USER:
+ return "مستخدم";
+ default:
+ return "غير محدد";
+ }
+}
+
+// Get status display name (client-side version)
+export function getStatusName(status: string): string {
+ switch (status) {
+ case USER_STATUS.ACTIVE:
+ return "نشط";
+ case USER_STATUS.INACTIVE:
+ return "غير نشط";
+ default:
+ return "غير محدد";
+ }
+}
\ No newline at end of file
diff --git a/app/lib/validation-utils.ts b/app/lib/validation-utils.ts
new file mode 100644
index 0000000..b27d837
--- /dev/null
+++ b/app/lib/validation-utils.ts
@@ -0,0 +1,291 @@
+// Validation utility functions with Arabic error messages
+
+export interface ValidationRule {
+ required?: boolean;
+ minLength?: number;
+ maxLength?: number;
+ min?: number;
+ max?: number;
+ pattern?: RegExp;
+ email?: boolean;
+ phone?: boolean;
+ url?: boolean;
+ custom?: (value: any) => string | null;
+}
+
+export interface ValidationResult {
+ isValid: boolean;
+ error?: string;
+}
+
+// Arabic error messages
+export const ERROR_MESSAGES = {
+ required: 'هذا الحقل مطلوب',
+ email: 'البريد الإلكتروني غير صحيح',
+ phone: 'رقم الهاتف غير صحيح',
+ url: 'الرابط غير صحيح',
+ minLength: (min: number) => `يجب أن يكون على الأقل ${min} أحرف`,
+ maxLength: (max: number) => `يجب أن يكون أقل من ${max} حرف`,
+ min: (min: number) => `يجب أن يكون على الأقل ${min}`,
+ max: (max: number) => `يجب أن يكون أقل من ${max}`,
+ pattern: 'التنسيق غير صحيح',
+ number: 'يجب أن يكون رقم صحيح',
+ integer: 'يجب أن يكون رقم صحيح',
+ positive: 'يجب أن يكون رقم موجب',
+ negative: 'يجب أن يكون رقم سالب',
+ date: 'التاريخ غير صحيح',
+ time: 'الوقت غير صحيح',
+ datetime: 'التاريخ والوقت غير صحيح',
+};
+
+// Regular expressions for validation
+export const PATTERNS = {
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ phone: /^[\+]?[0-9\s\-\(\)]+$/,
+ url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
+ arabicText: /^[\u0600-\u06FF\s\d\p{P}]+$/u,
+ englishText: /^[a-zA-Z\s\d\p{P}]+$/u,
+ alphanumeric: /^[a-zA-Z0-9]+$/,
+ numeric: /^\d+$/,
+ decimal: /^\d+(\.\d+)?$/,
+ plateNumber: /^[A-Z0-9\u0600-\u06FF\s\-]+$/u,
+ username: /^[a-zA-Z0-9_]+$/,
+};
+
+// Validate a single field
+export function validateField(value: any, rules: ValidationRule): ValidationResult {
+ // Convert value to string for most validations
+ const stringValue = value != null ? String(value).trim() : '';
+
+ // Required validation
+ if (rules.required && (!value || stringValue === '')) {
+ return { isValid: false, error: ERROR_MESSAGES.required };
+ }
+
+ // Skip other validations if value is empty and not required
+ if (!rules.required && (!value || stringValue === '')) {
+ return { isValid: true };
+ }
+
+ // Email validation
+ if (rules.email && !PATTERNS.email.test(stringValue)) {
+ return { isValid: false, error: ERROR_MESSAGES.email };
+ }
+
+ // Phone validation
+ if (rules.phone && !PATTERNS.phone.test(stringValue)) {
+ return { isValid: false, error: ERROR_MESSAGES.phone };
+ }
+
+ // URL validation
+ if (rules.url && !PATTERNS.url.test(stringValue)) {
+ return { isValid: false, error: ERROR_MESSAGES.url };
+ }
+
+ // Pattern validation
+ if (rules.pattern && !rules.pattern.test(stringValue)) {
+ return { isValid: false, error: ERROR_MESSAGES.pattern };
+ }
+
+ // Length validations
+ if (rules.minLength && stringValue.length < rules.minLength) {
+ return { isValid: false, error: ERROR_MESSAGES.minLength(rules.minLength) };
+ }
+
+ if (rules.maxLength && stringValue.length > rules.maxLength) {
+ return { isValid: false, error: ERROR_MESSAGES.maxLength(rules.maxLength) };
+ }
+
+ // Numeric validations
+ if (rules.min !== undefined || rules.max !== undefined) {
+ const numValue = Number(value);
+ if (isNaN(numValue)) {
+ return { isValid: false, error: ERROR_MESSAGES.number };
+ }
+
+ if (rules.min !== undefined && numValue < rules.min) {
+ return { isValid: false, error: ERROR_MESSAGES.min(rules.min) };
+ }
+
+ if (rules.max !== undefined && numValue > rules.max) {
+ return { isValid: false, error: ERROR_MESSAGES.max(rules.max) };
+ }
+ }
+
+ // Custom validation
+ if (rules.custom) {
+ const customError = rules.custom(value);
+ if (customError) {
+ return { isValid: false, error: customError };
+ }
+ }
+
+ return { isValid: true };
+}
+
+// Validate multiple fields
+export function validateFields(data: Record, rules: Record): {
+ isValid: boolean;
+ errors: Record;
+} {
+ const errors: Record = {};
+
+ Object.entries(rules).forEach(([fieldName, fieldRules]) => {
+ const result = validateField(data[fieldName], fieldRules);
+ if (!result.isValid && result.error) {
+ errors[fieldName] = result.error;
+ }
+ });
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors,
+ };
+}
+
+// Specific validation functions
+export function validateEmail(email: string): ValidationResult {
+ return validateField(email, { required: true, email: true });
+}
+
+export function validatePhone(phone: string): ValidationResult {
+ return validateField(phone, { phone: true });
+}
+
+export function validatePassword(password: string, minLength = 8): ValidationResult {
+ return validateField(password, { required: true, minLength });
+}
+
+export function validateUsername(username: string): ValidationResult {
+ return validateField(username, {
+ required: true,
+ minLength: 3,
+ pattern: PATTERNS.username
+ });
+}
+
+export function validatePlateNumber(plateNumber: string): ValidationResult {
+ return validateField(plateNumber, {
+ required: true,
+ pattern: PATTERNS.plateNumber
+ });
+}
+
+export function validateYear(year: number, minYear = 1900, maxYear = new Date().getFullYear() + 1): ValidationResult {
+ return validateField(year, {
+ required: true,
+ min: minYear,
+ max: maxYear
+ });
+}
+
+export function validateCurrency(amount: number, min = 0, max = 999999999): ValidationResult {
+ return validateField(amount, {
+ required: true,
+ min,
+ max
+ });
+}
+
+// Form data sanitization
+export function sanitizeString(value: string): string {
+ return value.trim().replace(/\s+/g, ' ');
+}
+
+export function sanitizeNumber(value: string | number): number | null {
+ const num = Number(value);
+ return isNaN(num) ? null : num;
+}
+
+export function sanitizeInteger(value: string | number): number | null {
+ const num = parseInt(String(value));
+ return isNaN(num) ? null : num;
+}
+
+export function sanitizeFormData(data: Record): Record {
+ const sanitized: Record = {};
+
+ Object.entries(data).forEach(([key, value]) => {
+ if (typeof value === 'string') {
+ sanitized[key] = sanitizeString(value);
+ } else {
+ sanitized[key] = value;
+ }
+ });
+
+ return sanitized;
+}
+
+// Date validation helpers
+export function isValidDate(date: any): boolean {
+ return date instanceof Date && !isNaN(date.getTime());
+}
+
+export function validateDateRange(startDate: Date, endDate: Date): ValidationResult {
+ if (!isValidDate(startDate) || !isValidDate(endDate)) {
+ return { isValid: false, error: ERROR_MESSAGES.date };
+ }
+
+ if (startDate >= endDate) {
+ return { isValid: false, error: 'تاريخ البداية يجب أن يكون قبل تاريخ النهاية' };
+ }
+
+ return { isValid: true };
+}
+
+// File validation helpers
+export function validateFileSize(file: File, maxSizeInMB: number): ValidationResult {
+ const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
+
+ if (file.size > maxSizeInBytes) {
+ return {
+ isValid: false,
+ error: `حجم الملف يجب أن يكون أقل من ${maxSizeInMB} ميجابايت`
+ };
+ }
+
+ return { isValid: true };
+}
+
+export function validateFileType(file: File, allowedTypes: string[]): ValidationResult {
+ if (!allowedTypes.includes(file.type)) {
+ return {
+ isValid: false,
+ error: `نوع الملف غير مدعوم. الأنواع المدعومة: ${allowedTypes.join(', ')}`
+ };
+ }
+
+ return { isValid: true };
+}
+
+// Array validation helpers
+export function validateArrayLength(array: any[], min?: number, max?: number): ValidationResult {
+ if (min !== undefined && array.length < min) {
+ return {
+ isValid: false,
+ error: `يجب اختيار ${min} عنصر على الأقل`
+ };
+ }
+
+ if (max !== undefined && array.length > max) {
+ return {
+ isValid: false,
+ error: `يجب اختيار ${max} عنصر كحد أقصى`
+ };
+ }
+
+ return { isValid: true };
+}
+
+// Conditional validation
+export function validateConditional(
+ value: any,
+ condition: boolean,
+ rules: ValidationRule
+): ValidationResult {
+ if (!condition) {
+ return { isValid: true };
+ }
+
+ return validateField(value, rules);
+}
\ No newline at end of file
diff --git a/app/lib/validation.ts b/app/lib/validation.ts
new file mode 100644
index 0000000..889094c
--- /dev/null
+++ b/app/lib/validation.ts
@@ -0,0 +1,342 @@
+import { VALIDATION, AUTH_LEVELS, USER_STATUS, TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, PAYMENT_STATUS } from './constants';
+
+// User validation
+export function validateUser(data: {
+ name?: string;
+ username?: string;
+ email?: string;
+ password?: string;
+ authLevel?: number;
+ status?: string;
+}) {
+ const errors: Record = {};
+
+ if (data.name !== undefined) {
+ if (!data.name || data.name.trim().length === 0) {
+ errors.name = 'الاسم مطلوب';
+ } else if (data.name.length > VALIDATION.MAX_NAME_LENGTH) {
+ errors.name = `الاسم يجب أن يكون أقل من ${VALIDATION.MAX_NAME_LENGTH} حرف`;
+ }
+ }
+
+ if (data.username !== undefined) {
+ if (!data.username || data.username.trim().length === 0) {
+ errors.username = 'اسم المستخدم مطلوب';
+ } else if (data.username.length < 3) {
+ errors.username = 'اسم المستخدم يجب أن يكون على الأقل 3 أحرف';
+ } else if (!/^[a-zA-Z0-9_]+$/.test(data.username)) {
+ errors.username = 'اسم المستخدم يجب أن يحتوي على أحرف وأرقام فقط';
+ }
+ }
+
+ if (data.email !== undefined) {
+ if (!data.email || data.email.trim().length === 0) {
+ errors.email = 'البريد الإلكتروني مطلوب';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
+ errors.email = 'البريد الإلكتروني غير صحيح';
+ }
+ }
+
+ if (data.password !== undefined) {
+ if (!data.password || data.password.length === 0) {
+ errors.password = 'كلمة المرور مطلوبة';
+ } else if (data.password.length < VALIDATION.MIN_PASSWORD_LENGTH) {
+ errors.password = `كلمة المرور يجب أن تكون على الأقل ${VALIDATION.MIN_PASSWORD_LENGTH} أحرف`;
+ }
+ }
+
+ if (data.authLevel !== undefined) {
+ if (!Object.values(AUTH_LEVELS).includes(data.authLevel as any)) {
+ errors.authLevel = 'مستوى الصلاحية غير صحيح';
+ }
+ }
+
+ if (data.status !== undefined) {
+ if (!Object.values(USER_STATUS).includes(data.status as any)) {
+ errors.status = 'حالة المستخدم غير صحيحة';
+ }
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors,
+ };
+}
+
+// Customer validation
+export function validateCustomer(data: {
+ name?: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+}) {
+ const errors: Record = {};
+
+ if (data.name !== undefined) {
+ if (!data.name || data.name.trim().length === 0) {
+ errors.name = 'اسم العميل مطلوب';
+ } else if (data.name.length > VALIDATION.MAX_NAME_LENGTH) {
+ errors.name = `الاسم يجب أن يكون أقل من ${VALIDATION.MAX_NAME_LENGTH} حرف`;
+ }
+ }
+
+ if (data.phone && data.phone.trim().length > 0) {
+ if (!/^[\+]?[0-9\s\-\(\)]+$/.test(data.phone)) {
+ errors.phone = 'رقم الهاتف غير صحيح';
+ }
+ }
+
+ if (data.email && data.email.trim().length > 0) {
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
+ errors.email = 'البريد الإلكتروني غير صحيح';
+ }
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors,
+ };
+}
+
+// Vehicle validation
+export function validateVehicle(data: {
+ plateNumber?: string;
+ bodyType?: string;
+ manufacturer?: string;
+ model?: string;
+ year?: number;
+ transmission?: string;
+ fuel?: string;
+ cylinders?: number;
+ engineDisplacement?: number;
+ useType?: string;
+ ownerId?: number;
+}) {
+ const errors: Record = {};
+
+ if (data.plateNumber !== undefined) {
+ if (!data.plateNumber || data.plateNumber.trim().length === 0) {
+ errors.plateNumber = 'رقم اللوحة مطلوب';
+ }
+ }
+
+ if (data.bodyType !== undefined) {
+ if (!data.bodyType || data.bodyType.trim().length === 0) {
+ errors.bodyType = 'نوع الهيكل مطلوب';
+ }
+ }
+
+ if (data.manufacturer !== undefined) {
+ if (!data.manufacturer || data.manufacturer.trim().length === 0) {
+ errors.manufacturer = 'الشركة المصنعة مطلوبة';
+ }
+ }
+
+ if (data.model !== undefined) {
+ if (!data.model || data.model.trim().length === 0) {
+ errors.model = 'الموديل مطلوب';
+ }
+ }
+
+ if (data.year !== undefined) {
+ if (!data.year || data.year < VALIDATION.MIN_YEAR || data.year > VALIDATION.MAX_YEAR) {
+ errors.year = `السنة يجب أن تكون بين ${VALIDATION.MIN_YEAR} و ${VALIDATION.MAX_YEAR}`;
+ }
+ }
+
+ if (data.transmission !== undefined) {
+ const validTransmissions = TRANSMISSION_TYPES.map(t => t.value);
+ if (!validTransmissions.includes(data.transmission as any)) {
+ errors.transmission = 'نوع ناقل الحركة غير صحيح';
+ }
+ }
+
+ if (data.fuel !== undefined) {
+ const validFuels = FUEL_TYPES.map(f => f.value);
+ if (!validFuels.includes(data.fuel as any)) {
+ errors.fuel = 'نوع الوقود غير صحيح';
+ }
+ }
+
+ if (data.cylinders !== undefined && data.cylinders !== null) {
+ if (data.cylinders < 1 || data.cylinders > VALIDATION.MAX_CYLINDERS) {
+ errors.cylinders = `عدد الأسطوانات يجب أن يكون بين 1 و ${VALIDATION.MAX_CYLINDERS}`;
+ }
+ }
+
+ if (data.engineDisplacement !== undefined && data.engineDisplacement !== null) {
+ if (data.engineDisplacement <= 0 || data.engineDisplacement > VALIDATION.MAX_ENGINE_DISPLACEMENT) {
+ errors.engineDisplacement = `سعة المحرك يجب أن تكون بين 0.1 و ${VALIDATION.MAX_ENGINE_DISPLACEMENT}`;
+ }
+ }
+
+ if (data.useType !== undefined) {
+ const validUseTypes = USE_TYPES.map(u => u.value);
+ if (!validUseTypes.includes(data.useType as any)) {
+ errors.useType = 'نوع الاستخدام غير صحيح';
+ }
+ }
+
+ if (data.ownerId !== undefined) {
+ if (!data.ownerId || data.ownerId <= 0) {
+ errors.ownerId = 'مالك المركبة مطلوب';
+ }
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors,
+ };
+}
+
+// Maintenance visit validation
+export function validateMaintenanceVisit(data: {
+ vehicleId?: number;
+ customerId?: number;
+ maintenanceJobs?: Array<{typeId: number; job: string; notes?: string}>;
+ description?: string;
+ cost?: number;
+ paymentStatus?: string;
+ kilometers?: number;
+ nextVisitDelay?: number;
+}) {
+ const errors: Record = {};
+
+ if (data.vehicleId !== undefined) {
+ if (!data.vehicleId || data.vehicleId <= 0) {
+ errors.vehicleId = 'المركبة مطلوبة';
+ }
+ }
+
+ if (data.customerId !== undefined) {
+ if (!data.customerId || data.customerId <= 0) {
+ errors.customerId = 'العميل مطلوب';
+ }
+ }
+
+ if (data.maintenanceJobs !== undefined) {
+ if (!data.maintenanceJobs || data.maintenanceJobs.length === 0) {
+ errors.maintenanceJobs = 'يجب إضافة عمل صيانة واحد على الأقل';
+ } else {
+ // Validate each maintenance job
+ const invalidJobs = data.maintenanceJobs.filter(job =>
+ !job.typeId || job.typeId <= 0 || !job.job || job.job.trim().length === 0
+ );
+ if (invalidJobs.length > 0) {
+ errors.maintenanceJobs = 'جميع أعمال الصيانة يجب أن تحتوي على نوع ووصف صحيح';
+ }
+ }
+ }
+
+ if (data.description !== undefined) {
+ if (!data.description || data.description.trim().length === 0) {
+ errors.description = 'وصف الصيانة مطلوب';
+ } else if (data.description.length > VALIDATION.MAX_DESCRIPTION_LENGTH) {
+ errors.description = `الوصف يجب أن يكون أقل من ${VALIDATION.MAX_DESCRIPTION_LENGTH} حرف`;
+ }
+ }
+
+ if (data.cost !== undefined) {
+ if (data.cost === null || data.cost < VALIDATION.MIN_COST || data.cost > VALIDATION.MAX_COST) {
+ errors.cost = `التكلفة يجب أن تكون بين ${VALIDATION.MIN_COST} و ${VALIDATION.MAX_COST}`;
+ }
+ }
+
+ if (data.paymentStatus !== undefined) {
+ if (!Object.values(PAYMENT_STATUS).includes(data.paymentStatus as any)) {
+ errors.paymentStatus = 'حالة الدفع غير صحيحة';
+ }
+ }
+
+ if (data.kilometers !== undefined) {
+ if (data.kilometers === null || data.kilometers < 0) {
+ errors.kilometers = 'عدد الكيلومترات يجب أن يكون رقم موجب';
+ }
+ }
+
+ if (data.nextVisitDelay !== undefined) {
+ if (!data.nextVisitDelay || ![1, 2, 3, 4].includes(data.nextVisitDelay)) {
+ errors.nextVisitDelay = 'فترة الزيارة التالية يجب أن تكون 1، 2، 3، أو 4 أشهر';
+ }
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors,
+ };
+}
+
+// Expense validation
+export function validateExpense(data: {
+ description?: string;
+ category?: string;
+ amount?: number;
+}) {
+ const errors: Record = {};
+
+ if (data.description !== undefined) {
+ if (!data.description || data.description.trim().length === 0) {
+ errors.description = 'وصف المصروف مطلوب';
+ } else if (data.description.length > VALIDATION.MAX_DESCRIPTION_LENGTH) {
+ errors.description = `الوصف يجب أن يكون أقل من ${VALIDATION.MAX_DESCRIPTION_LENGTH} حرف`;
+ }
+ }
+
+ if (data.category !== undefined) {
+ if (!data.category || data.category.trim().length === 0) {
+ errors.category = 'فئة المصروف مطلوبة';
+ }
+ }
+
+ if (data.amount !== undefined) {
+ if (data.amount === null || data.amount <= VALIDATION.MIN_COST || data.amount > VALIDATION.MAX_COST) {
+ errors.amount = `المبلغ يجب أن يكون بين ${VALIDATION.MIN_COST + 0.01} و ${VALIDATION.MAX_COST}`;
+ }
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors,
+ };
+}
+
+// Generic validation helpers
+export function isValidDate(date: any): date is Date {
+ return date instanceof Date && !isNaN(date.getTime());
+}
+
+export function isValidNumber(value: any): value is number {
+ return typeof value === 'number' && !isNaN(value) && isFinite(value);
+}
+
+export function isValidString(value: any, minLength = 1): value is string {
+ return typeof value === 'string' && value.trim().length >= minLength;
+}
+
+export function sanitizeString(value: string): string {
+ return value.trim().replace(/\s+/g, ' ');
+}
+
+export function formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'JOD',
+ }).format(amount);
+}
+
+export function formatDate(date: Date, format: string = 'dd/MM/yyyy'): string {
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const year = date.getFullYear();
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+
+ switch (format) {
+ case 'dd/MM/yyyy':
+ return `${day}/${month}/${year}`;
+ case 'dd/MM/yyyy HH:mm':
+ return `${day}/${month}/${year} ${hours}:${minutes}`;
+ default:
+ return date.toLocaleDateString('ar-SA');
+ }
+}
\ No newline at end of file
diff --git a/app/lib/vehicle-management.server.ts b/app/lib/vehicle-management.server.ts
new file mode 100644
index 0000000..4c12b92
--- /dev/null
+++ b/app/lib/vehicle-management.server.ts
@@ -0,0 +1,368 @@
+import { prisma } from "./db.server";
+import type { Vehicle } from "@prisma/client";
+import type { CreateVehicleData, UpdateVehicleData, VehicleWithOwner, VehicleWithRelations } from "~/types/database";
+
+// Get all vehicles with search and pagination
+export async function getVehicles(
+ searchQuery?: string,
+ page: number = 1,
+ limit: number = 10,
+ ownerId?: number,
+ plateNumber?: string
+): Promise<{
+ vehicles: VehicleWithOwner[];
+ total: number;
+ totalPages: number;
+}> {
+ const offset = (page - 1) * limit;
+
+ // Build where clause for search
+ const whereClause: any = {};
+
+ if (ownerId) {
+ whereClause.ownerId = ownerId;
+ }
+
+ if (plateNumber) {
+ whereClause.plateNumber = { contains: plateNumber.toLowerCase() };
+ }
+
+ if (searchQuery) {
+ const searchLower = searchQuery.toLowerCase();
+ whereClause.OR = [
+ { plateNumber: { contains: searchLower } },
+ { manufacturer: { contains: searchLower } },
+ { model: { contains: searchLower } },
+ { bodyType: { contains: searchLower } },
+ { owner: { name: { contains: searchLower } } },
+ ];
+ }
+
+ const [vehicles, total] = await Promise.all([
+ prisma.vehicle.findMany({
+ where: whereClause,
+ include: {
+ owner: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ skip: offset,
+ take: limit,
+ }),
+ prisma.vehicle.count({ where: whereClause }),
+ ]);
+
+ return {
+ vehicles,
+ total,
+ totalPages: Math.ceil(total / limit),
+ };
+}
+
+// Get vehicle by ID with full relationships
+export async function getVehicleById(id: number): Promise {
+ return await prisma.vehicle.findUnique({
+ where: { id },
+ include: {
+ owner: true,
+ maintenanceVisits: {
+ orderBy: { visitDate: 'desc' },
+ take: 10,
+ },
+ },
+ });
+}
+
+// Create new vehicle
+export async function createVehicle(
+ vehicleData: CreateVehicleData
+): Promise<{ success: boolean; vehicle?: Vehicle; error?: string }> {
+ try {
+ // Check if vehicle with same plate number already exists
+ const existingVehicle = await prisma.vehicle.findUnique({
+ where: { plateNumber: vehicleData.plateNumber },
+ });
+
+ if (existingVehicle) {
+ return { success: false, error: "رقم اللوحة موجود بالفعل" };
+ }
+
+ // Check if owner exists
+ const owner = await prisma.customer.findUnique({
+ where: { id: vehicleData.ownerId },
+ });
+
+ if (!owner) {
+ return { success: false, error: "المالك غير موجود" };
+ }
+
+ // Create vehicle
+ const vehicle = await prisma.vehicle.create({
+ data: {
+ plateNumber: vehicleData.plateNumber.trim(),
+ bodyType: vehicleData.bodyType.trim(),
+ manufacturer: vehicleData.manufacturer.trim(),
+ model: vehicleData.model.trim(),
+ trim: vehicleData.trim?.trim() || null,
+ year: vehicleData.year,
+ transmission: vehicleData.transmission,
+ fuel: vehicleData.fuel,
+ cylinders: vehicleData.cylinders || null,
+ engineDisplacement: vehicleData.engineDisplacement || null,
+ useType: vehicleData.useType,
+ ownerId: vehicleData.ownerId,
+ },
+ });
+
+ return { success: true, vehicle };
+ } catch (error) {
+ console.error("Error creating vehicle:", error);
+ return { success: false, error: "حدث خطأ أثناء إنشاء المركبة" };
+ }
+}
+
+// Update vehicle
+export async function updateVehicle(
+ id: number,
+ vehicleData: UpdateVehicleData
+): Promise<{ success: boolean; vehicle?: Vehicle; error?: string }> {
+ try {
+ // Check if vehicle exists
+ const existingVehicle = await prisma.vehicle.findUnique({
+ where: { id },
+ });
+
+ if (!existingVehicle) {
+ return { success: false, error: "المركبة غير موجودة" };
+ }
+
+ // Check for plate number conflicts with other vehicles
+ if (vehicleData.plateNumber) {
+ const conflictVehicle = await prisma.vehicle.findFirst({
+ where: {
+ AND: [
+ { id: { not: id } },
+ { plateNumber: vehicleData.plateNumber },
+ ],
+ },
+ });
+
+ if (conflictVehicle) {
+ return { success: false, error: "رقم اللوحة موجود بالفعل" };
+ }
+ }
+
+ // Check if new owner exists (if changing owner)
+ if (vehicleData.ownerId && vehicleData.ownerId !== existingVehicle.ownerId) {
+ const owner = await prisma.customer.findUnique({
+ where: { id: vehicleData.ownerId },
+ });
+
+ if (!owner) {
+ return { success: false, error: "المالك الجديد غير موجود" };
+ }
+ }
+
+ // Prepare update data
+ const updateData: any = {};
+ if (vehicleData.plateNumber !== undefined) updateData.plateNumber = vehicleData.plateNumber.trim();
+ if (vehicleData.bodyType !== undefined) updateData.bodyType = vehicleData.bodyType.trim();
+ if (vehicleData.manufacturer !== undefined) updateData.manufacturer = vehicleData.manufacturer.trim();
+ if (vehicleData.model !== undefined) updateData.model = vehicleData.model.trim();
+ if (vehicleData.trim !== undefined) updateData.trim = vehicleData.trim?.trim() || null;
+ if (vehicleData.year !== undefined) updateData.year = vehicleData.year;
+ if (vehicleData.transmission !== undefined) updateData.transmission = vehicleData.transmission;
+ if (vehicleData.fuel !== undefined) updateData.fuel = vehicleData.fuel;
+ if (vehicleData.cylinders !== undefined) updateData.cylinders = vehicleData.cylinders || null;
+ if (vehicleData.engineDisplacement !== undefined) updateData.engineDisplacement = vehicleData.engineDisplacement || null;
+ if (vehicleData.useType !== undefined) updateData.useType = vehicleData.useType;
+ if (vehicleData.ownerId !== undefined) updateData.ownerId = vehicleData.ownerId;
+ if (vehicleData.lastVisitDate !== undefined) updateData.lastVisitDate = vehicleData.lastVisitDate;
+ if (vehicleData.suggestedNextVisitDate !== undefined) updateData.suggestedNextVisitDate = vehicleData.suggestedNextVisitDate;
+
+ // Update vehicle
+ const vehicle = await prisma.vehicle.update({
+ where: { id },
+ data: updateData,
+ });
+
+ return { success: true, vehicle };
+ } catch (error) {
+ console.error("Error updating vehicle:", error);
+ return { success: false, error: "حدث خطأ أثناء تحديث المركبة" };
+ }
+}
+
+// Delete vehicle with relationship handling
+export async function deleteVehicle(
+ id: number
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ // Check if vehicle exists
+ const existingVehicle = await prisma.vehicle.findUnique({
+ where: { id },
+ include: {
+ maintenanceVisits: true,
+ },
+ });
+
+ if (!existingVehicle) {
+ return { success: false, error: "المركبة غير موجودة" };
+ }
+
+ // Check if vehicle has maintenance visits
+ if (existingVehicle.maintenanceVisits.length > 0) {
+ return {
+ success: false,
+ error: `لا يمكن حذف المركبة لأنها تحتوي على ${existingVehicle.maintenanceVisits.length} زيارة صيانة. يرجى حذف الزيارات أولاً`
+ };
+ }
+
+ // Delete vehicle
+ await prisma.vehicle.delete({
+ where: { id },
+ });
+
+ return { success: true };
+ } catch (error) {
+ console.error("Error deleting vehicle:", error);
+ return { success: false, error: "حدث خطأ أثناء حذف المركبة" };
+ }
+}
+
+// Get vehicles for dropdown/select options
+export async function getVehiclesForSelect(ownerId?: number): Promise<{
+ id: number;
+ plateNumber: string;
+ manufacturer: string;
+ model: string;
+ year: number;
+}[]> {
+ const whereClause = ownerId ? { ownerId } : {};
+
+ return await prisma.vehicle.findMany({
+ where: whereClause,
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ },
+ orderBy: { plateNumber: 'asc' },
+ });
+}
+
+// Get vehicle statistics
+export async function getVehicleStats(vehicleId: number): Promise<{
+ totalVisits: number;
+ totalSpent: number;
+ lastVisitDate?: Date;
+ nextSuggestedVisitDate?: Date;
+ averageVisitCost: number;
+} | null> {
+ const vehicle = await prisma.vehicle.findUnique({
+ where: { id: vehicleId },
+ include: {
+ maintenanceVisits: {
+ select: {
+ cost: true,
+ visitDate: true,
+ },
+ orderBy: { visitDate: 'desc' },
+ },
+ },
+ });
+
+ if (!vehicle) return null;
+
+ const totalSpent = vehicle.maintenanceVisits.reduce((sum, visit) => sum + visit.cost, 0);
+ const averageVisitCost = vehicle.maintenanceVisits.length > 0
+ ? totalSpent / vehicle.maintenanceVisits.length
+ : 0;
+ const lastVisitDate = vehicle.maintenanceVisits.length > 0
+ ? vehicle.maintenanceVisits[0].visitDate
+ : undefined;
+
+ return {
+ totalVisits: vehicle.maintenanceVisits.length,
+ totalSpent,
+ lastVisitDate,
+ nextSuggestedVisitDate: vehicle.suggestedNextVisitDate || undefined,
+ averageVisitCost,
+ };
+}
+
+// Search vehicles by plate number or manufacturer (for autocomplete)
+export async function searchVehicles(query: string, limit: number = 10): Promise<{
+ id: number;
+ plateNumber: string;
+ manufacturer: string;
+ model: string;
+ year: number;
+ owner: { id: number; name: string; };
+}[]> {
+ if (!query || query.trim().length < 2) {
+ return [];
+ }
+
+ const searchLower = query.toLowerCase();
+
+ return await prisma.vehicle.findMany({
+ where: {
+ OR: [
+ { plateNumber: { contains: searchLower } },
+ { manufacturer: { contains: searchLower } },
+ { model: { contains: searchLower } },
+ ],
+ },
+ select: {
+ id: true,
+ plateNumber: true,
+ manufacturer: true,
+ model: true,
+ year: true,
+ owner: {
+ select: {
+ id: true,
+ name: true,
+ },
+ },
+ },
+ orderBy: { plateNumber: 'asc' },
+ take: limit,
+ });
+}
+
+// Get vehicles by owner ID
+export async function getVehiclesByOwner(ownerId: number): Promise {
+ return await prisma.vehicle.findMany({
+ where: { ownerId },
+ include: {
+ owner: {
+ select: {
+ id: true,
+ name: true,
+ phone: true,
+ email: true,
+ },
+ },
+ },
+ orderBy: { createdDate: 'desc' },
+ });
+}
+
+
+export async function getMaintenanceVisitsByVehicle(vehicleId: number) {
+ const visits = await prisma.maintenanceVisit.findMany({
+ where: { vehicleId: vehicleId },
+ orderBy: { createdDate: 'desc' },
+ });
+ return visits
+}
\ No newline at end of file
diff --git a/app/root.tsx b/app/root.tsx
new file mode 100644
index 0000000..ff65405
--- /dev/null
+++ b/app/root.tsx
@@ -0,0 +1,84 @@
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+ useLoaderData,
+} from "@remix-run/react";
+import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { getAppSettings, initializeDefaultSettings } from "~/lib/settings-management.server";
+import { SettingsProvider } from "~/contexts/SettingsContext";
+
+import "./tailwind.css";
+
+export const links: LinksFunction = () => [
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Cairo:wght@200;300;400;500;600;700;800;900&display=swap",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@100;200;300;400;500;600;700;800;900&display=swap",
+ },
+];
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Initialize default settings if needed
+ await initializeDefaultSettings();
+ const settings = await getAppSettings();
+ return json({ settings });
+ } catch (error) {
+ console.error('Root loader error:', error);
+ // Return default settings if there's an error
+ return json({
+ settings: {
+ dateFormat: 'ar-SA' as const,
+ currency: 'JOD',
+ numberFormat: 'ar-SA' as const,
+ currencySymbol: 'د.أ',
+ dateDisplayFormat: 'dd/MM/yyyy'
+ }
+ });
+ }
+}
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default function App() {
+ const { settings } = useLoaderData();
+
+ return (
+
+
+
+ );
+}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
new file mode 100644
index 0000000..63bbe5a
--- /dev/null
+++ b/app/routes/_index.tsx
@@ -0,0 +1,22 @@
+import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { redirect } from "@remix-run/node";
+import { getUserId } from "~/lib/auth.server";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "نظام إدارة صيانة السيارات" },
+ { name: "description", content: "نظام شامل لإدارة صيانة السيارات" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const userId = await getUserId(request);
+
+ if (userId) {
+ // User is authenticated, redirect to dashboard
+ return redirect("/dashboard");
+ } else {
+ // User is not authenticated, redirect to signin
+ return redirect("/signin");
+ }
+}
\ No newline at end of file
diff --git a/app/routes/admin.enable-signup.tsx b/app/routes/admin.enable-signup.tsx
new file mode 100644
index 0000000..a8e9da9
--- /dev/null
+++ b/app/routes/admin.enable-signup.tsx
@@ -0,0 +1,66 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { json, redirect } from "@remix-run/node";
+import { Form, useLoaderData } from "@remix-run/react";
+import { requireAuthLevel } from "~/lib/auth-helpers.server";
+import { AUTH_LEVELS } from "~/types/auth";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Only superadmins can access this route
+ const user = await requireAuthLevel(request, AUTH_LEVELS.SUPERADMIN);
+
+ return json({ user });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ // Only superadmins can perform this action
+ await requireAuthLevel(request, AUTH_LEVELS.SUPERADMIN);
+
+ const formData = await request.formData();
+ const action = formData.get("action");
+
+ if (action === "enable_signup") {
+ // Redirect to signup with a special parameter that bypasses the check
+ return redirect("/signup?admin_override=true");
+ }
+
+ return redirect("/admin/enable-signup");
+}
+
+export default function EnableSignup() {
+ const { user } = useLoaderData();
+
+ return (
+
+
+
+
+ تفعيل التسجيل للمسؤولين
+
+
+ مرحباً {user.name}، يمكنك تفعيل صفحة التسجيل مؤقتاً لإنشاء حسابات جديدة.
+
+
+
+
+ الانتقال إلى صفحة التسجيل
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/api.car-dataset.ts b/app/routes/api.car-dataset.ts
new file mode 100644
index 0000000..18c6d9c
--- /dev/null
+++ b/app/routes/api.car-dataset.ts
@@ -0,0 +1,44 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { getManufacturers, getModelsByManufacturer, getBodyType } from "~/lib/car-dataset-management.server";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const url = new URL(request.url);
+ const action = url.searchParams.get("action");
+ const manufacturer = url.searchParams.get("manufacturer");
+ const model = url.searchParams.get("model");
+
+ try {
+ switch (action) {
+ case "manufacturers": {
+ const manufacturers = await getManufacturers();
+ return json({ success: true, data: manufacturers });
+ }
+
+ case "models": {
+ if (!manufacturer) {
+ return json({ success: false, error: "Manufacturer is required" }, { status: 400 });
+ }
+ const models = await getModelsByManufacturer(manufacturer);
+ return json({ success: true, data: models });
+ }
+
+ case "bodyType": {
+ if (!manufacturer || !model) {
+ return json({ success: false, error: "Manufacturer and model are required" }, { status: 400 });
+ }
+ const bodyType = await getBodyType(manufacturer, model);
+ return json({ success: true, data: bodyType });
+ }
+
+ default:
+ return json({ success: false, error: "Invalid action" }, { status: 400 });
+ }
+ } catch (error) {
+ console.error("Car dataset API error:", error);
+ return json(
+ { success: false, error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/routes/api.customers.search.tsx b/app/routes/api.customers.search.tsx
new file mode 100644
index 0000000..eeee127
--- /dev/null
+++ b/app/routes/api.customers.search.tsx
@@ -0,0 +1,20 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { requireUser } from "~/lib/auth.server";
+import { searchCustomers } from "~/lib/customer-management.server";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ await requireUser(request);
+
+ const url = new URL(request.url);
+ const query = url.searchParams.get("q") || "";
+ const limit = parseInt(url.searchParams.get("limit") || "10");
+
+ if (!query || query.trim().length < 2) {
+ return json({ customers: [] });
+ }
+
+ const customers = await searchCustomers(query, limit);
+
+ return json({ customers });
+}
\ No newline at end of file
diff --git a/app/routes/customers.tsx b/app/routes/customers.tsx
new file mode 100644
index 0000000..32122bf
--- /dev/null
+++ b/app/routes/customers.tsx
@@ -0,0 +1,387 @@
+import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useLoaderData, useActionData, useNavigation, useSearchParams } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { requireUser } from "~/lib/auth.server";
+import { useDebounce } from "~/hooks/useDebounce";
+import {
+ getCustomers,
+ createCustomer,
+ updateCustomer,
+ deleteCustomer,
+ getCustomerById
+} from "~/lib/customer-management.server";
+import { validateCustomer } from "~/lib/validation";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { CustomerList } from "~/components/customers/CustomerList";
+import { CustomerForm } from "~/components/customers/CustomerForm";
+import { CustomerDetailsView } from "~/components/customers/CustomerDetailsView";
+import { Modal } from "~/components/ui/Modal";
+import { Button } from "~/components/ui/Button";
+import { Input } from "~/components/ui/Input";
+import { Flex } from "~/components/layout/Flex";
+import type { CustomerWithVehicles } from "~/types/database";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireUser(request);
+
+ const url = new URL(request.url);
+ const searchQuery = url.searchParams.get("search") || "";
+ const page = parseInt(url.searchParams.get("page") || "1");
+ const limit = parseInt(url.searchParams.get("limit") || "10");
+
+ const { customers, total, totalPages } = await getCustomers(searchQuery, page, limit);
+
+ return json({
+ customers,
+ total,
+ totalPages,
+ currentPage: page,
+ searchQuery,
+ user,
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await requireUser(request);
+
+ const formData = await request.formData();
+ const action = formData.get("_action") as string;
+
+ switch (action) {
+ case "create": {
+ const customerData = {
+ name: formData.get("name") as string,
+ phone: formData.get("phone") as string || undefined,
+ email: formData.get("email") as string || undefined,
+ address: formData.get("address") as string || undefined,
+ };
+
+ // Validate customer data
+ const validation = validateCustomer(customerData);
+ if (!validation.isValid) {
+ return json({
+ success: false,
+ errors: validation.errors,
+ action: "create"
+ }, { status: 400 });
+ }
+
+ const result = await createCustomer(customerData);
+
+ if (result.success) {
+ return json({
+ success: true,
+ customer: result.customer,
+ action: "create",
+ message: "تم إنشاء العميل بنجاح"
+ });
+ } else {
+ return json({
+ success: false,
+ error: result.error,
+ action: "create"
+ }, { status: 400 });
+ }
+ }
+
+ case "update": {
+ const id = parseInt(formData.get("id") as string);
+ const customerData = {
+ name: formData.get("name") as string,
+ phone: formData.get("phone") as string || undefined,
+ email: formData.get("email") as string || undefined,
+ address: formData.get("address") as string || undefined,
+ };
+
+ // Validate customer data
+ const validation = validateCustomer(customerData);
+ if (!validation.isValid) {
+ return json({
+ success: false,
+ errors: validation.errors,
+ action: "update"
+ }, { status: 400 });
+ }
+
+ const result = await updateCustomer(id, customerData);
+
+ if (result.success) {
+ return json({
+ success: true,
+ customer: result.customer,
+ action: "update",
+ message: "تم تحديث العميل بنجاح"
+ });
+ } else {
+ return json({
+ success: false,
+ error: result.error,
+ action: "update"
+ }, { status: 400 });
+ }
+ }
+
+ case "delete": {
+ const id = parseInt(formData.get("id") as string);
+
+ const result = await deleteCustomer(id);
+
+ if (result.success) {
+ return json({
+ success: true,
+ action: "delete",
+ message: "تم حذف العميل بنجاح"
+ });
+ } else {
+ return json({
+ success: false,
+ error: result.error,
+ action: "delete"
+ }, { status: 400 });
+ }
+ }
+
+ case "get": {
+ const id = parseInt(formData.get("id") as string);
+ const customer = await getCustomerById(id);
+
+ if (customer) {
+ return json({
+ success: true,
+ customer,
+ action: "get"
+ });
+ } else {
+ return json({
+ success: false,
+ error: "العميل غير موجود",
+ action: "get"
+ }, { status: 404 });
+ }
+ }
+
+ default:
+ return json({
+ success: false,
+ error: "إجراء غير صحيح",
+ action: "unknown"
+ }, { status: 400 });
+ }
+}
+
+export default function CustomersPage() {
+ const { customers, total, totalPages, currentPage, searchQuery, user } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [showViewModal, setShowViewModal] = useState(false);
+ const [selectedCustomer, setSelectedCustomer] = useState(null);
+ const [searchValue, setSearchValue] = useState(searchQuery);
+
+ const isLoading = navigation.state !== "idle";
+
+ // Debounce search value to avoid too many requests
+ const debouncedSearchValue = useDebounce(searchValue, 300);
+
+ // Handle search automatically when debounced value changes
+ useEffect(() => {
+ if (debouncedSearchValue !== searchQuery) {
+ const newSearchParams = new URLSearchParams(searchParams);
+ if (debouncedSearchValue) {
+ newSearchParams.set("search", debouncedSearchValue);
+ } else {
+ newSearchParams.delete("search");
+ }
+ newSearchParams.set("page", "1"); // Reset to first page
+ setSearchParams(newSearchParams);
+ }
+ }, [debouncedSearchValue, searchQuery, searchParams, setSearchParams]);
+
+ // Clear search function
+ const clearSearch = () => {
+ setSearchValue("");
+ };
+
+ // Handle pagination
+ const handlePageChange = (page: number) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set("page", page.toString());
+ setSearchParams(newSearchParams);
+ };
+
+ // Handle create customer
+ const handleCreateCustomer = () => {
+ setSelectedCustomer(null);
+ setShowCreateModal(true);
+ };
+
+ // Handle view customer
+ const handleViewCustomer = (customer: CustomerWithVehicles) => {
+ setSelectedCustomer(customer);
+ setShowViewModal(true);
+ };
+
+ // Handle edit customer
+ const handleEditCustomer = (customer: CustomerWithVehicles) => {
+ setSelectedCustomer(customer);
+ setShowEditModal(true);
+ };
+
+ // Close modals on successful action
+ useEffect(() => {
+ if (actionData?.success && actionData.action === "create") {
+ setShowCreateModal(false);
+ }
+ if (actionData?.success && actionData.action === "update") {
+ setShowEditModal(false);
+ }
+ }, [actionData]);
+
+ return (
+
+
+ {/* Header */}
+
+
+
إدارة العملاء
+
+ إجمالي العملاء: {total}
+
+
+
+
+ إضافة عميل جديد
+
+
+
+ {/* Search */}
+
+
+
+
setSearchValue(e.target.value)}
+ startIcon={
+
+
+
+ }
+ endIcon={
+ searchValue && (
+
+ )
+ }
+ />
+
+ {(searchQuery || debouncedSearchValue !== searchQuery) && (
+
+ {debouncedSearchValue !== searchQuery && (
+
+
+
+
+
+ جاري البحث...
+
+ )}
+
+ )}
+
+
+
+ {/* Action Messages */}
+ {actionData?.success && actionData.message && (
+
+ {actionData.message}
+
+ )}
+
+ {actionData?.error && (
+
+ {actionData.error}
+
+ )}
+
+ {/* Customer List */}
+
+
+ {/* Create Customer Modal */}
+
setShowCreateModal(false)}
+ title="إضافة عميل جديد"
+ >
+ setShowCreateModal(false)}
+ errors={actionData?.action === "create" ? actionData.errors : undefined}
+ isLoading={isLoading}
+ />
+
+
+ {/* View Customer Modal */}
+
setShowViewModal(false)}
+ title={selectedCustomer ? `تفاصيل العميل - ${selectedCustomer.name}` : "تفاصيل العميل"}
+ size="xl"
+ >
+ {selectedCustomer && (
+ {
+ setShowViewModal(false);
+ handleEditCustomer(selectedCustomer);
+ }}
+ onClose={() => setShowViewModal(false)}
+ />
+ )}
+
+
+ {/* Edit Customer Modal */}
+
setShowEditModal(false)}
+ title="تعديل العميل"
+ >
+ {selectedCustomer && (
+ setShowEditModal(false)}
+ errors={actionData?.action === "update" ? actionData.errors : undefined}
+ isLoading={isLoading}
+ />
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx
new file mode 100644
index 0000000..04a5539
--- /dev/null
+++ b/app/routes/dashboard.tsx
@@ -0,0 +1,233 @@
+import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { requireAuthentication } from "~/lib/auth-middleware.server";
+import { getFinancialSummary } from "~/lib/financial-reporting.server";
+import { prisma } from "~/lib/db.server";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { useSettings } from "~/contexts/SettingsContext";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "لوحة التحكم - نظام إدارة صيانة السيارات" },
+ { name: "description", content: "لوحة التحكم الرئيسية لنظام إدارة صيانة السيارات" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireAuthentication(request);
+
+ // Get dashboard statistics
+ const [
+ customersCount,
+ vehiclesCount,
+ maintenanceVisitsCount,
+ financialSummary
+ ] = await Promise.all([
+ prisma.customer.count(),
+ prisma.vehicle.count(),
+ prisma.maintenanceVisit.count(),
+ user.authLevel <= 2 ? getFinancialSummary() : null, // Only for admin and above
+ ]);
+
+ return json({
+ user,
+ stats: {
+ customersCount,
+ vehiclesCount,
+ maintenanceVisitsCount,
+ financialSummary,
+ }
+ });
+}
+
+export default function Dashboard() {
+ const { formatCurrency } = useSettings();
+ const { user, stats } = useLoaderData();
+
+ return (
+
+
+
+
لوحة التحكم
+
مرحباً بك، {user.name}
+
+
+ {/* Statistics Cards */}
+
+ {/* Customers Card */}
+
+
+
+
العملاء
+
+ {stats.customersCount}
+
+
إجمالي العملاء المسجلين
+
+
+
+
+
+ {/* Vehicles Card */}
+
+
+
+
المركبات
+
+ {stats.vehiclesCount}
+
+
إجمالي المركبات المسجلة
+
+
+
+
+
+ {/* Maintenance Visits Card */}
+
+
+
+
زيارات الصيانة
+
+ {stats.maintenanceVisitsCount}
+
+
إجمالي زيارات الصيانة
+
+
+
+
+
+ {/* Financial Summary Card (Admin only) */}
+ {stats.financialSummary && (
+
+
+
+
صافي الربح
+
= 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {formatCurrency(stats.financialSummary.netProfit)}
+
+
+ هامش الربح: {stats.financialSummary.profitMargin.toFixed(1)}%
+
+
+
= 0 ? 'bg-green-100' : 'bg-red-100'
+ }`}>
+
= 0 ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
+
+
+
+
+ )}
+
+
+ {/* Financial Summary Details (Admin only) */}
+ {stats.financialSummary && (
+
+
+
+
+
إجمالي الإيرادات
+
+ {formatCurrency(stats.financialSummary.totalIncome)}
+
+
+ {stats.financialSummary.incomeCount} عملية
+
+
+
+
إجمالي المصروفات
+
+ {formatCurrency(stats.financialSummary.totalExpenses)}
+
+
+ {stats.financialSummary.expenseCount} مصروف
+
+
+
+
صافي الربح
+
= 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {formatCurrency(stats.financialSummary.netProfit)}
+
+
+ {stats.financialSummary.profitMargin.toFixed(1)}% هامش ربح
+
+
+
+
+ )}
+
+ {/* Quick Actions */}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/expenses.tsx b/app/routes/expenses.tsx
new file mode 100644
index 0000000..94ca61a
--- /dev/null
+++ b/app/routes/expenses.tsx
@@ -0,0 +1,498 @@
+import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
+import { json, redirect } from "@remix-run/node";
+import { useLoaderData, useActionData, useNavigation, useSearchParams } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { requireAuth } from "~/lib/auth-middleware.server";
+import { useDebounce } from "~/hooks/useDebounce";
+import {
+ getExpenses,
+ createExpense,
+ updateExpense,
+ deleteExpense,
+ getExpenseById,
+ getExpenseCategories
+} from "~/lib/expense-management.server";
+import { validateExpense } from "~/lib/validation";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { ExpenseForm } from "~/components/expenses/ExpenseForm";
+import { Button } from "~/components/ui/Button";
+import { Input } from "~/components/ui/Input";
+import { Modal } from "~/components/ui/Modal";
+import { DataTable } from "~/components/ui/DataTable";
+import { Flex } from "~/components/layout/Flex";
+import { useSettings } from "~/contexts/SettingsContext";
+import { EXPENSE_CATEGORIES, PAGINATION } from "~/lib/constants";
+import type { Expense } from "@prisma/client";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireAuth(request, 2); // Admin level required
+
+ const url = new URL(request.url);
+ const searchQuery = url.searchParams.get("search") || "";
+ const page = parseInt(url.searchParams.get("page") || "1");
+ const category = url.searchParams.get("category") || "";
+ const dateFrom = url.searchParams.get("dateFrom")
+ ? new Date(url.searchParams.get("dateFrom")!)
+ : undefined;
+ const dateTo = url.searchParams.get("dateTo")
+ ? new Date(url.searchParams.get("dateTo")!)
+ : undefined;
+
+ const { expenses, total, totalPages } = await getExpenses(
+ searchQuery,
+ page,
+ PAGINATION.DEFAULT_PAGE_SIZE,
+ category,
+ dateFrom,
+ dateTo
+ );
+
+ const categories = await getExpenseCategories();
+
+ return json({
+ user,
+ expenses,
+ total,
+ totalPages,
+ currentPage: page,
+ searchQuery,
+ category,
+ dateFrom: dateFrom?.toISOString().split('T')[0] || "",
+ dateTo: dateTo?.toISOString().split('T')[0] || "",
+ categories,
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ await requireAuth(request, 2); // Admin level required
+
+ const formData = await request.formData();
+ const action = formData.get("_action") as string;
+
+ switch (action) {
+ case "create": {
+ const expenseData = {
+ description: formData.get("description") as string,
+ category: formData.get("category") as string,
+ amount: parseFloat(formData.get("amount") as string),
+ expenseDate: formData.get("expenseDate") as string,
+ };
+
+ const validation = validateExpense({
+ description: expenseData.description,
+ category: expenseData.category,
+ amount: expenseData.amount,
+ });
+
+ if (!validation.isValid) {
+ return json({
+ success: false,
+ errors: validation.errors,
+ action: "create"
+ }, { status: 400 });
+ }
+
+ try {
+ const expense = await createExpense({
+ description: expenseData.description,
+ category: expenseData.category,
+ amount: expenseData.amount,
+ expenseDate: expenseData.expenseDate ? new Date(expenseData.expenseDate) : undefined,
+ });
+
+ return json({
+ success: true,
+ expense,
+ action: "create",
+ message: "تم إنشاء المصروف بنجاح"
+ });
+ } catch (error) {
+ return json({
+ success: false,
+ error: "حدث خطأ أثناء إضافة المصروف",
+ action: "create"
+ }, { status: 500 });
+ }
+ }
+
+ case "update": {
+ const id = parseInt(formData.get("id") as string);
+ const expenseData = {
+ description: formData.get("description") as string,
+ category: formData.get("category") as string,
+ amount: parseFloat(formData.get("amount") as string),
+ expenseDate: formData.get("expenseDate") as string,
+ };
+
+ const validation = validateExpense({
+ description: expenseData.description,
+ category: expenseData.category,
+ amount: expenseData.amount,
+ });
+
+ if (!validation.isValid) {
+ return json({
+ success: false,
+ errors: validation.errors,
+ action: "update"
+ }, { status: 400 });
+ }
+
+ try {
+ const expense = await updateExpense(id, {
+ description: expenseData.description,
+ category: expenseData.category,
+ amount: expenseData.amount,
+ expenseDate: expenseData.expenseDate ? new Date(expenseData.expenseDate) : undefined,
+ });
+
+ return json({
+ success: true,
+ expense,
+ action: "update",
+ message: "تم تحديث المصروف بنجاح"
+ });
+ } catch (error) {
+ return json({
+ success: false,
+ error: "حدث خطأ أثناء تحديث المصروف",
+ action: "update"
+ }, { status: 500 });
+ }
+ }
+
+ case "delete": {
+ const id = parseInt(formData.get("id") as string);
+
+ try {
+ await deleteExpense(id);
+ return json({
+ success: true,
+ action: "delete",
+ message: "تم حذف المصروف بنجاح"
+ });
+ } catch (error) {
+ return json({
+ success: false,
+ error: "حدث خطأ أثناء حذف المصروف",
+ action: "delete"
+ }, { status: 500 });
+ }
+ }
+
+ case "get": {
+ const id = parseInt(formData.get("id") as string);
+ const expense = await getExpenseById(id);
+
+ if (expense) {
+ return json({
+ success: true,
+ expense,
+ action: "get"
+ });
+ } else {
+ return json({
+ success: false,
+ error: "المصروف غير موجود",
+ action: "get"
+ }, { status: 404 });
+ }
+ }
+
+ default:
+ return json({
+ success: false,
+ error: "إجراء غير صحيح",
+ action: "unknown"
+ }, { status: 400 });
+ }
+}
+
+export default function ExpensesPage() {
+ const { formatCurrency, formatDate } = useSettings();
+ const {
+ user,
+ expenses,
+ total,
+ totalPages,
+ currentPage,
+ searchQuery,
+ category,
+ dateFrom,
+ dateTo,
+ categories
+ } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [selectedExpense, setSelectedExpense] = useState(null);
+ const [searchValue, setSearchValue] = useState(searchQuery);
+
+ const isLoading = navigation.state !== "idle";
+
+ // Debounce search value to avoid too many requests
+ const debouncedSearchValue = useDebounce(searchValue, 300);
+
+ // Handle search automatically when debounced value changes
+ useEffect(() => {
+ if (debouncedSearchValue !== searchQuery) {
+ const newSearchParams = new URLSearchParams(searchParams);
+ if (debouncedSearchValue) {
+ newSearchParams.set("search", debouncedSearchValue);
+ } else {
+ newSearchParams.delete("search");
+ }
+ newSearchParams.set("page", "1"); // Reset to first page
+ setSearchParams(newSearchParams);
+ }
+ }, [debouncedSearchValue, searchQuery, searchParams, setSearchParams]);
+
+ // Clear search function
+ const clearSearch = () => {
+ setSearchValue("");
+ };
+
+ const handleFilter = (filterType: string, value: string) => {
+ const newParams = new URLSearchParams(searchParams);
+ if (value) {
+ newParams.set(filterType, value);
+ } else {
+ newParams.delete(filterType);
+ }
+ newParams.set("page", "1");
+ setSearchParams(newParams);
+ };
+
+ // Handle pagination
+ const handlePageChange = (page: number) => {
+ const newParams = new URLSearchParams(searchParams);
+ newParams.set("page", page.toString());
+ setSearchParams(newParams);
+ };
+
+ // Handle create expense
+ const handleCreateExpense = () => {
+ setSelectedExpense(null);
+ setShowCreateModal(true);
+ };
+
+ // Handle edit expense
+ const handleEditExpense = (expense: Expense) => {
+ setSelectedExpense(expense);
+ setShowEditModal(true);
+ };
+
+ // Close modals on successful action
+ useEffect(() => {
+ if (actionData?.success && actionData.action === "create") {
+ setShowCreateModal(false);
+ }
+ if (actionData?.success && actionData.action === "update") {
+ setShowEditModal(false);
+ }
+ }, [actionData]);
+
+ const columns = [
+ {
+ key: "description",
+ header: "الوصف",
+ render: (expense: Expense) => expense.description,
+ },
+ {
+ key: "category",
+ header: "الفئة",
+ render: (expense: Expense) => {
+ const categoryLabel = EXPENSE_CATEGORIES.find(c => c.value === expense.category)?.label;
+ return categoryLabel || expense.category;
+ },
+ },
+ {
+ key: "amount",
+ header: "المبلغ",
+ render: (expense: Expense) => formatCurrency(expense.amount),
+ },
+ {
+ key: "expenseDate",
+ header: "تاريخ المصروف",
+ render: (expense: Expense) => formatDate(expense.expenseDate),
+ },
+ {
+ key: "createdDate",
+ header: "تاريخ الإضافة",
+ render: (expense: Expense) => formatDate(expense.createdDate),
+ },
+ {
+ key: "actions",
+ header: "الإجراءات",
+ render: (expense: Expense) => (
+
+ handleEditExpense(expense)}
+ disabled={isLoading}
+ >
+ تعديل
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ {/* Header */}
+
+
+
إدارة المصروفات
+
+ إجمالي المصروفات: {total}
+
+
+
+
+ إضافة مصروف جديد
+
+
+
+ {/* Search */}
+
+
+
+
setSearchValue(e.target.value)}
+ startIcon={
+
+
+
+ }
+ endIcon={
+ searchValue && (
+
+ )
+ }
+ />
+
+ {(searchQuery || debouncedSearchValue !== searchQuery) && (
+
+ {debouncedSearchValue !== searchQuery && (
+
+
+
+
+
+ جاري البحث...
+
+ )}
+
+ )}
+
+
+
+ {/* Filters */}
+
+
+ {/* Action Messages */}
+ {actionData?.success && actionData.message && (
+
+ {actionData.message}
+
+ )}
+
+ {actionData?.error && (
+
+ {actionData.error}
+
+ )}
+
+ {/* Expenses Table */}
+
+
+ {/* Create Expense Modal */}
+
setShowCreateModal(false)}
+ title="إضافة مصروف جديد"
+ >
+ setShowCreateModal(false)}
+ errors={actionData?.action === "create" ? actionData.errors : undefined}
+ isLoading={isLoading}
+ />
+
+
+ {/* Edit Expense Modal */}
+
setShowEditModal(false)}
+ title="تعديل المصروف"
+ >
+ {selectedExpense && (
+ setShowEditModal(false)}
+ errors={actionData?.action === "update" ? actionData.errors : undefined}
+ isLoading={isLoading}
+ />
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/financial-reports.tsx b/app/routes/financial-reports.tsx
new file mode 100644
index 0000000..88a636a
--- /dev/null
+++ b/app/routes/financial-reports.tsx
@@ -0,0 +1,396 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useLoaderData, useSearchParams } from "@remix-run/react";
+import { requireAuth } from "~/lib/auth-middleware.server";
+import {
+ getFinancialSummary,
+ getMonthlyFinancialData,
+ getIncomeByMaintenanceType,
+ getExpenseBreakdown,
+ getTopCustomersByRevenue,
+ getFinancialTrends
+} from "~/lib/financial-reporting.server";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { Button } from "~/components/ui/Button";
+import { Input } from "~/components/ui/Input";
+import { useSettings } from "~/contexts/SettingsContext";
+
+// Arabic Gregorian month names
+const ARABIC_GREGORIAN_MONTHS: Record = {
+ "1": "كانون الثاني",
+ "2": "شباط",
+ "3": "آذار",
+ "4": "نيسان",
+ "5": "أيار",
+ "6": "حزيران",
+ "7": "تموز",
+ "8": "آب",
+ "9": "أيلول",
+ "10": "تشرين الأول",
+ "11": "تشرين الثاني",
+ "12": "كانون الأول",
+};
+
+function getArabicMonthName(monthNumber: string): string {
+ return `${ARABIC_GREGORIAN_MONTHS[monthNumber]} (${monthNumber})`;
+}
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireAuth(request, 2); // Admin level required
+
+ const url = new URL(request.url);
+ const dateFrom = url.searchParams.get("dateFrom")
+ ? new Date(url.searchParams.get("dateFrom")!)
+ : undefined;
+ const dateTo = url.searchParams.get("dateTo")
+ ? new Date(url.searchParams.get("dateTo")!)
+ : undefined;
+
+ // Get all financial data
+ const [
+ financialSummary,
+ monthlyData,
+ incomeByType,
+ expenseBreakdown,
+ topCustomers,
+ trends
+ ] = await Promise.all([
+ getFinancialSummary(dateFrom, dateTo),
+ getMonthlyFinancialData(),
+ getIncomeByMaintenanceType(dateFrom, dateTo),
+ getExpenseBreakdown(dateFrom, dateTo),
+ getTopCustomersByRevenue(10, dateFrom, dateTo),
+ dateFrom && dateTo ? getFinancialTrends(dateFrom, dateTo) : null,
+ ]);
+
+ return json({
+ user,
+ financialSummary,
+ monthlyData,
+ incomeByType,
+ expenseBreakdown,
+ topCustomers,
+ trends,
+ dateFrom: dateFrom?.toISOString().split('T')[0] || "",
+ dateTo: dateTo?.toISOString().split('T')[0] || "",
+ });
+}
+
+export default function FinancialReportsPage() {
+ const { formatCurrency } = useSettings();
+ const {
+ user,
+ financialSummary,
+ monthlyData,
+ incomeByType,
+ expenseBreakdown,
+ topCustomers,
+ trends,
+ dateFrom,
+ dateTo
+ } = useLoaderData();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const handleDateFilter = (type: string, value: string) => {
+ const newParams = new URLSearchParams(searchParams);
+ if (value) {
+ newParams.set(type, value);
+ } else {
+ newParams.delete(type);
+ }
+ setSearchParams(newParams);
+ };
+
+ const clearFilters = () => {
+ setSearchParams({});
+ };
+
+ return (
+
+
+
+
+
التقارير المالية
+
تحليل شامل للوضع المالي للمؤسسة
+
+
+
+ {/* Date Filters */}
+
+
+
+
+ من تاريخ
+
+ handleDateFilter("dateFrom", e.target.value)}
+ />
+
+
+
+ إلى تاريخ
+
+ handleDateFilter("dateTo", e.target.value)}
+ />
+
+
+ مسح الفلاتر
+
+
+
+
+ {/* Financial Summary Cards */}
+
+
+
+
+
إجمالي الإيرادات
+
+ {formatCurrency(financialSummary.totalIncome)}
+
+
+ {financialSummary.incomeCount} عملية
+
+
+
+
+
+
+
+
+
+
إجمالي المصروفات
+
+ {formatCurrency(financialSummary.totalExpenses)}
+
+
+ {financialSummary.expenseCount} مصروف
+
+
+
+
+
+
+
+
+
+
صافي الربح
+
= 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {formatCurrency(financialSummary.netProfit)}
+
+
+ هامش الربح: {financialSummary.profitMargin.toFixed(1)}%
+
+
+
= 0 ? 'bg-green-100' : 'bg-red-100'
+ }`}>
+
= 0 ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
+
+
+
+
+
+
+
+
+
متوسط الإيراد الشهري
+
+ {formatCurrency(monthlyData.reduce((sum, month) => sum + month.income, 0) / Math.max(monthlyData.length, 1))}
+
+
+ آخر 12 شهر
+
+
+
+
+
+
+
+ {/* Trends (if date range is selected) */}
+ {trends && (
+
+
مقارنة الفترات
+
+
+
نمو الإيرادات
+
= 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {trends.trends.incomeGrowth >= 0 ? '+' : ''}{trends.trends.incomeGrowth.toFixed(1)}%
+
+
+
+
نمو المصروفات
+
+ {trends.trends.expenseGrowth >= 0 ? '+' : ''}{trends.trends.expenseGrowth.toFixed(1)}%
+
+
+
+
نمو الأرباح
+
= 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {trends.trends.profitGrowth >= 0 ? '+' : ''}{trends.trends.profitGrowth.toFixed(1)}%
+
+
+
+
+ )}
+
+ {/* Charts and Breakdowns */}
+
+ {/* Income by Maintenance Type */}
+
+
الإيرادات حسب نوع الصيانة
+
+ {incomeByType.map((item, index) => (
+
+
+
+
+ {item.category}
+
+
+ {item.percentage.toFixed(1)}%
+
+
+
+
+
+ {formatCurrency(item.amount)}
+
+
+ {item.count} عملية
+
+
+
+
+ ))}
+
+
+
+ {/* Expense Breakdown */}
+
+
تفصيل المصروفات
+
+ {expenseBreakdown.map((item, index) => (
+
+
+
+
+ {item.category}
+
+
+ {item.percentage.toFixed(1)}%
+
+
+
+
+
+ {formatCurrency(item.amount)}
+
+
+ {item.count} مصروف
+
+
+
+
+ ))}
+
+
+
+
+ {/* Top Customers and Monthly Data */}
+
+ {/* Top Customers */}
+
+
أفضل العملاء
+
+ {topCustomers.map((customer, index) => (
+
+
+
+
+ {index + 1}
+
+
+
+
{customer.customerName}
+
{customer.visitCount} زيارة
+
+
+
+
+ {formatCurrency(customer.totalRevenue)}
+
+
+
+ ))}
+
+
+
+ {/* Monthly Performance */}
+
+
الأداء الشهري
+
+ {monthlyData.slice(-6).reverse().map((month, index) => (
+
+
+
+ {getArabicMonthName(month.month)} {month.year}
+
+ = 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {formatCurrency(month.profit)}
+
+
+
+
+ الإيرادات:
+
+ {formatCurrency(month.income)}
+
+
+
+ المصروفات:
+
+ {formatCurrency(month.expenses)}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/financial.tsx b/app/routes/financial.tsx
new file mode 100644
index 0000000..07b791c
--- /dev/null
+++ b/app/routes/financial.tsx
@@ -0,0 +1,66 @@
+import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { protectFinancialRoute } from "~/lib/auth-middleware.server";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { Text, Card, CardHeader, CardBody } from "~/components/ui";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "الإدارة المالية - نظام إدارة صيانة السيارات" },
+ { name: "description", content: "إدارة الأمور المالية والمصروفات" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await protectFinancialRoute(request);
+ return json({ user });
+}
+
+export default function Financial() {
+ const { user } = useLoaderData();
+
+ return (
+
+
+
+
+ الإدارة المالية
+
+
+ إدارة الإيرادات والمصروفات والتقارير المالية
+
+
+
+
+
+ التقارير المالية
+
+
+
+
+
+
+
+ لا توجد بيانات مالية
+
+
+ سيتم إضافة وظائف الإدارة المالية في المهام القادمة
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx
new file mode 100644
index 0000000..def46f1
--- /dev/null
+++ b/app/routes/logout.tsx
@@ -0,0 +1,11 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { redirect } from "@remix-run/node";
+import { logout } from "~/lib/auth.server";
+
+export async function action({ request }: ActionFunctionArgs) {
+ return logout(request);
+}
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ return logout(request);
+}
\ No newline at end of file
diff --git a/app/routes/maintenance-visits.tsx b/app/routes/maintenance-visits.tsx
new file mode 100644
index 0000000..dc58676
--- /dev/null
+++ b/app/routes/maintenance-visits.tsx
@@ -0,0 +1,552 @@
+import { useState, useEffect } from "react";
+import type { LoaderFunctionArgs, ActionFunctionArgs, MetaFunction } from "@remix-run/node";
+import { useDebounce } from "~/hooks/useDebounce";
+import { json, redirect } from "@remix-run/node";
+import { useLoaderData, useActionData, useSearchParams, useNavigation } from "@remix-run/react";
+import { useSettings } from "~/contexts/SettingsContext";
+import { protectMaintenanceRoute } from "~/lib/auth-middleware.server";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { Text } from "~/components/ui/Text";
+import { Button } from "~/components/ui/Button";
+import { Modal } from "~/components/ui/Modal";
+import { Input } from "~/components/ui/Input";
+import { Select } from "~/components/ui/Select";
+import { Flex } from "~/components/layout/Flex";
+import { MaintenanceVisitList } from "~/components/maintenance-visits/MaintenanceVisitList";
+import { MaintenanceVisitForm } from "~/components/maintenance-visits/MaintenanceVisitForm";
+import { MaintenanceVisitDetailsView } from "~/components/maintenance-visits/MaintenanceVisitDetailsView";
+import {
+ getMaintenanceVisits,
+ createMaintenanceVisit,
+ updateMaintenanceVisit,
+ deleteMaintenanceVisit,
+ getMaintenanceVisitById
+} from "~/lib/maintenance-visit-management.server";
+import { getCustomers } from "~/lib/customer-management.server";
+import { getVehicles } from "~/lib/vehicle-management.server";
+import { getMaintenanceTypesForSelect } from "~/lib/maintenance-type-management.server";
+import { validateMaintenanceVisit } from "~/lib/validation";
+import type { MaintenanceVisitWithRelations } from "~/types/database";
+import { PAGINATION } from "~/lib/constants";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "زيارات الصيانة - نظام إدارة صيانة السيارات" },
+ { name: "description", content: "إدارة زيارات الصيانة وتسجيل الأعمال المنجزة" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await protectMaintenanceRoute(request);
+
+ const url = new URL(request.url);
+ const searchQuery = url.searchParams.get("search") || "";
+ const paymentStatusFilter = url.searchParams.get("paymentStatus") || "";
+ const customerId = url.searchParams.get("customerId") ? parseInt(url.searchParams.get("customerId")!) : undefined;
+ const vehicleId = url.searchParams.get("vehicleId") ? parseInt(url.searchParams.get("vehicleId")!) : undefined;
+ const page = parseInt(url.searchParams.get("page") || "1");
+ const limit = parseInt(url.searchParams.get("limit") || PAGINATION.DEFAULT_PAGE_SIZE.toString());
+
+ // Get maintenance visits with filters
+ const { visits, total, totalPages } = await getMaintenanceVisits(
+ searchQuery,
+ page,
+ limit,
+ vehicleId,
+ customerId
+ );
+
+ // Get customers, vehicles, and maintenance types for the form
+ const { customers } = await getCustomers("", 1, 1000); // Get all customers
+ const { vehicles } = await getVehicles("", 1, 1000); // Get all vehicles
+ const maintenanceTypes = await getMaintenanceTypesForSelect(); // Get all maintenance types
+
+ return json({
+ user,
+ visits,
+ customers,
+ vehicles,
+ maintenanceTypes,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages,
+ },
+ searchQuery,
+ paymentStatusFilter,
+ customerId,
+ vehicleId,
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await protectMaintenanceRoute(request);
+
+ const formData = await request.formData();
+ const intent = formData.get("intent") as string;
+
+ try {
+ switch (intent) {
+ case "create": {
+ // Debug: Log all form data
+ console.log("Form data received:");
+ for (const [key, value] of formData.entries()) {
+ console.log(`${key}: ${value}`);
+ }
+
+ // Check if the required fields are missing from form data
+ if (!formData.has("customerId")) {
+ console.error("customerId field is missing from form data!");
+ return json({
+ success: false,
+ errors: { customerId: "العميل مطلوب" }
+ }, { status: 400 });
+ }
+ if (!formData.has("vehicleId")) {
+ console.error("vehicleId field is missing from form data!");
+ return json({
+ success: false,
+ errors: { vehicleId: "المركبة مطلوبة" }
+ }, { status: 400 });
+ }
+ if (!formData.has("description")) {
+ console.error("description field is missing from form data!");
+ return json({
+ success: false,
+ errors: { description: "وصف الصيانة مطلوب" }
+ }, { status: 400 });
+ }
+ if (!formData.has("cost")) {
+ console.error("cost field is missing from form data!");
+ return json({
+ success: false,
+ errors: { cost: "التكلفة مطلوبة" }
+ }, { status: 400 });
+ }
+ if (!formData.has("kilometers")) {
+ console.error("kilometers field is missing from form data!");
+ return json({
+ success: false,
+ errors: { kilometers: "عدد الكيلومترات مطلوب" }
+ }, { status: 400 });
+ }
+
+ const vehicleIdRaw = formData.get("vehicleId") as string;
+ const customerIdRaw = formData.get("customerId") as string;
+ const maintenanceJobsRaw = formData.get("maintenanceJobsData") as string;
+ const costRaw = formData.get("cost") as string;
+ const kilometersRaw = formData.get("kilometers") as string;
+ const nextVisitDelayRaw = formData.get("nextVisitDelay") as string;
+
+ console.log("Raw values:", {
+ vehicleIdRaw,
+ customerIdRaw,
+ maintenanceJobsRaw,
+ costRaw,
+ kilometersRaw,
+ nextVisitDelayRaw
+ });
+
+ // Parse maintenance jobs
+ let maintenanceJobs;
+ try {
+ maintenanceJobs = JSON.parse(maintenanceJobsRaw || "[]");
+ // Jobs are already in the correct format from the form
+ } catch {
+ maintenanceJobs = [];
+ }
+
+ // Check for empty strings and convert them to undefined for proper validation
+ const data = {
+ vehicleId: vehicleIdRaw && vehicleIdRaw.trim() !== "" ? parseInt(vehicleIdRaw) : undefined,
+ customerId: customerIdRaw && customerIdRaw.trim() !== "" ? parseInt(customerIdRaw) : undefined,
+ maintenanceJobs,
+ description: formData.get("description") as string,
+ cost: costRaw && costRaw.trim() !== "" ? parseFloat(costRaw) : undefined,
+ paymentStatus: formData.get("paymentStatus") as string,
+ kilometers: kilometersRaw && kilometersRaw.trim() !== "" ? parseInt(kilometersRaw) : undefined,
+ nextVisitDelay: nextVisitDelayRaw && nextVisitDelayRaw.trim() !== "" ? parseInt(nextVisitDelayRaw) : undefined,
+ visitDate: formData.get("visitDate") ? new Date(formData.get("visitDate") as string) : new Date(),
+ };
+
+ console.log("Parsed data:", data);
+
+ const validation = validateMaintenanceVisit(data);
+ console.log("Validation result:", validation);
+
+ if (!validation.isValid) {
+ return json({ success: false, errors: validation.errors }, { status: 400 });
+ }
+
+ await createMaintenanceVisit(data);
+ return json({ success: true, message: "تم إنشاء زيارة الصيانة بنجاح" });
+ }
+
+ case "update": {
+ const id = parseInt(formData.get("id") as string);
+ const maintenanceJobsRaw = formData.get("maintenanceJobsData") as string;
+
+ // Parse maintenance jobs
+ let maintenanceJobs;
+ try {
+ maintenanceJobs = JSON.parse(maintenanceJobsRaw || "[]");
+ // Jobs are already in the correct format from the form
+ } catch {
+ maintenanceJobs = [];
+ }
+
+ const data = {
+ maintenanceJobs,
+ description: formData.get("description") as string,
+ cost: parseFloat(formData.get("cost") as string),
+ paymentStatus: formData.get("paymentStatus") as string,
+ kilometers: parseInt(formData.get("kilometers") as string),
+ nextVisitDelay: parseInt(formData.get("nextVisitDelay") as string),
+ visitDate: formData.get("visitDate") ? new Date(formData.get("visitDate") as string) : undefined,
+ };
+
+ const validation = validateMaintenanceVisit(data);
+ if (!validation.isValid) {
+ return json({ success: false, errors: validation.errors }, { status: 400 });
+ }
+
+ await updateMaintenanceVisit(id, data);
+ return json({ success: true, message: "تم تحديث زيارة الصيانة بنجاح" });
+ }
+
+ case "delete": {
+ const id = parseInt(formData.get("id") as string);
+ await deleteMaintenanceVisit(id);
+ return json({ success: true, message: "تم حذف زيارة الصيانة بنجاح" });
+ }
+
+ default:
+ return json({ success: false, error: "إجراء غير صحيح" }, { status: 400 });
+ }
+ } catch (error) {
+ console.error("Maintenance visit action error:", error);
+ return json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : "حدث خطأ غير متوقع"
+ },
+ { status: 500 }
+ );
+ }
+}
+
+export default function MaintenanceVisits() {
+ const {
+ user,
+ visits,
+ customers,
+ vehicles,
+ maintenanceTypes,
+ pagination,
+ searchQuery,
+ paymentStatusFilter,
+ customerId,
+ vehicleId
+ } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [showForm, setShowForm] = useState(false);
+ const [showViewModal, setShowViewModal] = useState(false);
+ const [editingVisit, setEditingVisit] = useState(null);
+ const [viewingVisit, setViewingVisit] = useState(null);
+ const [searchValue, setSearchValue] = useState(searchQuery);
+ const [selectedPaymentStatus, setSelectedPaymentStatus] = useState(paymentStatusFilter);
+ const [justOpenedForm, setJustOpenedForm] = useState(false);
+
+ // Debounce search values to avoid too many requests
+ const debouncedSearchValue = useDebounce(searchValue, 300);
+ const debouncedPaymentStatus = useDebounce(selectedPaymentStatus, 300);
+
+ const handleEdit = (visit: MaintenanceVisitWithRelations) => {
+ console.log("Opening edit form for visit:", visit.id);
+ setEditingVisit(visit);
+ setJustOpenedForm(true);
+ setShowForm(true);
+ };
+
+ const handleView = (visit: MaintenanceVisitWithRelations) => {
+ setViewingVisit(visit);
+ setShowViewModal(true);
+ };
+
+ const handleCloseForm = () => {
+ setShowForm(false);
+ setEditingVisit(null);
+ };
+
+ const handleOpenCreateForm = () => {
+ console.log("Opening create form");
+ setEditingVisit(null);
+ setJustOpenedForm(true);
+ setShowForm(true);
+ };
+
+ const handleCloseViewModal = () => {
+ setShowViewModal(false);
+ setViewingVisit(null);
+ };
+
+ // Handle search automatically when debounced values change
+ useEffect(() => {
+ if (debouncedSearchValue !== searchQuery || debouncedPaymentStatus !== paymentStatusFilter) {
+ const newSearchParams = new URLSearchParams(searchParams);
+ if (debouncedSearchValue) {
+ newSearchParams.set("search", debouncedSearchValue);
+ } else {
+ newSearchParams.delete("search");
+ }
+ if (debouncedPaymentStatus) {
+ newSearchParams.set("paymentStatus", debouncedPaymentStatus);
+ } else {
+ newSearchParams.delete("paymentStatus");
+ }
+ newSearchParams.set("page", "1"); // Reset to first page
+ setSearchParams(newSearchParams);
+ }
+ }, [debouncedSearchValue, debouncedPaymentStatus, searchQuery, paymentStatusFilter, searchParams, setSearchParams]);
+
+ // Clear search function
+ const clearSearch = () => {
+ setSearchValue("");
+ setSelectedPaymentStatus("");
+ };
+
+ // Handle pagination
+ const handlePageChange = (page: number) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set("page", page.toString());
+ setSearchParams(newSearchParams);
+ };
+
+ // Track when we've just completed a form submission
+ const [wasSubmitting, setWasSubmitting] = useState(false);
+
+ // Track navigation state changes
+ useEffect(() => {
+ if (navigation.state === "submitting") {
+ setWasSubmitting(true);
+ } else if (navigation.state === "idle" && wasSubmitting) {
+ // We just finished submitting
+ setWasSubmitting(false);
+
+ // Close form only if the submission was successful
+ if (actionData?.success && showForm) {
+ console.log("Closing form after successful submission");
+ setShowForm(false);
+ setEditingVisit(null);
+ }
+ }
+ }, [navigation.state, wasSubmitting, actionData?.success, showForm]);
+
+ // Reset the justOpenedForm flag after a short delay
+ useEffect(() => {
+ if (justOpenedForm) {
+ console.log("Setting timer to reset justOpenedForm flag");
+ const timer = setTimeout(() => {
+ console.log("Resetting justOpenedForm flag");
+ setJustOpenedForm(false);
+ }, 500);
+ return () => clearTimeout(timer);
+ }
+ }, [justOpenedForm]);
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ زيارات الصيانة
+
+
+
+ إدارة زيارات الصيانة وتسجيل الأعمال المنجزة
+
+ {customerId && (
+
+ مفلترة حسب العميل
+
+ )}
+ {vehicleId && (
+
+ مفلترة حسب المركبة
+
+ )}
+
+
+
+ إضافة زيارة صيانة
+
+
+
+ {/* Success/Error Messages */}
+ {actionData?.success && (
+
+ {actionData.message}
+
+ )}
+ {actionData?.error && (
+
+ {actionData.error}
+
+ )}
+
+ {/* Search and Filters */}
+
+
+
+
setSearchValue(e.target.value)}
+ startIcon={
+
+
+
+ }
+ endIcon={
+ searchValue && (
+
setSearchValue("")}
+ className="text-gray-400 hover:text-gray-600"
+ type="button"
+ >
+
+
+
+
+ )
+ }
+ />
+
+
+
+ setSelectedPaymentStatus(e.target.value)}
+ options={[
+ { value: "", label: "جميع حالات الدفع" },
+ { value: "paid", label: "مدفوع" },
+ { value: "pending", label: "معلق" },
+ { value: "partial", label: "مدفوع جزئياً" },
+ { value: "cancelled", label: "ملغي" },
+ ]}
+ placeholder="جميع حالات الدفع"
+ />
+
+
+ {(searchQuery || paymentStatusFilter || debouncedSearchValue !== searchQuery || debouncedPaymentStatus !== paymentStatusFilter) && (
+
+ {(debouncedSearchValue !== searchQuery || debouncedPaymentStatus !== paymentStatusFilter) && (
+
+
+
+
+
+ جاري البحث...
+
+ )}
+ {(searchQuery || paymentStatusFilter) && (
+
+ مسح البحث
+
+ )}
+
+ )}
+
+
+
+ {/* Maintenance Visits List */}
+
+
+ {/* Pagination */}
+ {pagination.totalPages > 1 && (
+
+
+
handlePageChange(pagination.page - 1)}
+ disabled={pagination.page === 1}
+ >
+ السابق
+
+
+
+ {Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
+ const page = i + 1;
+ return (
+ handlePageChange(page)}
+ >
+ {page}
+
+ );
+ })}
+
+
+
handlePageChange(pagination.page + 1)}
+ disabled={pagination.page === pagination.totalPages}
+ >
+ التالي
+
+
+
+ )}
+
+ {/* Form Modal */}
+
+
+
+
+ {/* View Modal */}
+
+ {viewingVisit && (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx
new file mode 100644
index 0000000..841c030
--- /dev/null
+++ b/app/routes/settings.tsx
@@ -0,0 +1,314 @@
+import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
+import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react';
+import { useState } from 'react';
+import { requireAuth } from '~/lib/auth-middleware.server';
+import { DashboardLayout } from '~/components/layout/DashboardLayout';
+import {
+ getAppSettings,
+ updateSettings,
+ type AppSettings,
+ initializeDefaultSettings
+} from '~/lib/settings-management.server';
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireAuth(request, 2); // Admin level required (1=superadmin, 2=admin)
+
+ try {
+ // Initialize default settings if needed
+ await initializeDefaultSettings();
+ const settings = await getAppSettings();
+ return json({ user, settings, success: true });
+ } catch (error) {
+ console.error('Settings loader error:', error);
+ return json({
+ user,
+ settings: null,
+ success: false,
+ error: 'Failed to load settings'
+ });
+ }
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ await requireAuth(request, 2); // Admin level required
+
+ const formData = await request.formData();
+ const intent = formData.get('intent');
+
+ if (intent === 'updateSettings') {
+ try {
+ const settings: Partial = {
+ dateFormat: formData.get('dateFormat') as 'ar-SA' | 'en-US',
+ currency: formData.get('currency') as string,
+ numberFormat: formData.get('numberFormat') as 'ar-SA' | 'en-US',
+ currencySymbol: formData.get('currencySymbol') as string,
+ dateDisplayFormat: formData.get('dateDisplayFormat') as string,
+ };
+
+ await updateSettings(settings);
+
+ return json({
+ success: true,
+ message: 'Settings updated successfully'
+ });
+ } catch (error) {
+ console.error('Settings update error:', error);
+ return json({
+ success: false,
+ error: 'Failed to update settings'
+ });
+ }
+ }
+
+ return json({ success: false, error: 'Invalid action' });
+}
+
+export default function SettingsPage() {
+ const { user, settings, success, error } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
+
+ const [formData, setFormData] = useState(
+ settings || {
+ dateFormat: 'ar-SA',
+ currency: 'JOD',
+ numberFormat: 'ar-SA',
+ currencySymbol: 'د.أ',
+ dateDisplayFormat: 'dd/MM/yyyy'
+ }
+ );
+
+ const handleInputChange = (key: keyof AppSettings, value: string) => {
+ setFormData(prev => ({ ...prev, [key]: value }));
+ };
+
+ if (!success || !settings) {
+ return (
+
+
+
+
خطأ في تحميل الإعدادات
+
{error || 'Failed to load settings'}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
⚙️ إعدادات النظام
+
تكوين إعدادات التطبيق العامة
+
+
+
+ {actionData?.success && (
+
+
✅ {actionData.message}
+
+ )}
+
+ {actionData?.error && (
+
+ )}
+
+
+
+
+ {/* Date Format Settings */}
+
+
📅 إعدادات التاريخ
+
+
+
+
+ تنسيق التاريخ
+
+ handleInputChange('dateFormat', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ عربي (ar-SA) - ٢٠٢٥/١١/٩
+ إنجليزي (en-US) - 11/9/2025
+
+
+
+
+
+ نمط عرض التاريخ
+
+ handleInputChange('dateDisplayFormat', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ يوم/شهر/سنة (09/11/2025)
+ شهر/يوم/سنة (11/09/2025)
+ سنة-شهر-يوم (2025-11-09)
+
+
+
+
+
+
+ معاينة: {new Date().toLocaleDateString(formData.dateFormat)}
+
+
+
+
+ {/* Currency Settings */}
+
+
💰 إعدادات العملة
+
+
+
+
+ رمز العملة
+
+ handleInputChange('currency', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ دينار أردني (JOD)
+ دولار أمريكي (USD)
+ يورو (EUR)
+ ريال سعودي (SAR)
+ درهم إماراتي (AED)
+
+
+
+
+
+ رمز العملة المعروض
+
+ handleInputChange('currencySymbol', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ placeholder="د.أ"
+ />
+
+
+
+
+
+ معاينة: {(1234.56).toLocaleString(formData.numberFormat, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ })} {formData.currencySymbol}
+
+
+
+
+ {/* Number Format Settings */}
+
+
🔢 إعدادات الأرقام
+
+
+
+ تنسيق الأرقام
+
+ handleInputChange('numberFormat', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ عربي (ar-SA) - ١٬٢٣٤٫٥٦
+ إنجليزي (en-US) - 1,234.56
+
+
+
+
+
+ معاينة الأرقام: {(123456.789).toLocaleString(formData.numberFormat)}
+
+
+ معاينة الكيلومترات: {(45000).toLocaleString(formData.numberFormat)} كم
+
+
+
+
+ {/* Action Buttons */}
+
+ setFormData(settings)}
+ className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors"
+ disabled={isSubmitting}
+ >
+ إعادة تعيين
+
+
+ {isSubmitting ? 'جاري الحفظ...' : 'حفظ الإعدادات'}
+
+
+
+
+
+
+ {/* Settings Preview Section */}
+
+
+
👁️ معاينة الإعدادات
+
+
+
+
+
التاريخ والوقت
+
+ التاريخ: {new Date().toLocaleDateString(formData.dateFormat)}
+
+
+ التاريخ والوقت: {new Date().toLocaleString(formData.dateFormat)}
+
+
+
+
+
العملة والأرقام
+
+ السعر: {(250.75).toLocaleString(formData.numberFormat, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ })} {formData.currencySymbol}
+
+
+ الكيلومترات: {(87500).toLocaleString(formData.numberFormat)} كم
+
+
+
+
+
أرقام كبيرة
+
+ المبلغ الإجمالي: {(1234567.89).toLocaleString(formData.numberFormat, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ })} {formData.currencySymbol}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/signin.tsx b/app/routes/signin.tsx
new file mode 100644
index 0000000..c1f9563
--- /dev/null
+++ b/app/routes/signin.tsx
@@ -0,0 +1,282 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { json, redirect } from "@remix-run/node";
+import { Form, Link, useActionData, useLoaderData, useNavigation } from "@remix-run/react";
+import { validateSignIn } from "~/lib/auth-helpers.server";
+import { createUserSession, getUserId } from "~/lib/auth.server";
+import { AUTH_ERRORS } from "~/lib/auth-constants";
+import type { SignInFormData } from "~/types/auth";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "تسجيل الدخول - نظام إدارة صيانة السيارات" },
+ { name: "description", content: "تسجيل الدخول إلى نظام إدارة صيانة السيارات" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Import the redirect middleware
+ const { redirectIfAuthenticated } = await import("~/lib/auth-middleware.server");
+ await redirectIfAuthenticated(request);
+
+ const url = new URL(request.url);
+ const redirectTo = url.searchParams.get("redirectTo") || "/dashboard";
+ const error = url.searchParams.get("error");
+
+ return json({ redirectTo, error });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const formData = await request.formData();
+ const usernameOrEmail = formData.get("usernameOrEmail");
+ const password = formData.get("password");
+ const redirectTo = formData.get("redirectTo") || "/dashboard";
+
+ // Validate form data
+ if (
+ typeof usernameOrEmail !== "string" ||
+ typeof password !== "string" ||
+ typeof redirectTo !== "string"
+ ) {
+ return json(
+ {
+ errors: [{ message: "بيانات النموذج غير صحيحة" }],
+ values: { usernameOrEmail: usernameOrEmail || "" }
+ },
+ { status: 400 }
+ );
+ }
+
+ const signInData: SignInFormData = {
+ usernameOrEmail: usernameOrEmail.trim(),
+ password,
+ redirectTo,
+ };
+
+ // Validate credentials
+ const result = await validateSignIn(signInData);
+
+ if (!result.success) {
+ return json(
+ {
+ errors: result.errors || [{ message: AUTH_ERRORS.INVALID_CREDENTIALS }],
+ values: { usernameOrEmail: signInData.usernameOrEmail }
+ },
+ { status: 400 }
+ );
+ }
+
+ if (!result.user) {
+ return json(
+ {
+ errors: [{ message: AUTH_ERRORS.INVALID_CREDENTIALS }],
+ values: { usernameOrEmail: signInData.usernameOrEmail }
+ },
+ { status: 400 }
+ );
+ }
+
+ // Create session and redirect
+ return createUserSession(result.user.id, redirectTo);
+}
+
+export default function SignIn() {
+ const { redirectTo, error } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === "submitting";
+
+ const getErrorMessage = (field?: string) => {
+ if (!actionData?.errors) return null;
+ const error = actionData.errors.find(e => e.field === field || !e.field);
+ return error?.message;
+ };
+
+ const getErrorForUrl = (errorParam: string | null) => {
+ switch (errorParam) {
+ case "account_inactive":
+ return AUTH_ERRORS.ACCOUNT_INACTIVE;
+ case "session_expired":
+ return AUTH_ERRORS.SESSION_EXPIRED;
+ default:
+ return null;
+ }
+ };
+
+ const urlError = getErrorForUrl(error);
+
+ return (
+
+
+
+
+
+ تسجيل الدخول
+
+
+ أو{" "}
+
+ إنشاء حساب جديد
+
+
+
+
+
+
+
+ {/* Display URL error */}
+ {urlError && (
+
+ )}
+
+ {/* Display form errors */}
+ {getErrorMessage() && (
+
+ )}
+
+
+
+
+
+
+ {isSubmitting ? (
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {isSubmitting ? "جاري تسجيل الدخول..." : "تسجيل الدخول"}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/signup.tsx b/app/routes/signup.tsx
new file mode 100644
index 0000000..6f8b9a1
--- /dev/null
+++ b/app/routes/signup.tsx
@@ -0,0 +1,413 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { json, redirect } from "@remix-run/node";
+import { Form, Link, useActionData, useLoaderData, useNavigation } from "@remix-run/react";
+import { validateSignUp, createUser, isSignupAllowed } from "~/lib/auth-helpers.server";
+import { createUserSession, getUserId } from "~/lib/auth.server";
+import { AUTH_ERRORS } from "~/lib/auth-constants";
+import type { SignUpFormData } from "~/types/auth";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "إنشاء حساب - نظام إدارة صيانة السيارات" },
+ { name: "description", content: "إنشاء حساب جديد في نظام إدارة صيانة السيارات" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Import the redirect middleware
+ const { redirectIfAuthenticated } = await import("~/lib/auth-middleware.server");
+ await redirectIfAuthenticated(request);
+
+ const url = new URL(request.url);
+ const adminOverride = url.searchParams.get("admin_override") === "true";
+
+ // Check if signup is allowed (only when no admin users exist or admin override)
+ const signupAllowed = await isSignupAllowed();
+ if (!signupAllowed && !adminOverride) {
+ return redirect("/signin?error=signup_disabled");
+ }
+
+ return json({ signupAllowed: signupAllowed || adminOverride });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const formData = await request.formData();
+ const adminOverride = formData.get("admin_override") === "true";
+
+ // Check if signup is still allowed
+ const signupAllowed = await isSignupAllowed();
+ if (!signupAllowed && !adminOverride) {
+ return json(
+ {
+ errors: [{ message: AUTH_ERRORS.SIGNUP_DISABLED }],
+ values: {}
+ },
+ { status: 403 }
+ );
+ }
+
+ const name = formData.get("name");
+ const username = formData.get("username");
+ const email = formData.get("email");
+ const password = formData.get("password");
+ const confirmPassword = formData.get("confirmPassword");
+
+ // Validate form data types
+ if (
+ typeof name !== "string" ||
+ typeof username !== "string" ||
+ typeof email !== "string" ||
+ typeof password !== "string" ||
+ typeof confirmPassword !== "string"
+ ) {
+ return json(
+ {
+ errors: [{ message: "بيانات النموذج غير صحيحة" }],
+ values: {
+ name: name || "",
+ username: username || "",
+ email: email || ""
+ }
+ },
+ { status: 400 }
+ );
+ }
+
+ const signUpData: SignUpFormData = {
+ name: name.trim(),
+ username: username.trim(),
+ email: email.trim(),
+ password,
+ confirmPassword,
+ };
+
+ // Validate signup data
+ const validationResult = await validateSignUp(signUpData);
+
+ if (!validationResult.success) {
+ return json(
+ {
+ errors: validationResult.errors || [{ message: "فشل في التحقق من البيانات" }],
+ values: {
+ name: signUpData.name,
+ username: signUpData.username,
+ email: signUpData.email
+ }
+ },
+ { status: 400 }
+ );
+ }
+
+ try {
+ // Create the user
+ const user = await createUser(signUpData);
+
+ // Create session and redirect to dashboard
+ return createUserSession(user.id, "/dashboard");
+ } catch (error) {
+ console.error("Error creating user:", error);
+ return json(
+ {
+ errors: [{ message: "حدث خطأ أثناء إنشاء الحساب" }],
+ values: {
+ name: signUpData.name,
+ username: signUpData.username,
+ email: signUpData.email
+ }
+ },
+ { status: 500 }
+ );
+ }
+}
+
+export default function SignUp() {
+ const { signupAllowed } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === "submitting";
+
+ // Check if this is an admin override
+ const url = typeof window !== "undefined" ? new URL(window.location.href) : null;
+ const adminOverride = url?.searchParams.get("admin_override") === "true";
+
+ const getErrorMessage = (field?: string) => {
+ if (!actionData?.errors) return null;
+ const error = actionData.errors.find(e => e.field === field || (!e.field && !field));
+ return error?.message;
+ };
+
+ if (!signupAllowed) {
+ return (
+
+
+
+
+
+ التسجيل غير متاح
+
+
+ التسجيل غير متاح حالياً. يرجى الاتصال بالمسؤول.
+
+
+
+ العودة إلى تسجيل الدخول
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ إنشاء حساب جديد
+
+
+ أو{" "}
+
+ تسجيل الدخول إلى حساب موجود
+
+
+
+
+
+ {adminOverride && (
+
+ )}
+ {/* Display general errors */}
+ {getErrorMessage() && (
+
+ )}
+
+
+ {/* Name field */}
+
+
+ الاسم الكامل
+
+
+ {getErrorMessage("name") && (
+
{getErrorMessage("name")}
+ )}
+
+
+ {/* Username field */}
+
+
+ اسم المستخدم
+
+
+ {getErrorMessage("username") && (
+
{getErrorMessage("username")}
+ )}
+
+
+ {/* Email field */}
+
+
+ البريد الإلكتروني
+
+
+ {getErrorMessage("email") && (
+
{getErrorMessage("email")}
+ )}
+
+
+ {/* Password field */}
+
+
+ كلمة المرور
+
+
+ {getErrorMessage("password") && (
+
{getErrorMessage("password")}
+ )}
+
+
+ {/* Confirm Password field */}
+
+
+ تأكيد كلمة المرور
+
+
+ {getErrorMessage("confirmPassword") && (
+
{getErrorMessage("confirmPassword")}
+ )}
+
+
+
+
+
+
+ {isSubmitting ? (
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {isSubmitting ? "جاري إنشاء الحساب..." : "إنشاء الحساب"}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/users.tsx b/app/routes/users.tsx
new file mode 100644
index 0000000..ed6a54f
--- /dev/null
+++ b/app/routes/users.tsx
@@ -0,0 +1,382 @@
+import type { LoaderFunctionArgs, ActionFunctionArgs, MetaFunction } from "@remix-run/node";
+import { json, redirect } from "@remix-run/node";
+import { useLoaderData, useSearchParams, useNavigation, useActionData } from "@remix-run/react";
+import { useState, useEffect, useCallback } from "react";
+import { protectUserManagementRoute } from "~/lib/auth-middleware.server";
+import { getUsers, createUser, updateUser, deleteUser, toggleUserStatus } from "~/lib/user-management.server";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { Text, Card, CardHeader, CardBody, Button, SearchInput, Modal } from "~/components/ui";
+import { UserList } from "~/components/users/UserList";
+import { UserForm } from "~/components/users/UserForm";
+import type { UserWithoutPassword } from "~/types/database";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "إدارة المستخدمين - نظام إدارة صيانة السيارات" },
+ { name: "description", content: "إدارة حسابات المستخدمين" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await protectUserManagementRoute(request);
+
+ const url = new URL(request.url);
+ const searchQuery = url.searchParams.get("search") || "";
+ const page = parseInt(url.searchParams.get("page") || "1");
+ const limit = 10;
+
+ const { users, total, totalPages } = await getUsers(
+ user.authLevel,
+ searchQuery,
+ page,
+ limit
+ );
+
+ return json({
+ user,
+ users,
+ currentPage: page,
+ totalPages,
+ total,
+ searchQuery,
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await protectUserManagementRoute(request);
+ const formData = await request.formData();
+ const action = formData.get("_action") as string;
+
+ try {
+ switch (action) {
+ case "create": {
+ const userData = {
+ name: formData.get("name") as string,
+ username: formData.get("username") as string,
+ email: formData.get("email") as string,
+ password: formData.get("password") as string,
+ authLevel: parseInt(formData.get("authLevel") as string),
+ status: formData.get("status") as string,
+ };
+
+ const result = await createUser(userData, user.authLevel);
+
+ if (result.success) {
+ return json({ success: true, message: "تم إنشاء المستخدم بنجاح" });
+ } else {
+ return json({ success: false, error: result.error }, { status: 400 });
+ }
+ }
+
+ case "update": {
+ const userId = parseInt(formData.get("userId") as string);
+ const userData = {
+ name: formData.get("name") as string,
+ username: formData.get("username") as string,
+ email: formData.get("email") as string,
+ authLevel: parseInt(formData.get("authLevel") as string),
+ status: formData.get("status") as string,
+ };
+
+ const password = formData.get("password") as string;
+ if (password) {
+ (userData as any).password = password;
+ }
+
+ const result = await updateUser(userId, userData, user.authLevel);
+
+ if (result.success) {
+ return json({ success: true, message: "تم تحديث المستخدم بنجاح" });
+ } else {
+ return json({ success: false, error: result.error }, { status: 400 });
+ }
+ }
+
+ case "delete": {
+ const userId = parseInt(formData.get("userId") as string);
+ const result = await deleteUser(userId, user.authLevel);
+
+ if (result.success) {
+ return json({ success: true, message: "تم حذف المستخدم بنجاح" });
+ } else {
+ return json({ success: false, error: result.error }, { status: 400 });
+ }
+ }
+
+ case "toggle-status": {
+ const userId = parseInt(formData.get("userId") as string);
+ const result = await toggleUserStatus(userId, user.authLevel);
+
+ if (result.success) {
+ return json({ success: true, message: "تم تغيير حالة المستخدم بنجاح" });
+ } else {
+ return json({ success: false, error: result.error }, { status: 400 });
+ }
+ }
+
+ default:
+ return json({ success: false, error: "إجراء غير صحيح" }, { status: 400 });
+ }
+ } catch (error) {
+ console.error("User management action error:", error);
+ return json({ success: false, error: "حدث خطأ في الخادم" }, { status: 500 });
+ }
+}
+
+export default function Users() {
+ const { user, users, currentPage, totalPages, total, searchQuery } = useLoaderData();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const navigation = useNavigation();
+ const actionData = useActionData();
+
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [editingUser, setEditingUser] = useState(null);
+ const [notification, setNotification] = useState<{
+ type: 'success' | 'error';
+ message: string;
+ } | null>(null);
+
+ const isLoading = navigation.state === "loading";
+ const isSubmitting = navigation.state === "submitting";
+
+ // Handle action results
+ useEffect(() => {
+ if (actionData) {
+ if (actionData.success) {
+ setNotification({
+ type: 'success',
+ message: actionData.message || 'تم تنفيذ العملية بنجاح',
+ });
+ setShowCreateModal(false);
+ setEditingUser(null);
+ } else {
+ setNotification({
+ type: 'error',
+ message: actionData.error || 'حدث خطأ أثناء تنفيذ العملية',
+ });
+ }
+ }
+ }, [actionData]);
+
+ // Clear notification after 5 seconds
+ useEffect(() => {
+ if (notification) {
+ const timer = setTimeout(() => {
+ setNotification(null);
+ }, 5000);
+ return () => clearTimeout(timer);
+ }
+ }, [notification]);
+
+ const handleSearch = useCallback((query: string) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+ if (query) {
+ newSearchParams.set("search", query);
+ } else {
+ newSearchParams.delete("search");
+ }
+ newSearchParams.delete("page"); // Reset to first page
+ setSearchParams(newSearchParams);
+ }, [searchParams, setSearchParams]);
+
+ const handlePageChange = useCallback((page: number) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set("page", page.toString());
+ setSearchParams(newSearchParams);
+ }, [searchParams, setSearchParams]);
+
+ const handleEdit = useCallback((userToEdit: UserWithoutPassword) => {
+ setEditingUser(userToEdit);
+ }, []);
+
+ const handleDelete = useCallback((userId: number) => {
+ // Create a form and submit it
+ const form = document.createElement("form");
+ form.method = "POST";
+ form.style.display = "none";
+
+ const actionInput = document.createElement("input");
+ actionInput.type = "hidden";
+ actionInput.name = "_action";
+ actionInput.value = "delete";
+ form.appendChild(actionInput);
+
+ const userIdInput = document.createElement("input");
+ userIdInput.type = "hidden";
+ userIdInput.name = "userId";
+ userIdInput.value = userId.toString();
+ form.appendChild(userIdInput);
+
+ document.body.appendChild(form);
+ form.submit();
+ document.body.removeChild(form);
+ }, []);
+
+ const handleToggleStatus = useCallback((userId: number) => {
+ // Create a form and submit it
+ const form = document.createElement("form");
+ form.method = "POST";
+ form.style.display = "none";
+
+ const actionInput = document.createElement("input");
+ actionInput.type = "hidden";
+ actionInput.name = "_action";
+ actionInput.value = "toggle-status";
+ form.appendChild(actionInput);
+
+ const userIdInput = document.createElement("input");
+ userIdInput.type = "hidden";
+ userIdInput.name = "userId";
+ userIdInput.value = userId.toString();
+ form.appendChild(userIdInput);
+
+ document.body.appendChild(form);
+ form.submit();
+ document.body.removeChild(form);
+ }, []);
+
+ const handleFormSubmit = useCallback((formData: FormData) => {
+ // Create a form and submit it
+ const form = document.createElement("form");
+ form.method = "POST";
+ form.style.display = "none";
+
+ for (const [key, value] of formData.entries()) {
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = key;
+ input.value = value as string;
+ form.appendChild(input);
+ }
+
+ document.body.appendChild(form);
+ form.submit();
+ document.body.removeChild(form);
+ }, []);
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ إدارة المستخدمين
+
+
+ إدارة حسابات المستخدمين وصلاحيات الوصول ({total} مستخدم)
+
+
+
setShowCreateModal(true)}>
+ إضافة مستخدم جديد
+
+
+
+ {/* Notification */}
+ {notification && (
+
+
+
+ {notification.type === 'success' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {notification.message}
+
+
+
setNotification(null)}
+ className="inline-flex text-gray-400 hover:text-gray-600"
+ >
+
+
+
+
+
+
+
+ )}
+
+ {/* Search and Filters */}
+
+
+
+
+
+
+ {/* Users List */}
+
+
+ قائمة المستخدمين
+
+
+
+
+
+
+ {/* Create User Modal */}
+
setShowCreateModal(false)}
+ title="إضافة مستخدم جديد"
+ size="lg"
+ >
+ setShowCreateModal(false)}
+ loading={isSubmitting}
+ currentUserAuthLevel={user.authLevel}
+ />
+
+
+ {/* Edit User Modal */}
+
setEditingUser(null)}
+ title="تعديل المستخدم"
+ size="lg"
+ >
+ {editingUser && (
+ setEditingUser(null)}
+ loading={isSubmitting}
+ currentUserAuthLevel={user.authLevel}
+ />
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/routes/vehicles.tsx b/app/routes/vehicles.tsx
new file mode 100644
index 0000000..d24db29
--- /dev/null
+++ b/app/routes/vehicles.tsx
@@ -0,0 +1,497 @@
+import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useLoaderData, useActionData, useNavigation, useSearchParams } from "@remix-run/react";
+import { useState, useEffect } from "react";
+import { requireUser } from "~/lib/auth.server";
+import { useDebounce } from "~/hooks/useDebounce";
+import {
+ getVehicles,
+ createVehicle,
+ updateVehicle,
+ deleteVehicle,
+ getVehicleById
+} from "~/lib/vehicle-management.server";
+import { getCustomersForSelect } from "~/lib/customer-management.server";
+import { validateVehicle } from "~/lib/validation";
+import { DashboardLayout } from "~/components/layout/DashboardLayout";
+import { VehicleList } from "~/components/vehicles/VehicleList";
+import { VehicleForm } from "~/components/vehicles/VehicleForm";
+import { VehicleDetailsView } from "~/components/vehicles/VehicleDetailsView";
+import { Modal } from "~/components/ui/Modal";
+import { Button } from "~/components/ui/Button";
+import { Input } from "~/components/ui/Input";
+import { Flex } from "~/components/layout/Flex";
+import type { VehicleWithOwner, VehicleWithRelations } from "~/types/database";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await requireUser(request);
+
+ const url = new URL(request.url);
+ const searchQuery = url.searchParams.get("search") || "";
+ const page = parseInt(url.searchParams.get("page") || "1");
+ const limit = parseInt(url.searchParams.get("limit") || "10");
+ const ownerId = url.searchParams.get("ownerId") ? parseInt(url.searchParams.get("ownerId")!) : undefined;
+ const customerId = url.searchParams.get("customerId") ? parseInt(url.searchParams.get("customerId")!) : undefined;
+ const plateNumber = url.searchParams.get("plateNumber") || undefined;
+
+ const [vehiclesResult, customers] = await Promise.all([
+ getVehicles(searchQuery, page, limit, customerId || ownerId, plateNumber),
+ getCustomersForSelect(),
+ ]);
+
+ return json({
+ vehicles: vehiclesResult.vehicles,
+ total: vehiclesResult.total,
+ totalPages: vehiclesResult.totalPages,
+ currentPage: page,
+ searchQuery,
+ ownerId: customerId || ownerId,
+ customerId,
+ plateNumber,
+ customers,
+ user,
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await requireUser(request);
+
+ const formData = await request.formData();
+ const action = formData.get("_action") as string;
+
+ switch (action) {
+ case "create": {
+ const vehicleData = {
+ plateNumber: formData.get("plateNumber") as string,
+ bodyType: formData.get("bodyType") as string,
+ manufacturer: formData.get("manufacturer") as string,
+ model: formData.get("model") as string,
+ trim: formData.get("trim") as string || undefined,
+ year: parseInt(formData.get("year") as string),
+ transmission: formData.get("transmission") as string,
+ fuel: formData.get("fuel") as string,
+ cylinders: formData.get("cylinders") ? parseInt(formData.get("cylinders") as string) : undefined,
+ engineDisplacement: formData.get("engineDisplacement") ? parseFloat(formData.get("engineDisplacement") as string) : undefined,
+ useType: formData.get("useType") as string,
+ ownerId: parseInt(formData.get("ownerId") as string),
+ };
+
+ // Validate vehicle data
+ const validation = validateVehicle(vehicleData);
+ if (!validation.isValid) {
+ return json({
+ success: false,
+ errors: validation.errors,
+ action: "create"
+ }, { status: 400 });
+ }
+
+ const result = await createVehicle(vehicleData);
+
+ if (result.success) {
+ return json({
+ success: true,
+ vehicle: result.vehicle,
+ action: "create",
+ message: "تم إنشاء المركبة بنجاح"
+ });
+ } else {
+ return json({
+ success: false,
+ error: result.error,
+ action: "create"
+ }, { status: 400 });
+ }
+ }
+
+ case "update": {
+ const id = parseInt(formData.get("id") as string);
+ const vehicleData = {
+ plateNumber: formData.get("plateNumber") as string,
+ bodyType: formData.get("bodyType") as string,
+ manufacturer: formData.get("manufacturer") as string,
+ model: formData.get("model") as string,
+ trim: formData.get("trim") as string || undefined,
+ year: parseInt(formData.get("year") as string),
+ transmission: formData.get("transmission") as string,
+ fuel: formData.get("fuel") as string,
+ cylinders: formData.get("cylinders") ? parseInt(formData.get("cylinders") as string) : undefined,
+ engineDisplacement: formData.get("engineDisplacement") ? parseFloat(formData.get("engineDisplacement") as string) : undefined,
+ useType: formData.get("useType") as string,
+ ownerId: parseInt(formData.get("ownerId") as string),
+ };
+
+ // Validate vehicle data
+ const validation = validateVehicle(vehicleData);
+ if (!validation.isValid) {
+ return json({
+ success: false,
+ errors: validation.errors,
+ action: "update"
+ }, { status: 400 });
+ }
+
+ const result = await updateVehicle(id, vehicleData);
+
+ if (result.success) {
+ return json({
+ success: true,
+ vehicle: result.vehicle,
+ action: "update",
+ message: "تم تحديث المركبة بنجاح"
+ });
+ } else {
+ return json({
+ success: false,
+ error: result.error,
+ action: "update"
+ }, { status: 400 });
+ }
+ }
+
+ case "delete": {
+ const id = parseInt(formData.get("id") as string);
+
+ const result = await deleteVehicle(id);
+
+ if (result.success) {
+ return json({
+ success: true,
+ action: "delete",
+ message: "تم حذف المركبة بنجاح"
+ });
+ } else {
+ return json({
+ success: false,
+ error: result.error,
+ action: "delete"
+ }, { status: 400 });
+ }
+ }
+
+ case "get": {
+ const id = parseInt(formData.get("id") as string);
+ const vehicle = await getVehicleById(id);
+
+ if (vehicle) {
+ return json({
+ success: true,
+ vehicle,
+ action: "get"
+ });
+ } else {
+ return json({
+ success: false,
+ error: "المركبة غير موجودة",
+ action: "get"
+ }, { status: 404 });
+ }
+ }
+
+ default:
+ return json({
+ success: false,
+ error: "إجراء غير صحيح",
+ action: "unknown"
+ }, { status: 400 });
+ }
+}
+
+export default function VehiclesPage() {
+ const { vehicles, total, totalPages, currentPage, searchQuery, ownerId, customerId, plateNumber, customers, user } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [showViewModal, setShowViewModal] = useState(false);
+ const [selectedVehicle, setSelectedVehicle] = useState(null);
+ const [searchValue, setSearchValue] = useState(searchQuery);
+ const [selectedOwnerId, setSelectedOwnerId] = useState(ownerId?.toString() || "");
+ const [isLoadingVehicleDetails, setIsLoadingVehicleDetails] = useState(false);
+
+ const isLoading = navigation.state !== "idle";
+
+ // Debounce search value to avoid too many requests
+ const debouncedSearchValue = useDebounce(searchValue, 300);
+ const debouncedOwnerId = useDebounce(selectedOwnerId, 300);
+
+ // Handle search automatically when debounced values change
+ useEffect(() => {
+ if (debouncedSearchValue !== searchQuery || debouncedOwnerId !== (ownerId?.toString() || "")) {
+ const newSearchParams = new URLSearchParams(searchParams);
+ if (debouncedSearchValue) {
+ newSearchParams.set("search", debouncedSearchValue);
+ } else {
+ newSearchParams.delete("search");
+ }
+ if (debouncedOwnerId) {
+ newSearchParams.set("ownerId", debouncedOwnerId);
+ } else {
+ newSearchParams.delete("ownerId");
+ }
+ newSearchParams.set("page", "1"); // Reset to first page
+ setSearchParams(newSearchParams);
+ }
+ }, [debouncedSearchValue, debouncedOwnerId, searchQuery, ownerId, searchParams, setSearchParams]);
+
+ // Clear search function
+ const clearSearch = () => {
+ setSearchValue("");
+ setSelectedOwnerId("");
+ };
+
+ // Handle pagination
+ const handlePageChange = (page: number) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set("page", page.toString());
+ setSearchParams(newSearchParams);
+ };
+
+ // Handle create vehicle
+ const handleCreateVehicle = () => {
+ setSelectedVehicle(null);
+ setShowCreateModal(true);
+ };
+
+ // Handle edit vehicle
+ const handleEditVehicle = (vehicle: VehicleWithOwner | VehicleWithRelations) => {
+ setSelectedVehicle(vehicle);
+ setShowEditModal(true);
+ };
+
+ // Handle view vehicle
+ const handleViewVehicle = async (vehicle: VehicleWithOwner) => {
+ // First show the modal with basic data
+ setSelectedVehicle(vehicle);
+ setShowViewModal(true);
+ setIsLoadingVehicleDetails(true);
+
+ // Then fetch full vehicle details with maintenance visits in the background
+ try {
+ const form = new FormData();
+ form.append("_action", "get");
+ form.append("id", vehicle.id.toString());
+
+ const response = await fetch(window.location.pathname, {
+ method: "POST",
+ body: form,
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ if (result.success && result.vehicle) {
+ setSelectedVehicle(result.vehicle);
+ }
+ }
+ } catch (error) {
+ console.error("Failed to fetch full vehicle details:", error);
+ // Keep the basic vehicle data if fetch fails
+ } finally {
+ setIsLoadingVehicleDetails(false);
+ }
+ };
+
+ // Close modals on successful action
+ useEffect(() => {
+ if (actionData?.success && actionData.action === "create") {
+ setShowCreateModal(false);
+ }
+ if (actionData?.success && actionData.action === "update") {
+ setShowEditModal(false);
+ }
+ }, [actionData]);
+
+ return (
+
+
+ {/* Header */}
+
+
+
إدارة المركبات
+
+
+ إجمالي المركبات: {total}
+
+ {customerId && (
+
+ مفلترة حسب العميل
+
+ )}
+ {plateNumber && (
+
+ مفلترة حسب رقم اللوحة: {plateNumber}
+
+ )}
+
+
+
+
+ إضافة مركبة جديدة
+
+
+
+ {/* Search and Filters */}
+
+
+
+
setSearchValue(e.target.value)}
+ startIcon={
+
+
+
+ }
+ endIcon={
+ searchValue && (
+
setSearchValue("")}
+ className="text-gray-400 hover:text-gray-600"
+ type="button"
+ >
+
+
+
+
+ )
+ }
+ />
+
+
+
+ setSelectedOwnerId(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ >
+ جميع المالكين
+ {customers.map((customer) => (
+
+ {customer.name}
+
+ ))}
+
+
+
+ {(searchQuery || ownerId || debouncedSearchValue !== searchQuery || debouncedOwnerId !== (ownerId?.toString() || "")) && (
+
+ {(debouncedSearchValue !== searchQuery || debouncedOwnerId !== (ownerId?.toString() || "")) && (
+
+
+
+
+
+ جاري البحث...
+
+ )}
+ {(searchQuery || ownerId) && (
+
+ مسح البحث
+
+ )}
+
+ )}
+
+
+
+ {/* Action Messages */}
+ {actionData?.success && actionData.message && (
+
+ {actionData.message}
+
+ )}
+
+ {actionData?.error && (
+
+ {actionData.error}
+
+ )}
+
+ {/* Vehicle List */}
+
+
+ {/* Create Vehicle Modal */}
+
setShowCreateModal(false)}
+ title="إضافة مركبة جديدة"
+ size="lg"
+ >
+ setShowCreateModal(false)}
+ errors={actionData?.action === "create" ? actionData.errors : undefined}
+ isLoading={isLoading}
+ />
+
+
+ {/* Edit Vehicle Modal */}
+
setShowEditModal(false)}
+ title="تعديل المركبة"
+ size="lg"
+ >
+ {selectedVehicle && (
+ setShowEditModal(false)}
+ errors={actionData?.action === "update" ? actionData.errors : undefined}
+ isLoading={isLoading}
+
+ />
+ )}
+
+
+ {/* View Vehicle Modal */}
+
{
+ setShowViewModal(false);
+ setIsLoadingVehicleDetails(false);
+ }}
+ title={selectedVehicle ? `تفاصيل المركبة - ${selectedVehicle.plateNumber}` : "تفاصيل المركبة"}
+ size="xl"
+ >
+ {selectedVehicle && (
+ {
+ setShowViewModal(false);
+ handleEditVehicle(selectedVehicle);
+ }}
+ onClose={() => {
+ setShowViewModal(false);
+ setIsLoadingVehicleDetails(false);
+ }}
+ isLoadingVisits={isLoadingVehicleDetails}
+ />
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/tailwind.css b/app/tailwind.css
new file mode 100644
index 0000000..41f48fd
--- /dev/null
+++ b/app/tailwind.css
@@ -0,0 +1,170 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html,
+body {
+ @apply bg-white dark:bg-gray-950;
+
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+ }
+}
+
+/* RTL Support */
+[dir="rtl"] {
+ direction: rtl;
+ text-align: right;
+}
+
+[dir="ltr"] {
+ direction: ltr;
+ text-align: left;
+}
+
+/* Arabic Text Rendering */
+.arabic-text {
+ font-feature-settings: "liga" 1, "kern" 1;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* RTL-specific utilities */
+@layer utilities {
+ .rtl-flip {
+ transform: scaleX(-1);
+ }
+
+ .rtl-space-x-reverse > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 1;
+ }
+
+ .rtl-divide-x-reverse > :not([hidden]) ~ :not([hidden]) {
+ --tw-divide-x-reverse: 1;
+ }
+}
+
+/* Responsive breakpoints for RTL */
+@layer components {
+ .container-rtl {
+ @apply container mx-auto px-4 sm:px-6 lg:px-8;
+ }
+
+ .grid-rtl {
+ @apply grid gap-4 sm:gap-6 lg:gap-8;
+ }
+
+ .flex-rtl {
+ @apply flex items-center;
+ }
+
+ .flex-rtl-reverse {
+ @apply flex items-center flex-row-reverse;
+ }
+}
+
+/* Form elements RTL support */
+input[type="text"],
+input[type="email"],
+input[type="password"],
+input[type="number"],
+input[type="tel"],
+textarea,
+select {
+ direction: rtl;
+ text-align: right;
+}
+
+input[type="text"]:focus,
+input[type="email"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+input[type="tel"]:focus,
+textarea:focus,
+select:focus {
+ @apply ring-2 ring-blue-500 ring-opacity-50;
+}
+
+/* Button and interactive elements */
+button,
+.btn {
+ @apply transition-all duration-200 ease-in-out;
+}
+
+/* Navigation and layout components */
+.sidebar-transition {
+ @apply transition-all duration-300 ease-in-out;
+}
+
+/* Sidebar hover effects */
+.sidebar-item {
+ @apply transition-colors duration-150 ease-in-out;
+}
+
+.sidebar-item:hover {
+ @apply bg-gray-50 text-gray-900;
+}
+
+.sidebar-item.active {
+ @apply bg-blue-100 text-blue-900 border-r-2 border-blue-600;
+}
+
+/* Mobile menu overlay */
+.mobile-overlay {
+ @apply fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300;
+}
+
+/* Smooth transitions for layout changes */
+.layout-transition {
+ @apply transition-all duration-300 ease-in-out;
+}
+
+/* Focus styles for accessibility */
+.focus-ring {
+ @apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2;
+}
+
+/* Custom scrollbar for sidebar */
+.sidebar-scroll {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
+}
+
+.sidebar-scroll::-webkit-scrollbar {
+ width: 6px;
+}
+
+.sidebar-scroll::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.sidebar-scroll::-webkit-scrollbar-thumb {
+ background-color: rgba(156, 163, 175, 0.5);
+ border-radius: 3px;
+}
+
+.sidebar-scroll::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(156, 163, 175, 0.7);
+}
+
+/* RTL Layout fixes */
+[dir="rtl"] .space-x-reverse > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 1;
+}
+
+/* Ensure proper RTL text alignment */
+[dir="rtl"] {
+ text-align: right;
+}
+
+/* Fix specific margin issues for RTL */
+[dir="rtl"] .ml-1 {
+ margin-right: 0.25rem;
+ margin-left: 0;
+}
+
+[dir="rtl"] .ml-3 {
+ margin-right: 0.75rem;
+ margin-left: 0;
+}
diff --git a/app/types/auth.ts b/app/types/auth.ts
new file mode 100644
index 0000000..1f35e3a
--- /dev/null
+++ b/app/types/auth.ts
@@ -0,0 +1,61 @@
+import type { User } from "@prisma/client";
+
+// Authentication levels
+export const AUTH_LEVELS = {
+ SUPERADMIN: 1,
+ ADMIN: 2,
+ USER: 3,
+} as const;
+
+export type AuthLevel = typeof AUTH_LEVELS[keyof typeof AUTH_LEVELS];
+
+// User status types
+export const USER_STATUS = {
+ ACTIVE: "active",
+ INACTIVE: "inactive",
+} as const;
+
+export type UserStatus = typeof USER_STATUS[keyof typeof USER_STATUS];
+
+// Authentication form data types
+export interface SignInFormData {
+ usernameOrEmail: string;
+ password: string;
+ redirectTo?: string;
+}
+
+export interface SignUpFormData {
+ name: string;
+ username: string;
+ email: string;
+ password: string;
+ confirmPassword: string;
+}
+
+// User data without sensitive information
+export type SafeUser = Omit;
+
+// Authentication error types
+export interface AuthError {
+ field?: string;
+ message: string;
+}
+
+// Authentication result types
+export interface AuthResult {
+ success: boolean;
+ user?: SafeUser;
+ errors?: AuthError[];
+}
+
+// Session data type
+export interface SessionData {
+ userId: number;
+}
+
+// Route protection types
+export interface RouteProtectionOptions {
+ requiredAuthLevel?: AuthLevel;
+ allowInactive?: boolean;
+ redirectTo?: string;
+}
\ No newline at end of file
diff --git a/app/types/database.ts b/app/types/database.ts
new file mode 100644
index 0000000..ccd1093
--- /dev/null
+++ b/app/types/database.ts
@@ -0,0 +1,290 @@
+import type { User, Customer, Vehicle, MaintenanceVisit, MaintenanceType, Expense, Income, CarDataset, Settings } from '@prisma/client';
+
+// Re-export Prisma types for easier imports
+export type {
+ User,
+ Customer,
+ Vehicle,
+ MaintenanceVisit,
+ MaintenanceType,
+ Expense,
+ Income,
+ CarDataset,
+ Settings,
+} from '@prisma/client';
+
+// Extended types with relationships
+export type UserWithoutPassword = Omit;
+
+export type CustomerWithVehicles = Customer & {
+ vehicles: Vehicle[];
+ maintenanceVisits: (MaintenanceVisit & {
+ vehicle: {
+ id: number;
+ plateNumber: string;
+ manufacturer: string;
+ model: string;
+ year: number;
+ };
+ })[];
+};
+
+export type VehicleWithOwner = Vehicle & {
+ owner: Customer;
+};
+
+export type VehicleWithRelations = Vehicle & {
+ owner: Customer;
+ maintenanceVisits: MaintenanceVisit[];
+};
+
+// Maintenance job interface for JSON field
+export interface MaintenanceJob {
+ typeId: number;
+ job: string;
+ cost: number;
+ notes?: string;
+}
+
+export type MaintenanceVisitWithRelations = MaintenanceVisit & {
+ vehicle: Vehicle;
+ customer: Customer;
+ income: Income[];
+ // maintenanceJobs will be parsed from JSON string
+};
+
+export type IncomeWithVisit = Income & {
+ maintenanceVisit: MaintenanceVisit;
+};
+
+// Enums for form validation and type safety
+export const UserStatus = {
+ ACTIVE: 'active',
+ INACTIVE: 'inactive',
+} as const;
+
+export const AuthLevel = {
+ SUPERADMIN: 1,
+ ADMIN: 2,
+ USER: 3,
+} as const;
+
+export const TransmissionType = {
+ AUTOMATIC: 'Automatic',
+ MANUAL: 'Manual',
+} as const;
+
+export const FuelType = {
+ GASOLINE: 'Gasoline',
+ DIESEL: 'Diesel',
+ HYBRID: 'Hybrid',
+ MILD_HYBRID: 'Mild Hybrid',
+ ELECTRIC: 'Electric',
+} as const;
+
+export const UseType = {
+ PERSONAL: 'personal',
+ TAXI: 'taxi',
+ APPS: 'apps',
+ LOADING: 'loading',
+ TRAVEL: 'travel',
+} as const;
+
+export const PaymentStatus = {
+ PENDING: 'pending',
+ PAID: 'paid',
+ PARTIAL: 'partial',
+ CANCELLED: 'cancelled',
+} as const;
+
+// Form data types for validation
+export interface CreateUserData {
+ name: string;
+ username: string;
+ email: string;
+ password: string;
+ authLevel: number;
+ status?: string;
+}
+
+export interface UpdateUserData {
+ name?: string;
+ username?: string;
+ email?: string;
+ password?: string;
+ authLevel?: number;
+ status?: string;
+}
+
+export interface CreateCustomerData {
+ name: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+}
+
+export interface UpdateCustomerData {
+ name?: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+}
+
+export interface CreateVehicleData {
+ plateNumber: string;
+ bodyType: string;
+ manufacturer: string;
+ model: string;
+ trim?: string;
+ year: number;
+ transmission: string;
+ fuel: string;
+ cylinders?: number;
+ engineDisplacement?: number;
+ useType: string;
+ ownerId: number;
+}
+
+export interface UpdateVehicleData {
+ plateNumber?: string;
+ bodyType?: string;
+ manufacturer?: string;
+ model?: string;
+ trim?: string;
+ year?: number;
+ transmission?: string;
+ fuel?: string;
+ cylinders?: number;
+ engineDisplacement?: number;
+ useType?: string;
+ ownerId?: number;
+ lastVisitDate?: Date;
+ suggestedNextVisitDate?: Date;
+}
+
+export interface CreateMaintenanceTypeData {
+ name: string;
+ description?: string;
+ isActive?: boolean;
+}
+
+export interface UpdateMaintenanceTypeData {
+ name?: string;
+ description?: string;
+ isActive?: boolean;
+}
+
+export interface CreateMaintenanceVisitData {
+ vehicleId: number;
+ customerId: number;
+ maintenanceJobs: MaintenanceJob[];
+ description: string;
+ cost: number;
+ paymentStatus?: string;
+ kilometers: number;
+ visitDate?: Date;
+ nextVisitDelay: number;
+}
+
+export interface UpdateMaintenanceVisitData {
+ vehicleId?: number;
+ customerId?: number;
+ maintenanceJobs?: MaintenanceJob[];
+ description?: string;
+ cost?: number;
+ paymentStatus?: string;
+ kilometers?: number;
+ visitDate?: Date;
+ nextVisitDelay?: number;
+}
+
+export interface CreateExpenseData {
+ description: string;
+ category: string;
+ amount: number;
+ expenseDate?: Date;
+}
+
+export interface UpdateExpenseData {
+ description?: string;
+ category?: string;
+ amount?: number;
+ expenseDate?: Date;
+}
+
+export interface CreateIncomeData {
+ maintenanceVisitId: number;
+ amount: number;
+ incomeDate?: Date;
+}
+
+export interface CreateCarDatasetData {
+ manufacturer: string;
+ model: string;
+ bodyType: string;
+ isActive?: boolean;
+}
+
+export interface UpdateCarDatasetData {
+ manufacturer?: string;
+ model?: string;
+ bodyType?: string;
+ isActive?: boolean;
+}
+
+// Utility types for API responses
+export interface ApiResponse {
+ success: boolean;
+ data?: T;
+ error?: {
+ code: string;
+ message: string;
+ details?: any;
+ };
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ pagination: {
+ page: number;
+ limit: number;
+ total: number;
+ totalPages: number;
+ };
+}
+
+// Search and filter types
+export interface SearchParams {
+ query?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'asc' | 'desc';
+}
+
+export interface CustomerSearchParams extends SearchParams {
+ status?: string;
+}
+
+export interface VehicleSearchParams extends SearchParams {
+ manufacturer?: string;
+ year?: number;
+ useType?: string;
+ ownerId?: number;
+}
+
+export interface MaintenanceVisitSearchParams extends SearchParams {
+ vehicleId?: number;
+ customerId?: number;
+ dateFrom?: Date;
+ dateTo?: Date;
+ paymentStatus?: string;
+}
+
+export interface FinancialSearchParams extends SearchParams {
+ category?: string;
+ dateFrom?: Date;
+ dateTo?: Date;
+ amountMin?: number;
+ amountMax?: number;
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..9df8cc4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,13992 @@
+{
+ "name": "car_mms_test",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "car_mms_test",
+ "dependencies": {
+ "@prisma/client": "^6.13.0",
+ "@remix-run/node": "^2.16.0",
+ "@remix-run/react": "^2.16.0",
+ "@remix-run/serve": "^2.16.0",
+ "bcryptjs": "^3.0.2",
+ "isbot": "^4.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwindcss-rtl": "^0.9.0",
+ "zod": "^4.0.17"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "^2.16.0",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
+ "@typescript-eslint/parser": "^6.7.4",
+ "@vitest/ui": "^3.2.4",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.38.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "postcss": "^8.4.38",
+ "prisma": "^6.13.0",
+ "tailwindcss": "^3.4.4",
+ "tsx": "^4.20.3",
+ "typescript": "^5.1.6",
+ "vite": "^6.0.0",
+ "vite-tsconfig-paths": "^4.2.1",
+ "vitest": "^3.2.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "devOptional": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
+ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
+ "dev": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.6",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
+ "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
+ "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz",
+ "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz",
+ "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
+ "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
+ "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
+ "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
+ "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.0.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
+ "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
+ "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "dev": true
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
+ "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz",
+ "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz",
+ "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz",
+ "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz",
+ "integrity": "sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz",
+ "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz",
+ "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz",
+ "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz",
+ "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz",
+ "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz",
+ "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz",
+ "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz",
+ "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz",
+ "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz",
+ "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz",
+ "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz",
+ "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz",
+ "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz",
+ "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
+ "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz",
+ "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz",
+ "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz",
+ "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz",
+ "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "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/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@jspm/core": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.1.0.tgz",
+ "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==",
+ "dev": true
+ },
+ "node_modules/@mdx-js/mdx": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
+ "integrity": "sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/mdx": "^2.0.0",
+ "estree-util-build-jsx": "^2.0.0",
+ "estree-util-is-identifier-name": "^2.0.0",
+ "estree-util-to-js": "^1.1.0",
+ "estree-walker": "^3.0.0",
+ "hast-util-to-estree": "^2.0.0",
+ "markdown-extensions": "^1.0.0",
+ "periscopic": "^3.0.0",
+ "remark-mdx": "^2.0.0",
+ "remark-parse": "^10.0.0",
+ "remark-rehype": "^10.0.0",
+ "unified": "^10.0.0",
+ "unist-util-position-from-estree": "^1.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "unist-util-visit": "^4.0.0",
+ "vfile": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
+ "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.10.0"
+ }
+ },
+ "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,
+ "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,
+ "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,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nolyfill/is-core-module": {
+ "version": "1.0.39",
+ "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
+ "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.4.0"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
+ "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz",
+ "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/promise-spawn": "^6.0.0",
+ "lru-cache": "^7.4.4",
+ "npm-pick-manifest": "^8.0.0",
+ "proc-log": "^3.0.0",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz",
+ "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/git": "^4.1.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^6.1.1",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^5.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/hosted-git-info": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz",
+ "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^7.5.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/normalize-package-data": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
+ "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz",
+ "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==",
+ "dev": true,
+ "dependencies": {
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "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
+ },
+ "node_modules/@prisma/client": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.13.0.tgz",
+ "integrity": "sha512-8m2+I3dQovkV8CkDMluiwEV1TxV9EXdT6xaCz39O6jYw7mkf5gwfmi+cL4LJsEPwz5tG7sreBwkRpEMJedGYUQ==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/config": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.13.0.tgz",
+ "integrity": "sha512-OYMM+pcrvj/NqNWCGESSxVG3O7kX6oWuGyvufTUNnDw740KIQvNyA4v0eILgkpuwsKIDU36beZCkUtIt0naTog==",
+ "devOptional": true,
+ "dependencies": {
+ "c12": "3.1.0",
+ "deepmerge-ts": "7.1.5",
+ "effect": "3.16.12",
+ "read-package-up": "11.0.0"
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.13.0.tgz",
+ "integrity": "sha512-um+9pfKJW0ihmM83id9FXGi5qEbVJ0Vxi1Gm0xpYsjwUBnw6s2LdPBbrsG9QXRX46K4CLWCTNvskXBup4i9hlw==",
+ "devOptional": true
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.13.0.tgz",
+ "integrity": "sha512-D+1B79LFvtWA0KTt8ALekQ6A/glB9w10ETknH5Y9g1k2NYYQOQy93ffiuqLn3Pl6IPJG3EsK/YMROKEaq8KBrA==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/debug": "6.13.0",
+ "@prisma/engines-version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd",
+ "@prisma/fetch-engine": "6.13.0",
+ "@prisma/get-platform": "6.13.0"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd.tgz",
+ "integrity": "sha512-MpPyKSzBX7P/ZY9odp9TSegnS/yH3CSbchQE9f0yBg3l2QyN59I6vGXcoYcqKC9VTniS1s18AMmhyr1OWavjHg==",
+ "devOptional": true
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.13.0.tgz",
+ "integrity": "sha512-grmmq+4FeFKmaaytA8Ozc2+Tf3BC8xn/DVJos6LL022mfRlMZYjT3hZM0/xG7+5fO95zFG9CkDUs0m1S2rXs5Q==",
+ "devOptional": true,
+ "dependencies": {
+ "@prisma/debug": "6.13.0",
+ "@prisma/engines-version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd",
+ "@prisma/get-platform": "6.13.0"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.13.0.tgz",
+ "integrity": "sha512-Nii2pX50fY4QKKxQwm7/vvqT6Ku8yYJLZAFX4e2vzHwRdMqjugcOG5hOSLjxqoXb0cvOspV70TOhMzrw8kqAnw==",
+ "devOptional": true,
+ "dependencies": {
+ "@prisma/debug": "6.13.0"
+ }
+ },
+ "node_modules/@remix-run/dev": {
+ "version": "2.17.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.17.2.tgz",
+ "integrity": "sha512-gfc4Hu2Sysr+GyU/F12d2uvEfQwUwWvsOjBtiemhdN1xGOn1+FyYzlLl/aJ7AL9oYko8sDqOPyJCiApWJJGCCw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.21.8",
+ "@babel/generator": "^7.21.5",
+ "@babel/parser": "^7.21.8",
+ "@babel/plugin-syntax-decorators": "^7.22.10",
+ "@babel/plugin-syntax-jsx": "^7.21.4",
+ "@babel/preset-typescript": "^7.21.5",
+ "@babel/traverse": "^7.23.2",
+ "@babel/types": "^7.22.5",
+ "@mdx-js/mdx": "^2.3.0",
+ "@npmcli/package-json": "^4.0.1",
+ "@remix-run/node": "2.17.2",
+ "@remix-run/router": "1.23.0",
+ "@remix-run/server-runtime": "2.17.2",
+ "@types/mdx": "^2.0.5",
+ "@vanilla-extract/integration": "^6.2.0",
+ "arg": "^5.0.1",
+ "cacache": "^17.1.3",
+ "chalk": "^4.1.2",
+ "chokidar": "^3.5.1",
+ "cross-spawn": "^7.0.3",
+ "dotenv": "^16.0.0",
+ "es-module-lexer": "^1.3.1",
+ "esbuild": "0.17.6",
+ "esbuild-plugins-node-modules-polyfill": "^1.6.0",
+ "execa": "5.1.1",
+ "exit-hook": "2.2.1",
+ "express": "^4.20.0",
+ "fs-extra": "^10.0.0",
+ "get-port": "^5.1.1",
+ "gunzip-maybe": "^1.4.2",
+ "jsesc": "3.0.2",
+ "json5": "^2.2.2",
+ "lodash": "^4.17.21",
+ "lodash.debounce": "^4.0.8",
+ "minimatch": "^9.0.0",
+ "ora": "^5.4.1",
+ "pathe": "^1.1.2",
+ "picocolors": "^1.0.0",
+ "picomatch": "^2.3.1",
+ "pidtree": "^0.6.0",
+ "postcss": "^8.4.19",
+ "postcss-discard-duplicates": "^5.1.0",
+ "postcss-load-config": "^4.0.1",
+ "postcss-modules": "^6.0.0",
+ "prettier": "^2.7.1",
+ "pretty-ms": "^7.0.1",
+ "react-refresh": "^0.14.0",
+ "remark-frontmatter": "4.0.1",
+ "remark-mdx-frontmatter": "^1.0.1",
+ "semver": "^7.3.7",
+ "set-cookie-parser": "^2.6.0",
+ "tar-fs": "^2.1.3",
+ "tsconfig-paths": "^4.0.0",
+ "valibot": "^0.41.0",
+ "vite-node": "^3.1.3",
+ "ws": "^7.5.10"
+ },
+ "bin": {
+ "remix": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@remix-run/react": "^2.17.0",
+ "@remix-run/serve": "^2.17.0",
+ "typescript": "^5.1.0",
+ "vite": "^5.1.0 || ^6.0.0",
+ "wrangler": "^3.28.2"
+ },
+ "peerDependenciesMeta": {
+ "@remix-run/serve": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ },
+ "wrangler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/dev/node_modules/@remix-run/node": {
+ "version": "2.17.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.17.2.tgz",
+ "integrity": "sha512-NHBIQI1Fap3ZmyHMPVsMcma6mvi2oUunvTzOcuWHHkkx1LrvWRzQTlaWqEnqCp/ff5PfX5r0eBEPrSkC8zrHRQ==",
+ "dev": true,
+ "dependencies": {
+ "@remix-run/server-runtime": "2.17.2",
+ "@remix-run/web-fetch": "^4.4.2",
+ "@web3-storage/multipart-parser": "^1.0.0",
+ "cookie-signature": "^1.1.0",
+ "source-map-support": "^0.5.21",
+ "stream-slice": "^0.1.2",
+ "undici": "^6.21.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/dev/node_modules/@remix-run/server-runtime": {
+ "version": "2.17.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.17.2.tgz",
+ "integrity": "sha512-dTrAG1SgOLgz1DFBDsLHN0V34YqLsHEReVHYOI4UV/p+ALbn/BRQMw1MaUuqGXmX21ZTuCzzPegtTLNEOc8ixA==",
+ "dev": true,
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "@types/cookie": "^0.6.0",
+ "@web3-storage/multipart-parser": "^1.0.0",
+ "cookie": "^0.7.2",
+ "set-cookie-parser": "^2.4.8",
+ "source-map": "^0.7.3",
+ "turbo-stream": "2.4.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/dev/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/@remix-run/dev/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,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@remix-run/dev/node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true
+ },
+ "node_modules/@remix-run/dev/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/@remix-run/express": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.17.0.tgz",
+ "integrity": "sha512-VUNpdrX3WSLPOkRBbsTQao5Vu/xdKcB8AY+44pAyC7iW5iIKHDb6EYlDvpbMLcMNh9ErYGhpPtshaBiBTMvjiw==",
+ "dependencies": {
+ "@remix-run/node": "2.17.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "express": "^4.20.0",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/node": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.17.0.tgz",
+ "integrity": "sha512-ISy3N4peKB+Fo8ddh+mU6ki3HzQqLXwJxUrAtqxYxrBDM4Pwc7EvISrcQ4QasB6ORBknJeEZSBu69WDRhGzrjA==",
+ "dependencies": {
+ "@remix-run/server-runtime": "2.17.0",
+ "@remix-run/web-fetch": "^4.4.2",
+ "@web3-storage/multipart-parser": "^1.0.0",
+ "cookie-signature": "^1.1.0",
+ "source-map-support": "^0.5.21",
+ "stream-slice": "^0.1.2",
+ "undici": "^6.21.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/react": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-2.17.0.tgz",
+ "integrity": "sha512-muOLHqcimMCrIk6VOuqIn51P3buYjKpdYc6qpNy6zE5HlKfyaKEY00a5pzdutRmevYTQy7FiEF/LK4M8sxk70Q==",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "@remix-run/server-runtime": "2.17.0",
+ "react-router": "6.30.0",
+ "react-router-dom": "6.30.0",
+ "turbo-stream": "2.4.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@remix-run/serve": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/serve/-/serve-2.17.0.tgz",
+ "integrity": "sha512-eq0A7A89uqg+rQiGVHoUwb1NUawmPpjAY3RWn4KG4uCp4QwhqJausML63fIxnfdp7zu2OplXhfSXCNiTekQ0rw==",
+ "dependencies": {
+ "@remix-run/express": "2.17.0",
+ "@remix-run/node": "2.17.0",
+ "chokidar": "^3.5.3",
+ "compression": "^1.7.4",
+ "express": "^4.20.0",
+ "get-port": "5.1.1",
+ "morgan": "^1.10.0",
+ "source-map-support": "^0.5.21"
+ },
+ "bin": {
+ "remix-serve": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@remix-run/serve/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/@remix-run/serve/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==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@remix-run/serve/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/@remix-run/server-runtime": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.17.0.tgz",
+ "integrity": "sha512-X0zfGLgvukhuTIL0tdWKnlvHy4xUe7Z17iQ0KMQoITK0SkTZPSud/6cJCsKhPqC8kfdYT1GNFLJKRhHz7Aapmw==",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "@types/cookie": "^0.6.0",
+ "@web3-storage/multipart-parser": "^1.0.0",
+ "cookie": "^0.7.2",
+ "set-cookie-parser": "^2.4.8",
+ "source-map": "^0.7.3",
+ "turbo-stream": "2.4.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/web-blob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.1.0.tgz",
+ "integrity": "sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==",
+ "dependencies": {
+ "@remix-run/web-stream": "^1.1.0",
+ "web-encoding": "1.1.5"
+ }
+ },
+ "node_modules/@remix-run/web-fetch": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/web-fetch/-/web-fetch-4.4.2.tgz",
+ "integrity": "sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==",
+ "dependencies": {
+ "@remix-run/web-blob": "^3.1.0",
+ "@remix-run/web-file": "^3.1.0",
+ "@remix-run/web-form-data": "^3.1.0",
+ "@remix-run/web-stream": "^1.1.0",
+ "@web3-storage/multipart-parser": "^1.0.0",
+ "abort-controller": "^3.0.0",
+ "data-uri-to-buffer": "^3.0.1",
+ "mrmime": "^1.0.0"
+ },
+ "engines": {
+ "node": "^10.17 || >=12.3"
+ }
+ },
+ "node_modules/@remix-run/web-file": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/web-file/-/web-file-3.1.0.tgz",
+ "integrity": "sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==",
+ "dependencies": {
+ "@remix-run/web-blob": "^3.1.0"
+ }
+ },
+ "node_modules/@remix-run/web-form-data": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz",
+ "integrity": "sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==",
+ "dependencies": {
+ "web-encoding": "1.1.5"
+ }
+ },
+ "node_modules/@remix-run/web-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/web-stream/-/web-stream-1.1.0.tgz",
+ "integrity": "sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==",
+ "dependencies": {
+ "web-streams-polyfill": "^3.1.1"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
+ "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
+ "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
+ "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
+ "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
+ "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
+ "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
+ "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
+ "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
+ "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
+ "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
+ "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
+ "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
+ "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
+ "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
+ "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
+ "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
+ "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
+ "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
+ "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
+ "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "devOptional": true
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
+ "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/acorn": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
+ "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/bcryptjs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz",
+ "integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==",
+ "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "bcryptjs": "*"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
+ "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+ "dev": true,
+ "dependencies": {
+ "@types/deep-eql": "*"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
+ "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "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
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/mdast": {
+ "version": "3.0.15",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
+ "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "node_modules/@types/mdx": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz",
+ "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==",
+ "dev": true
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "24.2.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
+ "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~7.10.0"
+ }
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+ "devOptional": true
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
+ "dev": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
+ "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
+ "dev": true
+ },
+ "node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+ "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/type-utils": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+ "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "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": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true
+ },
+ "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
+ "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-android-arm64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
+ "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
+ "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-x64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
+ "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-freebsd-x64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
+ "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
+ "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
+ "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
+ "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
+ "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
+ "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
+ "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
+ "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
+ "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
+ "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
+ "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
+ "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
+ "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
+ "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
+ "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@vanilla-extract/babel-plugin-debug-ids": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.2.2.tgz",
+ "integrity": "sha512-MeDWGICAF9zA/OZLOKwhoRlsUW+fiMwnfuOAqFVohL31Agj7Q/RBWAYweqjHLgFBCsdnr6XIfwjJnmb2znEWxw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.23.9"
+ }
+ },
+ "node_modules/@vanilla-extract/css": {
+ "version": "1.17.4",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.4.tgz",
+ "integrity": "sha512-m3g9nQDWPtL+sTFdtCGRMI1Vrp86Ay4PBYq1Bo7Bnchj5ElNtAJpOqD+zg+apthVA4fB7oVpMWNjwpa6ElDWFQ==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/hash": "^0.9.0",
+ "@vanilla-extract/private": "^1.0.9",
+ "css-what": "^6.1.0",
+ "cssesc": "^3.0.0",
+ "csstype": "^3.0.7",
+ "dedent": "^1.5.3",
+ "deep-object-diff": "^1.1.9",
+ "deepmerge": "^4.2.2",
+ "lru-cache": "^10.4.3",
+ "media-query-parser": "^2.0.2",
+ "modern-ahocorasick": "^1.0.0",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "node_modules/@vanilla-extract/integration": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.5.0.tgz",
+ "integrity": "sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.20.7",
+ "@babel/plugin-syntax-typescript": "^7.20.0",
+ "@vanilla-extract/babel-plugin-debug-ids": "^1.0.4",
+ "@vanilla-extract/css": "^1.14.0",
+ "esbuild": "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0",
+ "eval": "0.1.8",
+ "find-up": "^5.0.0",
+ "javascript-stringify": "^2.0.1",
+ "lodash": "^4.17.21",
+ "mlly": "^1.4.2",
+ "outdent": "^0.8.0",
+ "vite": "^5.0.11",
+ "vite-node": "^1.2.0"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.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",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/vite-node": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
+ "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
+ "dev": true,
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.4",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/@vanilla-extract/private": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz",
+ "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==",
+ "dev": true
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/ui": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz",
+ "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "fflate": "^0.8.2",
+ "flatted": "^3.3.3",
+ "pathe": "^2.0.3",
+ "sirv": "^3.0.1",
+ "tinyglobby": "^0.2.14",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "3.2.4"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@web3-storage/multipart-parser": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz",
+ "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="
+ },
+ "node_modules/@zxing/text-encoding": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
+ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
+ "optional": true
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "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,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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,
+ "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-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true
+ },
+ "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
+ },
+ "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,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true
+ },
+ "node_modules/astring": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
+ "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
+ "dev": true,
+ "bin": {
+ "astring": "bin/astring"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.10.3",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
+ "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "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
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "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"
+ }
+ ]
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/bcryptjs": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
+ "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "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==",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
+ "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==",
+ "dev": true,
+ "dependencies": {
+ "pako": "~0.2.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz",
+ "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001733",
+ "electron-to-chromium": "^1.5.199",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "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"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/c12": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
+ "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+ "devOptional": true,
+ "dependencies": {
+ "chokidar": "^4.0.3",
+ "confbox": "^0.2.2",
+ "defu": "^6.1.4",
+ "dotenv": "^16.6.1",
+ "exsolve": "^1.0.7",
+ "giget": "^2.0.0",
+ "jiti": "^2.4.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^1.0.0",
+ "pkg-types": "^2.2.0",
+ "rc9": "^2.1.2"
+ },
+ "peerDependencies": {
+ "magicast": "^0.3.5"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "17.1.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz",
+ "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/fs": "^3.1.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^10.2.2",
+ "lru-cache": "^7.7.1",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^4.0.0",
+ "ssri": "^10.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001734",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
+ "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz",
+ "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==",
+ "dev": true,
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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,
+ "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/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "devOptional": true,
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "devOptional": true,
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "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,
+ "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
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "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
+ },
+ "node_modules/confbox": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+ "devOptional": true
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "devOptional": true,
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "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,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/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,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "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,
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
+ "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
+ "dev": true,
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
+ "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
+ "dev": true,
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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
+ },
+ "node_modules/deep-object-diff": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz",
+ "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==",
+ "dev": true
+ },
+ "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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deepmerge-ts": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+ "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dev": true,
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "devOptional": true
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "devOptional": true
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true
+ },
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "node_modules/duplexify/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "node_modules/duplexify/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/duplexify/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/duplexify/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/effect": {
+ "version": "3.16.12",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
+ "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
+ "devOptional": true,
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "fast-check": "^3.23.1"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.200",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz",
+ "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
+ "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.6",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.4",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "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/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.6.tgz",
+ "integrity": "sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.6",
+ "@esbuild/android-arm64": "0.17.6",
+ "@esbuild/android-x64": "0.17.6",
+ "@esbuild/darwin-arm64": "0.17.6",
+ "@esbuild/darwin-x64": "0.17.6",
+ "@esbuild/freebsd-arm64": "0.17.6",
+ "@esbuild/freebsd-x64": "0.17.6",
+ "@esbuild/linux-arm": "0.17.6",
+ "@esbuild/linux-arm64": "0.17.6",
+ "@esbuild/linux-ia32": "0.17.6",
+ "@esbuild/linux-loong64": "0.17.6",
+ "@esbuild/linux-mips64el": "0.17.6",
+ "@esbuild/linux-ppc64": "0.17.6",
+ "@esbuild/linux-riscv64": "0.17.6",
+ "@esbuild/linux-s390x": "0.17.6",
+ "@esbuild/linux-x64": "0.17.6",
+ "@esbuild/netbsd-x64": "0.17.6",
+ "@esbuild/openbsd-x64": "0.17.6",
+ "@esbuild/sunos-x64": "0.17.6",
+ "@esbuild/win32-arm64": "0.17.6",
+ "@esbuild/win32-ia32": "0.17.6",
+ "@esbuild/win32-x64": "0.17.6"
+ }
+ },
+ "node_modules/esbuild-plugins-node-modules-polyfill": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.7.1.tgz",
+ "integrity": "sha512-IEaUhaS1RukGGcatDzqJmR+AzUWJ2upTJZP2i7FzR37Iw5Lk0LReCTnWnPMWnGG9lO4MWTGKEGGLWEOPegTRJA==",
+ "dev": true,
+ "dependencies": {
+ "@jspm/core": "^2.1.0",
+ "local-pkg": "^1.1.1",
+ "resolve.exports": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "esbuild": ">=0.14.0 <=0.25.x"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "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,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
+ "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
+ "dev": true,
+ "dependencies": {
+ "@nolyfill/is-core-module": "1.0.39",
+ "debug": "^4.4.0",
+ "get-tsconfig": "^4.10.0",
+ "is-bun-module": "^2.0.0",
+ "stable-hash": "^0.0.5",
+ "tinyglobby": "^0.2.13",
+ "unrs-resolver": "^1.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-import-resolver-typescript"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*",
+ "eslint-plugin-import-x": "*"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-import": {
+ "optional": true
+ },
+ "eslint-plugin-import-x": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
+ "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "dev": true,
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/eslint-plugin-import/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+ "dev": true,
+ "dependencies": {
+ "aria-query": "^5.3.2",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
+ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "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,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.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,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "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,
+ "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,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-util-attach-comments": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz",
+ "integrity": "sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-build-jsx": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz",
+ "integrity": "sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "estree-util-is-identifier-name": "^2.0.0",
+ "estree-walker": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz",
+ "integrity": "sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-to-js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz",
+ "integrity": "sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "astring": "^1.8.0",
+ "source-map": "^0.7.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-value-to-estree": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz",
+ "integrity": "sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-obj": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/estree-util-visit": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.2.1.tgz",
+ "integrity": "sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eval": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz",
+ "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "require-like": ">= 0.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit-hook": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
+ "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
+ "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
+ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
+ "devOptional": true
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "node_modules/fast-check": {
+ "version": "3.23.2",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
+ "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "dependencies": {
+ "pure-rand": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.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
+ },
+ "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,
+ "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,
+ "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
+ },
+ "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
+ },
+ "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,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fault": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz",
+ "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==",
+ "dev": true,
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.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==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "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,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-up-simple": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+ "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "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
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+ "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "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==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generic-names": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz",
+ "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^3.2.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-port": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
+ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
+ "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+ "dev": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/giget": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
+ "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+ "devOptional": true,
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.0",
+ "defu": "^6.1.4",
+ "node-fetch-native": "^1.6.6",
+ "nypm": "^0.6.0",
+ "pathe": "^2.0.3"
+ },
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "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,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globals/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/gunzip-maybe": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz",
+ "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==",
+ "dev": true,
+ "dependencies": {
+ "browserify-zlib": "^0.1.4",
+ "is-deflate": "^1.0.0",
+ "is-gzip": "^1.0.0",
+ "peek-stream": "^1.1.0",
+ "pumpify": "^1.3.3",
+ "through2": "^2.0.3"
+ },
+ "bin": {
+ "gunzip-maybe": "bin.js"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-to-estree": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz",
+ "integrity": "sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^2.0.0",
+ "@types/unist": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "estree-util-attach-comments": "^2.0.0",
+ "estree-util-is-identifier-name": "^2.0.0",
+ "hast-util-whitespace": "^2.0.0",
+ "mdast-util-mdx-expression": "^1.0.0",
+ "mdast-util-mdxjs-esm": "^1.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-object": "^0.4.1",
+ "unist-util-position": "^4.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz",
+ "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+ "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+ "devOptional": true,
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "dev": true,
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "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"
+ }
+ ]
+ },
+ "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,
+ "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,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/index-to-position": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz",
+ "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
+ "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "dev": true,
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
+ "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"
+ }
+ ],
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-bun-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
+ "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.7.1"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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,
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-deflate": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz",
+ "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==",
+ "dev": true
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+ "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.0",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-gzip": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz",
+ "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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,
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "node_modules/isbot": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/isbot/-/isbot-4.4.0.tgz",
+ "integrity": "sha512-8ZvOWUA68kyJO4hHJdWjyreq7TYNWTS9y15IzeqVdKxR9pPr3P/3r9AHcoIv9M0Rllkao5qWz2v1lmcyKIVCzQ==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/javascript-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
+ "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
+ "dev": true
+ },
+ "node_modules/jiti": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
+ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
+ "devOptional": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz",
+ "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "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
+ },
+ "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
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "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,
+ "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,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "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,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/loader-utils": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
+ "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/local-pkg": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
+ "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
+ "dev": true,
+ "dependencies": {
+ "mlly": "^1.7.4",
+ "pkg-types": "^2.0.1",
+ "quansync": "^0.2.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "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,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "dev": true
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true
+ },
+ "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
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz",
+ "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "devOptional": true
+ },
+ "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,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/markdown-extensions": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
+ "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.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==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdast-util-definitions": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
+ "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "unist-util-visit": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+ "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "mdast-util-to-string": "^3.1.0",
+ "micromark": "^3.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "uvu": "^0.5.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-frontmatter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.1.tgz",
+ "integrity": "sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-to-markdown": "^1.3.0",
+ "micromark-extension-frontmatter": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz",
+ "integrity": "sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==",
+ "dev": true,
+ "dependencies": {
+ "mdast-util-from-markdown": "^1.0.0",
+ "mdast-util-mdx-expression": "^1.0.0",
+ "mdast-util-mdx-jsx": "^2.0.0",
+ "mdast-util-mdxjs-esm": "^1.0.0",
+ "mdast-util-to-markdown": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz",
+ "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "mdast-util-from-markdown": "^1.0.0",
+ "mdast-util-to-markdown": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz",
+ "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "ccount": "^2.0.0",
+ "mdast-util-from-markdown": "^1.1.0",
+ "mdast-util-to-markdown": "^1.3.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-remove-position": "^4.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "vfile-message": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz",
+ "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "mdast-util-from-markdown": "^1.0.0",
+ "mdast-util-to-markdown": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz",
+ "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "unist-util-is": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "12.3.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz",
+ "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==",
+ "dev": true,
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "mdast-util-definitions": "^5.0.0",
+ "micromark-util-sanitize-uri": "^1.1.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-generated": "^2.0.0",
+ "unist-util-position": "^4.0.0",
+ "unist-util-visit": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz",
+ "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^3.0.0",
+ "mdast-util-to-string": "^3.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "unist-util-visit": "^4.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+ "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/media-query-parser": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz",
+ "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "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,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
+ "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-core-commonmark": "^1.0.1",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-combine-extensions": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-sanitize-uri": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+ "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-factory-destination": "^1.0.0",
+ "micromark-factory-label": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-factory-title": "^1.0.0",
+ "micromark-factory-whitespace": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-classify-character": "^1.0.0",
+ "micromark-util-html-tag-name": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-extension-frontmatter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-1.1.1.tgz",
+ "integrity": "sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ==",
+ "dev": true,
+ "dependencies": {
+ "fault": "^2.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdx-expression": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz",
+ "integrity": "sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "micromark-factory-mdx-expression": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-events-to-acorn": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-extension-mdx-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz",
+ "integrity": "sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==",
+ "dev": true,
+ "dependencies": {
+ "@types/acorn": "^4.0.0",
+ "@types/estree": "^1.0.0",
+ "estree-util-is-identifier-name": "^2.0.0",
+ "micromark-factory-mdx-expression": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0",
+ "vfile-message": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdx-md": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz",
+ "integrity": "sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==",
+ "dev": true,
+ "dependencies": {
+ "micromark-util-types": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdxjs": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz",
+ "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.0.0",
+ "acorn-jsx": "^5.0.0",
+ "micromark-extension-mdx-expression": "^1.0.0",
+ "micromark-extension-mdx-jsx": "^1.0.0",
+ "micromark-extension-mdx-md": "^1.0.0",
+ "micromark-extension-mdxjs-esm": "^1.0.0",
+ "micromark-util-combine-extensions": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdxjs-esm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz",
+ "integrity": "sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "micromark-core-commonmark": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-events-to-acorn": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-position-from-estree": "^1.1.0",
+ "uvu": "^0.5.0",
+ "vfile-message": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+ "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+ "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-factory-mdx-expression": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz",
+ "integrity": "sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-events-to-acorn": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-position-from-estree": "^1.0.0",
+ "uvu": "^0.5.0",
+ "vfile-message": "^3.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz",
+ "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+ "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+ "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz",
+ "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+ "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+ "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+ "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+ "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+ "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+ "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-events-to-acorn": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz",
+ "integrity": "sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/acorn": "^4.0.0",
+ "@types/estree": "^1.0.0",
+ "@types/unist": "^2.0.0",
+ "estree-util-visit": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0",
+ "vfile-message": "^3.0.0"
+ }
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+ "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+ "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+ "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+ "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+ "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz",
+ "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-types": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz",
+ "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "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,
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-collect/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "dev": true
+ },
+ "node_modules/mlly": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
+ "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "pathe": "^2.0.1",
+ "pkg-types": "^1.3.0",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/mlly/node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "dev": true
+ },
+ "node_modules/mlly/node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dev": true,
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/modern-ahocorasick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz",
+ "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==",
+ "dev": true
+ },
+ "node_modules/morgan": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/morgan/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
+ "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==",
+ "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=="
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "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"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-postinstall": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
+ "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
+ "dev": true,
+ "bin": {
+ "napi-postinstall": "lib/cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/napi-postinstall"
+ }
+ },
+ "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
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-fetch-native": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+ "devOptional": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true
+ },
+ "node_modules/normalize-package-data": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+ "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
+ "devOptional": true,
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz",
+ "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz",
+ "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/hosted-git-info": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz",
+ "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^7.5.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz",
+ "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==",
+ "dev": true,
+ "dependencies": {
+ "npm-install-checks": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0",
+ "npm-package-arg": "^10.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nypm": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
+ "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
+ "devOptional": true,
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.2",
+ "pathe": "^2.0.3",
+ "pkg-types": "^2.2.0",
+ "tinyexec": "^1.0.1"
+ },
+ "bin": {
+ "nypm": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": "^14.16.0 || >=16.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "devOptional": true
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "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/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dev": true,
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/outdent": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz",
+ "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==",
+ "dev": true
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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,
+ "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,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true
+ },
+ "node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "dev": true
+ },
+ "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,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+ "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+ "devOptional": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "index-to-position": "^1.1.0",
+ "type-fest": "^4.39.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
+ "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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,
+ "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
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "devOptional": true
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/peek-stream": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
+ "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "duplexify": "^3.5.0",
+ "through2": "^2.0.3"
+ }
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "devOptional": true
+ },
+ "node_modules/periscopic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
+ "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^3.0.0",
+ "is-reference": "^3.0.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "devOptional": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
+ "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
+ "devOptional": true,
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "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"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-discard-duplicates": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
+ "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
+ "dev": true,
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "dev": true,
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-modules": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-6.0.1.tgz",
+ "integrity": "sha512-zyo2sAkVvuZFFy0gc2+4O+xar5dYlaVy/ebO24KT0ftk/iJevSNyPyQellsBLlnccwh7f6V6Y4GvuKRYToNgpQ==",
+ "dev": true,
+ "dependencies": {
+ "generic-names": "^4.0.0",
+ "icss-utils": "^5.1.0",
+ "lodash.camelcase": "^4.3.0",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
+ "postcss-modules-values": "^4.0.0",
+ "string-hash": "^1.1.3"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+ "dev": true,
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+ "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+ "dev": true,
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+ "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+ "dev": true,
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dev": true,
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true
+ },
+ "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,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/pretty-ms": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
+ "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
+ "dev": true,
+ "dependencies": {
+ "parse-ms": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/prisma": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.13.0.tgz",
+ "integrity": "sha512-dfzORf0AbcEyyzxuv2lEwG8g+WRGF/qDQTpHf/6JoHsyF5MyzCEZwClVaEmw3WXcobgadosOboKUgQU0kFs9kw==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/config": "6.13.0",
+ "@prisma/engines": "6.13.0"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+ "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
+ "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+ "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/pumpify": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+ "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+ "dev": true,
+ "dependencies": {
+ "duplexify": "^3.6.0",
+ "inherits": "^2.0.3",
+ "pump": "^2.0.0"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ]
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/quansync": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
+ "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ]
+ },
+ "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"
+ }
+ ]
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc9": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
+ "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+ "devOptional": true,
+ "dependencies": {
+ "defu": "^6.1.4",
+ "destr": "^2.0.3"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
+ "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
+ "dependencies": {
+ "@remix-run/router": "1.23.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz",
+ "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/read-package-up": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
+ "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
+ "devOptional": true,
+ "dependencies": {
+ "find-up-simple": "^1.0.0",
+ "read-pkg": "^9.0.0",
+ "type-fest": "^4.6.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
+ "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.3",
+ "normalize-package-data": "^6.0.0",
+ "parse-json": "^8.0.0",
+ "type-fest": "^4.6.0",
+ "unicorn-magic": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/remark-frontmatter": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz",
+ "integrity": "sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-frontmatter": "^1.0.0",
+ "micromark-extension-frontmatter": "^1.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-mdx": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-2.3.0.tgz",
+ "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==",
+ "dev": true,
+ "dependencies": {
+ "mdast-util-mdx": "^2.0.0",
+ "micromark-extension-mdxjs": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-mdx-frontmatter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-1.1.1.tgz",
+ "integrity": "sha512-7teX9DW4tI2WZkXS4DBxneYSY7NHiXl4AKdWDO9LXVweULlCT8OPWsOjLEnMIXViN1j+QcY8mfbq3k0EK6x3uA==",
+ "dev": true,
+ "dependencies": {
+ "estree-util-is-identifier-name": "^1.0.0",
+ "estree-util-value-to-estree": "^1.0.0",
+ "js-yaml": "^4.0.0",
+ "toml": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=12.2.0"
+ }
+ },
+ "node_modules/remark-mdx-frontmatter/node_modules/estree-util-is-identifier-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-1.1.0.tgz",
+ "integrity": "sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz",
+ "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==",
+ "dev": true,
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-from-markdown": "^1.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz",
+ "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==",
+ "dev": true,
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "mdast-util-to-hast": "^12.1.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/require-like": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
+ "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "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,
+ "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,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
+ "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "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,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/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,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
+ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.46.2",
+ "@rollup/rollup-android-arm64": "4.46.2",
+ "@rollup/rollup-darwin-arm64": "4.46.2",
+ "@rollup/rollup-darwin-x64": "4.46.2",
+ "@rollup/rollup-freebsd-arm64": "4.46.2",
+ "@rollup/rollup-freebsd-x64": "4.46.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.46.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.46.2",
+ "@rollup/rollup-linux-arm64-musl": "4.46.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.46.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-musl": "4.46.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.46.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.46.2",
+ "@rollup/rollup-win32-x64-msvc": "4.46.2",
+ "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"
+ }
+ ],
+ "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,
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "devOptional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "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=="
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "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,
+ "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,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "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,
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/sirv/node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "devOptional": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "devOptional": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "devOptional": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.22",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz",
+ "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==",
+ "devOptional": true
+ },
+ "node_modules/ssri": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz",
+ "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/stable-hash": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
+ "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
+ "dev": true
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "dev": true
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+ "dev": true
+ },
+ "node_modules/stream-slice": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz",
+ "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA=="
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-hash": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
+ "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/string.prototype.includes": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
+ "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "dev": true,
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
+ "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+ "dev": true,
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true
+ },
+ "node_modules/style-to-object": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz",
+ "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==",
+ "dev": true,
+ "dependencies": {
+ "inline-style-parser": "0.1.1"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "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,
+ "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,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "dev": true,
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-rtl": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/tailwindcss-rtl/-/tailwindcss-rtl-0.9.0.tgz",
+ "integrity": "sha512-y7yC8QXjluDBEFMSX33tV6xMYrf0B3sa+tOB5JSQb6/G6laBU313a+Z+qxu55M1Qyn8tDMttjomsA8IsJD+k+w=="
+ },
+ "node_modules/tailwindcss/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar/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,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "dev": true,
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "dev": true,
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
+ },
+ "node_modules/tar-fs/node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "dev": true,
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/through2/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "node_modules/through2/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/through2/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/through2/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
+ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
+ "devOptional": true
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
+ "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "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==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/toml": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
+ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
+ "dev": true
+ },
+ "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,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true
+ },
+ "node_modules/tsconfck": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
+ "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==",
+ "dev": true,
+ "bin": {
+ "tsconfck": "bin/tsconfck.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
+ "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+ "dev": true,
+ "dependencies": {
+ "json5": "^2.2.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/tsx": {
+ "version": "4.20.3",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
+ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "~0.25.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
+ "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
+ "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
+ "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
+ "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
+ "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
+ "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
+ "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
+ "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
+ "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
+ "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
+ "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
+ "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
+ "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
+ "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+ "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
+ "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
+ "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
+ "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
+ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.8",
+ "@esbuild/android-arm": "0.25.8",
+ "@esbuild/android-arm64": "0.25.8",
+ "@esbuild/android-x64": "0.25.8",
+ "@esbuild/darwin-arm64": "0.25.8",
+ "@esbuild/darwin-x64": "0.25.8",
+ "@esbuild/freebsd-arm64": "0.25.8",
+ "@esbuild/freebsd-x64": "0.25.8",
+ "@esbuild/linux-arm": "0.25.8",
+ "@esbuild/linux-arm64": "0.25.8",
+ "@esbuild/linux-ia32": "0.25.8",
+ "@esbuild/linux-loong64": "0.25.8",
+ "@esbuild/linux-mips64el": "0.25.8",
+ "@esbuild/linux-ppc64": "0.25.8",
+ "@esbuild/linux-riscv64": "0.25.8",
+ "@esbuild/linux-s390x": "0.25.8",
+ "@esbuild/linux-x64": "0.25.8",
+ "@esbuild/netbsd-arm64": "0.25.8",
+ "@esbuild/netbsd-x64": "0.25.8",
+ "@esbuild/openbsd-arm64": "0.25.8",
+ "@esbuild/openbsd-x64": "0.25.8",
+ "@esbuild/openharmony-arm64": "0.25.8",
+ "@esbuild/sunos-x64": "0.25.8",
+ "@esbuild/win32-arm64": "0.25.8",
+ "@esbuild/win32-ia32": "0.25.8",
+ "@esbuild/win32-x64": "0.25.8"
+ }
+ },
+ "node_modules/turbo-stream": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.1.tgz",
+ "integrity": "sha512-v8kOJXpG3WoTN/+at8vK7erSzo6nW6CIaeOvNOkHQVDajfz1ZVeSxCbc6tOH4hrGZW7VUCV0TOXd8CPzYnYkrw=="
+ },
+ "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,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "devOptional": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
+ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
+ "dev": true
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici": {
+ "version": "6.21.3",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
+ "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unified": {
+ "version": "10.1.2",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
+ "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "bail": "^2.0.0",
+ "extend": "^3.0.0",
+ "is-buffer": "^2.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unified/node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unique-filename": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
+ "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==",
+ "dev": true,
+ "dependencies": {
+ "unique-slug": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz",
+ "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/unist-util-generated": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz",
+ "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz",
+ "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz",
+ "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position-from-estree": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz",
+ "integrity": "sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-remove-position": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz",
+ "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-visit": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+ "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz",
+ "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0",
+ "unist-util-visit-parents": "^5.1.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz",
+ "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unrs-resolver": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
+ "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "napi-postinstall": "^0.3.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unrs-resolver"
+ },
+ "optionalDependencies": {
+ "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
+ "@unrs/resolver-binding-android-arm64": "1.11.1",
+ "@unrs/resolver-binding-darwin-arm64": "1.11.1",
+ "@unrs/resolver-binding-darwin-x64": "1.11.1",
+ "@unrs/resolver-binding-freebsd-x64": "1.11.1",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
+ "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.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,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "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
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uvu": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
+ "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.0",
+ "diff": "^5.0.0",
+ "kleur": "^4.0.3",
+ "sade": "^1.7.3"
+ },
+ "bin": {
+ "uvu": "bin.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/valibot": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz",
+ "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==",
+ "dev": true,
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "devOptional": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
+ "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "5.3.7",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz",
+ "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "is-buffer": "^2.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "vfile-message": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz",
+ "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==",
+ "dev": true,
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-stringify-position": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "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/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-tsconfig-paths": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
+ "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "globrex": "^0.1.2",
+ "tsconfck": "^3.0.3"
+ },
+ "peerDependencies": {
+ "vite": "*"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
+ "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
+ "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
+ "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
+ "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
+ "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
+ "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
+ "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
+ "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
+ "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
+ "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
+ "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
+ "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
+ "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
+ "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+ "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
+ "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
+ "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
+ "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/esbuild": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
+ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.8",
+ "@esbuild/android-arm": "0.25.8",
+ "@esbuild/android-arm64": "0.25.8",
+ "@esbuild/android-x64": "0.25.8",
+ "@esbuild/darwin-arm64": "0.25.8",
+ "@esbuild/darwin-x64": "0.25.8",
+ "@esbuild/freebsd-arm64": "0.25.8",
+ "@esbuild/freebsd-x64": "0.25.8",
+ "@esbuild/linux-arm": "0.25.8",
+ "@esbuild/linux-arm64": "0.25.8",
+ "@esbuild/linux-ia32": "0.25.8",
+ "@esbuild/linux-loong64": "0.25.8",
+ "@esbuild/linux-mips64el": "0.25.8",
+ "@esbuild/linux-ppc64": "0.25.8",
+ "@esbuild/linux-riscv64": "0.25.8",
+ "@esbuild/linux-s390x": "0.25.8",
+ "@esbuild/linux-x64": "0.25.8",
+ "@esbuild/netbsd-arm64": "0.25.8",
+ "@esbuild/netbsd-x64": "0.25.8",
+ "@esbuild/openbsd-arm64": "0.25.8",
+ "@esbuild/openbsd-x64": "0.25.8",
+ "@esbuild/openharmony-arm64": "0.25.8",
+ "@esbuild/sunos-x64": "0.25.8",
+ "@esbuild/win32-arm64": "0.25.8",
+ "@esbuild/win32-ia32": "0.25.8",
+ "@esbuild/win32-x64": "0.25.8"
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest/node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dev": true,
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/web-encoding": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
+ "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
+ "dependencies": {
+ "util": "^0.12.3"
+ },
+ "optionalDependencies": {
+ "@zxing/text-encoding": "0.9.0"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
+ "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yaml": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
+ "dev": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.0.17",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz",
+ "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7b04347
--- /dev/null
+++ b/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "car_mms_test",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "build": "remix vite:build",
+ "dev": "remix vite:dev",
+ "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+ "start": "remix-serve ./build/server/index.js",
+ "typecheck": "tsc",
+ "test": "vitest",
+ "test:run": "vitest run",
+ "db:seed": "tsx prisma/seed.ts",
+ "db:seed:maintenance-types": "tsx prisma/maintenanceTypeSeed.ts",
+ "db:migrate": "prisma migrate dev",
+ "db:generate": "prisma generate",
+ "db:push": "prisma db push",
+ "db:studio": "prisma studio"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ },
+ "dependencies": {
+ "@prisma/client": "^6.13.0",
+ "@remix-run/node": "^2.16.0",
+ "@remix-run/react": "^2.16.0",
+ "@remix-run/serve": "^2.16.0",
+ "bcryptjs": "^3.0.2",
+ "isbot": "^4.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwindcss-rtl": "^0.9.0",
+ "zod": "^4.0.17"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "^2.16.0",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
+ "@typescript-eslint/parser": "^6.7.4",
+ "@vitest/ui": "^3.2.4",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.38.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "postcss": "^8.4.38",
+ "prisma": "^6.13.0",
+ "tailwindcss": "^3.4.4",
+ "tsx": "^4.20.3",
+ "typescript": "^5.1.6",
+ "vite": "^6.0.0",
+ "vite-tsconfig-paths": "^4.2.1",
+ "vitest": "^3.2.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/prisma/CAR_DATASET_README.md b/prisma/CAR_DATASET_README.md
new file mode 100644
index 0000000..e552de9
--- /dev/null
+++ b/prisma/CAR_DATASET_README.md
@@ -0,0 +1,157 @@
+# Car Dataset System
+
+## Overview
+The Car Dataset system provides a structured way to manage vehicle manufacturers, models, and body types. This ensures data consistency and improves user experience when adding new vehicles.
+
+## Database Schema
+
+### CarDataset Table
+- `id`: Primary key (auto-increment)
+- `manufacturer`: Vehicle manufacturer name (e.g., "Toyota", "Honda")
+- `model`: Vehicle model name (e.g., "Camry", "Civic")
+- `bodyType`: Vehicle body type (e.g., "Sedan", "SUV", "Hatchback")
+- `isActive`: Boolean flag to enable/disable entries
+- `createdDate`: Record creation timestamp
+- `updateDate`: Record last update timestamp
+
+### Unique Constraint
+- Combination of `manufacturer` and `model` must be unique
+- This prevents duplicate entries for the same car model
+
+## How It Works
+
+### 1. Vehicle Form Enhancement
+The vehicle form now uses autocomplete inputs for:
+- **Manufacturer Selection**: Users type to search from available manufacturers
+- **Model Selection**: After selecting a manufacturer, users can search models for that manufacturer
+- **Body Type Auto-Fill**: When a model is selected, the body type is automatically filled from the dataset
+
+### 2. API Endpoints
+- `GET /api/car-dataset?action=manufacturers` - Returns all unique manufacturers
+- `GET /api/car-dataset?action=models&manufacturer=Toyota` - Returns models for a specific manufacturer
+- `GET /api/car-dataset?action=bodyType&manufacturer=Toyota&model=Camry` - Returns body type for a specific model
+
+### 3. User Experience Flow
+1. User enters plate number
+2. User starts typing manufacturer name → autocomplete shows matching manufacturers
+3. User selects manufacturer → model field becomes enabled
+4. User starts typing model name → autocomplete shows models for selected manufacturer
+5. User selects model → body type is automatically filled
+6. User continues with other vehicle details (year, transmission, etc.)
+
+## Seeding Data
+
+### Initial Dataset
+The system comes pre-loaded with popular car manufacturers and models:
+- Toyota (10 models)
+- Honda (8 models)
+- Nissan (9 models)
+- Hyundai (8 models)
+- Kia (8 models)
+- Ford (8 models)
+- Chevrolet (8 models)
+- BMW (8 models)
+- Mercedes-Benz (8 models)
+- Audi (8 models)
+- Lexus (8 models)
+- Mazda (7 models)
+- Mitsubishi (5 models)
+- Subaru (6 models)
+- Volkswagen (6 models)
+- Infiniti (5 models)
+- Acura (5 models)
+
+### Running the Seed
+```bash
+# Seed the car dataset
+npx tsx prisma/carDatasetSeed.ts
+```
+
+## Management Functions
+
+### Server Functions (`app/lib/car-dataset-management.server.ts`)
+- `getManufacturers()` - Get all unique manufacturers
+- `getModelsByManufacturer(manufacturer)` - Get models for a manufacturer
+- `getBodyType(manufacturer, model)` - Get body type for a specific model
+- `createCarDataset(data)` - Add new car dataset entry
+- `updateCarDataset(id, data)` - Update existing entry
+- `deleteCarDataset(id)` - Delete entry
+- `bulkImportCarDataset(data[])` - Bulk import multiple entries
+
+### Adding New Cars
+To add new cars to the dataset, you can:
+1. Use the bulk import function with an array of car data
+2. Create individual entries using the create function
+3. Manually insert into the database
+
+Example:
+```typescript
+await createCarDataset({
+ manufacturer: "Tesla",
+ model: "Model 3",
+ bodyType: "Sedan"
+});
+```
+
+## Benefits
+
+### 1. Data Consistency
+- Standardized manufacturer and model names
+- Consistent body type classifications
+- Prevents typos and variations in naming
+
+### 2. Improved User Experience
+- Fast autocomplete search
+- Reduced typing for common vehicles
+- Automatic body type detection
+
+### 3. Maintenance
+- Easy to add new manufacturers and models
+- Centralized vehicle data management
+- Can disable outdated models without deleting data
+
+### 4. Reporting
+- Accurate statistics by manufacturer
+- Consistent data for analytics
+- Better search and filtering capabilities
+
+## Migration Steps
+
+### 1. Database Migration
+```bash
+# Apply the schema changes
+npx prisma db push
+
+# Or run the SQL migration directly
+sqlite3 prisma/dev.db < prisma/migrations/add_car_dataset.sql
+```
+
+### 2. Generate Prisma Client
+```bash
+npx prisma generate
+```
+
+### 3. Seed the Dataset
+```bash
+npx tsx prisma/carDatasetSeed.ts
+```
+
+### 4. Update Application
+The vehicle form will automatically use the new car dataset system once the migration is complete.
+
+## Future Enhancements
+
+### Possible Additions
+- Vehicle trim levels per model
+- Engine specifications per model
+- Year ranges for model availability
+- Regional model variations
+- Integration with external vehicle databases
+- Admin interface for managing car dataset
+
+### API Extensions
+- Search across all fields
+- Pagination for large datasets
+- Filtering by body type or year
+- Export/import functionality
+- Validation against external sources
\ No newline at end of file
diff --git a/prisma/MAINTENANCE_TYPES_README.md b/prisma/MAINTENANCE_TYPES_README.md
new file mode 100644
index 0000000..1b325ba
--- /dev/null
+++ b/prisma/MAINTENANCE_TYPES_README.md
@@ -0,0 +1,141 @@
+# Maintenance Types Seeding
+
+This directory contains a dedicated seed file for populating the maintenance types table with comprehensive Arabic maintenance service types.
+
+## Files
+
+- `maintenanceTypeSeed.ts` - Standalone seed file for maintenance types
+- `MAINTENANCE_TYPES_README.md` - This documentation file
+
+## Usage
+
+### Run the maintenance types seed
+
+```bash
+npm run db:seed:maintenance-types
+```
+
+Or run directly with tsx:
+
+```bash
+npx tsx prisma/maintenanceTypeSeed.ts
+```
+
+### What it does
+
+The seed file will:
+1. Create new maintenance types that don't exist
+2. Update existing maintenance types with new descriptions
+3. Preserve existing data (no deletions)
+4. Show a summary of created/updated types
+5. Display all maintenance types in the database
+
+## Maintenance Types Included
+
+The seed includes 20 comprehensive maintenance types in Arabic:
+
+### Basic Maintenance
+- **صيانة دورية** - Comprehensive periodic maintenance
+- **تغيير زيت المحرك** - Engine oil and filter change
+- **فحص دوري** - Periodic inspection
+- **تنظيف شامل** - Complete cleaning
+
+### Engine & Transmission
+- **إصلاح المحرك** - Engine repair and maintenance
+- **إصلاح ناقل الحركة** - Transmission repair
+- **إصلاح الرادياتير** - Cooling system and radiator repair
+
+### Brakes & Suspension
+- **إصلاح الفرامل** - Brake system repair
+- **إصلاح التعليق** - Suspension system repair
+- **إصلاح الإطارات** - Tire repair and replacement
+
+### Electrical & Electronics
+- **إصلاح الكهرباء** - Electrical system repair
+- **إصلاح البطارية** - Battery and charging system
+- **إصلاح المصابيح** - Lighting system repair
+
+### Body & Interior
+- **إصلاح الهيكل** - Body and frame repair
+- **إصلاح الزجاج** - Glass repair and replacement
+- **إصلاح التكييف** - AC and climate control
+
+### Exhaust & Other
+- **إصلاح العادم** - Exhaust system repair
+- **فحص ما قبل السفر** - Pre-travel inspection
+- **صيانة طارئة** - Emergency maintenance
+- **أخرى** - Other maintenance types
+
+## Features
+
+### Smart Upsert Logic
+- Creates new maintenance types if they don't exist
+- Updates existing types with new descriptions
+- Preserves custom maintenance types added by users
+
+### Detailed Descriptions
+Each maintenance type includes:
+- Arabic name
+- Detailed description of what's included
+- Active status (all default to active)
+
+### Safe Operation
+- No data deletion
+- Preserves existing maintenance visits
+- Can be run multiple times safely
+
+### Comprehensive Logging
+- Shows progress for each maintenance type
+- Displays summary statistics
+- Lists all maintenance types in the database
+
+## Customization
+
+To add more maintenance types, edit the `maintenanceTypes` array in `maintenanceTypeSeed.ts`:
+
+```typescript
+{
+ name: 'نوع الصيانة الجديد',
+ description: 'وصف تفصيلي لنوع الصيانة',
+ isActive: true,
+}
+```
+
+## Integration with Main Seed
+
+The maintenance types seed is also integrated into the main seed file (`seed.ts`), so running `npm run db:seed` will also populate maintenance types.
+
+## Database Schema
+
+The maintenance types are stored in the `maintenance_types` table with:
+- `id` - Auto-increment primary key
+- `name` - Unique maintenance type name
+- `description` - Optional detailed description
+- `isActive` - Boolean flag for active/inactive status
+- `createdDate` - Creation timestamp
+- `updateDate` - Last update timestamp
+
+## Troubleshooting
+
+### Permission Errors
+Make sure your database connection has write permissions.
+
+### Duplicate Name Errors
+The seed handles duplicates gracefully by updating existing records.
+
+### TypeScript Errors
+Ensure all dependencies are installed:
+```bash
+npm install
+```
+
+### Database Connection Issues
+Check your `.env` file has the correct `DATABASE_URL`.
+
+## Future Enhancements
+
+- Add maintenance type categories
+- Include pricing suggestions
+- Add maintenance intervals
+- Support for multiple languages
+- Import/export functionality
\ No newline at end of file
diff --git a/prisma/carDatasetSeed.ts b/prisma/carDatasetSeed.ts
new file mode 100644
index 0000000..9a01524
--- /dev/null
+++ b/prisma/carDatasetSeed.ts
@@ -0,0 +1,1448 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+const carDataset = [
+ // Toyota (Very popular in Jordan) - Expanded
+ { manufacturer: 'Toyota', model: 'Camry', bodyType: 'Sedan' },
+ { manufacturer: 'Toyota', model: 'Corolla', bodyType: 'Sedan' },
+ { manufacturer: 'Toyota', model: 'Corolla Cross', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'RAV4', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Highlander', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Prius', bodyType: 'Hatchback' },
+ { manufacturer: 'Toyota', model: 'Prius Prime', bodyType: 'Hatchback' },
+ { manufacturer: 'Toyota', model: 'Land Cruiser', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Land Cruiser Prado', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Prado', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Hilux', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux Extra Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux Revo', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux Vigo', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux SR', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux SR5', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Hilux TRD', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Yaris', bodyType: 'Hatchback' },
+ { manufacturer: 'Toyota', model: 'Yaris Cross', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Yaris Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Toyota', model: 'Avalon', bodyType: 'Sedan' },
+ { manufacturer: 'Toyota', model: 'Fortuner', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'C-HR', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Venza', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: '4Runner', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Sequoia', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Sienna', bodyType: 'Van' },
+ { manufacturer: 'Toyota', model: 'Tacoma', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tacoma Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tacoma Access Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tacoma TRD Pro', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tacoma TRD Sport', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tacoma TRD Off-Road', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra CrewMax', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra TRD Pro', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra SR', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra SR5', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra Limited', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Tundra Platinum', bodyType: 'Pickup' },
+ { manufacturer: 'Toyota', model: 'Supra', bodyType: 'Coupe' },
+ { manufacturer: 'Toyota', model: '86', bodyType: 'Coupe' },
+ { manufacturer: 'Toyota', model: 'Mirai', bodyType: 'Sedan' },
+ { manufacturer: 'Toyota', model: 'Vitz', bodyType: 'Hatchback' },
+ { manufacturer: 'Toyota', model: 'Rush', bodyType: 'SUV' },
+ { manufacturer: 'Toyota', model: 'Innova', bodyType: 'Van' },
+ { manufacturer: 'Toyota', model: 'Hiace', bodyType: 'Van' },
+
+ // Hyundai (Very popular in Jordan) - Expanded
+ { manufacturer: 'Hyundai', model: 'Elantra', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Elantra N', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Sonata', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Sonata Hybrid', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Tucson', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Santa Fe', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Santa Cruz', bodyType: 'Pickup' },
+ { manufacturer: 'Hyundai', model: 'Accent', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Palisade', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Kona', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Kona Electric', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Creta', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'i10', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'i20', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'i30', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'i30 N', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'Veloster', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'Veloster N', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'Genesis', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Venue', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Nexo', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Ioniq', bodyType: 'Hatchback' },
+ { manufacturer: 'Hyundai', model: 'Ioniq 5', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Ioniq 6', bodyType: 'Sedan' },
+ { manufacturer: 'Hyundai', model: 'Bayon', bodyType: 'SUV' },
+ { manufacturer: 'Hyundai', model: 'Starex', bodyType: 'Van' },
+ { manufacturer: 'Hyundai', model: 'H1', bodyType: 'Van' },
+ { manufacturer: 'Hyundai', model: 'Azera', bodyType: 'Sedan' },
+
+ // Kia (Very popular in Jordan) - Expanded
+ { manufacturer: 'Kia', model: 'Optima', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'K5', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'K5 GT', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Forte', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Forte GT', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Cerato', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Cerato Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Kia', model: 'Sorento', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Sorento Hybrid', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Sportage', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Sportage Hybrid', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Rio', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Rio Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Kia', model: 'Telluride', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Soul', bodyType: 'Hatchback' },
+ { manufacturer: 'Kia', model: 'Soul EV', bodyType: 'Hatchback' },
+ { manufacturer: 'Kia', model: 'Stinger', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Stinger GT', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Picanto', bodyType: 'Hatchback' },
+ { manufacturer: 'Kia', model: 'Seltos', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Carnival', bodyType: 'Van' },
+ { manufacturer: 'Kia', model: 'Sedona', bodyType: 'Van' },
+ { manufacturer: 'Kia', model: 'Niro', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Niro EV', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Niro Hybrid', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'EV6', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'EV9', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Cadenza', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'K900', bodyType: 'Sedan' },
+ { manufacturer: 'Kia', model: 'Mohave', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Xceed', bodyType: 'SUV' },
+ { manufacturer: 'Kia', model: 'Proceed', bodyType: 'Wagon' },
+
+ // Nissan (Popular in Jordan) - Expanded
+ { manufacturer: 'Nissan', model: 'Altima', bodyType: 'Sedan' },
+ { manufacturer: 'Nissan', model: 'Sentra', bodyType: 'Sedan' },
+ { manufacturer: 'Nissan', model: 'Maxima', bodyType: 'Sedan' },
+ { manufacturer: 'Nissan', model: 'X-Trail', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Qashqai', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Pathfinder', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Armada', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Patrol', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Patrol Nismo', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Navara', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Navara Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Navara King Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Navara Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Navara Pro-4X', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Navara Calibre', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Frontier', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Frontier Crew Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Frontier King Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Frontier Pro-4X', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Frontier SV', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Titan', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Titan Crew Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Titan King Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Titan XD', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Titan Pro-4X', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Titan Platinum Reserve', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Hardbody', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'NP300', bodyType: 'Pickup' },
+ { manufacturer: 'Nissan', model: 'Micra', bodyType: 'Hatchback' },
+ { manufacturer: 'Nissan', model: 'Sunny', bodyType: 'Sedan' },
+ { manufacturer: 'Nissan', model: 'Versa', bodyType: 'Sedan' },
+ { manufacturer: 'Nissan', model: 'Kicks', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Juke', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Rogue', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Murano', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: 'Note', bodyType: 'Hatchback' },
+ { manufacturer: 'Nissan', model: 'Leaf', bodyType: 'Hatchback' },
+ { manufacturer: 'Nissan', model: 'Ariya', bodyType: 'SUV' },
+ { manufacturer: 'Nissan', model: '370Z', bodyType: 'Coupe' },
+ { manufacturer: 'Nissan', model: 'GT-R', bodyType: 'Coupe' },
+ { manufacturer: 'Nissan', model: 'NV200', bodyType: 'Van' },
+ { manufacturer: 'Nissan', model: 'Urvan', bodyType: 'Van' },
+ { manufacturer: 'Nissan', model: 'Teana', bodyType: 'Sedan' },
+ { manufacturer: 'Nissan', model: 'Tiida', bodyType: 'Hatchback' },
+
+ // Honda (Popular in Jordan) - Expanded
+ { manufacturer: 'Honda', model: 'Civic', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'Civic Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Honda', model: 'Civic Type R', bodyType: 'Hatchback' },
+ { manufacturer: 'Honda', model: 'Civic Si', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'Civic Coupe', bodyType: 'Coupe' },
+ { manufacturer: 'Honda', model: 'Accord', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'Accord Hybrid', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'Accord Sport', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'CR-V', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'CR-V Hybrid', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'HR-V', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'Pilot', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'Passport', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'City', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'City Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Honda', model: 'Jazz', bodyType: 'Hatchback' },
+ { manufacturer: 'Honda', model: 'Fit', bodyType: 'Hatchback' },
+ { manufacturer: 'Honda', model: 'Ridgeline', bodyType: 'Pickup' },
+ { manufacturer: 'Honda', model: 'Insight', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'Clarity', bodyType: 'Sedan' },
+ { manufacturer: 'Honda', model: 'Odyssey', bodyType: 'Van' },
+ { manufacturer: 'Honda', model: 'Element', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'S2000', bodyType: 'Convertible' },
+ { manufacturer: 'Honda', model: 'NSX', bodyType: 'Coupe' },
+ { manufacturer: 'Honda', model: 'BR-V', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'WR-V', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'NS1', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'Freed', bodyType: 'Van' },
+ { manufacturer: 'Honda', model: 'Vezel', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'Stepwgn', bodyType: 'Van' },
+ { manufacturer: 'Honda', model: 'Stream', bodyType: 'Van' },
+ { manufacturer: 'Honda', model: 'Crossroad', bodyType: 'SUV' },
+ { manufacturer: 'Honda', model: 'Crosstour', bodyType: 'SUV' },
+
+ // BYD (Chinese brand, increasingly popular in Jordan) - Expanded
+ { manufacturer: 'BYD', model: 'Atto 3', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Han', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'Han EV', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'Tang', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Tang EV', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Song Plus', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Song Pro', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Song Max', bodyType: 'Van' },
+ { manufacturer: 'BYD', model: 'Qin Plus', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'Qin Pro', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'Yuan Plus', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Yuan Pro', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'Dolphin', bodyType: 'Hatchback' },
+ { manufacturer: 'BYD', model: 'Seal', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'Seagull', bodyType: 'Hatchback' },
+ { manufacturer: 'BYD', model: 'e2', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'e3', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'e6', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'F3', bodyType: 'Sedan' },
+ { manufacturer: 'BYD', model: 'G3', bodyType: 'Hatchback' },
+ { manufacturer: 'BYD', model: 'S6', bodyType: 'SUV' },
+ { manufacturer: 'BYD', model: 'S7', bodyType: 'SUV' },
+
+ // NETA (Chinese EV brand) - Expanded
+ { manufacturer: 'NETA', model: 'V', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'V Pro', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'U', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'U Pro', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'U-II', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'S', bodyType: 'Sedan' },
+ { manufacturer: 'NETA', model: 'GT', bodyType: 'Coupe' },
+ { manufacturer: 'NETA', model: 'X', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'L', bodyType: 'SUV' },
+ { manufacturer: 'NETA', model: 'AYA', bodyType: 'Hatchback' },
+
+ // Tesla (Electric vehicles) - Expanded
+ { manufacturer: 'Tesla', model: 'Model 3', bodyType: 'Sedan' },
+ { manufacturer: 'Tesla', model: 'Model 3 Performance', bodyType: 'Sedan' },
+ { manufacturer: 'Tesla', model: 'Model Y', bodyType: 'SUV' },
+ { manufacturer: 'Tesla', model: 'Model Y Performance', bodyType: 'SUV' },
+ { manufacturer: 'Tesla', model: 'Model S', bodyType: 'Sedan' },
+ { manufacturer: 'Tesla', model: 'Model S Plaid', bodyType: 'Sedan' },
+ { manufacturer: 'Tesla', model: 'Model X', bodyType: 'SUV' },
+ { manufacturer: 'Tesla', model: 'Model X Plaid', bodyType: 'SUV' },
+ { manufacturer: 'Tesla', model: 'Cybertruck', bodyType: 'Pickup' },
+ { manufacturer: 'Tesla', model: 'Roadster', bodyType: 'Convertible' },
+
+ // Chery (Chinese brand popular in Jordan) - Expanded
+ { manufacturer: 'Chery', model: 'Tiggo 8', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 8 Pro', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 7', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 7 Pro', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 4', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 4 Pro', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 3x', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Tiggo 2', bodyType: 'SUV' },
+ { manufacturer: 'Chery', model: 'Arrizo 6', bodyType: 'Sedan' },
+ { manufacturer: 'Chery', model: 'Arrizo 6 Pro', bodyType: 'Sedan' },
+ { manufacturer: 'Chery', model: 'Arrizo 5', bodyType: 'Sedan' },
+ { manufacturer: 'Chery', model: 'Arrizo 5 Plus', bodyType: 'Sedan' },
+ { manufacturer: 'Chery', model: 'Arrizo 3', bodyType: 'Sedan' },
+ { manufacturer: 'Chery', model: 'QQ', bodyType: 'Hatchback' },
+ { manufacturer: 'Chery', model: 'QQ Ice Cream', bodyType: 'Hatchback' },
+ { manufacturer: 'Chery', model: 'eQ1', bodyType: 'Hatchback' },
+ { manufacturer: 'Chery', model: 'eQ5', bodyType: 'SUV' },
+
+ // Geely (Chinese brand)
+ { manufacturer: 'Geely', model: 'Coolray', bodyType: 'SUV' },
+ { manufacturer: 'Geely', model: 'Emgrand', bodyType: 'Sedan' },
+ { manufacturer: 'Geely', model: 'Atlas', bodyType: 'SUV' },
+ { manufacturer: 'Geely', model: 'Tugella', bodyType: 'SUV' },
+
+ // Great Wall (Chinese brand)
+ { manufacturer: 'Great Wall', model: 'Haval H6', bodyType: 'SUV' },
+ { manufacturer: 'Great Wall', model: 'Haval H9', bodyType: 'SUV' },
+ { manufacturer: 'Great Wall', model: 'Haval F7', bodyType: 'SUV' },
+ { manufacturer: 'Great Wall', model: 'Wingle', bodyType: 'Pickup' },
+
+ // MG (Chinese-owned British brand, popular in Jordan)
+ { manufacturer: 'MG', model: 'HS', bodyType: 'SUV' },
+ { manufacturer: 'MG', model: 'ZS', bodyType: 'SUV' },
+ { manufacturer: 'MG', model: '6', bodyType: 'Sedan' },
+ { manufacturer: 'MG', model: '5', bodyType: 'Sedan' },
+ { manufacturer: 'MG', model: 'RX5', bodyType: 'SUV' },
+
+ // BMW (Luxury German brand) - Expanded
+ { manufacturer: 'BMW', model: '1 Series', bodyType: 'Hatchback' },
+ { manufacturer: 'BMW', model: '2 Series', bodyType: 'Coupe' },
+ { manufacturer: 'BMW', model: '2 Series Gran Coupe', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: '3 Series', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: '3 Series Touring', bodyType: 'Wagon' },
+ { manufacturer: 'BMW', model: '4 Series', bodyType: 'Coupe' },
+ { manufacturer: 'BMW', model: '4 Series Gran Coupe', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: '4 Series Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'BMW', model: '5 Series', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: '5 Series Touring', bodyType: 'Wagon' },
+ { manufacturer: 'BMW', model: '6 Series', bodyType: 'Coupe' },
+ { manufacturer: 'BMW', model: '6 Series Gran Turismo', bodyType: 'Hatchback' },
+ { manufacturer: 'BMW', model: '7 Series', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: '8 Series', bodyType: 'Coupe' },
+ { manufacturer: 'BMW', model: '8 Series Gran Coupe', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: 'X1', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X2', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X3', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X3 M', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X4', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X4 M', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X5', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X5 M', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X6', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X6 M', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'X7', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'Z4', bodyType: 'Convertible' },
+ { manufacturer: 'BMW', model: 'i3', bodyType: 'Hatchback' },
+ { manufacturer: 'BMW', model: 'i4', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: 'i7', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: 'iX', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'iX1', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'iX3', bodyType: 'SUV' },
+ { manufacturer: 'BMW', model: 'M2', bodyType: 'Coupe' },
+ { manufacturer: 'BMW', model: 'M3', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: 'M4', bodyType: 'Coupe' },
+ { manufacturer: 'BMW', model: 'M5', bodyType: 'Sedan' },
+ { manufacturer: 'BMW', model: 'M8', bodyType: 'Coupe' },
+
+ // Mercedes-Benz (Luxury German brand) - Expanded
+ { manufacturer: 'Mercedes-Benz', model: 'A-Class', bodyType: 'Hatchback' },
+ { manufacturer: 'Mercedes-Benz', model: 'A-Class Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'B-Class', bodyType: 'Van' },
+ { manufacturer: 'Mercedes-Benz', model: 'C-Class', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'C-Class Wagon', bodyType: 'Wagon' },
+ { manufacturer: 'Mercedes-Benz', model: 'C-Class Coupe', bodyType: 'Coupe' },
+ { manufacturer: 'Mercedes-Benz', model: 'C-Class Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Mercedes-Benz', model: 'CLA', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'CLS', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'E-Class', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'E-Class Wagon', bodyType: 'Wagon' },
+ { manufacturer: 'Mercedes-Benz', model: 'E-Class Coupe', bodyType: 'Coupe' },
+ { manufacturer: 'Mercedes-Benz', model: 'E-Class Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Mercedes-Benz', model: 'S-Class', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'S-Class Coupe', bodyType: 'Coupe' },
+ { manufacturer: 'Mercedes-Benz', model: 'S-Class Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLA', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLB', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLC', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLC Coupe', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLE', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLE Coupe', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'GLS', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'G-Class', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'SL', bodyType: 'Convertible' },
+ { manufacturer: 'Mercedes-Benz', model: 'SLC', bodyType: 'Convertible' },
+ { manufacturer: 'Mercedes-Benz', model: 'AMG GT', bodyType: 'Coupe' },
+ { manufacturer: 'Mercedes-Benz', model: 'EQA', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'EQB', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'EQC', bodyType: 'SUV' },
+ { manufacturer: 'Mercedes-Benz', model: 'EQE', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'EQS', bodyType: 'Sedan' },
+ { manufacturer: 'Mercedes-Benz', model: 'EQV', bodyType: 'Van' },
+ { manufacturer: 'Mercedes-Benz', model: 'Sprinter', bodyType: 'Van' },
+ { manufacturer: 'Mercedes-Benz', model: 'Vito', bodyType: 'Van' },
+
+ // Audi (Luxury German brand) - Expanded
+ { manufacturer: 'Audi', model: 'A1', bodyType: 'Hatchback' },
+ { manufacturer: 'Audi', model: 'A3', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'A3 Sportback', bodyType: 'Hatchback' },
+ { manufacturer: 'Audi', model: 'A3 Cabriolet', bodyType: 'Convertible' },
+ { manufacturer: 'Audi', model: 'S3', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'RS3', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'A4', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'A4 Avant', bodyType: 'Wagon' },
+ { manufacturer: 'Audi', model: 'A4 Allroad', bodyType: 'Wagon' },
+ { manufacturer: 'Audi', model: 'S4', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'RS4', bodyType: 'Wagon' },
+ { manufacturer: 'Audi', model: 'A5', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'A5 Sportback', bodyType: 'Hatchback' },
+ { manufacturer: 'Audi', model: 'A5 Cabriolet', bodyType: 'Convertible' },
+ { manufacturer: 'Audi', model: 'S5', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'RS5', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'A6', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'A6 Avant', bodyType: 'Wagon' },
+ { manufacturer: 'Audi', model: 'A6 Allroad', bodyType: 'Wagon' },
+ { manufacturer: 'Audi', model: 'S6', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'RS6', bodyType: 'Wagon' },
+ { manufacturer: 'Audi', model: 'A7', bodyType: 'Hatchback' },
+ { manufacturer: 'Audi', model: 'S7', bodyType: 'Hatchback' },
+ { manufacturer: 'Audi', model: 'RS7', bodyType: 'Hatchback' },
+ { manufacturer: 'Audi', model: 'A8', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'S8', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'Q2', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q3', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q3 Sportback', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'SQ3', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'RSQ3', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q4 e-tron', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q5', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q5 Sportback', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'SQ5', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'RSQ5', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q7', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'SQ7', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'RSQ7', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'Q8', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'SQ8', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'RSQ8', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'e-tron', bodyType: 'SUV' },
+ { manufacturer: 'Audi', model: 'e-tron GT', bodyType: 'Sedan' },
+ { manufacturer: 'Audi', model: 'TT', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'TT Roadster', bodyType: 'Convertible' },
+ { manufacturer: 'Audi', model: 'TTS', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'TT RS', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'R8', bodyType: 'Coupe' },
+ { manufacturer: 'Audi', model: 'R8 Spyder', bodyType: 'Convertible' },
+
+ // Lexus (Toyota luxury brand)
+ { manufacturer: 'Lexus', model: 'ES', bodyType: 'Sedan' },
+ { manufacturer: 'Lexus', model: 'RX', bodyType: 'SUV' },
+ { manufacturer: 'Lexus', model: 'NX', bodyType: 'SUV' },
+ { manufacturer: 'Lexus', model: 'GX', bodyType: 'SUV' },
+ { manufacturer: 'Lexus', model: 'LX', bodyType: 'SUV' },
+ { manufacturer: 'Lexus', model: 'IS', bodyType: 'Sedan' },
+ { manufacturer: 'Lexus', model: 'LS', bodyType: 'Sedan' },
+ { manufacturer: 'Lexus', model: 'UX', bodyType: 'SUV' },
+
+ // Mazda (Japanese brand) - Expanded
+ { manufacturer: 'Mazda', model: 'Mazda2', bodyType: 'Hatchback' },
+ { manufacturer: 'Mazda', model: 'Mazda2 Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Mazda', model: 'Mazda3', bodyType: 'Sedan' },
+ { manufacturer: 'Mazda', model: 'Mazda3 Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Mazda', model: 'Mazda3 Turbo', bodyType: 'Sedan' },
+ { manufacturer: 'Mazda', model: 'Mazda6', bodyType: 'Sedan' },
+ { manufacturer: 'Mazda', model: 'Mazda6 Wagon', bodyType: 'Wagon' },
+ { manufacturer: 'Mazda', model: 'Mazda6 Turbo', bodyType: 'Sedan' },
+ { manufacturer: 'Mazda', model: 'CX-3', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-30', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-30 Turbo', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-5', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-5 Turbo', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-50', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-60', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-70', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-8', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-9', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'CX-90', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'MX-5 Miata', bodyType: 'Convertible' },
+ { manufacturer: 'Mazda', model: 'MX-5 RF', bodyType: 'Convertible' },
+ { manufacturer: 'Mazda', model: 'MX-30', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'BT-50', bodyType: 'Pickup' },
+ { manufacturer: 'Mazda', model: 'B-Series', bodyType: 'Pickup' },
+ { manufacturer: 'Mazda', model: 'Tribute', bodyType: 'SUV' },
+ { manufacturer: 'Mazda', model: 'MPV', bodyType: 'Van' },
+ { manufacturer: 'Mazda', model: 'Premacy', bodyType: 'Van' },
+ { manufacturer: 'Mazda', model: 'Biante', bodyType: 'Van' },
+
+ // Mitsubishi (Japanese brand) - Expanded
+ { manufacturer: 'Mitsubishi', model: 'Mirage', bodyType: 'Hatchback' },
+ { manufacturer: 'Mitsubishi', model: 'Mirage G4', bodyType: 'Sedan' },
+ { manufacturer: 'Mitsubishi', model: 'Lancer', bodyType: 'Sedan' },
+ { manufacturer: 'Mitsubishi', model: 'Lancer Evolution', bodyType: 'Sedan' },
+ { manufacturer: 'Mitsubishi', model: 'Galant', bodyType: 'Sedan' },
+ { manufacturer: 'Mitsubishi', model: 'Diamante', bodyType: 'Sedan' },
+ { manufacturer: 'Mitsubishi', model: 'ASX', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Eclipse Cross', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Outlander', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Outlander PHEV', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Outlander Sport', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Pajero', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Pajero Sport', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Montero', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'Endeavor', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'L200', bodyType: 'Pickup' },
+ { manufacturer: 'Mitsubishi', model: 'Triton', bodyType: 'Pickup' },
+ { manufacturer: 'Mitsubishi', model: 'Raider', bodyType: 'Pickup' },
+ { manufacturer: 'Mitsubishi', model: 'Delica', bodyType: 'Van' },
+ { manufacturer: 'Mitsubishi', model: 'Grandis', bodyType: 'Van' },
+ { manufacturer: 'Mitsubishi', model: 'Xpander', bodyType: 'Van' },
+ { manufacturer: 'Mitsubishi', model: 'Xpander Cross', bodyType: 'SUV' },
+ { manufacturer: 'Mitsubishi', model: 'i-MiEV', bodyType: 'Hatchback' },
+ { manufacturer: 'Mitsubishi', model: '3000GT', bodyType: 'Coupe' },
+ { manufacturer: 'Mitsubishi', model: 'Eclipse', bodyType: 'Coupe' },
+ { manufacturer: 'Mitsubishi', model: 'Spyder', bodyType: 'Convertible' },
+
+ // Suzuki (Japanese brand, popular in Jordan) - Expanded
+ { manufacturer: 'Suzuki', model: 'Swift', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Swift Sport', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Swift Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Suzuki', model: 'Vitara', bodyType: 'SUV' },
+ { manufacturer: 'Suzuki', model: 'S-Cross', bodyType: 'SUV' },
+ { manufacturer: 'Suzuki', model: 'Jimny', bodyType: 'SUV' },
+ { manufacturer: 'Suzuki', model: 'Jimny Sierra', bodyType: 'SUV' },
+ { manufacturer: 'Suzuki', model: 'Baleno', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Dzire', bodyType: 'Sedan' },
+ { manufacturer: 'Suzuki', model: 'Ciaz', bodyType: 'Sedan' },
+ { manufacturer: 'Suzuki', model: 'Ertiga', bodyType: 'Van' },
+ { manufacturer: 'Suzuki', model: 'XL6', bodyType: 'Van' },
+ { manufacturer: 'Suzuki', model: 'Celerio', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Wagon R', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Alto', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Ignis', bodyType: 'Hatchback' },
+ { manufacturer: 'Suzuki', model: 'Kizashi', bodyType: 'Sedan' },
+ { manufacturer: 'Suzuki', model: 'Grand Vitara', bodyType: 'SUV' },
+
+ // Ford (American brand) - Expanded
+ { manufacturer: 'Ford', model: 'Focus', bodyType: 'Hatchback' },
+ { manufacturer: 'Ford', model: 'Focus Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Ford', model: 'Focus ST', bodyType: 'Hatchback' },
+ { manufacturer: 'Ford', model: 'Fiesta', bodyType: 'Hatchback' },
+ { manufacturer: 'Ford', model: 'Fiesta ST', bodyType: 'Hatchback' },
+ { manufacturer: 'Ford', model: 'Fusion', bodyType: 'Sedan' },
+ { manufacturer: 'Ford', model: 'Fusion Hybrid', bodyType: 'Sedan' },
+ { manufacturer: 'Ford', model: 'Taurus', bodyType: 'Sedan' },
+ { manufacturer: 'Ford', model: 'Escape', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Explorer', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Expedition', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Edge', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Bronco', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Bronco Sport', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'EcoSport', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Territory', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'F-150', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 Regular Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 SuperCab', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 SuperCrew', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 Raptor', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 Lightning', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 XL', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 XLT', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 Lariat', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 King Ranch', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 Platinum', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-150 Limited', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-250 Super Duty', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-350 Super Duty', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-450 Super Duty', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'F-550 Super Duty', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger SuperCab', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger SuperCrew', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger Raptor', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger XL', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger XLT', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger Lariat', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Ranger Wildtrak', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Maverick', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Maverick SuperCrew', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Maverick XL', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Maverick XLT', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Maverick Lariat', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Courier', bodyType: 'Pickup' },
+ { manufacturer: 'Ford', model: 'Mustang', bodyType: 'Coupe' },
+ { manufacturer: 'Ford', model: 'Mustang Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Ford', model: 'Mustang Mach-E', bodyType: 'SUV' },
+ { manufacturer: 'Ford', model: 'Transit', bodyType: 'Van' },
+ { manufacturer: 'Ford', model: 'Transit Connect', bodyType: 'Van' },
+ { manufacturer: 'Ford', model: 'E-Transit', bodyType: 'Van' },
+
+ // Chevrolet (American brand) - Expanded
+ { manufacturer: 'Chevrolet', model: 'Spark', bodyType: 'Hatchback' },
+ { manufacturer: 'Chevrolet', model: 'Sonic', bodyType: 'Sedan' },
+ { manufacturer: 'Chevrolet', model: 'Cruze', bodyType: 'Sedan' },
+ { manufacturer: 'Chevrolet', model: 'Cruze Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Chevrolet', model: 'Malibu', bodyType: 'Sedan' },
+ { manufacturer: 'Chevrolet', model: 'Impala', bodyType: 'Sedan' },
+ { manufacturer: 'Chevrolet', model: 'Corvette', bodyType: 'Coupe' },
+ { manufacturer: 'Chevrolet', model: 'Camaro', bodyType: 'Coupe' },
+ { manufacturer: 'Chevrolet', model: 'Camaro Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Chevrolet', model: 'Camaro ZL1', bodyType: 'Coupe' },
+ { manufacturer: 'Chevrolet', model: 'Trax', bodyType: 'SUV' },
+ { manufacturer: 'Chevrolet', model: 'Equinox', bodyType: 'SUV' },
+ { manufacturer: 'Chevrolet', model: 'Blazer', bodyType: 'SUV' },
+ { manufacturer: 'Chevrolet', model: 'Traverse', bodyType: 'SUV' },
+ { manufacturer: 'Chevrolet', model: 'Tahoe', bodyType: 'SUV' },
+ { manufacturer: 'Chevrolet', model: 'Suburban', bodyType: 'SUV' },
+ { manufacturer: 'Chevrolet', model: 'Colorado', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Colorado Extended Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Colorado Crew Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Colorado ZR2', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Colorado Z71', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Colorado WT', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Colorado LT', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 Regular Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 Crew Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 Trail Boss', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 ZR2', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 Z71', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 RST', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 LT', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 LTZ', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 1500 High Country', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 2500HD', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 2500HD Crew Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 2500HD Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 2500HD Regular Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 3500HD', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 3500HD Crew Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 3500HD Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Silverado 3500HD Regular Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'S-10', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Avalanche', bodyType: 'Pickup' },
+ { manufacturer: 'Chevrolet', model: 'Express', bodyType: 'Van' },
+ { manufacturer: 'Chevrolet', model: 'Bolt EV', bodyType: 'Hatchback' },
+ { manufacturer: 'Chevrolet', model: 'Bolt EUV', bodyType: 'SUV' },
+
+ // Volkswagen (German brand) - Expanded
+ { manufacturer: 'Volkswagen', model: 'Golf', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'Golf GTI', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'Golf R', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'Golf Variant', bodyType: 'Wagon' },
+ { manufacturer: 'Volkswagen', model: 'Jetta', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Jetta GLI', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Passat', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Passat Variant', bodyType: 'Wagon' },
+ { manufacturer: 'Volkswagen', model: 'Passat Alltrack', bodyType: 'Wagon' },
+ { manufacturer: 'Volkswagen', model: 'Arteon', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Arteon R', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'CC', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Polo', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'Polo GTI', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'Polo Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Up!', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'Tiguan', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Tiguan R', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Tiguan Allspace', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Touareg', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Touareg R', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'T-Cross', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'T-Roc', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Atlas', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Atlas Cross Sport', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Amarok', bodyType: 'Pickup' },
+ { manufacturer: 'Volkswagen', model: 'Caddy', bodyType: 'Van' },
+ { manufacturer: 'Volkswagen', model: 'Transporter', bodyType: 'Van' },
+ { manufacturer: 'Volkswagen', model: 'Crafter', bodyType: 'Van' },
+ { manufacturer: 'Volkswagen', model: 'Multivan', bodyType: 'Van' },
+ { manufacturer: 'Volkswagen', model: 'California', bodyType: 'Van' },
+ // VW Electric ID Series
+ { manufacturer: 'Volkswagen', model: 'ID.3', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'ID.4', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'ID.5', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'ID.6', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'ID.7', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'ID.Buzz', bodyType: 'Van' },
+ { manufacturer: 'Volkswagen', model: 'e-Golf', bodyType: 'Hatchback' },
+ { manufacturer: 'Volkswagen', model: 'e-up!', bodyType: 'Hatchback' },
+ // VW Chinese Market Models
+ { manufacturer: 'Volkswagen', model: 'Bora', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Lavida', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Sagitar', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Magotan', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Phideon', bodyType: 'Sedan' },
+ { manufacturer: 'Volkswagen', model: 'Teramont', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Tharu', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Tayron', bodyType: 'SUV' },
+ { manufacturer: 'Volkswagen', model: 'Viloran', bodyType: 'Van' },
+
+ // Peugeot (French brand, popular in Jordan) - Expanded
+ { manufacturer: 'Peugeot', model: '108', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '208', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '208 GTi', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: 'e-208', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '301', bodyType: 'Sedan' },
+ { manufacturer: 'Peugeot', model: '308', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '308 GTi', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '308 SW', bodyType: 'Wagon' },
+ { manufacturer: 'Peugeot', model: '408', bodyType: 'Sedan' },
+ { manufacturer: 'Peugeot', model: '508', bodyType: 'Sedan' },
+ { manufacturer: 'Peugeot', model: '508 SW', bodyType: 'Wagon' },
+ { manufacturer: 'Peugeot', model: '508 PSE', bodyType: 'Sedan' },
+ { manufacturer: 'Peugeot', model: '607', bodyType: 'Sedan' },
+ { manufacturer: 'Peugeot', model: '807', bodyType: 'Van' },
+ { manufacturer: 'Peugeot', model: '1007', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '2008', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: 'e-2008', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: '3008', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: '3008 Hybrid', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: '4008', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: '5008', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: '5008 Hybrid', bodyType: 'SUV' },
+ { manufacturer: 'Peugeot', model: 'Rifter', bodyType: 'Van' },
+ { manufacturer: 'Peugeot', model: 'Partner', bodyType: 'Van' },
+ { manufacturer: 'Peugeot', model: 'Expert', bodyType: 'Van' },
+ { manufacturer: 'Peugeot', model: 'Boxer', bodyType: 'Van' },
+ { manufacturer: 'Peugeot', model: 'Traveller', bodyType: 'Van' },
+ { manufacturer: 'Peugeot', model: 'RCZ', bodyType: 'Coupe' },
+ { manufacturer: 'Peugeot', model: '206', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '207', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '307', bodyType: 'Hatchback' },
+ { manufacturer: 'Peugeot', model: '407', bodyType: 'Sedan' },
+
+ // Renault (French brand) - Expanded
+ { manufacturer: 'Renault', model: 'Twingo', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Clio', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Clio RS', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Clio Estate', bodyType: 'Wagon' },
+ { manufacturer: 'Renault', model: 'Symbol', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Megane', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Megane RS', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Megane Estate', bodyType: 'Wagon' },
+ { manufacturer: 'Renault', model: 'Megane Sedan', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Fluence', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Laguna', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Laguna Estate', bodyType: 'Wagon' },
+ { manufacturer: 'Renault', model: 'Talisman', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Talisman Estate', bodyType: 'Wagon' },
+ { manufacturer: 'Renault', model: 'Latitude', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Safrane', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Vel Satis', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Captur', bodyType: 'SUV' },
+ { manufacturer: 'Renault', model: 'Kadjar', bodyType: 'SUV' },
+ { manufacturer: 'Renault', model: 'Koleos', bodyType: 'SUV' },
+ { manufacturer: 'Renault', model: 'Duster', bodyType: 'SUV' },
+ { manufacturer: 'Renault', model: 'Arkana', bodyType: 'SUV' },
+ { manufacturer: 'Renault', model: 'Austral', bodyType: 'SUV' },
+ { manufacturer: 'Renault', model: 'Scenic', bodyType: 'Van' },
+ { manufacturer: 'Renault', model: 'Grand Scenic', bodyType: 'Van' },
+ { manufacturer: 'Renault', model: 'Espace', bodyType: 'Van' },
+ { manufacturer: 'Renault', model: 'Kangoo', bodyType: 'Van' },
+ { manufacturer: 'Renault', model: 'Trafic', bodyType: 'Van' },
+ { manufacturer: 'Renault', model: 'Master', bodyType: 'Van' },
+ { manufacturer: 'Renault', model: 'Zoe', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Twizy', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Alaskan', bodyType: 'Pickup' },
+ { manufacturer: 'Renault', model: 'Oroch', bodyType: 'Pickup' },
+ { manufacturer: 'Renault', model: 'Wind', bodyType: 'Convertible' },
+ { manufacturer: 'Renault', model: 'Logan', bodyType: 'Sedan' },
+ { manufacturer: 'Renault', model: 'Sandero', bodyType: 'Hatchback' },
+ { manufacturer: 'Renault', model: 'Stepway', bodyType: 'SUV' },
+
+ // Skoda (Czech brand, Volkswagen Group)
+ { manufacturer: 'Skoda', model: 'Octavia', bodyType: 'Sedan' },
+ { manufacturer: 'Skoda', model: 'Superb', bodyType: 'Sedan' },
+ { manufacturer: 'Skoda', model: 'Kodiaq', bodyType: 'SUV' },
+ { manufacturer: 'Skoda', model: 'Karoq', bodyType: 'SUV' },
+ { manufacturer: 'Skoda', model: 'Fabia', bodyType: 'Hatchback' },
+
+ // SEAT (Spanish brand, Volkswagen Group)
+ { manufacturer: 'SEAT', model: 'Leon', bodyType: 'Hatchback' },
+ { manufacturer: 'SEAT', model: 'Ibiza', bodyType: 'Hatchback' },
+ { manufacturer: 'SEAT', model: 'Ateca', bodyType: 'SUV' },
+ { manufacturer: 'SEAT', model: 'Tarraco', bodyType: 'SUV' },
+
+ // Infiniti (Nissan luxury brand)
+ { manufacturer: 'Infiniti', model: 'Q50', bodyType: 'Sedan' },
+ { manufacturer: 'Infiniti', model: 'QX60', bodyType: 'SUV' },
+ { manufacturer: 'Infiniti', model: 'QX80', bodyType: 'SUV' },
+ { manufacturer: 'Infiniti', model: 'Q60', bodyType: 'Coupe' },
+ { manufacturer: 'Infiniti', model: 'QX50', bodyType: 'SUV' },
+
+ // Genesis (Hyundai luxury brand)
+ { manufacturer: 'Genesis', model: 'G70', bodyType: 'Sedan' },
+ { manufacturer: 'Genesis', model: 'G80', bodyType: 'Sedan' },
+ { manufacturer: 'Genesis', model: 'G90', bodyType: 'Sedan' },
+ { manufacturer: 'Genesis', model: 'GV70', bodyType: 'SUV' },
+ { manufacturer: 'Genesis', model: 'GV80', bodyType: 'SUV' },
+
+ // Subaru (Japanese brand) - Expanded
+ { manufacturer: 'Subaru', model: 'Impreza', bodyType: 'Sedan' },
+ { manufacturer: 'Subaru', model: 'Impreza Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Subaru', model: 'WRX', bodyType: 'Sedan' },
+ { manufacturer: 'Subaru', model: 'WRX STI', bodyType: 'Sedan' },
+ { manufacturer: 'Subaru', model: 'Legacy', bodyType: 'Sedan' },
+ { manufacturer: 'Subaru', model: 'Legacy Wagon', bodyType: 'Wagon' },
+ { manufacturer: 'Subaru', model: 'Outback', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'Forester', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'Forester XT', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'XV', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'Crosstrek', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'Ascent', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'Tribeca', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'BRZ', bodyType: 'Coupe' },
+ { manufacturer: 'Subaru', model: 'Solterra', bodyType: 'SUV' },
+ { manufacturer: 'Subaru', model: 'Baja', bodyType: 'Pickup' },
+ { manufacturer: 'Subaru', model: 'Justy', bodyType: 'Hatchback' },
+ { manufacturer: 'Subaru', model: 'Levorg', bodyType: 'Wagon' },
+
+ // Isuzu (Japanese brand, popular for commercial vehicles) - Expanded
+ { manufacturer: 'Isuzu', model: 'D-Max', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max Extended Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max X-Terrain', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max V-Cross', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max S-Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max LS', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max LX', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max Hi-Lander', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'D-Max Blade', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'KB', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'KB Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'KB Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'Faster', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'TF', bodyType: 'Pickup' },
+ { manufacturer: 'Isuzu', model: 'MU-X', bodyType: 'SUV' },
+ { manufacturer: 'Isuzu', model: 'MU-X Blue Power', bodyType: 'SUV' },
+ { manufacturer: 'Isuzu', model: 'Trooper', bodyType: 'SUV' },
+ { manufacturer: 'Isuzu', model: 'Rodeo', bodyType: 'SUV' },
+ { manufacturer: 'Isuzu', model: 'Ascender', bodyType: 'SUV' },
+ { manufacturer: 'Isuzu', model: 'NPR', bodyType: 'Van' },
+ { manufacturer: 'Isuzu', model: 'NQR', bodyType: 'Van' },
+ { manufacturer: 'Isuzu', model: 'FRR', bodyType: 'Van' },
+ { manufacturer: 'Isuzu', model: 'Giga', bodyType: 'Van' },
+ { manufacturer: 'Isuzu', model: 'Elf', bodyType: 'Van' },
+ { manufacturer: 'Isuzu', model: 'Forward', bodyType: 'Van' },
+
+ // JAC (Chinese brand)
+ { manufacturer: 'JAC', model: 'S3', bodyType: 'SUV' },
+ { manufacturer: 'JAC', model: 'S4', bodyType: 'SUV' },
+ { manufacturer: 'JAC', model: 'T6', bodyType: 'Pickup' },
+
+ // DFSK (Chinese brand)
+ { manufacturer: 'DFSK', model: 'Glory 580', bodyType: 'SUV' },
+ { manufacturer: 'DFSK', model: 'Glory 560', bodyType: 'SUV' },
+
+ // Proton (Malaysian brand) - Expanded
+ { manufacturer: 'Proton', model: 'X70', bodyType: 'SUV' },
+ { manufacturer: 'Proton', model: 'X50', bodyType: 'SUV' },
+ { manufacturer: 'Proton', model: 'Saga', bodyType: 'Sedan' },
+ { manufacturer: 'Proton', model: 'Persona', bodyType: 'Sedan' },
+ { manufacturer: 'Proton', model: 'Iriz', bodyType: 'Hatchback' },
+ { manufacturer: 'Proton', model: 'Exora', bodyType: 'Van' },
+
+ // Haval (Great Wall sub-brand, popular in Jordan)
+ { manufacturer: 'Haval', model: 'H6', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'H9', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'F7', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'F7x', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'H2', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'H4', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'Jolion', bodyType: 'SUV' },
+ { manufacturer: 'Haval', model: 'Dargo', bodyType: 'SUV' },
+
+ // GAC (Chinese brand)
+ { manufacturer: 'GAC', model: 'GS4', bodyType: 'SUV' },
+ { manufacturer: 'GAC', model: 'GS8', bodyType: 'SUV' },
+ { manufacturer: 'GAC', model: 'GA4', bodyType: 'Sedan' },
+ { manufacturer: 'GAC', model: 'GA6', bodyType: 'Sedan' },
+ { manufacturer: 'GAC', model: 'GA8', bodyType: 'Sedan' },
+ { manufacturer: 'GAC', model: 'GN6', bodyType: 'Van' },
+ { manufacturer: 'GAC', model: 'GN8', bodyType: 'Van' },
+
+ // Dongfeng (Chinese brand)
+ { manufacturer: 'Dongfeng', model: 'AX7', bodyType: 'SUV' },
+ { manufacturer: 'Dongfeng', model: 'AX5', bodyType: 'SUV' },
+ { manufacturer: 'Dongfeng', model: 'AX3', bodyType: 'SUV' },
+ { manufacturer: 'Dongfeng', model: 'A9', bodyType: 'Sedan' },
+ { manufacturer: 'Dongfeng', model: 'A30', bodyType: 'Sedan' },
+ { manufacturer: 'Dongfeng', model: 'Rich', bodyType: 'Pickup' },
+
+ // Lynk & Co (Chinese-Swedish brand)
+ { manufacturer: 'Lynk & Co', model: '01', bodyType: 'SUV' },
+ { manufacturer: 'Lynk & Co', model: '02', bodyType: 'SUV' },
+ { manufacturer: 'Lynk & Co', model: '03', bodyType: 'Sedan' },
+ { manufacturer: 'Lynk & Co', model: '05', bodyType: 'SUV' },
+ { manufacturer: 'Lynk & Co', model: '06', bodyType: 'SUV' },
+
+ // Jetour (Chery sub-brand)
+ { manufacturer: 'Jetour', model: 'X70', bodyType: 'SUV' },
+ { manufacturer: 'Jetour', model: 'X90', bodyType: 'SUV' },
+ { manufacturer: 'Jetour', model: 'X95', bodyType: 'SUV' },
+ { manufacturer: 'Jetour', model: 'Dashing', bodyType: 'SUV' },
+
+ // Exeed (Chery luxury brand)
+ { manufacturer: 'Exeed', model: 'TXL', bodyType: 'SUV' },
+ { manufacturer: 'Exeed', model: 'TX', bodyType: 'SUV' },
+ { manufacturer: 'Exeed', model: 'LX', bodyType: 'SUV' },
+ { manufacturer: 'Exeed', model: 'VX', bodyType: 'SUV' },
+
+ // Hongqi (Chinese luxury brand)
+ { manufacturer: 'Hongqi', model: 'H5', bodyType: 'Sedan' },
+ { manufacturer: 'Hongqi', model: 'H7', bodyType: 'Sedan' },
+ { manufacturer: 'Hongqi', model: 'H9', bodyType: 'Sedan' },
+ { manufacturer: 'Hongqi', model: 'HS5', bodyType: 'SUV' },
+ { manufacturer: 'Hongqi', model: 'HS7', bodyType: 'SUV' },
+
+ // Zotye (Chinese brand)
+ { manufacturer: 'Zotye', model: 'T600', bodyType: 'SUV' },
+ { manufacturer: 'Zotye', model: 'T700', bodyType: 'SUV' },
+ { manufacturer: 'Zotye', model: 'Z300', bodyType: 'Sedan' },
+ { manufacturer: 'Zotye', model: 'Z500', bodyType: 'Sedan' },
+ { manufacturer: 'Zotye', model: 'SR9', bodyType: 'SUV' },
+
+ // Brilliance (Chinese brand)
+ { manufacturer: 'Brilliance', model: 'V3', bodyType: 'SUV' },
+ { manufacturer: 'Brilliance', model: 'V5', bodyType: 'SUV' },
+ { manufacturer: 'Brilliance', model: 'V7', bodyType: 'SUV' },
+ { manufacturer: 'Brilliance', model: 'H230', bodyType: 'Sedan' },
+ { manufacturer: 'Brilliance', model: 'H330', bodyType: 'Sedan' },
+
+ // Foton (Chinese commercial brand)
+ { manufacturer: 'Foton', model: 'Sauvana', bodyType: 'SUV' },
+ { manufacturer: 'Foton', model: 'Tunland', bodyType: 'Pickup' },
+ { manufacturer: 'Foton', model: 'View', bodyType: 'Van' },
+
+ // Acura (Honda luxury brand) - Expanded
+ { manufacturer: 'Acura', model: 'ILX', bodyType: 'Sedan' },
+ { manufacturer: 'Acura', model: 'TLX', bodyType: 'Sedan' },
+ { manufacturer: 'Acura', model: 'TLX Type S', bodyType: 'Sedan' },
+ { manufacturer: 'Acura', model: 'RLX', bodyType: 'Sedan' },
+ { manufacturer: 'Acura', model: 'RDX', bodyType: 'SUV' },
+ { manufacturer: 'Acura', model: 'MDX', bodyType: 'SUV' },
+ { manufacturer: 'Acura', model: 'MDX Type S', bodyType: 'SUV' },
+ { manufacturer: 'Acura', model: 'NSX', bodyType: 'Coupe' },
+ { manufacturer: 'Acura', model: 'Integra', bodyType: 'Sedan' },
+
+ // Cadillac (American luxury brand)
+ { manufacturer: 'Cadillac', model: 'ATS', bodyType: 'Sedan' },
+ { manufacturer: 'Cadillac', model: 'CTS', bodyType: 'Sedan' },
+ { manufacturer: 'Cadillac', model: 'CT4', bodyType: 'Sedan' },
+ { manufacturer: 'Cadillac', model: 'CT5', bodyType: 'Sedan' },
+ { manufacturer: 'Cadillac', model: 'CT6', bodyType: 'Sedan' },
+ { manufacturer: 'Cadillac', model: 'XT4', bodyType: 'SUV' },
+ { manufacturer: 'Cadillac', model: 'XT5', bodyType: 'SUV' },
+ { manufacturer: 'Cadillac', model: 'XT6', bodyType: 'SUV' },
+ { manufacturer: 'Cadillac', model: 'Escalade', bodyType: 'SUV' },
+ { manufacturer: 'Cadillac', model: 'Escalade ESV', bodyType: 'SUV' },
+ { manufacturer: 'Cadillac', model: 'Lyriq', bodyType: 'SUV' },
+
+ // Lincoln (Ford luxury brand)
+ { manufacturer: 'Lincoln', model: 'MKZ', bodyType: 'Sedan' },
+ { manufacturer: 'Lincoln', model: 'Continental', bodyType: 'Sedan' },
+ { manufacturer: 'Lincoln', model: 'Corsair', bodyType: 'SUV' },
+ { manufacturer: 'Lincoln', model: 'Nautilus', bodyType: 'SUV' },
+ { manufacturer: 'Lincoln', model: 'Aviator', bodyType: 'SUV' },
+ { manufacturer: 'Lincoln', model: 'Navigator', bodyType: 'SUV' },
+
+ // Buick (American brand)
+ { manufacturer: 'Buick', model: 'Encore', bodyType: 'SUV' },
+ { manufacturer: 'Buick', model: 'Encore GX', bodyType: 'SUV' },
+ { manufacturer: 'Buick', model: 'Envision', bodyType: 'SUV' },
+ { manufacturer: 'Buick', model: 'Enclave', bodyType: 'SUV' },
+ { manufacturer: 'Buick', model: 'Regal', bodyType: 'Sedan' },
+ { manufacturer: 'Buick', model: 'LaCrosse', bodyType: 'Sedan' },
+
+ // GMC (American truck brand) - Expanded
+ { manufacturer: 'GMC', model: 'Sierra 1500', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 1500 AT4', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 1500 Denali', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 2500HD', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 2500HD AT4', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 2500HD Denali', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 3500HD', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 3500HD AT4', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Sierra 3500HD Denali', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Canyon', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Canyon AT4', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Canyon Denali', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Terrain', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Terrain AT4', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Terrain Denali', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Acadia', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Acadia AT4', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Acadia Denali', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Yukon', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Yukon AT4', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Yukon Denali', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Yukon XL', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Yukon XL AT4', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Yukon XL Denali', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Hummer EV', bodyType: 'Pickup' },
+ { manufacturer: 'GMC', model: 'Hummer EV SUV', bodyType: 'SUV' },
+ { manufacturer: 'GMC', model: 'Savana', bodyType: 'Van' },
+ { manufacturer: 'GMC', model: 'Savana Cargo', bodyType: 'Van' },
+ { manufacturer: 'GMC', model: 'Savana Passenger', bodyType: 'Van' },
+
+ // Jeep (American SUV brand) - Expanded
+ { manufacturer: 'Jeep', model: 'Renegade', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Renegade Trailhawk', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Compass', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Compass Trailhawk', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Cherokee', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Cherokee Trailhawk', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Grand Cherokee', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Grand Cherokee L', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Grand Cherokee SRT', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Grand Cherokee Trackhawk', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Grand Cherokee 4xe', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Wrangler', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Wrangler Unlimited', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Wrangler Sahara', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Wrangler Rubicon', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Wrangler 4xe', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Gladiator', bodyType: 'Pickup' },
+ { manufacturer: 'Jeep', model: 'Gladiator Rubicon', bodyType: 'Pickup' },
+ { manufacturer: 'Jeep', model: 'Gladiator Mojave', bodyType: 'Pickup' },
+ { manufacturer: 'Jeep', model: 'Wagoneer', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Grand Wagoneer', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Avenger', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Commander', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Patriot', bodyType: 'SUV' },
+ { manufacturer: 'Jeep', model: 'Liberty', bodyType: 'SUV' },
+
+ // Ram (American truck brand)
+ { manufacturer: 'Ram', model: '1500', bodyType: 'Pickup' },
+ { manufacturer: 'Ram', model: '2500', bodyType: 'Pickup' },
+ { manufacturer: 'Ram', model: '3500', bodyType: 'Pickup' },
+ { manufacturer: 'Ram', model: 'ProMaster', bodyType: 'Van' },
+ { manufacturer: 'Ram', model: 'ProMaster City', bodyType: 'Van' },
+
+ // Dodge (American brand)
+ { manufacturer: 'Dodge', model: 'Charger', bodyType: 'Sedan' },
+ { manufacturer: 'Dodge', model: 'Challenger', bodyType: 'Coupe' },
+ { manufacturer: 'Dodge', model: 'Durango', bodyType: 'SUV' },
+ { manufacturer: 'Dodge', model: 'Journey', bodyType: 'SUV' },
+
+ // Chrysler (American brand)
+ { manufacturer: 'Chrysler', model: '300', bodyType: 'Sedan' },
+ { manufacturer: 'Chrysler', model: 'Pacifica', bodyType: 'Van' },
+ { manufacturer: 'Chrysler', model: 'Voyager', bodyType: 'Van' },
+
+ // Fiat (Italian brand)
+ { manufacturer: 'Fiat', model: '500', bodyType: 'Hatchback' },
+ { manufacturer: 'Fiat', model: '500X', bodyType: 'SUV' },
+ { manufacturer: 'Fiat', model: '500L', bodyType: 'Van' },
+ { manufacturer: 'Fiat', model: 'Panda', bodyType: 'Hatchback' },
+ { manufacturer: 'Fiat', model: 'Punto', bodyType: 'Hatchback' },
+ { manufacturer: 'Fiat', model: 'Tipo', bodyType: 'Sedan' },
+ { manufacturer: 'Fiat', model: 'Doblo', bodyType: 'Van' },
+ { manufacturer: 'Fiat', model: 'Ducato', bodyType: 'Van' },
+
+ // Alfa Romeo (Italian luxury brand)
+ { manufacturer: 'Alfa Romeo', model: 'Giulia', bodyType: 'Sedan' },
+ { manufacturer: 'Alfa Romeo', model: 'Giulia Quadrifoglio', bodyType: 'Sedan' },
+ { manufacturer: 'Alfa Romeo', model: 'Stelvio', bodyType: 'SUV' },
+ { manufacturer: 'Alfa Romeo', model: 'Stelvio Quadrifoglio', bodyType: 'SUV' },
+ { manufacturer: 'Alfa Romeo', model: '4C', bodyType: 'Coupe' },
+ { manufacturer: 'Alfa Romeo', model: 'Tonale', bodyType: 'SUV' },
+
+ // Maserati (Italian luxury brand)
+ { manufacturer: 'Maserati', model: 'Ghibli', bodyType: 'Sedan' },
+ { manufacturer: 'Maserati', model: 'Quattroporte', bodyType: 'Sedan' },
+ { manufacturer: 'Maserati', model: 'Levante', bodyType: 'SUV' },
+ { manufacturer: 'Maserati', model: 'GranTurismo', bodyType: 'Coupe' },
+ { manufacturer: 'Maserati', model: 'GranCabrio', bodyType: 'Convertible' },
+ { manufacturer: 'Maserati', model: 'MC20', bodyType: 'Coupe' },
+
+ // Ferrari (Italian supercar brand)
+ { manufacturer: 'Ferrari', model: 'F8 Tributo', bodyType: 'Coupe' },
+ { manufacturer: 'Ferrari', model: 'F8 Spider', bodyType: 'Convertible' },
+ { manufacturer: 'Ferrari', model: 'Roma', bodyType: 'Coupe' },
+ { manufacturer: 'Ferrari', model: 'Portofino', bodyType: 'Convertible' },
+ { manufacturer: 'Ferrari', model: '812 Superfast', bodyType: 'Coupe' },
+ { manufacturer: 'Ferrari', model: 'SF90 Stradale', bodyType: 'Coupe' },
+ { manufacturer: 'Ferrari', model: 'LaFerrari', bodyType: 'Coupe' },
+
+ // Lamborghini (Italian supercar brand)
+ { manufacturer: 'Lamborghini', model: 'Huracan', bodyType: 'Coupe' },
+ { manufacturer: 'Lamborghini', model: 'Huracan Spyder', bodyType: 'Convertible' },
+ { manufacturer: 'Lamborghini', model: 'Aventador', bodyType: 'Coupe' },
+ { manufacturer: 'Lamborghini', model: 'Aventador Roadster', bodyType: 'Convertible' },
+ { manufacturer: 'Lamborghini', model: 'Urus', bodyType: 'SUV' },
+
+ // Porsche (German sports car brand)
+ { manufacturer: 'Porsche', model: '911', bodyType: 'Coupe' },
+ { manufacturer: 'Porsche', model: '911 Turbo', bodyType: 'Coupe' },
+ { manufacturer: 'Porsche', model: '911 GT3', bodyType: 'Coupe' },
+ { manufacturer: 'Porsche', model: 'Boxster', bodyType: 'Convertible' },
+ { manufacturer: 'Porsche', model: 'Cayman', bodyType: 'Coupe' },
+ { manufacturer: 'Porsche', model: 'Panamera', bodyType: 'Sedan' },
+ { manufacturer: 'Porsche', model: 'Cayenne', bodyType: 'SUV' },
+ { manufacturer: 'Porsche', model: 'Macan', bodyType: 'SUV' },
+ { manufacturer: 'Porsche', model: 'Taycan', bodyType: 'Sedan' },
+
+ // Volvo (Swedish brand)
+ { manufacturer: 'Volvo', model: 'S60', bodyType: 'Sedan' },
+ { manufacturer: 'Volvo', model: 'S90', bodyType: 'Sedan' },
+ { manufacturer: 'Volvo', model: 'V60', bodyType: 'Wagon' },
+ { manufacturer: 'Volvo', model: 'V90', bodyType: 'Wagon' },
+ { manufacturer: 'Volvo', model: 'XC40', bodyType: 'SUV' },
+ { manufacturer: 'Volvo', model: 'XC60', bodyType: 'SUV' },
+ { manufacturer: 'Volvo', model: 'XC90', bodyType: 'SUV' },
+ { manufacturer: 'Volvo', model: 'C40 Recharge', bodyType: 'SUV' },
+ { manufacturer: 'Volvo', model: 'EX30', bodyType: 'SUV' },
+
+ // Saab (Swedish brand - discontinued but still on roads)
+ { manufacturer: 'Saab', model: '9-3', bodyType: 'Sedan' },
+ { manufacturer: 'Saab', model: '9-5', bodyType: 'Sedan' },
+ { manufacturer: 'Saab', model: '9-4X', bodyType: 'SUV' },
+
+ // Land Rover (British luxury SUV brand)
+ { manufacturer: 'Land Rover', model: 'Defender', bodyType: 'SUV' },
+ { manufacturer: 'Land Rover', model: 'Discovery', bodyType: 'SUV' },
+ { manufacturer: 'Land Rover', model: 'Discovery Sport', bodyType: 'SUV' },
+ { manufacturer: 'Land Rover', model: 'Range Rover', bodyType: 'SUV' },
+ { manufacturer: 'Land Rover', model: 'Range Rover Sport', bodyType: 'SUV' },
+ { manufacturer: 'Land Rover', model: 'Range Rover Velar', bodyType: 'SUV' },
+ { manufacturer: 'Land Rover', model: 'Range Rover Evoque', bodyType: 'SUV' },
+
+ // Jaguar (British luxury brand)
+ { manufacturer: 'Jaguar', model: 'XE', bodyType: 'Sedan' },
+ { manufacturer: 'Jaguar', model: 'XF', bodyType: 'Sedan' },
+ { manufacturer: 'Jaguar', model: 'XJ', bodyType: 'Sedan' },
+ { manufacturer: 'Jaguar', model: 'F-Type', bodyType: 'Coupe' },
+ { manufacturer: 'Jaguar', model: 'F-Type Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Jaguar', model: 'E-Pace', bodyType: 'SUV' },
+ { manufacturer: 'Jaguar', model: 'F-Pace', bodyType: 'SUV' },
+ { manufacturer: 'Jaguar', model: 'I-Pace', bodyType: 'SUV' },
+
+ // Mini (British brand, BMW owned)
+ { manufacturer: 'Mini', model: 'Cooper', bodyType: 'Hatchback' },
+ { manufacturer: 'Mini', model: 'Cooper S', bodyType: 'Hatchback' },
+ { manufacturer: 'Mini', model: 'Cooper Convertible', bodyType: 'Convertible' },
+ { manufacturer: 'Mini', model: 'Clubman', bodyType: 'Wagon' },
+ { manufacturer: 'Mini', model: 'Countryman', bodyType: 'SUV' },
+ { manufacturer: 'Mini', model: 'Paceman', bodyType: 'SUV' },
+ { manufacturer: 'Mini', model: 'Electric', bodyType: 'Hatchback' },
+
+ // Bentley (British ultra-luxury brand)
+ { manufacturer: 'Bentley', model: 'Continental GT', bodyType: 'Coupe' },
+ { manufacturer: 'Bentley', model: 'Continental GTC', bodyType: 'Convertible' },
+ { manufacturer: 'Bentley', model: 'Flying Spur', bodyType: 'Sedan' },
+ { manufacturer: 'Bentley', model: 'Bentayga', bodyType: 'SUV' },
+ { manufacturer: 'Bentley', model: 'Mulsanne', bodyType: 'Sedan' },
+
+ // Rolls-Royce (British ultra-luxury brand)
+ { manufacturer: 'Rolls-Royce', model: 'Ghost', bodyType: 'Sedan' },
+ { manufacturer: 'Rolls-Royce', model: 'Phantom', bodyType: 'Sedan' },
+ { manufacturer: 'Rolls-Royce', model: 'Wraith', bodyType: 'Coupe' },
+ { manufacturer: 'Rolls-Royce', model: 'Dawn', bodyType: 'Convertible' },
+ { manufacturer: 'Rolls-Royce', model: 'Cullinan', bodyType: 'SUV' },
+
+ // Aston Martin (British luxury sports car brand)
+ { manufacturer: 'Aston Martin', model: 'Vantage', bodyType: 'Coupe' },
+ { manufacturer: 'Aston Martin', model: 'DB11', bodyType: 'Coupe' },
+ { manufacturer: 'Aston Martin', model: 'DBS Superleggera', bodyType: 'Coupe' },
+ { manufacturer: 'Aston Martin', model: 'DBX', bodyType: 'SUV' },
+ { manufacturer: 'Aston Martin', model: 'Rapide', bodyType: 'Sedan' },
+
+ // McLaren (British supercar brand)
+ { manufacturer: 'McLaren', model: '570S', bodyType: 'Coupe' },
+ { manufacturer: 'McLaren', model: '720S', bodyType: 'Coupe' },
+ { manufacturer: 'McLaren', model: 'GT', bodyType: 'Coupe' },
+ { manufacturer: 'McLaren', model: 'Artura', bodyType: 'Coupe' },
+ { manufacturer: 'McLaren', model: 'P1', bodyType: 'Coupe' },
+
+ // Lotus (British sports car brand)
+ { manufacturer: 'Lotus', model: 'Elise', bodyType: 'Convertible' },
+ { manufacturer: 'Lotus', model: 'Exige', bodyType: 'Coupe' },
+ { manufacturer: 'Lotus', model: 'Evora', bodyType: 'Coupe' },
+ { manufacturer: 'Lotus', model: 'Emira', bodyType: 'Coupe' },
+ { manufacturer: 'Lotus', model: 'Evija', bodyType: 'Coupe' },
+
+ // Citroen (French brand)
+ { manufacturer: 'Citroen', model: 'C1', bodyType: 'Hatchback' },
+ { manufacturer: 'Citroen', model: 'C3', bodyType: 'Hatchback' },
+ { manufacturer: 'Citroen', model: 'C4', bodyType: 'Hatchback' },
+ { manufacturer: 'Citroen', model: 'C5', bodyType: 'Sedan' },
+ { manufacturer: 'Citroen', model: 'C3 Aircross', bodyType: 'SUV' },
+ { manufacturer: 'Citroen', model: 'C5 Aircross', bodyType: 'SUV' },
+ { manufacturer: 'Citroen', model: 'Berlingo', bodyType: 'Van' },
+ { manufacturer: 'Citroen', model: 'Jumper', bodyType: 'Van' },
+
+ // DS (French luxury brand, Citroen premium)
+ { manufacturer: 'DS', model: 'DS 3', bodyType: 'Hatchback' },
+ { manufacturer: 'DS', model: 'DS 4', bodyType: 'Hatchback' },
+ { manufacturer: 'DS', model: 'DS 7', bodyType: 'SUV' },
+ { manufacturer: 'DS', model: 'DS 9', bodyType: 'Sedan' },
+
+ // Opel (German brand, now part of Stellantis)
+ { manufacturer: 'Opel', model: 'Corsa', bodyType: 'Hatchback' },
+ { manufacturer: 'Opel', model: 'Astra', bodyType: 'Hatchback' },
+ { manufacturer: 'Opel', model: 'Insignia', bodyType: 'Sedan' },
+ { manufacturer: 'Opel', model: 'Crossland', bodyType: 'SUV' },
+ { manufacturer: 'Opel', model: 'Grandland', bodyType: 'SUV' },
+ { manufacturer: 'Opel', model: 'Mokka', bodyType: 'SUV' },
+
+ // Dacia (Romanian budget brand, Renault owned)
+ { manufacturer: 'Dacia', model: 'Sandero', bodyType: 'Hatchback' },
+ { manufacturer: 'Dacia', model: 'Logan', bodyType: 'Sedan' },
+ { manufacturer: 'Dacia', model: 'Duster', bodyType: 'SUV' },
+ { manufacturer: 'Dacia', model: 'Lodgy', bodyType: 'Van' },
+ { manufacturer: 'Dacia', model: 'Dokker', bodyType: 'Van' },
+
+ // Lada (Russian brand)
+ { manufacturer: 'Lada', model: 'Granta', bodyType: 'Sedan' },
+ { manufacturer: 'Lada', model: 'Vesta', bodyType: 'Sedan' },
+ { manufacturer: 'Lada', model: 'XRAY', bodyType: 'Hatchback' },
+ { manufacturer: 'Lada', model: 'Largus', bodyType: 'Van' },
+ { manufacturer: 'Lada', model: 'Niva', bodyType: 'SUV' },
+
+ // SsangYong (Korean brand)
+ { manufacturer: 'SsangYong', model: 'Tivoli', bodyType: 'SUV' },
+ { manufacturer: 'SsangYong', model: 'Korando', bodyType: 'SUV' },
+ { manufacturer: 'SsangYong', model: 'Rexton', bodyType: 'SUV' },
+ { manufacturer: 'SsangYong', model: 'Musso', bodyType: 'Pickup' },
+ { manufacturer: 'SsangYong', model: 'Musso Grand', bodyType: 'Pickup' },
+ { manufacturer: 'SsangYong', model: 'Musso Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'SsangYong', model: 'Musso Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'SsangYong', model: 'Actyon Sports', bodyType: 'Pickup' },
+ { manufacturer: 'SsangYong', model: 'Korando Sports', bodyType: 'Pickup' },
+ { manufacturer: 'SsangYong', model: 'XLV', bodyType: 'SUV' },
+
+ // BAIC (Chinese brand) - Pickup focused
+ { manufacturer: 'BAIC', model: 'BJ40', bodyType: 'SUV' },
+ { manufacturer: 'BAIC', model: 'BJ80', bodyType: 'SUV' },
+ { manufacturer: 'BAIC', model: 'Warrior', bodyType: 'Pickup' },
+ { manufacturer: 'BAIC', model: 'Pickup BJ40', bodyType: 'Pickup' },
+ { manufacturer: 'BAIC', model: 'Hunter', bodyType: 'Pickup' },
+ { manufacturer: 'BAIC', model: 'Ruixiang', bodyType: 'Pickup' },
+
+ // Changan (Chinese brand) - Additional Pickups
+ { manufacturer: 'Changan', model: 'Hunter', bodyType: 'Pickup' },
+ { manufacturer: 'Changan', model: 'Kaicene F70', bodyType: 'Pickup' },
+ { manufacturer: 'Changan', model: 'Kaicene A800', bodyType: 'Pickup' },
+ { manufacturer: 'Changan', model: 'Star Card', bodyType: 'Pickup' },
+ { manufacturer: 'Changan', model: 'CS35', bodyType: 'SUV' },
+ { manufacturer: 'Changan', model: 'CS55', bodyType: 'SUV' },
+ { manufacturer: 'Changan', model: 'CS75', bodyType: 'SUV' },
+
+ // Zhengzhou Nissan (Chinese-Japanese JV) - Pickup focused
+ { manufacturer: 'Zhengzhou Nissan', model: 'Rich', bodyType: 'Pickup' },
+ { manufacturer: 'Zhengzhou Nissan', model: 'Pickup King', bodyType: 'Pickup' },
+ { manufacturer: 'Zhengzhou Nissan', model: 'D22', bodyType: 'Pickup' },
+ { manufacturer: 'Zhengzhou Nissan', model: 'Paladin', bodyType: 'SUV' },
+
+ // Maxus (Chinese commercial brand) - Pickup models
+ { manufacturer: 'Maxus', model: 'T60', bodyType: 'Pickup' },
+ { manufacturer: 'Maxus', model: 'T70', bodyType: 'Pickup' },
+ { manufacturer: 'Maxus', model: 'T90', bodyType: 'Pickup' },
+ { manufacturer: 'Maxus', model: 'T60 Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Maxus', model: 'T60 Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Maxus', model: 'D90', bodyType: 'SUV' },
+
+ // LDV (Australian-Chinese brand) - Commercial vehicles
+ { manufacturer: 'LDV', model: 'T60', bodyType: 'Pickup' },
+ { manufacturer: 'LDV', model: 'T60 Trailrider', bodyType: 'Pickup' },
+ { manufacturer: 'LDV', model: 'T60 Max', bodyType: 'Pickup' },
+ { manufacturer: 'LDV', model: 'T60 Pro', bodyType: 'Pickup' },
+ { manufacturer: 'LDV', model: 'D90', bodyType: 'SUV' },
+ { manufacturer: 'LDV', model: 'V80', bodyType: 'Van' },
+
+ // Zxauto (Chinese brand) - Pickup focused
+ { manufacturer: 'Zxauto', model: 'Grand Tiger', bodyType: 'Pickup' },
+ { manufacturer: 'Zxauto', model: 'Admiral', bodyType: 'Pickup' },
+ { manufacturer: 'Zxauto', model: 'Terralord', bodyType: 'Pickup' },
+ { manufacturer: 'Zxauto', model: 'BQ2031', bodyType: 'Pickup' },
+
+ // Jiangling (JMC) - Additional models
+ { manufacturer: 'Jiangling', model: 'Baodian', bodyType: 'Pickup' },
+ { manufacturer: 'Jiangling', model: 'Kaiyun', bodyType: 'Pickup' },
+ { manufacturer: 'Jiangling', model: 'Pickup JX1020', bodyType: 'Pickup' },
+ { manufacturer: 'Jiangling', model: 'Landwind X5', bodyType: 'SUV' },
+
+ // Gonow (Chinese brand) - Pickup models
+ { manufacturer: 'Gonow', model: 'Troy', bodyType: 'Pickup' },
+ { manufacturer: 'Gonow', model: 'GP150', bodyType: 'Pickup' },
+ { manufacturer: 'Gonow', model: 'GP200', bodyType: 'Pickup' },
+ { manufacturer: 'Gonow', model: 'Way', bodyType: 'SUV' },
+
+ // Daewoo (Korean brand, now part of GM) - Expanded
+ { manufacturer: 'Daewoo', model: 'Lanos', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Lanos Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Daewoo', model: 'Matiz', bodyType: 'Hatchback' },
+ { manufacturer: 'Daewoo', model: 'Matiz Creative', bodyType: 'Hatchback' },
+ { manufacturer: 'Daewoo', model: 'Nexia', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Nexia R3', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Lacetti', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Lacetti Hatchback', bodyType: 'Hatchback' },
+ { manufacturer: 'Daewoo', model: 'Lacetti Wagon', bodyType: 'Wagon' },
+ { manufacturer: 'Daewoo', model: 'Gentra', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Espero', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Cielo', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Nubira', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Nubira Wagon', bodyType: 'Wagon' },
+ { manufacturer: 'Daewoo', model: 'Leganza', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Kalos', bodyType: 'Hatchback' },
+ { manufacturer: 'Daewoo', model: 'Tosca', bodyType: 'Sedan' },
+ { manufacturer: 'Daewoo', model: 'Winstorm', bodyType: 'SUV' },
+ { manufacturer: 'Daewoo', model: 'Korando', bodyType: 'SUV' },
+ { manufacturer: 'Daewoo', model: 'Musso', bodyType: 'SUV' },
+ { manufacturer: 'Daewoo', model: 'Damas', bodyType: 'Pickup' },
+ { manufacturer: 'Daewoo', model: 'Labo', bodyType: 'Pickup' },
+
+ // Mahindra (Indian brand, popular for pickups and SUVs)
+ { manufacturer: 'Mahindra', model: 'Scorpio', bodyType: 'SUV' },
+ { manufacturer: 'Mahindra', model: 'XUV500', bodyType: 'SUV' },
+ { manufacturer: 'Mahindra', model: 'XUV700', bodyType: 'SUV' },
+ { manufacturer: 'Mahindra', model: 'Thar', bodyType: 'SUV' },
+ { manufacturer: 'Mahindra', model: 'Bolero', bodyType: 'SUV' },
+ { manufacturer: 'Mahindra', model: 'Pik Up', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Pik Up Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Pik Up Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Pik Up S4', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Pik Up S6', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Pik Up S10', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Genio', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Imperio', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Imperio Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Imperio Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Supro', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Jeeto', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Bolero Camper', bodyType: 'Pickup' },
+ { manufacturer: 'Mahindra', model: 'Bolero Maxi Truck', bodyType: 'Pickup' },
+
+ // Tata (Indian brand, popular for commercial vehicles)
+ { manufacturer: 'Tata', model: 'Safari', bodyType: 'SUV' },
+ { manufacturer: 'Tata', model: 'Harrier', bodyType: 'SUV' },
+ { manufacturer: 'Tata', model: 'Nexon', bodyType: 'SUV' },
+ { manufacturer: 'Tata', model: 'Punch', bodyType: 'SUV' },
+ { manufacturer: 'Tata', model: 'Xenon', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Xenon XT', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Xenon Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Xenon Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Yodha', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Yodha Extra Strong', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Intra', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Intra V10', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Intra V20', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Intra V30', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Ace', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Ace Gold', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Ace Mega', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Super Ace', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: '207 DI', bodyType: 'Pickup' },
+ { manufacturer: 'Tata', model: 'Magic', bodyType: 'Pickup' },
+
+ // JMC (Chinese commercial vehicle brand)
+ { manufacturer: 'JMC', model: 'Vigus', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Vigus Pro', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Vigus Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Vigus Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Boarding', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Carrying', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Conquer', bodyType: 'Pickup' },
+ { manufacturer: 'JMC', model: 'Landwind X7', bodyType: 'SUV' },
+
+ // Jinbei (Chinese brand)
+ { manufacturer: 'Jinbei', model: 'Pickup', bodyType: 'Pickup' },
+ { manufacturer: 'Jinbei', model: 'Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Jinbei', model: 'Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Jinbei', model: 'T30', bodyType: 'Pickup' },
+ { manufacturer: 'Jinbei', model: 'T50', bodyType: 'Pickup' },
+ { manufacturer: 'Jinbei', model: 'T52', bodyType: 'Pickup' },
+
+ // Dongfeng (Chinese brand) - Additional Pickups
+ { manufacturer: 'Dongfeng', model: 'Rich 6', bodyType: 'Pickup' },
+ { manufacturer: 'Dongfeng', model: 'Rich Double Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Dongfeng', model: 'Rich Single Cab', bodyType: 'Pickup' },
+ { manufacturer: 'Dongfeng', model: 'Captain', bodyType: 'Pickup' },
+ { manufacturer: 'Dongfeng', model: 'Hunter', bodyType: 'Pickup' },
+ { manufacturer: 'Dongfeng', model: 'Warrior', bodyType: 'Pickup' },
+ { manufacturer: 'Dongfeng', model: 'Forthing T5', bodyType: 'Pickup' },
+
+ // Foton (Chinese commercial brand) - Additional Pickups
+ { manufacturer: 'Foton', model: 'Thunder', bodyType: 'Pickup' },
+ { manufacturer: 'Foton', model: 'General', bodyType: 'Pickup' },
+ { manufacturer: 'Foton', model: 'Gratour T5', bodyType: 'Pickup' },
+ { manufacturer: 'Foton', model: 'Gratour T3', bodyType: 'Pickup' },
+ { manufacturer: 'Foton', model: 'Tunland G7', bodyType: 'Pickup' },
+ { manufacturer: 'Foton', model: 'Tunland G9', bodyType: 'Pickup' },
+
+ // Great Wall (Chinese brand) - Additional Pickups
+ { manufacturer: 'Great Wall', model: 'Wingle 5', bodyType: 'Pickup' },
+ { manufacturer: 'Great Wall', model: 'Wingle 6', bodyType: 'Pickup' },
+ { manufacturer: 'Great Wall', model: 'Wingle 7', bodyType: 'Pickup' },
+ { manufacturer: 'Great Wall', model: 'Steed 5', bodyType: 'Pickup' },
+ { manufacturer: 'Great Wall', model: 'Steed 6', bodyType: 'Pickup' },
+ { manufacturer: 'Great Wall', model: 'Poer', bodyType: 'Pickup' },
+ { manufacturer: 'Great Wall', model: 'Cannon', bodyType: 'Pickup' },
+
+ // JAC (Chinese brand) - Additional Pickups
+ { manufacturer: 'JAC', model: 'T6', bodyType: 'Pickup' },
+ { manufacturer: 'JAC', model: 'T8', bodyType: 'Pickup' },
+ { manufacturer: 'JAC', model: 'Hunter', bodyType: 'Pickup' },
+ { manufacturer: 'JAC', model: 'Pickup T40', bodyType: 'Pickup' },
+ { manufacturer: 'JAC', model: 'Pickup T60', bodyType: 'Pickup' },
+];
+
+async function seedCarDataset() {
+ console.log('🚗 Seeding car dataset...');
+
+ try {
+ // Clear existing data
+ const deletedCount = await prisma.carDataset.deleteMany();
+ console.log(`🗑️ Deleted ${deletedCount.count} existing car dataset entries`);
+
+ // Reset the auto-increment counter for SQLite
+ console.log('🔄 Resetting ID counter...');
+ await prisma.$executeRaw`DELETE FROM sqlite_sequence WHERE name = 'car_dataset'`;
+ console.log('✅ ID counter reset to start from 1');
+
+ console.log('📝 Inserting car dataset entries...');
+
+ let createdCount = 0;
+ let errorCount = 0;
+
+ for (const car of carDataset) {
+ try {
+ await prisma.carDataset.create({
+ data: {
+ manufacturer: car.manufacturer,
+ model: car.model,
+ bodyType: car.bodyType,
+ isActive: true,
+ },
+ });
+ createdCount++;
+ console.log(`✅ Created: ${car.manufacturer} ${car.model} (${car.bodyType})`);
+ } catch (error) {
+ errorCount++;
+ console.error(`❌ Error creating "${car.manufacturer} ${car.model}":`, error);
+ }
+ }
+
+ console.log(`\n📊 Summary:`);
+ console.log(` Created: ${createdCount} car dataset entries`);
+ console.log(` Errors: ${errorCount} entries`);
+ console.log(` Total: ${carDataset.length} entries processed`);
+
+ // Display statistics
+ const stats = await prisma.carDataset.groupBy({
+ by: ['manufacturer'],
+ _count: { manufacturer: true },
+ orderBy: { manufacturer: 'asc' },
+ });
+
+ console.log(`\n📋 Car dataset by manufacturer:`);
+ stats.forEach((stat) => {
+ console.log(` ${stat.manufacturer}: ${stat._count.manufacturer} models`);
+ });
+
+ console.log('\n🎉 Car dataset seeding completed successfully!');
+
+ } catch (error) {
+ console.error('❌ Error during car dataset seeding:', error);
+ throw error;
+ }
+}
+
+async function main() {
+ try {
+ await seedCarDataset();
+ } catch (error) {
+ console.error('❌ Error seeding car dataset:', error);
+ process.exit(1);
+ } finally {
+ await prisma.$disconnect();
+ }
+}
+
+// Always run the main function when this file is executed
+main();
+
+export { seedCarDataset };
\ No newline at end of file
diff --git a/prisma/dev.db b/prisma/dev.db
new file mode 100644
index 0000000..e69de29
diff --git a/prisma/maintenanceTypeSeed.ts b/prisma/maintenanceTypeSeed.ts
new file mode 100644
index 0000000..ea42a78
--- /dev/null
+++ b/prisma/maintenanceTypeSeed.ts
@@ -0,0 +1,264 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+const maintenanceTypes = [
+ {
+ name: 'صيانة دورية',
+ description: 'صيانة دورية شاملة للمركبة تشمل فحص جميع الأنظمة الأساسية',
+ isActive: true,
+ },
+ {
+ name: 'تغيير زيت المحرك',
+ description: 'تغيير زيت المحرك وفلتر الزيت وفحص مستوى السوائل',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح الفرامل',
+ description: 'صيانة وإصلاح نظام الفرامل بما في ذلك الأقراص والتيل والسوائل',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح المحرك',
+ description: 'إصلاح وصيانة المحرك وأجزائه الداخلية والخارجية',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح ناقل الحركة',
+ description: 'صيانة وإصلاح ناقل الحركة الأوتوماتيكي أو اليدوي',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح التكييف',
+ description: 'صيانة وإصلاح نظام التكييف والتبريد في المركبة',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح الإطارات',
+ description: 'تغيير وإصلاح الإطارات وضبط الهواء والتوازن',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح الكهرباء',
+ description: 'إصلاح الأنظمة الكهربائية والإلكترونية في المركبة',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح التعليق',
+ description: 'صيانة وإصلاح نظام التعليق والممتصات',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح العادم',
+ description: 'إصلاح وتغيير نظام العادم والكاتم',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح الرادياتير',
+ description: 'صيانة وإصلاح نظام التبريد والرادياتير',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح البطارية',
+ description: 'فحص وتغيير البطارية ونظام الشحن',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح المصابيح',
+ description: 'إصلاح وتغيير المصابيح الأمامية والخلفية',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح الزجاج',
+ description: 'إصلاح وتغيير الزجاج الأمامي والخلفي والجانبي',
+ isActive: true,
+ },
+ {
+ name: 'إصلاح الهيكل',
+ description: 'إصلاح أضرار الهيكل والصدمات والخدوش',
+ isActive: true,
+ },
+ {
+ name: 'تنظيف شامل',
+ description: 'تنظيف شامل للمركبة من الداخل والخارج',
+ isActive: true,
+ },
+ {
+ name: 'فحص دوري',
+ description: 'فحص دوري شامل لجميع أنظمة المركبة',
+ isActive: true,
+ },
+ {
+ name: 'فحص ما قبل السفر',
+ description: 'فحص شامل للمركبة قبل السفر الطويل',
+ isActive: true,
+ },
+ {
+ name: 'صيانة طارئة',
+ description: 'صيانة طارئة لحل مشاكل عاجلة في المركبة',
+ isActive: true,
+ },
+ {
+ name: 'أخرى',
+ description: 'أنواع صيانة أخرى غير مدرجة في القائمة',
+ isActive: true,
+ },
+];
+
+async function seedMaintenanceTypes() {
+ console.log('🔧 Seeding maintenance types...');
+
+ try {
+ // Check if there are any maintenance visits that might reference maintenance types in JSON
+ const visitCount = await prisma.maintenanceVisit.count();
+
+ if (visitCount > 0) {
+ console.log(`⚠️ Found ${visitCount} maintenance visits in database.`);
+ console.log('🔄 Analyzing maintenance jobs in existing visits...');
+
+ // Get all maintenance visits to check their JSON maintenance jobs
+ const visits = await prisma.maintenanceVisit.findMany({
+ select: { maintenanceJobs: true },
+ });
+
+ const referencedTypeIds = new Set();
+
+ // Parse JSON maintenance jobs to find referenced type IDs
+ visits.forEach(visit => {
+ try {
+ const jobs = JSON.parse(visit.maintenanceJobs);
+ if (Array.isArray(jobs)) {
+ jobs.forEach(job => {
+ if (job.typeId && typeof job.typeId === 'number') {
+ referencedTypeIds.add(job.typeId);
+ }
+ });
+ }
+ } catch (error) {
+ // Skip invalid JSON
+ }
+ });
+
+ if (referencedTypeIds.size > 0) {
+ console.log(`📋 Found ${referencedTypeIds.size} maintenance type IDs referenced in visits`);
+
+ // Delete only maintenance types that are NOT referenced
+ const deletedTypes = await prisma.maintenanceType.deleteMany({
+ where: {
+ id: {
+ notIn: Array.from(referencedTypeIds),
+ },
+ },
+ });
+ console.log(`🗑️ Deleted ${deletedTypes.count} unreferenced maintenance types`);
+
+ // Update referenced maintenance types to match our seed data
+ console.log('🔄 Updating referenced maintenance types...');
+ for (const type of maintenanceTypes) {
+ const existingType = await prisma.maintenanceType.findUnique({
+ where: { name: type.name },
+ });
+
+ if (existingType && referencedTypeIds.has(existingType.id)) {
+ await prisma.maintenanceType.update({
+ where: { id: existingType.id },
+ data: {
+ description: type.description,
+ isActive: type.isActive,
+ },
+ });
+ console.log(`✅ Updated referenced type: ${type.name}`);
+ }
+ }
+ } else {
+ // No maintenance types are actually referenced, safe to delete all
+ const deletedCount = await prisma.maintenanceType.deleteMany();
+ console.log(`🗑️ Deleted ${deletedCount.count} maintenance types`);
+ }
+ } else {
+ // No maintenance visits exist, safe to delete all maintenance types
+ const deletedCount = await prisma.maintenanceType.deleteMany();
+ console.log(`🗑️ Deleted ${deletedCount.count} existing maintenance types`);
+ }
+
+ // Reset the auto-increment counter for SQLite
+ console.log('🔄 Resetting ID counter...');
+ await prisma.$executeRaw`DELETE FROM sqlite_sequence WHERE name = 'maintenance_types'`;
+ console.log('✅ ID counter reset to start from 1');
+
+ console.log('📝 Inserting fresh maintenance types...');
+
+ let createdCount = 0;
+ let updatedCount = 0;
+
+ for (const type of maintenanceTypes) {
+ try {
+ console.log(`Processing: ${type.name}`);
+
+ const result = await prisma.maintenanceType.upsert({
+ where: { name: type.name },
+ update: {
+ description: type.description,
+ isActive: type.isActive,
+ },
+ create: {
+ name: type.name,
+ description: type.description,
+ isActive: type.isActive,
+ },
+ });
+
+ if (result.createdDate.getTime() === result.updateDate.getTime()) {
+ createdCount++;
+ console.log(`✅ Created: ${type.name} (ID: ${result.id})`);
+ } else {
+ updatedCount++;
+ console.log(`✅ Updated: ${type.name} (ID: ${result.id})`);
+ }
+ } catch (error) {
+ console.error(`❌ Error processing "${type.name}":`, error);
+ }
+ }
+
+ console.log(`\n📊 Summary:`);
+ console.log(` Created: ${createdCount} maintenance types`);
+ console.log(` Updated: ${updatedCount} maintenance types`);
+ console.log(` Total: ${maintenanceTypes.length} maintenance types processed`);
+
+ // Display all maintenance types
+ const allTypes = await prisma.maintenanceType.findMany({
+ orderBy: { name: 'asc' },
+ });
+
+ console.log(`\n📋 All maintenance types in database (${allTypes.length}):`);
+ allTypes.forEach((type, index) => {
+ const status = type.isActive ? '🟢' : '🔴';
+ console.log(` ${index + 1}. ${status} ${type.name}`);
+ if (type.description) {
+ console.log(` 📝 ${type.description}`);
+ }
+ });
+
+ console.log('\n🎉 Maintenance types seeding completed successfully!');
+
+ } catch (error) {
+ console.error('❌ Error during maintenance types seeding:', error);
+ throw error;
+ }
+}
+
+async function main() {
+ try {
+ await seedMaintenanceTypes();
+ } catch (error) {
+ console.error('❌ Error seeding maintenance types:', error);
+ process.exit(1);
+ } finally {
+ await prisma.$disconnect();
+ }
+}
+
+// Always run the main function when this file is executed
+main();
+
+export { seedMaintenanceTypes };
\ No newline at end of file
diff --git a/prisma/migrations/20250812120412_init/migration.sql b/prisma/migrations/20250812120412_init/migration.sql
new file mode 100644
index 0000000..9d274e0
--- /dev/null
+++ b/prisma/migrations/20250812120412_init/migration.sql
@@ -0,0 +1,94 @@
+-- CreateTable
+CREATE TABLE "users" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "username" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'active',
+ "authLevel" INTEGER NOT NULL,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "editDate" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "customers" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "phone" TEXT,
+ "email" TEXT,
+ "address" TEXT,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "vehicles" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "plateNumber" TEXT NOT NULL,
+ "bodyType" TEXT NOT NULL,
+ "manufacturer" TEXT NOT NULL,
+ "model" TEXT NOT NULL,
+ "trim" TEXT,
+ "year" INTEGER NOT NULL,
+ "transmission" TEXT NOT NULL,
+ "fuel" TEXT NOT NULL,
+ "cylinders" INTEGER,
+ "engineDisplacement" REAL,
+ "useType" TEXT NOT NULL,
+ "ownerId" INTEGER NOT NULL,
+ "lastVisitDate" DATETIME,
+ "suggestedNextVisitDate" DATETIME,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL,
+ CONSTRAINT "vehicles_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "maintenance_visits" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "vehicleId" INTEGER NOT NULL,
+ "customerId" INTEGER NOT NULL,
+ "maintenanceType" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "cost" REAL NOT NULL,
+ "paymentStatus" TEXT NOT NULL DEFAULT 'pending',
+ "kilometers" INTEGER NOT NULL,
+ "visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "nextVisitDelay" INTEGER NOT NULL,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL,
+ CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "expenses" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "description" TEXT NOT NULL,
+ "category" TEXT NOT NULL,
+ "amount" REAL NOT NULL,
+ "expenseDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "income" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "maintenanceVisitId" INTEGER NOT NULL,
+ "amount" REAL NOT NULL,
+ "incomeDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL,
+ CONSTRAINT "income_maintenanceVisitId_fkey" FOREIGN KEY ("maintenanceVisitId") REFERENCES "maintenance_visits" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "vehicles_plateNumber_key" ON "vehicles"("plateNumber");
diff --git a/prisma/migrations/20250827142342_newdis/migration.sql b/prisma/migrations/20250827142342_newdis/migration.sql
new file mode 100644
index 0000000..5b65361
--- /dev/null
+++ b/prisma/migrations/20250827142342_newdis/migration.sql
@@ -0,0 +1,45 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `maintenanceType` on the `maintenance_visits` table. All the data in the column will be lost.
+ - Added the required column `maintenanceTypeId` to the `maintenance_visits` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- CreateTable
+CREATE TABLE "maintenance_types" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL
+);
+
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_maintenance_visits" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "vehicleId" INTEGER NOT NULL,
+ "customerId" INTEGER NOT NULL,
+ "maintenanceTypeId" INTEGER NOT NULL,
+ "description" TEXT NOT NULL,
+ "cost" REAL NOT NULL,
+ "paymentStatus" TEXT NOT NULL DEFAULT 'pending',
+ "kilometers" INTEGER NOT NULL,
+ "visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "nextVisitDelay" INTEGER NOT NULL,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL,
+ CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "maintenance_visits_maintenanceTypeId_fkey" FOREIGN KEY ("maintenanceTypeId") REFERENCES "maintenance_types" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+INSERT INTO "new_maintenance_visits" ("cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate") SELECT "cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate" FROM "maintenance_visits";
+DROP TABLE "maintenance_visits";
+ALTER TABLE "new_maintenance_visits" RENAME TO "maintenance_visits";
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "maintenance_types_name_key" ON "maintenance_types"("name");
diff --git a/prisma/migrations/20250827220800_newdis2/migration.sql b/prisma/migrations/20250827220800_newdis2/migration.sql
new file mode 100644
index 0000000..4a19c08
--- /dev/null
+++ b/prisma/migrations/20250827220800_newdis2/migration.sql
@@ -0,0 +1,31 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `maintenanceTypeId` on the `maintenance_visits` table. All the data in the column will be lost.
+ - Added the required column `maintenanceJobs` to the `maintenance_visits` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_maintenance_visits" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "vehicleId" INTEGER NOT NULL,
+ "customerId" INTEGER NOT NULL,
+ "maintenanceJobs" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "cost" REAL NOT NULL,
+ "paymentStatus" TEXT NOT NULL DEFAULT 'pending',
+ "kilometers" INTEGER NOT NULL,
+ "visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "nextVisitDelay" INTEGER NOT NULL,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL,
+ CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+INSERT INTO "new_maintenance_visits" ("cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate") SELECT "cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate" FROM "maintenance_visits";
+DROP TABLE "maintenance_visits";
+ALTER TABLE "new_maintenance_visits" RENAME TO "maintenance_visits";
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
diff --git a/prisma/migrations/add_car_dataset.sql b/prisma/migrations/add_car_dataset.sql
new file mode 100644
index 0000000..5d4f810
--- /dev/null
+++ b/prisma/migrations/add_car_dataset.sql
@@ -0,0 +1,13 @@
+-- CreateTable
+CREATE TABLE "car_dataset" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "manufacturer" TEXT NOT NULL,
+ "model" TEXT NOT NULL,
+ "bodyType" TEXT NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "car_dataset_manufacturer_model_key" ON "car_dataset"("manufacturer", "model");
\ No newline at end of file
diff --git a/prisma/migrations/add_maintenance_types.sql b/prisma/migrations/add_maintenance_types.sql
new file mode 100644
index 0000000..3ccef20
--- /dev/null
+++ b/prisma/migrations/add_maintenance_types.sql
@@ -0,0 +1,59 @@
+-- CreateTable
+CREATE TABLE "maintenance_types" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "maintenance_types_name_key" ON "maintenance_types"("name");
+
+-- Add new column to maintenance_visits for JSON maintenance jobs
+ALTER TABLE "maintenance_visits" ADD COLUMN "maintenanceJobs" TEXT;
+
+-- Create default maintenance types (will be populated by seed script)
+-- Note: The seed script will handle populating maintenance types
+
+-- Convert existing maintenance visits to use JSON format
+-- First, let's handle the case where maintenanceType column exists (old format)
+UPDATE "maintenance_visits"
+SET "maintenanceJobs" = '[{"typeId": 1, "job": "' || COALESCE("maintenanceType", 'صيانة عامة') || '", "notes": ""}]'
+WHERE "maintenanceJobs" IS NULL;
+
+-- Set default JSON for any remaining NULL values
+UPDATE "maintenance_visits"
+SET "maintenanceJobs" = '[{"typeId": 1, "job": "صيانة عامة", "notes": ""}]'
+WHERE "maintenanceJobs" IS NULL OR "maintenanceJobs" = '';
+
+-- Make maintenanceJobs NOT NULL and remove old maintenanceType column if it exists
+-- Note: SQLite doesn't support ALTER COLUMN, so we need to recreate the table
+CREATE TABLE "maintenance_visits_new" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "vehicleId" INTEGER NOT NULL,
+ "customerId" INTEGER NOT NULL,
+ "maintenanceJobs" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "cost" REAL NOT NULL,
+ "paymentStatus" TEXT NOT NULL DEFAULT 'pending',
+ "kilometers" INTEGER NOT NULL,
+ "visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "nextVisitDelay" INTEGER NOT NULL,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL,
+ CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- Copy data from old table to new table (excluding old maintenanceType column)
+INSERT INTO "maintenance_visits_new"
+SELECT "id", "vehicleId", "customerId", "maintenanceJobs", "description", "cost", "paymentStatus", "kilometers", "visitDate", "nextVisitDelay", "createdDate", "updateDate"
+FROM "maintenance_visits";
+
+-- Drop old table and rename new table
+DROP TABLE "maintenance_visits";
+ALTER TABLE "maintenance_visits_new" RENAME TO "maintenance_visits";
+
+-- Note: Income table foreign key constraint should still work as it references maintenance_visits(id)
\ No newline at end of file
diff --git a/prisma/migrations/add_settings.sql b/prisma/migrations/add_settings.sql
new file mode 100644
index 0000000..12377c5
--- /dev/null
+++ b/prisma/migrations/add_settings.sql
@@ -0,0 +1,19 @@
+-- Migration: Add Settings Table
+-- Description: Creates settings table for application configuration (date format, currency, number format)
+
+CREATE TABLE IF NOT EXISTS "settings" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "key" TEXT NOT NULL UNIQUE,
+ "value" TEXT NOT NULL,
+ "description" TEXT,
+ "createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updateDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Insert default settings
+INSERT OR IGNORE INTO "settings" ("key", "value", "description") VALUES
+('dateFormat', 'ar-SA', 'Date format locale (ar-SA or en-US)'),
+('currency', 'JOD', 'Currency code (JOD, USD, EUR, etc.)'),
+('numberFormat', 'ar-SA', 'Number format locale (ar-SA or en-US)'),
+('currencySymbol', 'د.أ', 'Currency symbol display'),
+('dateDisplayFormat', 'dd/MM/yyyy', 'Date display format pattern');
\ No newline at end of file
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..2a5a444
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "sqlite"
diff --git a/prisma/prisma/dev.db b/prisma/prisma/dev.db
new file mode 100644
index 0000000..8579601
Binary files /dev/null and b/prisma/prisma/dev.db differ
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..0c7cdfa
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,160 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+// User model for authentication and access control
+model User {
+ id Int @id @default(autoincrement())
+ name String
+ username String @unique
+ email String @unique
+ password String // hashed password
+ status String @default("active") // "active" or "inactive"
+ authLevel Int // 1=superadmin, 2=admin, 3=user
+ createdDate DateTime @default(now())
+ editDate DateTime @updatedAt
+
+ @@map("users")
+}
+
+// Customer model for vehicle owners
+model Customer {
+ id Int @id @default(autoincrement())
+ name String
+ phone String?
+ email String?
+ address String?
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ // Relationships
+ vehicles Vehicle[]
+ maintenanceVisits MaintenanceVisit[]
+
+ @@map("customers")
+}
+
+// Vehicle model with comprehensive specifications
+model Vehicle {
+ id Int @id @default(autoincrement())
+ plateNumber String @unique
+ bodyType String
+ manufacturer String
+ model String
+ trim String?
+ year Int
+ transmission String // "Automatic" or "Manual"
+ fuel String // "Gasoline", "Diesel", "Hybrid", "Mild Hybrid", "Electric"
+ cylinders Int?
+ engineDisplacement Float?
+ useType String // "personal", "taxi", "apps", "loading", "travel"
+ ownerId Int
+ lastVisitDate DateTime?
+ suggestedNextVisitDate DateTime?
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ // Relationships
+ owner Customer @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ maintenanceVisits MaintenanceVisit[]
+
+ @@map("vehicles")
+}
+
+// Maintenance type model for categorizing maintenance services
+model MaintenanceType {
+ id Int @id @default(autoincrement())
+ name String @unique
+ description String?
+ isActive Boolean @default(true)
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ @@map("maintenance_types")
+}
+
+// Car dataset model for storing vehicle manufacturers, models, and body types
+model CarDataset {
+ id Int @id @default(autoincrement())
+ manufacturer String
+ model String
+ bodyType String
+ isActive Boolean @default(true)
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ // Unique constraint to prevent duplicate manufacturer-model combinations
+ @@unique([manufacturer, model])
+ @@map("car_dataset")
+}
+
+// Maintenance visit model for tracking service records
+model MaintenanceVisit {
+ id Int @id @default(autoincrement())
+ vehicleId Int
+ customerId Int
+ maintenanceJobs String // JSON field storing array of maintenance jobs: [{"typeId": 1, "job": "تغيير زيت المحرك", "notes": "..."}, ...]
+ description String
+ cost Float
+ paymentStatus String @default("pending")
+ kilometers Int
+ visitDate DateTime @default(now())
+ nextVisitDelay Int // months (1, 2, 3, or 4)
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ // Relationships
+ vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
+ customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
+ income Income[]
+
+ @@map("maintenance_visits")
+}
+
+// Expense model for business expense tracking
+model Expense {
+ id Int @id @default(autoincrement())
+ description String
+ category String
+ amount Float
+ expenseDate DateTime @default(now())
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ @@map("expenses")
+}
+
+// Income model for revenue tracking from maintenance visits
+model Income {
+ id Int @id @default(autoincrement())
+ maintenanceVisitId Int
+ amount Float
+ incomeDate DateTime @default(now())
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ // Relationships
+ maintenanceVisit MaintenanceVisit @relation(fields: [maintenanceVisitId], references: [id], onDelete: Cascade)
+
+ @@map("income")
+}
+
+// Settings model for application configuration
+model Settings {
+ id Int @id @default(autoincrement())
+ key String @unique
+ value String
+ description String?
+ createdDate DateTime @default(now())
+ updateDate DateTime @updatedAt
+
+ @@map("settings")
+}
diff --git a/prisma/seed.ts b/prisma/seed.ts
new file mode 100644
index 0000000..f15f811
--- /dev/null
+++ b/prisma/seed.ts
@@ -0,0 +1,168 @@
+import { PrismaClient } from '@prisma/client';
+import bcrypt from 'bcryptjs';
+import { seedMaintenanceTypes } from './maintenanceTypeSeed';
+
+const prisma = new PrismaClient();
+
+async function main() {
+ console.log('🌱 Seeding database...');
+
+ // Check if superadmin already exists
+ const existingSuperadmin = await prisma.user.findFirst({
+ where: { authLevel: 1 }
+ });
+
+ if (!existingSuperadmin) {
+ // Create superadmin account
+ const hashedPassword = await bcrypt.hash('admin123', 10);
+
+ const superadmin = await prisma.user.create({
+ data: {
+ name: 'Super Administrator',
+ username: 'superadmin',
+ email: 'admin@carmaintenance.com',
+ password: hashedPassword,
+ status: 'active',
+ authLevel: 1,
+ },
+ });
+
+ console.log('✅ Created superadmin user:', superadmin.username);
+ } else {
+ console.log('ℹ️ Superadmin user already exists');
+ }
+
+ // Seed maintenance types using the dedicated seed function
+ await seedMaintenanceTypes();
+
+ // Seed some sample data for development
+ const sampleCustomer = await prisma.customer.upsert({
+ where: { id: 1 },
+ update: {},
+ create: {
+ name: 'أحمد محمد',
+ phone: '+966501234567',
+ email: 'ahmed@example.com',
+ address: 'الرياض، المملكة العربية السعودية',
+ },
+ });
+
+ const sampleVehicle = await prisma.vehicle.upsert({
+ where: { id: 1 },
+ update: {},
+ create: {
+ plateNumber: 'ABC-1234',
+ bodyType: 'سيدان',
+ manufacturer: 'تويوتا',
+ model: 'كامري',
+ trim: 'GLE',
+ year: 2022,
+ transmission: 'Automatic',
+ fuel: 'Gasoline',
+ cylinders: 4,
+ engineDisplacement: 2.5,
+ useType: 'personal',
+ ownerId: sampleCustomer.id,
+ },
+ });
+
+ // Get maintenance types for sample visits
+ const periodicMaintenanceType = await prisma.maintenanceType.findFirst({
+ where: { name: 'صيانة دورية' }
+ });
+ const brakeRepairType = await prisma.maintenanceType.findFirst({
+ where: { name: 'إصلاح الفرامل' }
+ });
+
+ // Create some sample maintenance visits
+ const sampleVisit1 = await prisma.maintenanceVisit.upsert({
+ where: { id: 1 },
+ update: {},
+ create: {
+ vehicleId: sampleVehicle.id,
+ customerId: sampleCustomer.id,
+ maintenanceTypeId: periodicMaintenanceType!.id,
+ description: 'تغيير زيت المحرك وفلتر الزيت وفحص شامل للمركبة',
+ cost: 250.00,
+ paymentStatus: 'paid',
+ kilometers: 45000,
+ visitDate: new Date('2024-01-15'),
+ nextVisitDelay: 6,
+ },
+ });
+
+ const sampleVisit2 = await prisma.maintenanceVisit.upsert({
+ where: { id: 2 },
+ update: {},
+ create: {
+ vehicleId: sampleVehicle.id,
+ customerId: sampleCustomer.id,
+ maintenanceTypeId: brakeRepairType!.id,
+ description: 'تغيير أقراص الفرامل الأمامية وتيل الفرامل',
+ cost: 450.00,
+ paymentStatus: 'paid',
+ kilometers: 47500,
+ visitDate: new Date('2024-03-20'),
+ nextVisitDelay: 12,
+ },
+ });
+
+ const sampleVisit3 = await prisma.maintenanceVisit.upsert({
+ where: { id: 3 },
+ update: {},
+ create: {
+ vehicleId: sampleVehicle.id,
+ customerId: sampleCustomer.id,
+ maintenanceTypeId: periodicMaintenanceType!.id,
+ description: 'تغيير زيت المحرك وفلتر الهواء وفحص البطارية',
+ cost: 180.00,
+ paymentStatus: 'pending',
+ kilometers: 52000,
+ visitDate: new Date('2024-07-15'),
+ nextVisitDelay: 6,
+ },
+ });
+
+ // Create income records for the maintenance visits
+ await prisma.income.upsert({
+ where: { id: 1 },
+ update: {},
+ create: {
+ maintenanceVisitId: sampleVisit1.id,
+ amount: sampleVisit1.cost,
+ incomeDate: sampleVisit1.visitDate,
+ },
+ });
+
+ await prisma.income.upsert({
+ where: { id: 2 },
+ update: {},
+ create: {
+ maintenanceVisitId: sampleVisit2.id,
+ amount: sampleVisit2.cost,
+ incomeDate: sampleVisit2.visitDate,
+ },
+ });
+
+ // Update the vehicle with last visit information
+ await prisma.vehicle.update({
+ where: { id: sampleVehicle.id },
+ data: {
+ lastVisitDate: sampleVisit3.visitDate,
+ suggestedNextVisitDate: new Date('2025-01-15'), // 6 months after last visit
+ },
+ });
+
+ console.log('✅ Created sample customer, vehicle, and maintenance visits');
+
+ console.log('🎉 Database seeded successfully!');
+}
+
+main()
+ .catch((e) => {
+ console.error('❌ Error seeding database:', e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
\ No newline at end of file
diff --git a/prisma/settingsSeed.ts b/prisma/settingsSeed.ts
new file mode 100644
index 0000000..fae9d7a
--- /dev/null
+++ b/prisma/settingsSeed.ts
@@ -0,0 +1,54 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+async function seedSettings() {
+ console.log('🌱 Seeding settings...');
+
+ const defaultSettings = [
+ {
+ key: 'dateFormat',
+ value: 'ar-SA',
+ description: 'Date format locale (ar-SA or en-US)'
+ },
+ {
+ key: 'currency',
+ value: 'JOD',
+ description: 'Currency code (JOD, USD, EUR, etc.)'
+ },
+ {
+ key: 'numberFormat',
+ value: 'ar-SA',
+ description: 'Number format locale (ar-SA or en-US)'
+ },
+ {
+ key: 'currencySymbol',
+ value: 'د.أ',
+ description: 'Currency symbol display'
+ },
+ {
+ key: 'dateDisplayFormat',
+ value: 'dd/MM/yyyy',
+ description: 'Date display format pattern'
+ }
+ ];
+
+ for (const setting of defaultSettings) {
+ await prisma.settings.upsert({
+ where: { key: setting.key },
+ update: {},
+ create: setting
+ });
+ }
+
+ console.log('✅ Settings seeded successfully');
+}
+
+seedSettings()
+ .catch((e) => {
+ console.error('❌ Error seeding settings:', e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
\ No newline at end of file
diff --git a/project.md b/project.md
new file mode 100644
index 0000000..43b801f
--- /dev/null
+++ b/project.md
@@ -0,0 +1,100 @@
+this is remix web app with sqlite and prisma, create a comprehensive car maintenance management system with cool, sleek, modern responsive and Mobile-friendly UI/UX . Build a complete web application with the following detailed specifications:
+
+## Core Requirements
+- **Framework**: Remix web app with SQLite database and Prisma ORM
+- **Purpose**: Car maintenance management system for business owners to track vehicle visits and maintenance records
+- **Language**: Primary Arabic language support with RTL (Right-to-Left) design
+- **Responsiveness**: Mobile-friendly responsive design throughout
+
+## UI/UX Requirements
+1. **RTL Layout**: All components must support Arabic RTL text direction
+2. **Responsive Design**: Ensure optimal viewing on desktop, tablet, and mobile devices
+3. **Dashboard Layout**: Create a responsive dashboard with:
+ - Collapsible sidebar navigation menu (full menu, icons-only menu)
+ - Collapsible sidebar for mobile devices (and full browser)
+ - Smooth transitions and mobile-optimized interactions
+
+## Authentication System
+4. **Custom Auth Logic**: Implement custom authentication compatible with both web and mobile applications
+5. **User Schema**: Users must have the following fields:
+ - name (string)
+ - username (string, unique)
+ - email (string, unique)
+ - password (hashed)
+ - status (enum: "active", "inactive")
+ - authLevel (integer: 1=superadmin, 2=admin, 3=user)
+ - createdDate (datetime)
+ - editDate (datetime)
+
+6. **Auth Routes**: Create signin and signup routes with shared layout
+ - **Signup Restriction**: Signup page should only be accessible when no auth-level=2 users exist in database. and signin can be done using username or email.
+ - **auth-levels are**: 1 for user, 2 for admin, 3 for superadmin
+ - Implement proper form validation and error handling
+
+7. **Database Seeding**: Create seed file containing:
+ - One superadmin account (auth-level=1)
+ - Essential system data for initial setup
+
+## Access Control & User Management
+8. **User Management Page**:
+ - Accessible only to auth-level=3 (superadmin) and auth-level=2 (admin)
+ - Admin users (level=2) cannot view superadmin accounts (level=3)
+ - Superadmin users (level=3) can view and manage all accounts
+ - Include user creation, editing, status management, and deleting
+
+## Core Business Features
+9. **Data Management**: Implement full CRUD operations for:
+ - **Vehicles**
+ - **Customers**
+ - **Maintenance Visits**
+ - **Expenses**
+ - **Income** (records are generated from maintenance visits)
+
+10. **Vehicle Schema**: Include the following fields:
+ - id (integer @id @default(autoincrement()))
+ - plateNumber (string, unique)
+ - bodyType (string) - e.g., Sedan, Coupe, HatchBack, PickUp, Bus - Van..a dropdownlist
+ - manufacturer (string) - e.g., Toyota, Mercedes, Ford, BMW,
+ - model (string) - e.g., Camry, S-Class
+ - trim (string) optional - e.g., GLX, S 600
+ - year (integer)
+ - Transmission (string) - dropdownlist : Automatic, Manual
+ - Fuel (string) - dropdownlist : Gasoline, Diesel, Hybrid, Mild Hybrid, Electric
+ - cylinders (integer) optional
+ - engineDisplacement (decimal) optional
+ - useType (enum: "personal", "taxi", "apps", "loading", "travel")
+ - ownerId (foreign key to customer)
+ - lastVisitDate (datetime)
+ - suggestedNextVisitDate (datetime)
+ - createdDate (datetime)
+ - updateDate (datetime)
+
+ - **Recommended Next Visit Delay**: Dropdown selection in the Visits route with options: 1 months, 2 months, 3 months and 4 months... when a vehicle register a maintenance visit the lastVisitDate at the Vehicle table will be the current date (current maintenance visit date) and the suggestedNextVisitDate should be the current maintenance visit date + the delay in months that selected from the Dropdown list.
+
+11. **Maintenance Visit Schema**: Include standard maintenance visit fields plus:
+ - Link to vehicle and customer records
+ - Maintenance type and description
+ - Cost and payment status
+ - Kilometers
+ - visitDate, createdDate and updateDate
+
+## Technical Implementation Requirements
+- Set up proper Prisma schema with relationships
+- Implement proper error handling and validation
+- Create reusable components for forms and data tables
+- Ensure proper TypeScript typing throughout
+- Implement proper session management and route protection
+- Create responsive navigation with mobile hamburger menu
+- Use proper Arabic fonts and ensure text rendering works correctly in RTL
+
+## Deliverables
+Provide complete implementation including:
+- Database schema (Prisma)
+- Authentication system
+- All CRUD operations for (users, customers, Vehicle, Maintenance Visits and Expenses)
+- Responsive UI components
+- Seed data file
+- Route protection middleware
+- Mobile-optimized navigation
+
+Focus on creating a production-ready application with clean code architecture, proper error handling, and excellent user experience across all devices.
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..8830cf6
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/logo-dark.png b/public/logo-dark.png
new file mode 100644
index 0000000..b24c7ae
Binary files /dev/null and b/public/logo-dark.png differ
diff --git a/public/logo-light.png b/public/logo-light.png
new file mode 100644
index 0000000..4490ae7
Binary files /dev/null and b/public/logo-light.png differ
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..db6ad68
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,40 @@
+import type { Config } from "tailwindcss";
+
+export default {
+ content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: [
+ "Cairo",
+ "Noto Sans Arabic",
+ "Inter",
+ "ui-sans-serif",
+ "system-ui",
+ "sans-serif",
+ "Apple Color Emoji",
+ "Segoe UI Emoji",
+ "Segoe UI Symbol",
+ "Noto Color Emoji",
+ ],
+ arabic: [
+ "Cairo",
+ "Noto Sans Arabic",
+ "ui-sans-serif",
+ "system-ui",
+ "sans-serif",
+ ],
+ },
+ screens: {
+ 'xs': '475px',
+ },
+ spacing: {
+ '18': '4.5rem',
+ '88': '22rem',
+ },
+ },
+ },
+ plugins: [
+ require('tailwindcss-rtl'),
+ ],
+} satisfies Config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..9d87dd3
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/.server/**/*.ts",
+ "**/.server/**/*.tsx",
+ "**/.client/**/*.ts",
+ "**/.client/**/*.tsx"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@remix-run/node", "vite/client"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+
+ // Vite takes care of building everything, not tsc.
+ "noEmit": true
+ }
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..e4e8cef
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,24 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+declare module "@remix-run/node" {
+ interface Future {
+ v3_singleFetch: true;
+ }
+}
+
+export default defineConfig({
+ plugins: [
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ v3_singleFetch: true,
+ v3_lazyRouteDiscovery: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..6d12c02
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vitest/config";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [tsconfigPaths()],
+ test: {
+ environment: "node",
+ globals: true,
+ },
+});
\ No newline at end of file