diff --git a/.env b/.env deleted file mode 100644 index f99be90..0000000 --- a/.env +++ /dev/null @@ -1,12 +0,0 @@ -LC_ALL=en_US.UTF-8 -LANG=en_US.UTF-8 -MailConfig__EmailServer="" -MailConfig__EmailFrom="" -MailConfig__Port=587 -MailConfig__Username="" -MailConfig__Password="" -LOGGING__LOGLEVEL__DEFAULT=Error - -# This file is provided as a GUIDELINE ONLY -# Use the MotoVaultPro Configurator to configure your environment variables -# https://motovaultpro.com/configure \ No newline at end of file diff --git a/AI_IMPLEMENTATION_GUIDE.md b/AI_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index f29691c..0000000 --- a/AI_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,1266 +0,0 @@ -# MotoVaultPro Greenfield AI-First Implementation Guide - -## Executive Summary - -This guide provides a comprehensive approach to build MotoVaultPro from the ground up using AI-optimized patterns designed for maximum efficiency in AI-to-AI collaboration and maintenance. - -**Project Status**: Greenfield - Building from scratch with AI-first architecture -**Target State**: Self-documenting, progressively discoverable codebase with AI-friendly navigation, context-aware documentation, and feature-based modular architecture optimized for AI development from day one. - -**Key Advantage**: No legacy constraints - we can implement the optimal AI structure immediately. - ---- - -## AI-First Project Architecture - -### Optimal Directory Structure - -``` -motovaultpro-v2/ -├── AI_README.md # 200-token complete system overview -├── PROJECT_MAP.md # Navigation and quick reference -├── CONVENTIONS.md # Project patterns and standards -├── CLAUDE.md # AI interaction guide (existing) -├── CHANGELOG_AI.md # AI-focused change tracking -├── -├── .ai/ # AI-specific metadata -│ ├── context.json # Context navigation for AI -│ ├── dependencies.yaml # Module dependency graph -│ └── shortcuts.md # Quick command reference -├── -├── backend/ -│ ├── src/ -│ │ ├── index.ts # Application entry point -│ │ ├── app.ts # Express app configuration -│ │ ├── -│ │ ├── core/ # Stable, critical functionality -│ │ │ ├── config/ # Environment & app configuration -│ │ │ │ ├── database.ts # PostgreSQL connection -│ │ │ │ ├── redis.ts # Redis connection -│ │ │ │ ├── auth0.ts # Auth0 middleware -│ │ │ │ └── minio.ts # MinIO client setup -│ │ │ ├── security/ # Auth and security primitives -│ │ │ │ └── auth.middleware.ts -│ │ │ └── logging/ # Centralized logging -│ │ │ └── logger.ts -│ │ ├── -│ │ ├── features/ # Business logic modules -│ │ │ ├── vehicles/ # Vehicle management feature -│ │ │ │ ├── index.ts # Public API exports -│ │ │ │ ├── vehicle.service.ts # Business logic -│ │ │ │ ├── vehicle.controller.ts # HTTP handlers -│ │ │ │ ├── vehicle.repository.ts # Data access -│ │ │ │ ├── vehicle.types.ts # Type definitions -│ │ │ │ ├── vehicle.validators.ts # Input validation -│ │ │ │ ├── vehicle.routes.ts # Route definitions -│ │ │ │ └── __tests__/ # Feature tests -│ │ │ ├── fuel-logs/ # Fuel tracking feature -│ │ │ │ └── [same structure as vehicles] -│ │ │ ├── maintenance/ # Maintenance tracking feature -│ │ │ │ └── [same structure as vehicles] -│ │ │ └── stations/ # Gas station discovery feature -│ │ │ └── [same structure as vehicles] -│ │ ├── -│ │ ├── shared/ # Reusable utilities -│ │ │ ├── types/ # Shared type definitions -│ │ │ │ ├── common.ts # Common types -│ │ │ │ └── api.ts # API response types -│ │ │ └── utils/ # Pure utility functions -│ │ │ ├── validation.ts -│ │ │ └── formatters.ts -│ │ ├── -│ │ ├── external/ # External API integrations -│ │ │ ├── vpic/ # NHTSA vPIC service -│ │ │ │ ├── vpic.service.ts -│ │ │ │ ├── vpic.types.ts -│ │ │ │ └── __tests__/ -│ │ │ └── google-maps/ # Google Maps integration -│ │ │ ├── maps.service.ts -│ │ │ ├── maps.types.ts -│ │ │ └── __tests__/ -│ │ └── -│ │ └── database/ # Database layer -│ │ ├── migrations/ # Schema migrations -│ │ │ ├── 001_initial_schema.sql -│ │ │ ├── 002_vin_cache.sql -│ │ │ └── 003_stored_procedures.sql -│ │ └── seeds/ # Test/dev data -│ ├── -│ ├── package.json -│ ├── tsconfig.json -│ ├── Dockerfile -│ └── jest.config.js -├── -├── frontend/ -│ ├── src/ -│ │ ├── main.tsx # React entry point -│ │ ├── App.tsx # Root component -│ │ ├── -│ │ ├── features/ # Feature-based organization -│ │ │ ├── vehicles/ # Vehicle management UI -│ │ │ │ ├── components/ -│ │ │ │ │ ├── VehicleForm/ -│ │ │ │ │ ├── VehicleList/ -│ │ │ │ │ └── VINDecoder/ -│ │ │ │ ├── pages/ -│ │ │ │ │ ├── VehiclesPage.tsx -│ │ │ │ │ └── VehicleDetailPage.tsx -│ │ │ │ ├── hooks/ -│ │ │ │ │ └── useVehicles.ts -│ │ │ │ ├── api/ -│ │ │ │ │ └── vehicles.api.ts -│ │ │ │ └── types/ -│ │ │ │ └── vehicle.types.ts -│ │ │ └── [fuel-logs, maintenance, stations with same structure] -│ │ ├── -│ │ ├── shared/ # Shared UI components & utilities -│ │ │ ├── components/ # Reusable UI components -│ │ │ │ ├── common/ # Basic UI elements -│ │ │ │ └── layout/ # Layout components -│ │ │ ├── hooks/ # Shared React hooks -│ │ │ │ ├── useAuth.ts -│ │ │ │ └── useApi.ts -│ │ │ ├── utils/ # Utility functions -│ │ │ └── types/ # Shared TypeScript types -│ │ ├── -│ │ └── core/ # Core application setup -│ │ ├── api/ # API client configuration -│ │ │ └── client.ts # Axios instance -│ │ ├── auth/ # Authentication setup -│ │ │ └── auth0.config.ts -│ │ └── store/ # State management -│ │ └── index.ts # Zustand/Redux store -│ ├── -│ ├── package.json -│ ├── tsconfig.json -│ ├── vite.config.ts -│ ├── Dockerfile -│ └── index.html -├── -├── templates/ # Code generation templates -│ ├── feature/ # Feature module template -│ │ ├── backend/ -│ │ │ ├── [feature].service.ts -│ │ │ ├── [feature].controller.ts -│ │ │ ├── [feature].repository.ts -│ │ │ ├── [feature].types.ts -│ │ │ ├── [feature].validators.ts -│ │ │ └── [feature].routes.ts -│ │ └── frontend/ -│ │ ├── [Feature]Page.tsx -│ │ ├── [Feature]Form.tsx -│ │ ├── [Feature]List.tsx -│ │ ├── use[Feature].ts -│ │ └── [feature].api.ts -│ ├── external-service/ # External API integration template -│ └── database-migration/ # Database migration template -├── -├── docs/ # Comprehensive documentation -│ ├── ARCHITECTURE.md # System design decisions -│ ├── API.md # API documentation -│ ├── DEPLOYMENT.md # Deployment guide -│ ├── TROUBLESHOOTING.md # Common issues and solutions -│ ├── external-apis/ # External API documentation -│ │ ├── nhtsa-vpic.md -│ │ └── google-maps.md -│ └── diagrams/ # Architecture diagrams -├── -├── k8s/ # Kubernetes configurations -│ └── [existing k8s structure from MOTOVAULTPRO.md] -├── -├── scripts/ # Development & deployment scripts -│ ├── setup.sh # Initial project setup -│ ├── generate-feature.sh # Generate new feature from template -│ └── migrate-db.sh # Database migration runner -├── -├── tests/ # Cross-cutting tests -│ ├── integration/ # API integration tests -│ ├── e2e/ # End-to-end tests -│ └── fixtures/ # Shared test data -├── -├── docker-compose.yml # Local development environment -├── .env.example # Environment variables template -├── Makefile # Common commands -└── README.md # Traditional project documentation -``` - ---- - -## Implementation Phases - -### Phase 1: Project Foundation Setup (Day 1-2) - -**Objective**: Create the complete AI-optimized project structure with foundation files. - -#### 1.1 Initialize Project Structure - -**Create complete directory tree** (all folders from structure above) - -**Initialize package management**: -```bash -# Backend setup -cd backend && npm init -y -npm install express typescript @types/node @types/express -npm install --save-dev jest @types/jest ts-jest nodemon -npm install pg redis ioredis minio axios joi -npm install express-jwt jwks-rsa # Auth0 integration - -# Frontend setup -cd ../frontend && npm create vite@latest . -- --template react-ts -npm install @auth0/auth0-react axios zustand -npm install --save-dev @testing-library/react @testing-library/jest-dom vitest -``` - -#### 1.2 Create Foundation Files - -**AI_README.md** (Essential 200-token overview) -```markdown -# MotoVaultPro - AI-First Vehicle Management Platform - -## AI Quick Start (50 tokens) -Full-stack vehicle management platform built with React/TypeScript frontend, Node.js/Express backend, PostgreSQL database. Integrates NHTSA vPIC for VIN decoding and Google Maps for fuel station discovery. Features Auth0 authentication, Redis caching, MinIO storage. AI-optimized architecture with feature-based modules. - -## Navigation Guide -- Start here: PROJECT_MAP.md -- Conventions: CONVENTIONS.md -- Architecture: docs/ARCHITECTURE.md -- AI Context: .ai/context.json - -## Critical Context -- Feature-based architecture: backend/src/features/ & frontend/src/features/ -- External APIs: NHTSA vPIC (VIN decode), Google Maps (stations) -- Auth0 handles authentication (JWT tokens) -- All external calls use caching (Redis TTL-based) -- Database migrations in backend/src/database/migrations/ - -## Primary Entry Points -- Backend: backend/src/index.ts → backend/src/app.ts -- Frontend: frontend/src/main.tsx → frontend/src/App.tsx -- Features: backend/src/features/[feature]/index.ts -- Templates: templates/feature/ for new features -``` - -**PROJECT_MAP.md** (AI Navigation Guide) -```markdown -# MotoVaultPro AI Navigation Map - -## System Overview -AI-first vehicle management platform with React frontend and Node.js backend. Features organized into self-contained modules with clear dependencies. External integrations cached via Redis. PostgreSQL for persistence, MinIO for file storage. - -## Quick Navigation by Task - -### Adding New Feature -1. **Use template**: `scripts/generate-feature.sh [feature-name]` -2. **Backend path**: `backend/src/features/[feature]/` -3. **Frontend path**: `frontend/src/features/[feature]/` -4. **Update dependencies**: `.ai/dependencies.yaml` -5. **Add tests**: Follow existing `__tests__/` patterns - -### Debugging Issues -1. **Check logs**: `backend/src/core/logging/logger.ts` output -2. **API issues**: `backend/src/features/[feature]/[feature].controller.ts` -3. **External APIs**: `backend/src/external/[service]/` -4. **Database**: `backend/src/database/migrations/` for schema -5. **Frontend**: `frontend/src/features/[feature]/components/` - -### Modifying External APIs -1. **Service layer**: `backend/src/external/[service]/[service].service.ts` -2. **Cache strategy**: Check TTL settings and invalidation logic -3. **Error handling**: Implement circuit breaker patterns -4. **Rate limiting**: Monitor API usage and implement backoff -5. **Types**: Update `[service].types.ts` for API changes - -### Database Changes -1. **New migration**: `backend/src/database/migrations/` -2. **Follow naming**: `001_descriptive_name.sql` -3. **Update models**: `backend/src/features/[feature]/[feature].types.ts` -4. **Test migration**: Up AND down migrations -5. **Update seeds**: `backend/src/database/seeds/` if needed - -## Feature Module Map - -Each feature follows identical structure: -``` -features/[feature]/ -├── index.ts # Public API (exports only) -├── [feature].service.ts # Business logic & external calls -├── [feature].controller.ts # HTTP request/response handling -├── [feature].repository.ts # Database operations -├── [feature].types.ts # TypeScript type definitions -├── [feature].validators.ts # Input validation schemas -├── [feature].routes.ts # Express route definitions -└── __tests__/ # Feature-specific tests - ├── unit/ # Service & utility tests - ├── integration/ # API endpoint tests - └── fixtures/ # Test data -``` - -## AI Context Loading Guide - -### Always Load (Essential Context) -- `AI_README.md` - System understanding -- `CONVENTIONS.md` - Coding standards -- `backend/src/core/` - Core functionality -- `.ai/context.json` - Task-specific routing - -### Conditional Loading (Task-Specific) -- **Vehicle operations**: `backend/src/features/vehicles/`, `backend/src/external/vpic/` -- **Fuel tracking**: `backend/src/features/fuel-logs/` -- **Authentication issues**: `backend/src/core/security/`, Auth0 config -- **Database problems**: `backend/src/database/`, core config -- **Frontend bugs**: `frontend/src/features/[relevant-feature]/` -- **External API issues**: `backend/src/external/[service]/` - -## Critical Paths (High-Risk Areas) - -### Security-Critical -- `backend/src/core/security/auth.middleware.ts` - JWT validation -- `backend/src/core/config/auth0.ts` - Auth0 configuration -- All `.env` handling in config files - -### Performance-Critical -- `backend/src/external/` - External API integrations -- Redis caching logic in all services -- Database queries in repositories - -### Business-Critical -- `backend/src/features/vehicles/vehicle.service.ts` - Core vehicle operations -- VIN decoding pipeline (NHTSA integration) -- Fuel cost calculations and tracking -``` - -#### 1.3 Create AI Context System - -**.ai/context.json** -```json -{ - "version": "1.0.0", - "project_type": "full-stack-app", - "language": "typescript", - "ai_optimized": true, - "context_budget": { - "always_load": [ - "AI_README.md", - "CONVENTIONS.md", - "backend/src/core/", - "backend/src/shared/types/" - ], - "essential_paths": [ - "backend/src/index.ts", - "backend/src/app.ts", - "frontend/src/main.tsx", - "frontend/src/App.tsx" - ], - "conditional": { - "if_adding_feature": [ - "templates/feature/", - ".ai/dependencies.yaml", - "scripts/generate-feature.sh" - ], - "if_modifying_vehicles": [ - "backend/src/features/vehicles/", - "backend/src/external/vpic/", - "frontend/src/features/vehicles/" - ], - "if_modifying_fuel": [ - "backend/src/features/fuel-logs/", - "frontend/src/features/fuel-logs/" - ], - "if_debugging_auth": [ - "backend/src/core/security/", - "backend/src/core/config/auth0.ts", - "frontend/src/core/auth/" - ], - "if_external_api_issues": [ - "backend/src/external/", - "docs/external-apis/", - "backend/src/core/logging/" - ], - "if_database_issues": [ - "backend/src/database/", - "backend/src/core/config/database.ts", - "backend/src/features/*/[feature].repository.ts" - ], - "if_frontend_issues": [ - "frontend/src/features/", - "frontend/src/shared/components/", - "frontend/src/core/" - ] - } - }, - "common_tasks": { - "add_feature": { - "command": "./scripts/generate-feature.sh [feature-name]", - "template": "templates/feature/", - "checklist": [ - "Run feature generation script", - "Implement business logic in service", - "Add database operations to repository", - "Create API endpoints in controller", - "Add route definitions", - "Implement frontend components", - "Write comprehensive tests", - "Update .ai/dependencies.yaml", - "Add feature to main app routes" - ], - "auto_created": [ - "backend/src/features/[feature]/", - "frontend/src/features/[feature]/", - "Complete test structure" - ] - }, - "add_external_api": { - "template": "templates/external-service/", - "checklist": [ - "Create service in backend/src/external/[service]/", - "Implement caching strategy", - "Add comprehensive error handling", - "Implement rate limiting/backoff", - "Create TypeScript types", - "Write integration tests", - "Document in docs/external-apis/", - "Add environment variables", - "Update .ai/dependencies.yaml" - ] - }, - "debug_issue": { - "start_with": [ - "Check backend/src/core/logging/ output", - "Review TROUBLESHOOTING.md", - "Check recent CHANGELOG_AI.md", - "Run relevant test suite", - "Check external API status if applicable" - ], - "common_paths": [ - "logs → controllers → services → external APIs", - "frontend errors → API calls → backend logs", - "database issues → migrations → repositories" - ] - } - }, - "danger_zones": { - "backend/src/core/security/": { - "warning": "Authentication & authorization critical path", - "required_review": true, - "test_command": "npm test:security", - "checklist": [ - "Test with valid/invalid JWT tokens", - "Verify Auth0 configuration", - "Check token expiration handling", - "Validate error responses", - "Test rate limiting" - ] - }, - "backend/src/database/migrations/": { - "warning": "Database schema changes", - "required_review": true, - "test_command": "npm run migrate:test", - "checklist": [ - "Test migration UP and DOWN", - "Verify data preservation", - "Check foreign key constraints", - "Test on copy of production data", - "Update repository classes" - ] - }, - "backend/src/external/": { - "warning": "External API integrations", - "test_command": "npm test:integration", - "checklist": [ - "Test with API mocks", - "Verify caching behavior", - "Check error handling", - "Test rate limiting", - "Monitor API usage quotas" - ] - } - }, - "performance_monitoring": { - "critical_paths": [ - "VIN decoding pipeline", - "Fuel station discovery", - "Vehicle data queries", - "Authentication flow" - ], - "caching_strategy": { - "vin_decodes": "30 days (rarely change)", - "fuel_stations": "1 hour (prices change)", - "user_vehicles": "5 minutes (frequent updates)", - "auth_tokens": "Follow JWT expiration" - } - } -} -``` - -**.ai/dependencies.yaml** -```yaml -version: 1.0.0 -last_updated: [DATE] -project_status: greenfield - -# Module dependency graph for AI understanding -modules: - # Core Infrastructure - backend/src/core/config/: - depends_on: [] - consumed_by: ["*"] - critical: true - modification_impact: "critical - full system restart required" - test_command: "npm test:config" - - backend/src/core/logging/: - depends_on: ["backend/src/core/config/"] - consumed_by: ["*"] - critical: true - modification_impact: "medium - affects debugging capability" - test_command: "npm test:logging" - - backend/src/core/security/: - depends_on: ["backend/src/core/config/", "backend/src/core/logging/"] - consumed_by: ["backend/src/features/*/[feature].controller.ts"] - critical: true - modification_impact: "critical - authentication affects all endpoints" - test_command: "npm test:security" - - # Feature Modules (following identical patterns) - backend/src/features/vehicles/: - depends_on: ["backend/src/core/*", "backend/src/external/vpic/", "backend/src/shared/"] - consumed_by: ["backend/src/app.ts", "frontend/src/features/vehicles/"] - critical: true - modification_impact: "high - core business functionality" - test_command: "npm test:features:vehicles" - - backend/src/features/fuel-logs/: - depends_on: ["backend/src/core/*", "backend/src/features/vehicles/", "backend/src/shared/"] - consumed_by: ["backend/src/app.ts", "frontend/src/features/fuel-logs/"] - critical: false - modification_impact: "medium - isolated feature" - test_command: "npm test:features:fuel-logs" - - # External Integrations - backend/src/external/vpic/: - depends_on: ["backend/src/core/config/", "backend/src/core/logging/"] - consumed_by: ["backend/src/features/vehicles/"] - critical: false - modification_impact: "medium - affects VIN decoding only" - test_command: "npm test:external:vpic" - - backend/src/external/google-maps/: - depends_on: ["backend/src/core/config/", "backend/src/core/logging/"] - consumed_by: ["backend/src/features/stations/"] - critical: false - modification_impact: "low - affects fuel station discovery only" - test_command: "npm test:external:maps" - - # Frontend Features (mirror backend structure) - frontend/src/features/vehicles/: - depends_on: ["frontend/src/core/", "frontend/src/shared/"] - consumed_by: ["frontend/src/App.tsx"] - critical: true - modification_impact: "medium - user-facing functionality" - test_command: "npm test:frontend:vehicles" - -# Architectural rules enforced by AI -rules: - - "Features cannot depend on other features (except vehicles as base)" - - "Core modules cannot depend on features" - - "External services must use caching" - - "All modules must depend on core/config for environment" - - "Frontend features mirror backend structure" - - "No direct database access outside repositories" - -# Import cycles to prevent -forbidden_cycles: - - ["core/security", "features/*", "core/security"] - - ["features/vehicles", "features/fuel-logs", "features/vehicles"] - -# High-risk modification paths requiring extra validation -high_risk_paths: - - description: "Authentication flow" - modules: ["core/security", "core/config/auth0.ts", "frontend/src/core/auth/"] - test_suite: "npm test:auth:full" - - - description: "VIN decoding pipeline" - modules: ["external/vpic", "features/vehicles", "frontend/src/features/vehicles/components/VINDecoder"] - test_suite: "npm test:integration:vin" - - - description: "Database operations" - modules: ["database/migrations", "features/*/[feature].repository.ts", "core/config/database.ts"] - test_suite: "npm test:database:full" -``` - -### Phase 2: Feature Templates & Code Generation (Day 3-4) - -**Objective**: Create comprehensive templates that generate consistent, AI-friendly code. - -#### 2.1 Feature Generation Script - -**scripts/generate-feature.sh** -```bash -#!/bin/bash -set -e - -FEATURE_NAME=$1 -if [ -z "$FEATURE_NAME" ]; then - echo "Usage: $0 " - echo "Example: $0 maintenance-logs" - exit 1 -fi - -FEATURE_PASCAL=$(echo $FEATURE_NAME | sed -r 's/(^|-)([a-z])/\U\2/g') -FEATURE_CAMEL=$(echo $FEATURE_PASCAL | sed 's/^./\l&/') - -echo "Generating feature: $FEATURE_NAME" -echo "Pascal case: $FEATURE_PASCAL" -echo "Camel case: $FEATURE_CAMEL" - -# Create backend feature structure -BACKEND_DIR="backend/src/features/$FEATURE_NAME" -mkdir -p "$BACKEND_DIR/__tests__/unit" -mkdir -p "$BACKEND_DIR/__tests__/integration" -mkdir -p "$BACKEND_DIR/__tests__/fixtures" - -# Generate backend files from templates -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/index.template > "$BACKEND_DIR/index.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/service.template > "$BACKEND_DIR/$FEATURE_CAMEL.service.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/controller.template > "$BACKEND_DIR/$FEATURE_CAMEL.controller.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/repository.template > "$BACKEND_DIR/$FEATURE_CAMEL.repository.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/types.template > "$BACKEND_DIR/$FEATURE_CAMEL.types.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/validators.template > "$BACKEND_DIR/$FEATURE_CAMEL.validators.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/backend/routes.template > "$BACKEND_DIR/$FEATURE_CAMEL.routes.ts" - -# Create frontend feature structure -FRONTEND_DIR="frontend/src/features/$FEATURE_NAME" -mkdir -p "$FRONTEND_DIR/components" -mkdir -p "$FRONTEND_DIR/pages" -mkdir -p "$FRONTEND_DIR/hooks" -mkdir -p "$FRONTEND_DIR/api" -mkdir -p "$FRONTEND_DIR/types" -mkdir -p "$FRONTEND_DIR/__tests__" - -# Generate frontend files from templates -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/frontend/Page.template > "$FRONTEND_DIR/pages/${FEATURE_PASCAL}Page.tsx" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/frontend/Form.template > "$FRONTEND_DIR/components/${FEATURE_PASCAL}Form.tsx" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/frontend/List.template > "$FRONTEND_DIR/components/${FEATURE_PASCAL}List.tsx" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/frontend/hook.template > "$FRONTEND_DIR/hooks/use${FEATURE_PASCAL}.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/frontend/api.template > "$FRONTEND_DIR/api/$FEATURE_CAMEL.api.ts" - -sed "s/\[FEATURE_NAME\]/$FEATURE_NAME/g; s/\[FEATURE_PASCAL\]/$FEATURE_PASCAL/g; s/\[FEATURE_CAMEL\]/$FEATURE_CAMEL/g" \ - templates/feature/frontend/types.template > "$FRONTEND_DIR/types/$FEATURE_CAMEL.types.ts" - -echo "✅ Feature $FEATURE_NAME generated successfully!" -echo "" -echo "Next steps:" -echo "1. Implement business logic in backend/src/features/$FEATURE_NAME/$FEATURE_CAMEL.service.ts" -echo "2. Add database operations in $FEATURE_CAMEL.repository.ts" -echo "3. Customize React components in frontend/src/features/$FEATURE_NAME/components/" -echo "4. Add feature routes to backend/src/app.ts and frontend/src/App.tsx" -echo "5. Write tests and update .ai/dependencies.yaml" -``` - -#### 2.2 Comprehensive Templates - -**templates/feature/backend/service.template** (AI-optimized service pattern) -```typescript -/** - * @ai-summary Handles [FEATURE_NAME] business logic and external integrations - * @ai-examples backend/src/features/[FEATURE_NAME]/__tests__/fixtures/ - * @critical-context Requires authentication, uses caching for external calls - */ - -import { [FEATURE_PASCAL]Repository } from './[FEATURE_CAMEL].repository'; -import { - [FEATURE_PASCAL]Input, - [FEATURE_PASCAL]Output, - [FEATURE_PASCAL]Error, - Create[FEATURE_PASCAL]Request, - Update[FEATURE_PASCAL]Request -} from './[FEATURE_CAMEL].types'; -import { logger } from '../../core/logging/logger'; -import { CacheService } from '../../shared/utils/cache.service'; - -export class [FEATURE_PASCAL]Service { - private readonly cachePrefix = '[FEATURE_NAME]'; - private readonly cacheTTL = 300; // 5 minutes - - constructor( - private repository: [FEATURE_PASCAL]Repository, - private cacheService: CacheService - ) {} - - /** - * HANDLES: Creates new [FEATURE_NAME] record with validation - * THROWS: ValidationError, DuplicateError, DatabaseError - * SIDE_EFFECTS: Database insert, cache invalidation - * AUTH_REQUIRED: true - */ - async create[FEATURE_PASCAL]( - data: Create[FEATURE_PASCAL]Request, - userId: string - ): Promise<[FEATURE_PASCAL]Output> { - try { - logger.info('[FEATURE_PASCAL]Service.create[FEATURE_PASCAL] started', { - userId, - dataKeys: Object.keys(data) - }); - - // 1. Validate business rules - await this.validateCreate[FEATURE_PASCAL](data, userId); - - // 2. Transform input to domain model - const [FEATURE_CAMEL]Data = this.transformCreate[FEATURE_PASCAL](data, userId); - - // 3. Persist to database - const created[FEATURE_PASCAL] = await this.repository.create( - [FEATURE_CAMEL]Data - ); - - // 4. Invalidate relevant cache entries - await this.invalidate[FEATURE_PASCAL]Cache(userId); - - // 5. Transform to output format - const result = this.transformTo[FEATURE_PASCAL]Output(created[FEATURE_PASCAL]); - - logger.info('[FEATURE_PASCAL]Service.create[FEATURE_PASCAL] completed', { - userId, - [FEATURE_CAMEL]Id: result.id - }); - - return result; - - } catch (error) { - logger.error('[FEATURE_PASCAL]Service.create[FEATURE_PASCAL] failed', { - error: error.message, - userId, - data - }); - - if (error instanceof ValidationError) { - throw error; - } - - throw new [FEATURE_PASCAL]Error(`Failed to create [FEATURE_NAME]: ${error.message}`); - } - } - - /** - * HANDLES: Retrieves [FEATURE_NAME] records for user with caching - * THROWS: DatabaseError, NotFoundError - * SIDE_EFFECTS: Cache read/write - * AUTH_REQUIRED: true - */ - async get[FEATURE_PASCAL]sByUser(userId: string): Promise<[FEATURE_PASCAL]Output[]> { - const cacheKey = `${this.cachePrefix}:user:${userId}`; - - try { - // 1. Check cache first - const cached = await this.cacheService.get<[FEATURE_PASCAL]Output[]>(cacheKey); - if (cached) { - logger.debug('[FEATURE_PASCAL]Service cache hit', { userId, cacheKey }); - return cached; - } - - // 2. Query database - const [FEATURE_CAMEL]s = await this.repository.findByUserId(userId); - - // 3. Transform to output format - const result = [FEATURE_CAMEL]s.map(item => - this.transformTo[FEATURE_PASCAL]Output(item) - ); - - // 4. Cache successful result - await this.cacheService.set(cacheKey, result, this.cacheTTL); - - logger.info('[FEATURE_PASCAL]Service.get[FEATURE_PASCAL]sByUser completed', { - userId, - count: result.length - }); - - return result; - - } catch (error) { - logger.error('[FEATURE_PASCAL]Service.get[FEATURE_PASCAL]sByUser failed', { - error: error.message, - userId - }); - - throw new [FEATURE_PASCAL]Error(`Failed to get [FEATURE_NAME]s: ${error.message}`); - } - } - - // Private helper methods following AI-friendly patterns - private async validateCreate[FEATURE_PASCAL]( - data: Create[FEATURE_PASCAL]Request, - userId: string - ): Promise { - // Implement validation logic - // Check business rules - // Verify user permissions - // Validate foreign key relationships - } - - private transformCreate[FEATURE_PASCAL]( - data: Create[FEATURE_PASCAL]Request, - userId: string - ): [FEATURE_PASCAL]Input { - return { - ...data, - userId, - createdAt: new Date(), - updatedAt: new Date() - } as [FEATURE_PASCAL]Input; - } - - private transformTo[FEATURE_PASCAL]Output(item: [FEATURE_PASCAL]): [FEATURE_PASCAL]Output { - return { - id: item.id, - userId: item.userId, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - // Add other fields as needed - } as [FEATURE_PASCAL]Output; - } - - private async invalidate[FEATURE_PASCAL]Cache(userId: string): Promise { - const cacheKey = `${this.cachePrefix}:user:${userId}`; - await this.cacheService.del(cacheKey); - } -} - -// Custom error class -export class [FEATURE_PASCAL]Error extends Error { - constructor(message: string) { - super(message); - this.name = '[FEATURE_PASCAL]Error'; - } -} -``` - -### Phase 3: Core Infrastructure Implementation (Day 5-7) - -**Objective**: Implement the core system components using AI-optimized patterns. - -#### 3.1 Backend Core Setup - -**backend/src/core/config/database.ts** -```typescript -/** - * @ai-summary PostgreSQL connection setup with connection pooling - * @ai-details docs/database/CONNECTION_MANAGEMENT.md - * @critical-context Connection pooling, automatic reconnection, query logging - */ - -import { Pool, PoolConfig } from 'pg'; -import { logger } from '../logging/logger'; - -const poolConfig: PoolConfig = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - database: process.env.DB_NAME || 'motovaultpro', - user: process.env.DB_USER || 'postgres', - password: process.env.DB_PASSWORD || '', - ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, - max: parseInt(process.env.DB_POOL_MAX || '10'), - min: parseInt(process.env.DB_POOL_MIN || '2'), - idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'), - connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '10000'), -}; - -export const pool = new Pool(poolConfig); - -// Connection event handlers for monitoring -pool.on('connect', (client) => { - logger.info('Database client connected', { - totalCount: pool.totalCount, - idleCount: pool.idleCount - }); -}); - -pool.on('error', (err) => { - logger.error('Database pool error', { error: err.message }); -}); - -pool.on('remove', () => { - logger.info('Database client removed', { - totalCount: pool.totalCount, - idleCount: pool.idleCount - }); -}); - -// Graceful shutdown handler -process.on('SIGTERM', async () => { - logger.info('SIGTERM received, closing database pool'); - await pool.end(); - process.exit(0); -}); - -export default pool; -``` - -**backend/src/core/logging/logger.ts** -```typescript -/** - * @ai-summary Structured logging with correlation IDs and log levels - * @ai-details docs/logging/STRUCTURED_LOGGING.md - * @critical-context All logs include correlation ID, user context, performance metrics - */ - -import winston from 'winston'; - -const isDevelopment = process.env.NODE_ENV === 'development'; - -export const logger = winston.createLogger({ - level: process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info'), - format: winston.format.combine( - winston.format.timestamp(), - winston.format.errors({ stack: true }), - winston.format.json(), - winston.format.printf(({ timestamp, level, message, ...meta }) => { - const logEntry = { - timestamp, - level, - message, - service: 'motovaultpro-backend', - environment: process.env.NODE_ENV, - version: process.env.APP_VERSION, - ...meta - }; - return JSON.stringify(logEntry); - }) - ), - defaultMeta: { - service: 'motovaultpro-backend' - }, - transports: [ - new winston.transports.Console({ - format: isDevelopment - ? winston.format.combine( - winston.format.colorize(), - winston.format.simple() - ) - : winston.format.json() - }) - ] -}); - -// Add file transport for production -if (process.env.NODE_ENV === 'production') { - logger.add(new winston.transports.File({ - filename: 'logs/error.log', - level: 'error', - maxsize: 10485760, // 10MB - maxFiles: 5 - })); - - logger.add(new winston.transports.File({ - filename: 'logs/combined.log', - maxsize: 10485760, // 10MB - maxFiles: 5 - })); -} - -export default logger; -``` - -#### 3.2 Frontend Core Setup - -**frontend/src/core/api/client.ts** -```typescript -/** - * @ai-summary Axios client with Auth0 integration and error handling - * @ai-details docs/api/CLIENT_CONFIGURATION.md - * @critical-context Automatic token refresh, request/response logging, error transformation - */ - -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { useAuth0 } from '@auth0/auth0-react'; - -const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api'; - -// Create axios instance -export const apiClient: AxiosInstance = axios.create({ - baseURL, - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// Request interceptor for adding auth token -apiClient.interceptors.request.use( - async (config: AxiosRequestConfig) => { - // Note: In actual implementation, you'd get token from Auth0 context - // This is a placeholder for the pattern - const token = localStorage.getItem('access_token'); - if (token) { - config.headers = { - ...config.headers, - Authorization: `Bearer ${token}`, - }; - } - - // Add correlation ID for request tracking - config.headers['X-Correlation-ID'] = crypto.randomUUID(); - - console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`, { - correlationId: config.headers['X-Correlation-ID'], - params: config.params, - }); - - return config; - }, - (error) => { - console.error('API Request Error:', error); - return Promise.reject(error); - } -); - -// Response interceptor for error handling -apiClient.interceptors.response.use( - (response: AxiosResponse) => { - console.log(`API Response: ${response.status} ${response.config.url}`, { - correlationId: response.config.headers['X-Correlation-ID'], - duration: Date.now() - (response.config as any).startTime, - }); - return response; - }, - async (error) => { - const { response, config } = error; - - console.error('API Response Error:', { - status: response?.status, - url: config?.url, - correlationId: config?.headers['X-Correlation-ID'], - error: response?.data?.error || error.message, - }); - - // Handle specific error cases - if (response?.status === 401) { - // Token expired or invalid - localStorage.removeItem('access_token'); - window.location.href = '/login'; - } - - // Transform error to consistent format - const apiError = { - status: response?.status || 500, - message: response?.data?.error || error.message || 'An unexpected error occurred', - correlationId: config?.headers['X-Correlation-ID'], - }; - - return Promise.reject(apiError); - } -); - -export default apiClient; -``` - -### Phase 4: Feature Implementation (Day 8-12) - -**Objective**: Implement each feature using the generated templates and AI-optimized patterns. - -#### 4.1 Implementation Order & Dependencies - -1. **Vehicles Feature** (Days 8-9) - Foundation feature -2. **External APIs** (Day 10) - NHTSA vPIC and Google Maps -3. **Fuel Logs Feature** (Day 11) - Depends on vehicles -4. **Maintenance Feature** (Day 12) - Depends on vehicles -5. **Stations Feature** (Day 12) - Independent feature - -#### 4.2 Success Criteria for Each Feature - -**Per Feature Checklist:** -- [ ] Backend service, controller, repository implemented -- [ ] Frontend components, hooks, API client implemented -- [ ] Comprehensive test coverage (unit + integration) -- [ ] External API integrations cached appropriately -- [ ] Database migrations created and tested -- [ ] Documentation updated in feature README -- [ ] Added to main application routing -- [ ] Error handling follows established patterns - -### Phase 5: Testing & Documentation (Day 13-14) - -**Objective**: Comprehensive testing and AI-optimized documentation. - -#### 5.1 Testing Strategy - -**Test Structure** (mirrors feature structure) -``` -__tests__/ -├── unit/ # Individual function/method tests -│ ├── services/ -│ ├── controllers/ -│ └── utils/ -├── integration/ # API endpoint tests with database -│ ├── vehicles.test.ts -│ ├── fuel-logs.test.ts -│ └── auth.test.ts -├── external/ # External API integration tests (mocked) -│ ├── vpic.test.ts -│ └── google-maps.test.ts -└── e2e/ # End-to-end user flow tests - ├── vehicle-management.test.ts - └── fuel-tracking.test.ts -``` - -#### 5.2 AI-Optimized Documentation - -**docs/ARCHITECTURE.md** -```markdown -# MotoVaultPro Architecture Overview - -## System Design Philosophy - -AI-first architecture prioritizing: -- **Self-documenting code** through naming and structure -- **Progressive disclosure** of complexity -- **Feature isolation** with clear dependencies -- **Consistent patterns** across all modules -- **Context-efficient navigation** for AI agents - -## Core Architectural Patterns - -### Feature-Based Organization -Each feature is completely self-contained: -- Business logic in service layer -- Data access in repository layer -- HTTP handling in controller layer -- Type definitions co-located with feature -- Tests mirror implementation structure - -### External API Integration Pattern -All external APIs follow identical structure: -- Service class with caching -- Error handling with circuit breakers -- Rate limiting and backoff -- Comprehensive logging -- Mock-friendly for testing - -### Security Architecture -- Auth0 handles all authentication -- JWT tokens validated on every request -- User context available throughout request lifecycle -- No sensitive data in logs or cache -- Environment-based configuration - -## Database Design - -### Schema Organization -- Core entities: users, vehicles, fuel_logs, maintenance_logs -- Audit fields on all tables (created_at, updated_at, created_by) -- Foreign key constraints enforced -- Indexes on frequently queried columns -- Migration-based schema evolution - -### Caching Strategy -- External API responses: Appropriate TTL based on data volatility -- User data: Short TTL (5 minutes) for consistency -- Static data: Long TTL (30 days) for performance -- Cache invalidation on data mutations - -## Frontend Architecture - -### Component Organization -- Feature-based component grouping -- Shared components for common UI patterns -- Custom hooks for business logic -- API clients co-located with features -- Type definitions shared between frontend and backend - -### State Management -- Local state for UI-only concerns -- Global state for cross-feature data -- Server state managed by API clients -- Authentication state managed by Auth0 -``` - ---- - -## Implementation Timeline - -### Week 1: Foundation (Days 1-7) -- **Days 1-2**: Project structure setup, foundation files -- **Days 3-4**: Templates and code generation scripts -- **Days 5-7**: Core infrastructure implementation - -### Week 2: Feature Development (Days 8-14) -- **Days 8-9**: Vehicles feature (backend + frontend) -- **Day 10**: External API integrations (vPIC, Google Maps) -- **Day 11**: Fuel logs feature -- **Day 12**: Maintenance and stations features -- **Days 13-14**: Testing, documentation, refinement - ---- - -## Success Metrics - -### Quantitative Goals -- [ ] AI can understand system architecture in under 200 tokens -- [ ] Feature generation script creates working code in under 2 minutes -- [ ] AI requires 70% fewer file reads for common tasks vs traditional structure -- [ ] New feature implementation follows consistent patterns automatically -- [ ] Test coverage above 85% for all business logic - -### Qualitative Goals -- [ ] AI can navigate codebase without human guidance -- [ ] Code review focuses on business logic vs structure -- [ ] New developers understand patterns intuitively -- [ ] Feature development velocity increases over time -- [ ] Technical debt accumulates slower due to consistent patterns - ---- - -## Long-term Benefits - -### For AI Collaboration -- **Context Efficiency**: AI loads only relevant code for each task -- **Pattern Recognition**: Consistent structure enables better code generation -- **Self-Documenting**: Code explains itself through naming and organization -- **Progressive Discovery**: AI can start with overview and dive deeper as needed - -### For Human Developers -- **Reduced Cognitive Load**: Clear structure reduces context switching -- **Faster Onboarding**: New developers can follow established patterns -- **Better Maintainability**: Consistent patterns make changes predictable -- **Enhanced Productivity**: Templates and scripts automate repetitive tasks - -### For System Evolution -- **Scalable Architecture**: Feature isolation supports team scaling -- **Technology Migration**: Clear boundaries enable gradual technology updates -- **Performance Optimization**: Well-defined layers enable targeted optimizations -- **Quality Assurance**: Consistent patterns enable automated quality checks - ---- - -## Next Steps - -1. **Begin Phase 1**: Create project structure and foundation files -2. **Setup Development Environment**: Docker Compose for local development -3. **Configure CI/CD**: GitHub Actions for testing and deployment -4. **Team Training**: Onboard development team to AI-first patterns -5. **Iterative Refinement**: Improve templates and patterns based on usage - -This greenfield approach gives us the unique opportunity to build the ideal AI-collaborative codebase from day one, setting the foundation for efficient AI-to-AI development throughout the project lifecycle. \ No newline at end of file diff --git a/AI_STRUCTURE.md b/AI_STRUCTURE.md deleted file mode 100644 index 0d53b3b..0000000 --- a/AI_STRUCTURE.md +++ /dev/null @@ -1,591 +0,0 @@ -You are tasked with creating a new project that follows AI-optimized documentation and structure patterns designed for efficient AI-to-AI code maintenance. This project should be self-describing, context-efficient, and progressively discoverable. -Core Setup Instructions - -1. Initialize Project Structure -Create this exact directory structure: -project-root/ -├── AI_README.md # 200-token complete system overview -├── PROJECT_MAP.md # Navigation and quick reference -├── CONVENTIONS.md # Project patterns and standards -├── CLAUDE.md # This file - AI interaction guide -├── CHANGELOG_AI.md # AI-focused change tracking -├── src/ -│ ├── core/ # Stable, critical functionality -│ │ └── README.md # Core module descriptions -│ ├── features/ # Business logic modules -│ │ └── README.md # Feature development guide -│ ├── shared/ # Reusable utilities -│ │ └── README.md # Utility usage patterns -│ └── index.{ts|py} # Main entry point -├── docs/ -│ ├── ARCHITECTURE.md # System design decisions -│ ├── TROUBLESHOOTING.md # Common issues and solutions -│ ├── dependencies.md # External dependency documentation -│ └── diagrams/ # Visual architecture representations -├── tests/ -│ ├── README.md # Testing strategy -│ ├── fixtures/ # Shared test data -│ └── examples/ # Usage examples -├── templates/ # Code generation templates -│ └── README.md # Template usage guide -├── .ai/ # AI-specific metadata -│ ├── context.json # Context navigation for AI -│ ├── dependencies.yaml # Module dependency graph -│ └── shortcuts.md # Quick command reference -└── scripts/ - └── setup.{sh|py} # Project initialization scripts -2. Create Foundation Files -AI_README.md -markdown# Project Name - -## AI Quick Start (50 tokens) -[One paragraph describing what this system does, its primary purpose, and key technologies] - -## Navigation Guide -- Start here: PROJECT_MAP.md -- Conventions: CONVENTIONS.md -- Architecture: docs/ARCHITECTURE.md -- AI Metadata: .ai/context.json - -## Critical Context -- [List 3-5 things an AI must always know when working on this codebase] - -## Primary Entry Points -- Main application: src/index.{ts|py} -- Core modules: src/core/README.md -- Feature modules: src/features/*/index.{ts|py} -PROJECT_MAP.md -markdown# Project Navigation Map - -## System Overview -[2-3 sentences about the system architecture] - -## Quick Navigation - -### By Task -- **Adding a feature**: Start with templates/ → src/features/ -- **Fixing bugs**: TROUBLESHOOTING.md → tests/ → src/ -- **Modifying core**: Read CONVENTIONS.md first → src/core/ -- **Adding tests**: tests/README.md → tests/examples/ - -### By Frequency -- **Most changed**: [List top 3 most frequently modified paths] -- **Most critical**: src/core/* -- **Most complex**: [List complex modules needing extra context] - -## Module Map -\`\`\` -src/ -├── core/ -│ ├── config/ # Environment and app configuration -│ ├── database/ # Data layer abstractions -│ ├── logging/ # Centralized logging -│ └── security/ # Auth and security primitives -├── features/ -│ └── [feature]/ # Each feature is self-contained -│ ├── index # Public API -│ ├── service # Business logic -│ ├── types # Type definitions -│ └── tests # Feature tests -└── shared/ - ├── utils/ # Pure utility functions - └── types/ # Shared type definitions -\`\`\` - -## AI Interaction Guide -- **Before modifying**: Read .ai/context.json for the area you're working in -- **When debugging**: Start with src/core/logging -- **When adding features**: Use templates/ for consistent patterns -- **After changes**: Update CHANGELOG_AI.md and .ai/dependencies.yaml -CONVENTIONS.md -markdown# Project Conventions - -## Code Style - -### Naming Conventions -- **Files**: kebab-case.ts or snake_case.py -- **Classes**: PascalCase with descriptive names -- **Functions**: camelCase (JS/TS) or snake_case (Python), verb_noun pattern -- **Constants**: UPPER_SNAKE_CASE -- **Interfaces/Types**: PascalCase with 'I' prefix for interfaces (optional) - -### Self-Documenting Code Principles -1. **Prefer clear naming over comments** - \`\`\`typescript - // Bad - const d = new Date(); // Gets current date - - // Good - const currentDate = new Date(); - \`\`\` - -2. **Use type hints as documentation** - \`\`\`python - def process_order( - order_data: OrderDict, - validate: bool = True - ) -> Result[Order, OrderError]: - """Process order. See OrderDict for structure.""" - pass - \`\`\` - -3. **Function names should describe complete behavior** - \`\`\`typescript - // Bad: validate(data) - // Good: validateOrThrow(data) - // Good: validateAndReturnErrors(data) - \`\`\` - -## Documentation Standards - -### Progressive Disclosure Pattern -\`\`\`javascript -/** - * @ai-summary Handles payment processing - * @ai-details docs/payments/PROCESSING.md - * @ai-examples tests/examples/payment-processing.test.js - * @critical-context PCI compliance required, no card storage - */ -\`\`\` - -### Required Annotations -- **Services/Classes**: @ai-summary (one line) -- **Complex functions**: HANDLES, THROWS, SIDE_EFFECTS -- **Security-sensitive code**: SAFETY comment -- **External dependencies**: DEPENDS comment - -### When to Comment -ONLY add inline comments for: -- Regulatory/compliance requirements -- Non-obvious business logic -- Performance optimizations (with metrics) -- Security considerations -- Workarounds with issue references - -## Structure Patterns - -### Feature Module Structure -\`\`\` -features/[feature-name]/ -├── index.{ts|py} # Public API (exports only) -├── service.{ts|py} # Business logic -├── repository.{ts|py} # Data access -├── types.{ts|py} # Type definitions -├── validators.{ts|py} # Input validation -├── README.md # Feature documentation -└── __tests__/ # Feature tests -\`\`\` - -### Service Class Pattern -\`\`\`typescript -export class FeatureService { - constructor( - private repository: FeatureRepository, - private logger: Logger - ) {} - - // Public methods first, prefixed with action verb - async createFeature(data: FeatureInput): Promise> { - const validated = this.validateOrFail(data); - return this.repository.create(validated); - } - - // Private helpers last, prefixed with underscore - private validateOrFail(data: unknown): FeatureInput { - // Validation logic - } -} -\`\`\` - -## Testing Standards - -### Test Naming -- Test files: [module].test.{ts|py} or test_[module].py -- Test names: test_should_[expected_behavior]_when_[condition] -- Use @ai-coverage to link tests to code - -### Test Organization -\`\`\`python -class TestFeatureService: - """ - @ai-coverage FeatureService - @ai-fixtures tests/fixtures/features/ - """ - - def test_should_create_feature_when_data_valid(self): - # Arrange → Act → Assert pattern - pass - - def test_should_reject_when_missing_required_fields(self): - pass -\`\`\` - -## Git Commit Standards -- Format: "type(scope): description" -- Types: feat, fix, docs, refactor, test, chore -- Include "AI-CONTEXT:" line for significant changes - -## Error Handling -- Use Result pattern or explicit error types -- Never catch and ignore errors silently -- Log errors with context at catch point -- Throw early, catch late - -## Performance Guidelines -- Document Big-O complexity for algorithms -- Add benchmarks for critical paths -- Use caching for expensive computations -- Profile before optimizing -.ai/context.json -json{ - "version": "1.0.0", - "project_type": "[web-api|cli-tool|library|full-stack-app]", - "language": "[typescript|python|javascript]", - "context_budget": { - "always_load": [ - "AI_README.md", - "CONVENTIONS.md" - ], - "essential_paths": [ - "src/core/", - "src/index.{ts|py}" - ], - "conditional": { - "if_modifying_auth": [ - "src/features/auth/", - "docs/auth/", - "src/core/security/" - ], - "if_debugging": [ - "src/core/logging/", - "TROUBLESHOOTING.md", - "tests/" - ], - "if_adding_feature": [ - "templates/feature/", - "src/features/README.md", - "CONVENTIONS.md#structure-patterns" - ], - "if_modifying_database": [ - "src/core/database/", - "migrations/", - "docs/database/" - ] - } - }, - "common_tasks": { - "add_endpoint": { - "template": "templates/endpoint.template", - "checklist": [ - "Define route in router", - "Add request/response types", - "Implement handler", - "Add validation", - "Write tests", - "Update API documentation" - ], - "examples": "tests/examples/endpoints/" - }, - "add_feature": { - "template": "templates/feature/", - "checklist": [ - "Create feature folder", - "Define types", - "Implement service", - "Add repository if needed", - "Write tests", - "Update dependencies.yaml", - "Add to feature index" - ] - }, - "debug_issue": { - "start_with": [ - "Check TROUBLESHOOTING.md", - "Review recent CHANGELOG_AI.md", - "Check logs in src/core/logging", - "Run relevant tests" - ] - } - }, - "danger_zones": { - "src/core/security/": { - "warning": "Security-critical code", - "required_review": true, - "checklist": [ - "Run security tests", - "Check for credential exposure", - "Verify input validation" - ] - }, - "migrations/": { - "warning": "Database schema changes", - "required_review": true, - "checklist": [ - "Test rollback procedure", - "Verify data preservation", - "Check migration order" - ] - }, - "src/core/config/": { - "warning": "System configuration", - "checklist": [ - "Update .env.example", - "Check all environments", - "Verify defaults are safe" - ] - } - }, - "dependencies": { - "external": { - "critical": ["framework", "database", "auth-library"], - "optional": ["monitoring", "analytics"] - }, - "internal": { - "most_imported": ["src/core/logger", "src/shared/types"], - "most_dependent": ["src/core/database", "src/core/config"] - } - }, - "testing": { - "test_command": "npm test | pytest", - "coverage_threshold": 80, - "critical_paths": [ - "src/core/security", - "src/core/database", - "src/features/auth" - ] - }, - "performance": { - "bottlenecks": [], - "optimization_notes": [], - "benchmarks": "tests/benchmarks/" - } -} -.ai/dependencies.yaml -yaml# Module Dependency Graph -# Format: module -> depends_on, consumed_by, impact_level - -version: 1.0.0 -last_updated: [DATE] - -modules: - core/config: - depends_on: [] - consumed_by: ["*"] # Everything depends on config - critical: true - modification_impact: "high - requires full system test" - test_command: "npm test core/config" - - core/database: - depends_on: ["core/config", "core/logging"] - consumed_by: ["features/*", "migrations"] - critical: true - modification_impact: "high - affects all data operations" - test_command: "npm test core/database" - - core/logging: - depends_on: ["core/config"] - consumed_by: ["*"] - critical: true - modification_impact: "medium - affects debugging capability" - test_command: "npm test core/logging" - - core/security: - depends_on: ["core/config", "core/database", "core/logging"] - consumed_by: ["features/auth", "api/middleware"] - critical: true - modification_impact: "critical - security review required" - test_command: "npm test core/security" - - features/[example]: - depends_on: ["core/*", "shared/utils"] - consumed_by: ["api/routes/[example]"] - critical: false - modification_impact: "low - isolated feature" - test_command: "npm test features/[example]" - -# Dependency rules -rules: - - "Features cannot depend on other features" - - "Core modules cannot depend on features" - - "Shared utilities cannot depend on core or features" - - "All modules must depend on core/config" - -# Import cycles to avoid -forbidden_cycles: - - ["core/database", "core/security", "core/database"] - -# High-risk modification paths -high_risk_paths: - - description: "Authentication flow" - modules: ["core/security", "features/auth", "api/middleware/auth"] - test_suite: "npm test:auth:full" - - - description: "Data persistence layer" - modules: ["core/database", "migrations", "features/*/repository"] - test_suite: "npm test:database:full" -3. Create Template Files -templates/feature/index.template -typescript/** - * @ai-summary [Feature description in one line] - * @ai-details docs/features/[feature-name].md - * @critical-context [Any critical information] - */ - -export * from './types'; -export * from './service'; -export { [FeatureName]Repository } from './repository'; -templates/feature/service.template -typescript/** - * @ai-summary Handles [feature] business logic - * @ai-examples tests/examples/[feature]/ - */ -export class [FeatureName]Service { - constructor( - private repository: [FeatureName]Repository, - private logger: Logger - ) {} - - /** - * HANDLES: [What it processes] - * THROWS: [Error types] - * SIDE_EFFECTS: [External changes] - */ - async create[FeatureName]( - data: [FeatureName]Input - ): Promise> { - // Validate -> Process -> Persist -> Return - const validated = this.validateOrFail(data); - const processed = this.processBusinessRules(validated); - return this.repository.create(processed); - } - - private validateOrFail(data: unknown): [FeatureName]Input { - // Validation logic - throw new Error('Not implemented'); - } - - private processBusinessRules(data: [FeatureName]Input): [FeatureName]Data { - // Business logic transformations - throw new Error('Not implemented'); - } -} -4. Project-Specific Setup Commands -Based on the project type, run these initialization commands: -bash# For TypeScript projects -npm init -y -npm install --save-dev typescript @types/node prettier eslint -npx tsc --init - -# For Python projects -python -m venv venv -pip install black pytest mypy pylint - -# Initialize git with proper ignores -git init -echo "node_modules/\nvenv/\n.env\n*.log\n.DS_Store" > .gitignore - -# Create initial environment file -echo "# Development Environment Variables\nNODE_ENV=development\nLOG_LEVEL=debug" > .env.example -5. Initial CHANGELOG_AI.md Entry -markdown# AI-Focused Changelog - -## [Date] - Project Initialization -**Created**: Initial project structure -**Architecture**: [Chosen architecture pattern] -**Key Technologies**: [List main technologies] -**AI Context**: -- Project follows AI-optimized documentation patterns -- Self-describing code structure implemented -- Progressive disclosure documentation in place -**Entry Points**: -- Start with AI_README.md -- Conventions in CONVENTIONS.md -- Navigation via PROJECT_MAP.md -Instructions for Claude Code -When creating a new project with these standards: - -Start by asking the user: - -Project type (API, CLI tool, web app, library) -Primary language (TypeScript, Python, JavaScript) -Key features needed initially -Any specific frameworks or libraries required - - -Then systematically: - -Create the complete directory structure -Generate all foundation files with project-specific content -Set up the .ai/ metadata directory -Create initial templates based on project type -Initialize version control and package management -Write the first entry in CHANGELOG_AI.md - - -Customize based on project type: - -API: Add OpenAPI specs, route templates, middleware patterns -CLI: Add command templates, argument parsing patterns -Library: Add export patterns, public API documentation -Full-stack: Add frontend/backend separation, API contracts - - -For each file you create: - -Ensure it follows the self-documenting principles -Add appropriate @ai-* annotations -Include type hints/interfaces -Create corresponding test file structure -Update .ai/context.json with new paths - - -Before completing setup: - -Verify all imports resolve correctly -Ensure templates have clear replacement markers -Check that navigation paths in documentation are accurate -Confirm .ai/dependencies.yaml reflects actual structure -Test that the project runs with minimal setup - - -Final output should include: - -Complete file structure -All content for foundation files -Setup instructions in a setup.md -Initial test file examples -A "Getting Started for AI" section in AI_README.md - - - -Remember: The goal is to create a codebase that another AI can understand and modify efficiently with minimal context loading. Every decision should optimize for clarity and discoverability. -Example First Response -When you run this setup, respond with: -I'll create an AI-optimized project structure for your [project type]. This setup will follow AI-to-AI communication patterns for maximum clarity and minimal context usage. - -## Project Configuration -- Type: [selected type] -- Language: [selected language] -- Architecture: [chosen pattern] - -## Creating Structure... -[Show the directory tree being created] - -## Generating Foundation Files... -[List each file as it's created] - -## Project Ready! - -### Next Steps for AI Interaction: -1. Read AI_README.md for system overview -2. Check CONVENTIONS.md before making changes -3. Use PROJECT_MAP.md for navigation -4. Refer to .ai/context.json for task-specific guidance - -### For Human Developers: -1. Run: [setup command] -2. Configure: Copy .env.example to .env -3. Install: [package manager] install -4. Test: [test command] - -The project is now optimized for AI maintenance and collaboration! \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 46b5afe..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,3 +0,0 @@ -# Load Application Architecture - -Read the file @docs/architecture.md to understand the appliaction architecture. \ No newline at end of file diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs deleted file mode 100644 index c2963c4..0000000 --- a/Controllers/APIController.cs +++ /dev/null @@ -1,1815 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Logic; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; - -namespace MotoVaultPro.Controllers -{ - [Authorize] - public class APIController : Controller - { - private readonly IVehicleDataAccess _dataAccess; - private readonly INoteDataAccess _noteDataAccess; - private readonly IServiceRecordDataAccess _serviceRecordDataAccess; - private readonly IGasRecordDataAccess _gasRecordDataAccess; - private readonly ITaxRecordDataAccess _taxRecordDataAccess; - private readonly IReminderRecordDataAccess _reminderRecordDataAccess; - private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; - private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; - private readonly ISupplyRecordDataAccess _supplyRecordDataAccess; - private readonly IPlanRecordDataAccess _planRecordDataAccess; - private readonly IPlanRecordTemplateDataAccess _planRecordTemplateDataAccess; - private readonly IUserAccessDataAccess _userAccessDataAccess; - private readonly IUserRecordDataAccess _userRecordDataAccess; - private readonly IReminderHelper _reminderHelper; - private readonly IGasHelper _gasHelper; - private readonly IUserLogic _userLogic; - private readonly IVehicleLogic _vehicleLogic; - private readonly IOdometerLogic _odometerLogic; - private readonly IFileHelper _fileHelper; - private readonly IMailHelper _mailHelper; - private readonly IConfigHelper _config; - private readonly IWebHostEnvironment _webEnv; - public APIController(IVehicleDataAccess dataAccess, - IGasHelper gasHelper, - IReminderHelper reminderHelper, - INoteDataAccess noteDataAccess, - IServiceRecordDataAccess serviceRecordDataAccess, - IGasRecordDataAccess gasRecordDataAccess, - ITaxRecordDataAccess taxRecordDataAccess, - IReminderRecordDataAccess reminderRecordDataAccess, - IUpgradeRecordDataAccess upgradeRecordDataAccess, - IOdometerRecordDataAccess odometerRecordDataAccess, - ISupplyRecordDataAccess supplyRecordDataAccess, - IPlanRecordDataAccess planRecordDataAccess, - IPlanRecordTemplateDataAccess planRecordTemplateDataAccess, - IUserAccessDataAccess userAccessDataAccess, - IUserRecordDataAccess userRecordDataAccess, - IMailHelper mailHelper, - IFileHelper fileHelper, - IConfigHelper config, - IUserLogic userLogic, - IVehicleLogic vehicleLogic, - IOdometerLogic odometerLogic, - IWebHostEnvironment webEnv) - { - _dataAccess = dataAccess; - _noteDataAccess = noteDataAccess; - _serviceRecordDataAccess = serviceRecordDataAccess; - _gasRecordDataAccess = gasRecordDataAccess; - _taxRecordDataAccess = taxRecordDataAccess; - _reminderRecordDataAccess = reminderRecordDataAccess; - _upgradeRecordDataAccess = upgradeRecordDataAccess; - _odometerRecordDataAccess = odometerRecordDataAccess; - _supplyRecordDataAccess = supplyRecordDataAccess; - _planRecordDataAccess = planRecordDataAccess; - _planRecordTemplateDataAccess = planRecordTemplateDataAccess; - _userAccessDataAccess = userAccessDataAccess; - _userRecordDataAccess = userRecordDataAccess; - _mailHelper = mailHelper; - _gasHelper = gasHelper; - _reminderHelper = reminderHelper; - _userLogic = userLogic; - _odometerLogic = odometerLogic; - _vehicleLogic = vehicleLogic; - _fileHelper = fileHelper; - _config = config; - _webEnv = webEnv; - } - public IActionResult Index() - { - return View(); - } - private int GetUserID() - { - return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); - } - [HttpGet] - [Route("/api/whoami")] - public IActionResult WhoAmI() - { - var result = new UserExportModel - { - Username = User.FindFirstValue(ClaimTypes.Name), - EmailAddress = User.IsInRole(nameof(UserData.IsRootUser)) ? _config.GetDefaultReminderEmail() : User.FindFirstValue(ClaimTypes.Email), - IsAdmin = User.IsInRole(nameof(UserData.IsAdmin)).ToString(), - IsRoot = User.IsInRole(nameof(UserData.IsRootUser)).ToString() - }; - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - [HttpGet] - [Route("/api/version")] - public async Task ServerVersion(bool checkForUpdate = false) - { - var viewModel = new ReleaseVersion - { - CurrentVersion = StaticHelper.VersionNumber, - LatestVersion = StaticHelper.VersionNumber - }; - if (checkForUpdate) - { - try - { - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("request"); - var releaseResponse = await httpClient.GetFromJsonAsync(StaticHelper.ReleasePath) ?? new ReleaseResponse(); - if (!string.IsNullOrWhiteSpace(releaseResponse.tag_name)) - { - viewModel.LatestVersion = releaseResponse.tag_name; - } - } - catch (Exception ex) - { - return Json(OperationResponse.Failed($"Unable to retrieve latest version from GitHub API: {ex.Message}")); - } - } - return Json(viewModel); - } - [HttpGet] - [Route("/api/vehicles")] - public IActionResult Vehicles() - { - var result = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - result = _userLogic.FilterUserVehicles(result, GetUserID()); - } - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - - [HttpGet] - [Route("/api/vehicle/info")] - public IActionResult VehicleInfo(int vehicleId) - { - //stats for a specific or all vehicles - List vehicles = new List(); - if (vehicleId != default) - { - if (_userLogic.UserCanEditVehicle(GetUserID(), vehicleId)) - { - vehicles.Add(_dataAccess.GetVehicleById(vehicleId)); - } else - { - return new RedirectResult("/Error/Unauthorized"); - } - } else - { - var result = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - result = _userLogic.FilterUserVehicles(result, GetUserID()); - } - vehicles.AddRange(result); - } - - var apiResult = _vehicleLogic.GetVehicleInfo(vehicles); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(apiResult, StaticHelper.GetInvariantOption()); - } - else - { - return Json(apiResult); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/adjustedodometer")] - public IActionResult AdjustedOdometer(int vehicleId, int odometer) - { - var vehicle = _dataAccess.GetVehicleById(vehicleId); - if (vehicle == null || !vehicle.HasOdometerAdjustment) - { - return Json(odometer); - } else - { - var convertedOdometer = (odometer + int.Parse(vehicle.OdometerDifference)) * decimal.Parse(vehicle.OdometerMultiplier); - return Json(convertedOdometer); - } - } - #region PlanRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/planrecords")] - public IActionResult PlanRecords(int vehicleId, MethodParameter parameters) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); - if (parameters.Id != default) - { - vehicleRecords.RemoveAll(x => x.Id != parameters.Id); - } - if (!string.IsNullOrWhiteSpace(parameters.StartDate) && DateTime.TryParse(parameters.StartDate, out DateTime startDate)) - { - vehicleRecords.RemoveAll(x => x.DateCreated < startDate); - } - if (!string.IsNullOrWhiteSpace(parameters.EndDate) && DateTime.TryParse(parameters.EndDate, out DateTime endDate)) - { - vehicleRecords.RemoveAll(x => x.DateCreated > endDate); - } - var result = vehicleRecords.Select(x => new PlanRecordExportModel { - Id = x.Id.ToString(), - DateCreated = x.DateCreated.ToShortDateString(), - DateModified = x.DateModified.ToShortDateString(), - Description = x.Description, - Cost = x.Cost.ToString(), - Notes = x.Notes, - Type = x.ImportMode.ToString(), - Priority = x.Priority.ToString(), - Progress = x.Progress.ToString(), - ExtraFields = x.ExtraFields, - Files = x.Files }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/planrecords/add")] - [Consumes("application/json")] - public IActionResult AddPlanRecordJson(int vehicleId, [FromBody] PlanRecordExportModel input) => AddPlanRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/planrecords/add")] - public IActionResult AddPlanRecord(int vehicleId, PlanRecordExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Cost) || - string.IsNullOrWhiteSpace(input.Type) || - string.IsNullOrWhiteSpace(input.Priority) || - string.IsNullOrWhiteSpace(input.Progress)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Description, Cost, Type, Priority, and Progress cannot be empty.")); - } - bool validType = Enum.TryParse(input.Type, out ImportMode parsedType); - bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority); - bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress); - if (!validType || !validPriority || !validProgress) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid.")); - } - if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.UpgradeRecord) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord or UpgradeRecord")); - } - if (parsedProgress == PlanProgress.Done) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done.")); - } - //hardening - turns null values for List types into empty lists. - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - var planRecord = new PlanRecord() - { - VehicleId = vehicleId, - DateCreated = DateTime.Now, - DateModified = DateTime.Now, - Description = input.Description, - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Cost = decimal.Parse(input.Cost), - ImportMode = parsedType, - Priority = parsedPriority, - Progress = parsedProgress, - ExtraFields = input.ExtraFields, - Files = input.Files - }; - _planRecordDataAccess.SavePlanRecordToVehicle(planRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord, "planrecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Plan Record Added")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/planrecords/delete")] - public IActionResult DeletePlanRecord(int id) - { - var existingRecord = _planRecordDataAccess.GetPlanRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - //restore any requisitioned supplies. - if (existingRecord.RequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); - } - var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result, "Plan Record Deleted")); - } - [HttpPut] - [Route("/api/vehicle/planrecords/update")] - [Consumes("application/json")] - public IActionResult UpdatePlanRecordJson([FromBody] PlanRecordExportModel input) => UpdatePlanRecord(input); - [HttpPut] - [Route("/api/vehicle/planrecords/update")] - public IActionResult UpdatePlanRecord(PlanRecordExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Cost) || - string.IsNullOrWhiteSpace(input.Type) || - string.IsNullOrWhiteSpace(input.Priority) || - string.IsNullOrWhiteSpace(input.Progress)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Description, Cost, Type, Priority, and Progress cannot be empty.")); - } - bool validType = Enum.TryParse(input.Type, out ImportMode parsedType); - bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority); - bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress); - if (!validType || !validPriority || !validProgress) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid.")); - } - if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.UpgradeRecord) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord or UpgradeRecord")); - } - if (parsedProgress == PlanProgress.Done) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - //retrieve existing record - var existingRecord = _planRecordDataAccess.GetPlanRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.DateModified = DateTime.Now; - existingRecord.Description = input.Description; - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.Cost = decimal.Parse(input.Cost); - existingRecord.ImportMode = parsedType; - existingRecord.Priority = parsedPriority; - existingRecord.Progress = parsedProgress; - existingRecord.Files = input.Files; - existingRecord.ExtraFields = input.ExtraFields; - _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.update.api", User.Identity.Name)); - } - else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Plan Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - #endregion - #region ServiceRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/servicerecords")] - public IActionResult ServiceRecords(int vehicleId, MethodParameter parameters) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - if (parameters.Id != default) - { - vehicleRecords.RemoveAll(x => x.Id != parameters.Id); - } - if (!string.IsNullOrWhiteSpace(parameters.StartDate) && DateTime.TryParse(parameters.StartDate, out DateTime startDate)) - { - vehicleRecords.RemoveAll(x => x.Date < startDate); - } - if (!string.IsNullOrWhiteSpace(parameters.EndDate) && DateTime.TryParse(parameters.EndDate, out DateTime endDate)) - { - vehicleRecords.RemoveAll(x => x.Date > endDate); - } - if (!string.IsNullOrWhiteSpace(parameters.Tags)) - { - var tagsFilter = parameters.Tags.Split(' ').Distinct(); - vehicleRecords.RemoveAll(x => !x.Tags.Any(y => tagsFilter.Contains(y))); - } - var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } else - { - return Json(result); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/servicerecords/add")] - [Consumes("application/json")] - public IActionResult AddServiceRecordJson(int vehicleId, [FromBody] GenericRecordExportModel input) => AddServiceRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/servicerecords/add")] - public IActionResult AddServiceRecord(int vehicleId, GenericRecordExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Odometer) || - string.IsNullOrWhiteSpace(input.Cost)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - var serviceRecord = new ServiceRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Mileage = int.Parse(input.Odometer), - Description = input.Description, - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Cost = decimal.Parse(input.Cost), - ExtraFields = input.ExtraFields, - Files = input.Files, - Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList() - }; - _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord); - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - var odometerRecord = new OdometerRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Mileage = int.Parse(input.Odometer) - }; - _odometerLogic.AutoInsertOdometerRecord(odometerRecord); - } - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(serviceRecord, "servicerecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Service Record Added")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/servicerecords/delete")] - public IActionResult DeleteServiceRecord(int id) - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - //restore any requisitioned supplies. - if (existingRecord.RequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); - } - var result = _serviceRecordDataAccess.DeleteServiceRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "servicerecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result, "Service Record Deleted")); - } - [HttpPut] - [Route("/api/vehicle/servicerecords/update")] - [Consumes("application/json")] - public IActionResult UpdateServiceRecordJson([FromBody] GenericRecordExportModel input) => UpdateServiceRecord(input); - [HttpPut] - [Route("/api/vehicle/servicerecords/update")] - public IActionResult UpdateServiceRecord(GenericRecordExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Odometer) || - string.IsNullOrWhiteSpace(input.Cost)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - //retrieve existing record - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.Date = DateTime.Parse(input.Date); - existingRecord.Mileage = int.Parse(input.Odometer); - existingRecord.Description = input.Description; - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.Cost = decimal.Parse(input.Cost); - existingRecord.Files = input.Files; - existingRecord.ExtraFields = input.ExtraFields; - existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList(); - _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "servicerecord.update.api", User.Identity.Name)); - } else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Service Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - #endregion - #region UpgradeRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/upgraderecords")] - public IActionResult UpgradeRecords(int vehicleId, MethodParameter parameters) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - if (parameters.Id != default) - { - vehicleRecords.RemoveAll(x => x.Id != parameters.Id); - } - if (!string.IsNullOrWhiteSpace(parameters.StartDate) && DateTime.TryParse(parameters.StartDate, out DateTime startDate)) - { - vehicleRecords.RemoveAll(x => x.Date < startDate); - } - if (!string.IsNullOrWhiteSpace(parameters.EndDate) && DateTime.TryParse(parameters.EndDate, out DateTime endDate)) - { - vehicleRecords.RemoveAll(x => x.Date > endDate); - } - if (!string.IsNullOrWhiteSpace(parameters.Tags)) - { - var tagsFilter = parameters.Tags.Split(' ').Distinct(); - vehicleRecords.RemoveAll(x => !x.Tags.Any(y => tagsFilter.Contains(y))); - } - var result = vehicleRecords.Select(x => new GenericRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString(), ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/upgraderecords/add")] - [Consumes("application/json")] - public IActionResult AddUpgradeRecordJson(int vehicleId, [FromBody] GenericRecordExportModel input) => AddUpgradeRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/upgraderecords/add")] - public IActionResult AddUpgradeRecord(int vehicleId, GenericRecordExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Odometer) || - string.IsNullOrWhiteSpace(input.Cost)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - var upgradeRecord = new UpgradeRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Mileage = int.Parse(input.Odometer), - Description = input.Description, - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Cost = decimal.Parse(input.Cost), - ExtraFields = input.ExtraFields, - Files = input.Files, - Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList() - }; - _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord); - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - var odometerRecord = new OdometerRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Mileage = int.Parse(input.Odometer) - }; - _odometerLogic.AutoInsertOdometerRecord(odometerRecord); - } - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(upgradeRecord, "upgraderecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Upgrade Record Added")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/upgraderecords/delete")] - public IActionResult DeleteUpgradeRecord(int id) - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - //restore any requisitioned supplies. - if (existingRecord.RequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); - } - var result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "upgraderecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result,"Upgrade Record Deleted")); - } - [HttpPut] - [Route("/api/vehicle/upgraderecords/update")] - [Consumes("application/json")] - public IActionResult UpdateUpgradeRecordJson([FromBody] GenericRecordExportModel input) => UpdateUpgradeRecord(input); - [HttpPut] - [Route("/api/vehicle/upgraderecords/update")] - public IActionResult UpdateUpgradeRecord(GenericRecordExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Odometer) || - string.IsNullOrWhiteSpace(input.Cost)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - //retrieve existing record - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.Date = DateTime.Parse(input.Date); - existingRecord.Mileage = int.Parse(input.Odometer); - existingRecord.Description = input.Description; - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.Cost = decimal.Parse(input.Cost); - existingRecord.ExtraFields = input.ExtraFields; - existingRecord.Files = input.Files; - existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList(); - _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "upgraderecord.update.api", User.Identity.Name)); - } - else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Upgrade Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - #endregion - #region TaxRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/taxrecords")] - public IActionResult TaxRecords(int vehicleId, MethodParameter parameters) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - if (parameters.Id != default) - { - vehicleRecords.RemoveAll(x => x.Id != parameters.Id); - } - if (!string.IsNullOrWhiteSpace(parameters.StartDate) && DateTime.TryParse(parameters.StartDate, out DateTime startDate)) - { - vehicleRecords.RemoveAll(x => x.Date < startDate); - } - if (!string.IsNullOrWhiteSpace(parameters.EndDate) && DateTime.TryParse(parameters.EndDate, out DateTime endDate)) - { - vehicleRecords.RemoveAll(x => x.Date > endDate); - } - if (!string.IsNullOrWhiteSpace(parameters.Tags)) - { - var tagsFilter = parameters.Tags.Split(' ').Distinct(); - vehicleRecords.RemoveAll(x => !x.Tags.Any(y => tagsFilter.Contains(y))); - } - var result = vehicleRecords.Select(x => new TaxRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - [HttpGet] - [Route("/api/vehicle/taxrecords/check")] - public IActionResult CheckRecurringTaxRecords() - { - List vehicles = new List(); - try - { - var result = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - result = _userLogic.FilterUserVehicles(result, GetUserID()); - } - vehicles.AddRange(result); - int vehiclesUpdated = 0; - foreach(Vehicle vehicle in vehicles) - { - var updateResult = _vehicleLogic.UpdateRecurringTaxes(vehicle.Id); - if (updateResult) - { - vehiclesUpdated++; - } - } - if (vehiclesUpdated != default) - { - return Json(OperationResponse.Succeed($"Recurring Taxes for {vehiclesUpdated} Vehicles Updated!")); - } else - { - return Json(OperationResponse.Succeed("No Recurring Taxes Updated")); - } - } - catch (Exception ex) - { - return Json(OperationResponse.Failed($"No Recurring Taxes Updated Due To Error: {ex.Message}")); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/taxrecords/add")] - [Consumes("application/json")] - public IActionResult AddTaxRecordJson(int vehicleId, [FromBody] TaxRecordExportModel input) => AddTaxRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/taxrecords/add")] - public IActionResult AddTaxRecord(int vehicleId, TaxRecordExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Cost)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - var taxRecord = new TaxRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Description = input.Description, - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Cost = decimal.Parse(input.Cost), - ExtraFields = input.ExtraFields, - Files = input.Files, - Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList() - }; - _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord); - _vehicleLogic.UpdateRecurringTaxes(vehicleId); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(taxRecord, "taxrecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Tax Record Added")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/taxrecords/delete")] - public IActionResult DeleteTaxRecord(int id) - { - var existingRecord = _taxRecordDataAccess.GetTaxRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - var result = _taxRecordDataAccess.DeleteTaxRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(existingRecord, "taxrecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result, "Tax Record Deleted")); - } - [HttpPut] - [Route("/api/vehicle/taxrecords/update")] - [Consumes("application/json")] - public IActionResult UpdateTaxRecordJson([FromBody] TaxRecordExportModel input) => UpdateTaxRecord(input); - [HttpPut] - [Route("/api/vehicle/taxrecords/update")] - public IActionResult UpdateTaxRecord(TaxRecordExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Cost)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - //retrieve existing record - var existingRecord = _taxRecordDataAccess.GetTaxRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.Date = DateTime.Parse(input.Date); - existingRecord.Description = input.Description; - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.Cost = decimal.Parse(input.Cost); - existingRecord.ExtraFields = input.ExtraFields; - existingRecord.Files = input.Files; - existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList(); - _taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(existingRecord, "taxrecord.update.api", User.Identity.Name)); - } - else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Tax Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - #endregion - #region OdometerRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/odometerrecords/latest")] - public IActionResult LastOdometer(int vehicleId) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var result = _vehicleLogic.GetMaxMileage(vehicleId); - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/odometerrecords")] - public IActionResult OdometerRecords(int vehicleId, MethodParameter parameters) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - //determine if conversion is needed. - if (vehicleRecords.All(x => x.InitialMileage == default)) - { - vehicleRecords = _odometerLogic.AutoConvertOdometerRecord(vehicleRecords); - } - if (parameters.Id != default) - { - vehicleRecords.RemoveAll(x => x.Id != parameters.Id); - } - if (!string.IsNullOrWhiteSpace(parameters.StartDate) && DateTime.TryParse(parameters.StartDate, out DateTime startDate)) - { - vehicleRecords.RemoveAll(x => x.Date < startDate); - } - if (!string.IsNullOrWhiteSpace(parameters.EndDate) && DateTime.TryParse(parameters.EndDate, out DateTime endDate)) - { - vehicleRecords.RemoveAll(x => x.Date > endDate); - } - if (!string.IsNullOrWhiteSpace(parameters.Tags)) - { - var tagsFilter = parameters.Tags.Split(' ').Distinct(); - vehicleRecords.RemoveAll(x => !x.Tags.Any(y => tagsFilter.Contains(y))); - } - var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Id = x.Id.ToString(), Date = x.Date.ToShortDateString(), InitialOdometer = x.InitialMileage.ToString(), Odometer = x.Mileage.ToString(), Notes = x.Notes, ExtraFields = x.ExtraFields, Files = x.Files, Tags = string.Join(' ', x.Tags) }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/odometerrecords/add")] - [Consumes("application/json")] - public IActionResult AddOdometerRecordJson(int vehicleId, [FromBody] OdometerRecordExportModel input) => AddOdometerRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/odometerrecords/add")] - public IActionResult AddOdometerRecord(int vehicleId, OdometerRecordExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Odometer)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - var odometerRecord = new OdometerRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - InitialMileage = (string.IsNullOrWhiteSpace(input.InitialOdometer) || int.Parse(input.InitialOdometer) == default) ? _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List()) : int.Parse(input.InitialOdometer), - Mileage = int.Parse(input.Odometer), - ExtraFields = input.ExtraFields, - Files = input.Files, - Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList() - }; - _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(odometerRecord, "odometerrecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Odometer Record Added")); - } catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/odometerrecords/delete")] - public IActionResult DeleteOdometerRecord(int id) - { - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - var result = _odometerRecordDataAccess.DeleteOdometerRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(existingRecord, "odometerrecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result, "Odometer Record Deleted")); - } - [HttpPut] - [Route("/api/vehicle/odometerrecords/update")] - [Consumes("application/json")] - public IActionResult UpdateOdometerRecordJson([FromBody] OdometerRecordExportModel input) => UpdateOdometerRecord(input); - [HttpPut] - [Route("/api/vehicle/odometerrecords/update")] - public IActionResult UpdateOdometerRecord(OdometerRecordExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.InitialOdometer) || - string.IsNullOrWhiteSpace(input.Odometer)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - //retrieve existing record - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.Date = DateTime.Parse(input.Date); - existingRecord.Mileage = int.Parse(input.Odometer); - existingRecord.InitialMileage = int.Parse(input.InitialOdometer); - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.ExtraFields = input.ExtraFields; - existingRecord.Files = input.Files; - existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList(); - _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(existingRecord, "odometerrecord.update.api", User.Identity.Name)); - } - else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Odometer Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - #endregion - #region GasRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/gasrecords")] - public IActionResult GasRecords(int vehicleId, MethodParameter parameters) - { - if (vehicleId == default) - { - var response = OperationResponse.Failed("Must provide a valid vehicle id"); - Response.StatusCode = 400; - return Json(response); - } - var rawVehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - var vehicleRecords = _gasHelper.GetGasRecordViewModels(rawVehicleRecords, parameters.UseMPG, parameters.UseUKMPG); - if (parameters.Id != default) - { - vehicleRecords.RemoveAll(x => x.Id != parameters.Id); - } - if (!string.IsNullOrWhiteSpace(parameters.StartDate) && DateTime.TryParse(parameters.StartDate, out DateTime startDate)) - { - vehicleRecords.RemoveAll(x => DateTime.Parse(x.Date) < startDate); - } - if (!string.IsNullOrWhiteSpace(parameters.EndDate) && DateTime.TryParse(parameters.EndDate, out DateTime endDate)) - { - vehicleRecords.RemoveAll(x => DateTime.Parse(x.Date) > endDate); - } - if (!string.IsNullOrWhiteSpace(parameters.Tags)) - { - var tagsFilter = parameters.Tags.Split(' ').Distinct(); - vehicleRecords.RemoveAll(x => !x.Tags.Any(y => tagsFilter.Contains(y))); - } - var result = vehicleRecords - .Select(x => new GasRecordExportModel { - Id = x.Id.ToString(), - Date = x.Date, - Odometer = x.Mileage.ToString(), - Cost = x.Cost.ToString(), - FuelConsumed = x.Gallons.ToString(), - FuelEconomy = x.MilesPerGallon.ToString(), - IsFillToFull = x.IsFillToFull.ToString(), - MissedFuelUp = x.MissedFuelUp.ToString(), - Notes = x.Notes, - ExtraFields = x.ExtraFields, - Files = x.Files, - Tags = string.Join(' ', x.Tags) - }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(result, StaticHelper.GetInvariantOption()); - } - else - { - return Json(result); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/gasrecords/add")] - [Consumes("application/json")] - public IActionResult AddGasRecordJson(int vehicleId, [FromBody] GasRecordExportModel input) => AddGasRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/gasrecords/add")] - public IActionResult AddGasRecord(int vehicleId, GasRecordExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Odometer) || - string.IsNullOrWhiteSpace(input.FuelConsumed) || - string.IsNullOrWhiteSpace(input.Cost) || - string.IsNullOrWhiteSpace(input.IsFillToFull) || - string.IsNullOrWhiteSpace(input.MissedFuelUp) - ) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - var gasRecord = new GasRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Mileage = int.Parse(input.Odometer), - Gallons = decimal.Parse(input.FuelConsumed), - IsFillToFull = bool.Parse(input.IsFillToFull), - MissedFuelUp = bool.Parse(input.MissedFuelUp), - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Cost = decimal.Parse(input.Cost), - ExtraFields = input.ExtraFields, - Files = input.Files, - Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList() - }; - _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord); - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - var odometerRecord = new OdometerRecord() - { - VehicleId = vehicleId, - Date = DateTime.Parse(input.Date), - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Mileage = int.Parse(input.Odometer) - }; - _odometerLogic.AutoInsertOdometerRecord(odometerRecord); - } - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(gasRecord, "gasrecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Gas Record Added")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/gasrecords/delete")] - public IActionResult DeleteGasRecord(int id) - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - var result = _gasRecordDataAccess.DeleteGasRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result, "Gas Record Deleted")); - } - [HttpPut] - [Route("/api/vehicle/gasrecords/update")] - [Consumes("application/json")] - public IActionResult UpdateGasRecordJson([FromBody] GasRecordExportModel input) => UpdateGasRecord(input); - [HttpPut] - [Route("/api/vehicle/gasrecords/update")] - public IActionResult UpdateGasRecord(GasRecordExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Date) || - string.IsNullOrWhiteSpace(input.Odometer) || - string.IsNullOrWhiteSpace(input.FuelConsumed) || - string.IsNullOrWhiteSpace(input.Cost) || - string.IsNullOrWhiteSpace(input.IsFillToFull) || - string.IsNullOrWhiteSpace(input.MissedFuelUp)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty.")); - } - if (input.Files == null) - { - input.Files = new List(); - } - if (input.ExtraFields == null) - { - input.ExtraFields = new List(); - } - try - { - //retrieve existing record - var existingRecord = _gasRecordDataAccess.GetGasRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.Date = DateTime.Parse(input.Date); - existingRecord.Mileage = int.Parse(input.Odometer); - existingRecord.Gallons = decimal.Parse(input.FuelConsumed); - existingRecord.IsFillToFull = bool.Parse(input.IsFillToFull); - existingRecord.MissedFuelUp = bool.Parse(input.MissedFuelUp); - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.Cost = decimal.Parse(input.Cost); - existingRecord.ExtraFields = input.ExtraFields; - existingRecord.Files = input.Files; - existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList(); - _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.update.api", User.Identity.Name)); - } - else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Gas Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - #endregion - #region ReminderRecord - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - [Route("/api/vehicle/reminders")] - public IActionResult Reminders(int vehicleId) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId); - var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); - var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderAPIExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), UserMetric = x.UserMetric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), DueDays = x.DueDays.ToString(), DueDistance = x.DueMileage.ToString(), Tags = string.Join(' ', x.Tags) }); - if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) - { - return Json(results, StaticHelper.GetInvariantOption()); - } - else - { - return Json(results); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/reminders/add")] - [Consumes("application/json")] - public IActionResult AddReminderRecordJson(int vehicleId, [FromBody] ReminderExportModel input) => AddReminderRecord(vehicleId, input); - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - [Route("/api/vehicle/reminders/add")] - public IActionResult AddReminderRecord(int vehicleId, ReminderExportModel input) - { - if (vehicleId == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Must provide a valid vehicle id")); - } - if (string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Metric)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Description and Metric cannot be empty.")); - } - bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric); - bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate); - bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer); - if (!validMetric) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid.")); - } - //validate metrics - switch (parsedMetric) - { - case ReminderMetric.Both: - //validate due date and odometer - if (!validDate || !validOdometer) - { - return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both")); - } - break; - case ReminderMetric.Date: - if (!validDate) - { - return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date")); - } - break; - case ReminderMetric.Odometer: - if (!validOdometer) - { - return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer")); - } - break; - } - try - { - var reminderRecord = new ReminderRecord() - { - VehicleId = vehicleId, - Description = input.Description, - Mileage = parsedOdometer, - Date = parsedDate, - Metric = parsedMetric, - Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, - Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList() - }; - _reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(reminderRecord, "reminderrecord.add.api", User.Identity.Name)); - return Json(OperationResponse.Succeed("Reminder Record Added")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpPut] - [Route("/api/vehicle/reminders/update")] - [Consumes("application/json")] - public IActionResult UpdateReminderRecordJson([FromBody] ReminderExportModel input) => UpdateReminderRecord(input); - [HttpPut] - [Route("/api/vehicle/reminders/update")] - public IActionResult UpdateReminderRecord(ReminderExportModel input) - { - if (string.IsNullOrWhiteSpace(input.Id) || - string.IsNullOrWhiteSpace(input.Description) || - string.IsNullOrWhiteSpace(input.Metric)) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, Id, Description and Metric cannot be empty.")); - } - bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric); - bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate); - bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer); - if (!validMetric) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid.")); - } - //validate metrics - switch (parsedMetric) - { - case ReminderMetric.Both: - //validate due date and odometer - if (!validDate || !validOdometer) - { - return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both")); - } - break; - case ReminderMetric.Date: - if (!validDate) - { - return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date")); - } - break; - case ReminderMetric.Odometer: - if (!validOdometer) - { - return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer")); - } - break; - } - try - { - //retrieve existing record - var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(int.Parse(input.Id)); - if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) - { - //check if user has access to the vehicleId - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - existingRecord.Date = parsedDate; - existingRecord.Mileage = parsedOdometer; - existingRecord.Description = input.Description; - existingRecord.Metric = parsedMetric; - existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; - existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List() : input.Tags.Split(' ').Distinct().ToList(); - _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.update.api", User.Identity.Name)); - } - else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - return Json(OperationResponse.Succeed("Reminder Record Updated")); - } - catch (Exception ex) - { - Response.StatusCode = 500; - return Json(OperationResponse.Failed(ex.Message)); - } - } - [HttpDelete] - [Route("/api/vehicle/reminders/delete")] - public IActionResult DeleteReminderRecord(int id) - { - var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(id); - if (existingRecord == null || existingRecord.Id == default) - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("Invalid Record Id")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - Response.StatusCode = 401; - return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); - } - var result = _reminderRecordDataAccess.DeleteReminderRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.delete.api", User.Identity.Name)); - } - return Json(OperationResponse.Conditional(result, "Reminder Record Deleted")); - } - [HttpGet] - [Route("/api/calendar")] - public IActionResult Calendar() - { - var vehiclesStored = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID()); - } - var reminders = _vehicleLogic.GetReminders(vehiclesStored, true); - var calendarContent = StaticHelper.RemindersToCalendar(reminders); - return File(calendarContent, "text/calendar"); - } - #endregion - [HttpPost] - [Route("/api/documents/upload")] - public IActionResult UploadDocument(List documents) - { - if (documents.Any()) - { - List uploadedFiles = new List(); - string uploadDirectory = "documents/"; - string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory); - if (!Directory.Exists(uploadPath)) - Directory.CreateDirectory(uploadPath); - foreach (IFormFile document in documents) - { - string fileName = Guid.NewGuid() + Path.GetExtension(document.FileName); - string filePath = Path.Combine(uploadPath, fileName); - using (var stream = System.IO.File.Create(filePath)) - { - document.CopyTo(stream); - } - uploadedFiles.Add(new UploadedFiles - { - Location = Path.Combine("/", uploadDirectory, fileName), - Name = Path.GetFileName(document.FileName) - }); - } - return Json(uploadedFiles); - } else - { - Response.StatusCode = 400; - return Json(OperationResponse.Failed("No files to upload")); - } - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - [Route("/api/vehicle/reminders/send")] - public IActionResult SendReminders(List urgencies) - { - if (!urgencies.Any()) - { - //if no urgencies parameter, we will default to all urgencies. - urgencies = new List { ReminderUrgency.NotUrgent, ReminderUrgency.Urgent, ReminderUrgency.VeryUrgent, ReminderUrgency.PastDue }; - } - var vehicles = _dataAccess.GetVehicles(); - List operationResponses = new List(); - var defaultEmailAddress = _config.GetDefaultReminderEmail(); - foreach(Vehicle vehicle in vehicles) - { - var vehicleId = vehicle.Id; - //get reminders - var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId); - var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); - var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).OrderByDescending(x => x.Urgency).ToList(); - results.RemoveAll(x => !urgencies.Contains(x.Urgency)); - if (!results.Any()) - { - continue; - } - //get list of recipients. - var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId); - List emailRecipients = new List(); - if (!string.IsNullOrWhiteSpace(defaultEmailAddress)) - { - emailRecipients.Add(defaultEmailAddress); - } - foreach (int userId in userIds) - { - var userData = _userRecordDataAccess.GetUserRecordById(userId); - emailRecipients.Add(userData.EmailAddress); - }; - if (!emailRecipients.Any()) - { - continue; - } - var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results); - operationResponses.Add(result); - } - if (!operationResponses.Any()) - { - return Json(OperationResponse.Failed("No Emails Sent, No Vehicles Available or No Recipients Configured")); - } - else if (operationResponses.All(x => x.Success)) - { - return Json(OperationResponse.Succeed($"Emails Sent({operationResponses.Count()})")); - } else if (operationResponses.All(x => !x.Success)) - { - return Json(OperationResponse.Failed($"All Emails Failed({operationResponses.Count()}), Check SMTP Settings")); - } else - { - return Json(OperationResponse.Succeed($"Emails Sent({operationResponses.Count(x => x.Success)}), Emails Failed({operationResponses.Count(x => !x.Success)}), Check Recipient Settings")); - } - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - [Route("/api/makebackup")] - public IActionResult MakeBackup() - { - var result = _fileHelper.MakeBackup(); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - [Route("/api/cleanup")] - public IActionResult CleanUp(bool deepClean = false) - { - var jsonResponse = new Dictionary(); - //Clear out temp folder - var tempFilesDeleted = _fileHelper.ClearTempFolder(); - jsonResponse.Add("temp_files_deleted", tempFilesDeleted.ToString()); - if (deepClean) - { - //clear out unused vehicle thumbnails - var vehicles = _dataAccess.GetVehicles(); - var vehicleImages = vehicles.Select(x => x.ImageLocation).Where(x => x.StartsWith("/images/")).Select(x=>Path.GetFileName(x)).ToList(); - if (vehicleImages.Any()) - { - var thumbnailsDeleted = _fileHelper.ClearUnlinkedThumbnails(vehicleImages); - jsonResponse.Add("unlinked_thumbnails_deleted", thumbnailsDeleted.ToString()); - } - var vehicleDocuments = new List(); - foreach(Vehicle vehicle in vehicles) - { - vehicleDocuments.AddRange(_serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y=>Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_noteDataAccess.GetNotesByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - vehicleDocuments.AddRange(_planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicle.Id).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - } - //shop supplies - vehicleDocuments.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0).SelectMany(x => x.Files).Select(y => Path.GetFileName(y.Location))); - if (vehicleDocuments.Any()) - { - var documentsDeleted = _fileHelper.ClearUnlinkedDocuments(vehicleDocuments); - jsonResponse.Add("unlinked_documents_deleted", documentsDeleted.ToString()); - } - } - return Json(jsonResponse); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - [Route("/api/demo/restore")] - public IActionResult RestoreDemo() - { - var result = _fileHelper.RestoreBackup("/defaults/demo_default.zip", true); - return Json(result); - } - } -} diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs deleted file mode 100644 index 2503d40..0000000 --- a/Controllers/AdminController.cs +++ /dev/null @@ -1,85 +0,0 @@ -using MotoVaultPro.Helper; -using MotoVaultPro.Logic; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - [Authorize(Roles = nameof(UserData.IsAdmin))] - public class AdminController : Controller - { - private ILoginLogic _loginLogic; - private IUserLogic _userLogic; - private IConfigHelper _configHelper; - public AdminController(ILoginLogic loginLogic, IUserLogic userLogic, IConfigHelper configHelper) - { - _loginLogic = loginLogic; - _userLogic = userLogic; - _configHelper = configHelper; - } - public IActionResult Index() - { - var viewModel = new AdminViewModel - { - Users = _loginLogic.GetAllUsers().OrderBy(x=>x.Id).ToList(), - Tokens = _loginLogic.GetAllTokens() - }; - return View(viewModel); - } - public IActionResult GetTokenPartialView() - { - var viewModel = _loginLogic.GetAllTokens(); - return PartialView("_Tokens", viewModel); - } - public IActionResult GetUserPartialView() - { - var viewModel = _loginLogic.GetAllUsers().OrderBy(x => x.Id).ToList(); - return PartialView("_Users", viewModel); - } - public IActionResult GenerateNewToken(string emailAddress, bool autoNotify) - { - if (emailAddress.Contains(",")) - { - string[] emailAddresses = emailAddress.Split(','); - foreach(string emailAdd in emailAddresses) - { - var trimmedEmail = emailAdd.Trim(); - if (!string.IsNullOrWhiteSpace(trimmedEmail)) - { - var result = _loginLogic.GenerateUserToken(emailAdd.Trim(), autoNotify); - if (!result.Success) - { - //if fail, return prematurely - return Json(result); - } - } - } - var successResponse = OperationResponse.Succeed("Token Generated!"); - return Json(successResponse); - } else - { - var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify); - return Json(result); - } - } - [HttpPost] - public IActionResult DeleteToken(int tokenId) - { - var result = _loginLogic.DeleteUserToken(tokenId); - return Json(result); - } - [HttpPost] - public IActionResult DeleteUser(int userId) - { - var result =_userLogic.DeleteAllAccessToUser(userId) && _configHelper.DeleteUserConfig(userId) && _loginLogic.DeleteUser(userId); - return Json(result); - } - [HttpPost] - public IActionResult UpdateUserAdminStatus(int userId, bool isAdmin) - { - var result = _loginLogic.MakeUserAdmin(userId, isAdmin); - return Json(result); - } - } -} diff --git a/Controllers/ErrorController.cs b/Controllers/ErrorController.cs deleted file mode 100644 index 3cabc9f..0000000 --- a/Controllers/ErrorController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public class ErrorController : Controller - { - public IActionResult Unauthorized() - { - if (User.IsInRole("APIAuth")) - { - Response.StatusCode = 403; - return new EmptyResult(); - } - return View("401"); - } - } -} diff --git a/Controllers/FilesController.cs b/Controllers/FilesController.cs deleted file mode 100644 index f3e1c00..0000000 --- a/Controllers/FilesController.cs +++ /dev/null @@ -1,109 +0,0 @@ -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; -using MotoVaultPro.Helper; -using Microsoft.AspNetCore.Authorization; - -namespace MotoVaultPro.Controllers -{ - [Authorize] - public class FilesController : Controller - { - private readonly ILogger _logger; - private readonly IWebHostEnvironment _webEnv; - private readonly IFileHelper _fileHelper; - - public FilesController(ILogger logger, IFileHelper fileHelper, IWebHostEnvironment webEnv) - { - _logger = logger; - _webEnv = webEnv; - _fileHelper = fileHelper; - } - - [HttpPost] - public IActionResult HandleFileUpload(IFormFile file) - { - var fileName = UploadFile(file); - return Json(fileName); - } - - [HttpPost] - public IActionResult HandleTranslationFileUpload(IFormFile file) - { - var originalFileName = Path.GetFileNameWithoutExtension(file.FileName); - if (originalFileName == "en_US") - { - return Json(OperationResponse.Failed("The translation file name en_US is reserved.")); - } - var fileName = UploadFile(file); - //move file from temp to translation folder. - var uploadedFilePath = _fileHelper.MoveFileFromTemp(fileName, "translations/"); - //rename uploaded file so that it preserves original name. - if (!string.IsNullOrWhiteSpace(uploadedFilePath)) - { - var result = _fileHelper.RenameFile(uploadedFilePath, originalFileName); - return Json(OperationResponse.Conditional(result)); - } - return Json(OperationResponse.Failed()); - } - - [HttpPost] - public IActionResult HandleMultipleFileUpload(List file) - { - List uploadedFiles = new List(); - foreach (IFormFile fileToUpload in file) - { - var fileName = UploadFile(fileToUpload); - uploadedFiles.Add(new UploadedFiles { Name = fileToUpload.FileName, Location = fileName, IsPending = true}); - } - return Json(uploadedFiles); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult DeleteFiles(string fileLocation) - { - var result = _fileHelper.DeleteFile(fileLocation); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public IActionResult MakeBackup() - { - var result = _fileHelper.MakeBackup(); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult RestoreBackup(string fileName) - { - var result = _fileHelper.RestoreBackup(fileName); - return Json(result); - } - private string UploadFile(IFormFile fileToUpload) - { - string uploadDirectory = "temp/"; - string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory); - if (!Directory.Exists(uploadPath)) - Directory.CreateDirectory(uploadPath); - string fileName = Guid.NewGuid() + Path.GetExtension(fileToUpload.FileName); - string filePath = Path.Combine(uploadPath, fileName); - using (var stream = System.IO.File.Create(filePath)) - { - fileToUpload.CopyTo(stream); - } - return Path.Combine("/", uploadDirectory, fileName); - } - public IActionResult UploadCoordinates(List coordinates) - { - string uploadDirectory = "temp/"; - string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory); - if (!Directory.Exists(uploadPath)) - Directory.CreateDirectory(uploadPath); - string fileName = Guid.NewGuid() + ".csv"; - string filePath = Path.Combine(uploadPath, fileName); - string fileData = string.Join("\r\n", coordinates); - System.IO.File.WriteAllText(filePath, fileData); - var uploadedFile = new UploadedFiles { Name = "coordinates.csv", Location = Path.Combine("/", uploadDirectory, fileName) }; - return Json(uploadedFile); - } - } -} diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs deleted file mode 100644 index a30c10e..0000000 --- a/Controllers/HomeController.cs +++ /dev/null @@ -1,597 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; -using System.Diagnostics; -using MotoVaultPro.Helper; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; -using MotoVaultPro.Logic; - -namespace MotoVaultPro.Controllers -{ - [Authorize] - public class HomeController : Controller - { - private readonly ILogger _logger; - private readonly IVehicleDataAccess _dataAccess; - private readonly IUserLogic _userLogic; - private readonly ILoginLogic _loginLogic; - private readonly IVehicleLogic _vehicleLogic; - private readonly IFileHelper _fileHelper; - private readonly IConfigHelper _config; - private readonly IExtraFieldDataAccess _extraFieldDataAccess; - private readonly IReminderRecordDataAccess _reminderRecordDataAccess; - private readonly IReminderHelper _reminderHelper; - private readonly ITranslationHelper _translationHelper; - private readonly IMailHelper _mailHelper; - public HomeController(ILogger logger, - IVehicleDataAccess dataAccess, - IUserLogic userLogic, - ILoginLogic loginLogic, - IVehicleLogic vehicleLogic, - IConfigHelper configuration, - IFileHelper fileHelper, - IExtraFieldDataAccess extraFieldDataAccess, - IReminderRecordDataAccess reminderRecordDataAccess, - IReminderHelper reminderHelper, - ITranslationHelper translationHelper, - IMailHelper mailHelper) - { - _logger = logger; - _dataAccess = dataAccess; - _config = configuration; - _userLogic = userLogic; - _fileHelper = fileHelper; - _extraFieldDataAccess = extraFieldDataAccess; - _reminderRecordDataAccess = reminderRecordDataAccess; - _reminderHelper = reminderHelper; - _loginLogic = loginLogic; - _vehicleLogic = vehicleLogic; - _translationHelper = translationHelper; - _mailHelper = mailHelper; - } - private int GetUserID() - { - return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); - } - public IActionResult Index(string tab = "garage") - { - return View(model: tab); - } - [Route("/kiosk")] - public IActionResult Kiosk(string exclusions, KioskMode kioskMode = KioskMode.Vehicle) - { - try { - var viewModel = new KioskViewModel - { - Exclusions = string.IsNullOrWhiteSpace(exclusions) ? new List() : exclusions.Split(',').Select(x => int.Parse(x)).ToList(), - KioskMode = kioskMode - }; - return View(viewModel); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return View(new KioskViewModel()); - } - } - [HttpPost] - public IActionResult KioskContent(KioskViewModel kioskParameters) - { - var vehiclesStored = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID()); - } - vehiclesStored.RemoveAll(x => kioskParameters.Exclusions.Contains(x.Id)); - var userConfig = _config.GetUserConfig(User); - if (userConfig.HideSoldVehicles) - { - vehiclesStored.RemoveAll(x => !string.IsNullOrWhiteSpace(x.SoldDate)); - } - switch (kioskParameters.KioskMode) - { - case KioskMode.Vehicle: - { - var kioskResult = _vehicleLogic.GetVehicleInfo(vehiclesStored); - return PartialView("_Kiosk", kioskResult); - } - case KioskMode.Plan: - { - var kioskResult = _vehicleLogic.GetPlans(vehiclesStored, true); - return PartialView("_KioskPlan", kioskResult); - } - case KioskMode.Reminder: - { - var kioskResult = _vehicleLogic.GetReminders(vehiclesStored, false); - return PartialView("_KioskReminder", kioskResult); - } - } - var result = _vehicleLogic.GetVehicleInfo(vehiclesStored); - return PartialView("_Kiosk", result); - } - public IActionResult Garage() - { - var vehiclesStored = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID()); - } - var vehicleViewModels = vehiclesStored.Select(x => - { - var vehicleVM = new VehicleViewModel - { - Id = x.Id, - ImageLocation = x.ImageLocation, - Year = x.Year, - Make = x.Make, - Model = x.Model, - LicensePlate = x.LicensePlate, - VinNumber = x.VinNumber, - SoldDate = x.SoldDate, - IsElectric = x.IsElectric, - IsDiesel = x.IsDiesel, - UseHours = x.UseHours, - OdometerOptional = x.OdometerOptional, - ExtraFields = x.ExtraFields, - Tags = x.Tags, - DashboardMetrics = x.DashboardMetrics, - VehicleIdentifier = x.VehicleIdentifier - }; - //dashboard metrics - if (x.DashboardMetrics.Any()) - { - var vehicleRecords = _vehicleLogic.GetVehicleRecords(x.Id); - var userConfig = _config.GetUserConfig(User); - var distanceUnit = x.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km"; - if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.Default)) - { - vehicleVM.LastReportedMileage = _vehicleLogic.GetMaxMileage(vehicleRecords); - vehicleVM.HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id, vehicleVM.LastReportedMileage); - } - if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.CostPerMile)) - { - var vehicleTotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords); - var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords); - var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords); - var totalDistance = maxMileage - minMileage; - vehicleVM.CostPerMile = totalDistance != default ? vehicleTotalCost / totalDistance : 0.00M; - vehicleVM.DistanceUnit = distanceUnit; - } - if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.TotalCost)) - { - vehicleVM.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords); - } - } - return vehicleVM; - }).ToList(); - return PartialView("_GarageDisplay", vehicleViewModels); - } - public IActionResult Calendar() - { - var vehiclesStored = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID()); - } - var reminders = _vehicleLogic.GetReminders(vehiclesStored, true); - return PartialView("_Calendar", reminders); - } - public IActionResult ViewCalendarReminder(int reminderId) - { - var reminder = _reminderRecordDataAccess.GetReminderRecordById(reminderId); - var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(new List { reminder }, 0, DateTime.Now).FirstOrDefault(); - return PartialView("_ReminderRecordCalendarModal", reminderUrgency); - } - public async Task Settings() - { - var userConfig = _config.GetUserConfig(User); - var languages = _fileHelper.GetLanguages(); - var viewModel = new SettingsViewModel - { - UserConfig = userConfig, - UILanguages = languages - }; - return PartialView("_Settings", viewModel); - } - public async Task Sponsors() - { - try - { - var httpClient = new HttpClient(); - var sponsorsData = await httpClient.GetFromJsonAsync(StaticHelper.SponsorsPath) ?? new Sponsors(); - return PartialView("_Sponsors", sponsorsData); - } - catch (Exception ex) - { - _logger.LogError($"Unable to retrieve sponsors: {ex.Message}"); - return PartialView("_Sponsors", new Sponsors()); - } - } - [HttpPost] - public IActionResult WriteToSettings(UserConfig userConfig) - { - //retrieve existing userConfig. - var existingConfig = _config.GetUserConfig(User); - //copy over stuff that persists - userConfig.UserColumnPreferences = existingConfig.UserColumnPreferences; - var result = _config.SaveUserConfig(User, userConfig); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - public IActionResult GetExtraFieldsModal(int importMode = 0) - { - var recordExtraFields = _extraFieldDataAccess.GetExtraFieldsById(importMode); - if (recordExtraFields.Id != importMode) - { - recordExtraFields.Id = importMode; - } - return PartialView("_ExtraFields", recordExtraFields); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - public IActionResult UpdateExtraFields(RecordExtraField record) - { - try - { - var result = _extraFieldDataAccess.SaveExtraFields(record); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - var recordExtraFields = _extraFieldDataAccess.GetExtraFieldsById(record.Id); - return PartialView("_ExtraFields", recordExtraFields); - } - [HttpPost] - public IActionResult GenerateTokenForUser() - { - try - { - //get current user email address. - var emailAddress = User.FindFirstValue(ClaimTypes.Email); - if (!string.IsNullOrWhiteSpace(emailAddress)) - { - var result = _loginLogic.GenerateTokenForEmailAddress(emailAddress, false); - return Json(result); - } - return Json(false); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(false); - } - } - [HttpPost] - public IActionResult UpdateUserAccount(LoginModel userAccount) - { - try - { - var userId = GetUserID(); - if (userId > 0) - { - var result = _loginLogic.UpdateUserDetails(userId, userAccount); - return Json(result); - } - return Json(OperationResponse.Failed()); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(OperationResponse.Failed()); - } - } - [HttpGet] - public IActionResult GetUserAccountInformationModal() - { - var emailAddress = User.FindFirstValue(ClaimTypes.Email); - var userName = User.Identity.Name; - return PartialView("_AccountModal", new UserData() { EmailAddress = emailAddress, UserName = userName }); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public IActionResult GetRootAccountInformationModal() - { - var userName = User.Identity.Name; - return PartialView("_RootAccountModal", new UserData() { UserName = userName }); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public IActionResult GetTranslatorEditor(string userLanguage) - { - var translationData = _translationHelper.GetTranslations(userLanguage); - return PartialView("_TranslationEditor", translationData); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult SaveTranslation(string userLanguage, Dictionary translationData) - { - var result = _translationHelper.SaveTranslation(userLanguage, translationData); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult ExportTranslation(Dictionary translationData) - { - var result = _translationHelper.ExportTranslation(translationData); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public async Task GetAvailableTranslations() - { - try - { - var httpClient = new HttpClient(); - var translations = await httpClient.GetFromJsonAsync(StaticHelper.TranslationDirectoryPath) ?? new Translations(); - return PartialView("_Translations", translations); - } - catch (Exception ex) - { - _logger.LogError($"Unable to retrieve translations: {ex.Message}"); - return PartialView("_Translations", new Translations()); - } - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public async Task DownloadTranslation(string continent, string name) - { - try - { - var httpClient = new HttpClient(); - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath(continent, name)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(name, translationData); - if (!result.Success) - { - return Json(false); - } - } - else - { - _logger.LogError($"Unable to download translation: {name}"); - return Json(false); - } - return Json(true); - } - catch (Exception ex) - { - _logger.LogError($"Unable to download translation: {ex.Message}"); - return Json(false); - } - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public async Task DownloadAllTranslations() - { - try - { - var httpClient = new HttpClient(); - var translations = await httpClient.GetFromJsonAsync(StaticHelper.TranslationDirectoryPath) ?? new Translations(); - int translationsDownloaded = 0; - foreach (string translation in translations.Asia) - { - try - { - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath("Asia", translation)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(translation, translationData); - if (result.Success) - { - translationsDownloaded++; - }; - } - } - catch (Exception ex) - { - _logger.LogError($"Error Downloading Translation {translation}: {ex.Message} "); - } - } - foreach (string translation in translations.Africa) - { - try - { - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath("Africa", translation)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(translation, translationData); - if (result.Success) - { - translationsDownloaded++; - }; - } - } - catch (Exception ex) - { - _logger.LogError($"Error Downloading Translation {translation}: {ex.Message} "); - } - } - foreach (string translation in translations.Europe) - { - try - { - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath("Europe", translation)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(translation, translationData); - if (result.Success) - { - translationsDownloaded++; - }; - } - } - catch (Exception ex) - { - _logger.LogError($"Error Downloading Translation {translation}: {ex.Message} "); - } - } - foreach (string translation in translations.NorthAmerica) - { - try - { - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath("NorthAmerica", translation)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(translation, translationData); - if (result.Success) - { - translationsDownloaded++; - }; - } - } - catch (Exception ex) - { - _logger.LogError($"Error Downloading Translation {translation}: {ex.Message} "); - } - } - foreach (string translation in translations.SouthAmerica) - { - try - { - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath("SouthAmerica", translation)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(translation, translationData); - if (result.Success) - { - translationsDownloaded++; - }; - } - } - catch (Exception ex) - { - _logger.LogError($"Error Downloading Translation {translation}: {ex.Message} "); - } - } - foreach (string translation in translations.Oceania) - { - try - { - var translationData = await httpClient.GetFromJsonAsync>(StaticHelper.GetTranslationDownloadPath("Oceania", translation)) ?? new Dictionary(); - if (translationData.Any()) - { - var result = _translationHelper.SaveTranslation(translation, translationData); - if (result.Success) - { - translationsDownloaded++; - }; - } - } - catch (Exception ex) - { - _logger.LogError($"Error Downloading Translation {translation}: {ex.Message} "); - } - } - if (translationsDownloaded > 0) - { - return Json(OperationResponse.Succeed($"{translationsDownloaded} Translations Downloaded")); - } else - { - return Json(OperationResponse.Failed("No Translations Downloaded")); - } - } - catch (Exception ex) - { - _logger.LogError($"Unable to retrieve translations: {ex.Message}"); - return Json(OperationResponse.Failed()); - } - } - public ActionResult GetVehicleSelector(int vehicleId) - { - var vehiclesStored = _dataAccess.GetVehicles(); - if (!User.IsInRole(nameof(UserData.IsRootUser))) - { - vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID()); - } - if (vehicleId != default) - { - vehiclesStored.RemoveAll(x => x.Id == vehicleId); - } - var userConfig = _config.GetUserConfig(User); - if (userConfig.HideSoldVehicles) - { - vehiclesStored.RemoveAll(x => !string.IsNullOrWhiteSpace(x.SoldDate)); - } - return PartialView("_VehicleSelector", vehiclesStored); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpGet] - public IActionResult GetCustomWidgetEditor() - { - if (_config.GetCustomWidgetsEnabled()) - { - var customWidgetData = _fileHelper.GetWidgets(); - return PartialView("_WidgetEditor", customWidgetData); - } - return Json(string.Empty); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult SaveCustomWidgets(string widgetsData) - { - if (_config.GetCustomWidgetsEnabled()) - { - var saveResult = _fileHelper.SaveWidgets(widgetsData); - return Json(saveResult); - } - return Json(false); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult DeleteCustomWidgets() - { - if (_config.GetCustomWidgetsEnabled()) - { - var deleteResult = _fileHelper.DeleteWidgets(); - return Json(deleteResult); - } - return Json(false); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [Route("/setup")] - public IActionResult Setup() - { - var viewModel = new ServerSettingsViewModel - { - PostgresConnection = _config.GetServerPostgresConnection(), - AllowedFileExtensions = _config.GetAllowedFileUploadExtensions(), - CustomLogoURL = _config.GetLogoUrl(), - CustomSmallLogoURL = _config.GetSmallLogoUrl(), - MessageOfTheDay = _config.GetMOTD(), - WebHookURL = _config.GetWebHookUrl(), - CustomWidgetsEnabled = _config.GetCustomWidgetsEnabled(), - InvariantAPIEnabled = _config.GetInvariantApi(), - SMTPConfig = _config.GetMailConfig(), - Domain = _config.GetServerDomain(), - OIDCConfig = _config.GetOpenIDConfig(), - OpenRegistration = _config.GetServerOpenRegistration(), - DisableRegistration = _config.GetServerDisabledRegistration(), - ReminderUrgencyConfig = _config.GetReminderUrgencyConfig(), - EnableAuth = _config.GetServerAuthEnabled(), - DefaultReminderEmail = _config.GetDefaultReminderEmail(), - EnableRootUserOIDC = _config.GetEnableRootUserOIDC() - }; - return View(viewModel); - } - [HttpPost] - [Authorize(Roles = nameof(UserData.IsRootUser))] - public IActionResult WriteServerConfiguration(ServerConfig serverConfig) - { - var result = _config.SaveServerConfig(serverConfig); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - public IActionResult SendTestEmail(string emailAddress, MailConfig mailConfig) - { - var result = _mailHelper.SendTestEmail(emailAddress, mailConfig); - return Json(result); - } - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } - } -} diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs deleted file mode 100644 index 62c92c2..0000000 --- a/Controllers/LoginController.cs +++ /dev/null @@ -1,464 +0,0 @@ -using MotoVaultPro.Helper; -using MotoVaultPro.Logic; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Mvc; -using System.IdentityModel.Tokens.Jwt; -using System.Text.Json; - -namespace MotoVaultPro.Controllers -{ - public class LoginController : Controller - { - private IDataProtector _dataProtector; - private ILoginLogic _loginLogic; - private IConfigHelper _config; - private readonly ILogger _logger; - public LoginController( - ILogger logger, - IDataProtectionProvider securityProvider, - ILoginLogic loginLogic, - IConfigHelper config - ) - { - _dataProtector = securityProvider.CreateProtector("login"); - _logger = logger; - _loginLogic = loginLogic; - _config = config; - } - public IActionResult Index(string redirectURL = "") - { - var remoteAuthConfig = _config.GetOpenIDConfig(); - if (remoteAuthConfig.DisableRegularLogin && !string.IsNullOrWhiteSpace(remoteAuthConfig.LogOutURL)) - { - var generatedState = Guid.NewGuid().ToString().Substring(0, 8); - remoteAuthConfig.State = generatedState; - var pkceKeyPair = _loginLogic.GetPKCEChallengeCode(); - remoteAuthConfig.CodeChallenge = pkceKeyPair.Value; - if (remoteAuthConfig.ValidateState) - { - Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) }); - } - if (remoteAuthConfig.UsePKCE) - { - Response.Cookies.Append("OIDC_VERIFIER", pkceKeyPair.Key, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) }); - } - var remoteAuthURL = remoteAuthConfig.RemoteAuthURL; - return Redirect(remoteAuthURL); - } - return View(model: redirectURL); - } - public IActionResult Registration(string token = "", string email = "") - { - if (_config.GetServerDisabledRegistration()) - { - return RedirectToAction("Index"); - } - var viewModel = new LoginModel - { - EmailAddress = string.IsNullOrWhiteSpace(email) ? string.Empty : email, - Token = string.IsNullOrWhiteSpace(token) ? string.Empty : token - }; - return View(viewModel); - } - public IActionResult ForgotPassword() - { - return View(); - } - public IActionResult ResetPassword(string token = "", string email = "") - { - var viewModel = new LoginModel - { - EmailAddress = string.IsNullOrWhiteSpace(email) ? string.Empty : email, - Token = string.IsNullOrWhiteSpace(token) ? string.Empty : token - }; - return View(viewModel); - } - public IActionResult GetRemoteLoginLink() - { - var remoteAuthConfig = _config.GetOpenIDConfig(); - var generatedState = Guid.NewGuid().ToString().Substring(0, 8); - remoteAuthConfig.State = generatedState; - var pkceKeyPair = _loginLogic.GetPKCEChallengeCode(); - remoteAuthConfig.CodeChallenge = pkceKeyPair.Value; - if (remoteAuthConfig.ValidateState) - { - Response.Cookies.Append("OIDC_STATE", remoteAuthConfig.State, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) }); - } - if (remoteAuthConfig.UsePKCE) - { - Response.Cookies.Append("OIDC_VERIFIER", pkceKeyPair.Key, new CookieOptions { Expires = new DateTimeOffset(DateTime.Now.AddMinutes(5)) }); - } - var remoteAuthURL = remoteAuthConfig.RemoteAuthURL; - return Json(remoteAuthURL); - } - public async Task RemoteAuth(string code, string state = "") - { - try - { - if (!string.IsNullOrWhiteSpace(code)) - { - //received code from OIDC provider - //create http client to retrieve user token from OIDC - var httpClient = new HttpClient(); - var openIdConfig = _config.GetOpenIDConfig(); - //check if validate state is enabled. - if (openIdConfig.ValidateState) - { - var storedStateValue = Request.Cookies["OIDC_STATE"]; - if (!string.IsNullOrWhiteSpace(storedStateValue)) - { - Response.Cookies.Delete("OIDC_STATE"); - } - if (string.IsNullOrWhiteSpace(storedStateValue) || string.IsNullOrWhiteSpace(state) || storedStateValue != state) - { - _logger.LogInformation("Failed OIDC State Validation - Try disabling state validation if you are confident this is not a malicious attempt."); - return new RedirectResult("/Login"); - } - } - var httpParams = new List> - { - new KeyValuePair("code", code), - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("client_id", openIdConfig.ClientId), - new KeyValuePair("client_secret", openIdConfig.ClientSecret), - new KeyValuePair("redirect_uri", openIdConfig.RedirectURL) - }; - if (openIdConfig.UsePKCE) - { - //retrieve stored challenge verifier - var storedVerifier = Request.Cookies["OIDC_VERIFIER"]; - if (!string.IsNullOrWhiteSpace(storedVerifier)) - { - httpParams.Add(new KeyValuePair("code_verifier", storedVerifier)); - Response.Cookies.Delete("OIDC_VERIFIER"); - } - } - var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL) - { - Content = new FormUrlEncodedContent(httpParams) - }; - var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync(); - var decodedToken = JsonSerializer.Deserialize(tokenResult); - var userJwt = decodedToken?.id_token ?? string.Empty; - var userAccessToken = decodedToken?.access_token ?? string.Empty; - if (!string.IsNullOrWhiteSpace(userJwt)) - { - //validate JWT token - var tokenParser = new JwtSecurityTokenHandler(); - var parsedToken = tokenParser.ReadJwtToken(userJwt); - var userEmailAddress = string.Empty; - if (parsedToken.Claims.Any(x => x.Type == "email")) - { - userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value; - } - else if (!string.IsNullOrWhiteSpace(openIdConfig.UserInfoURL) && !string.IsNullOrWhiteSpace(userAccessToken)) - { - //retrieve claims from userinfo endpoint if no email claims are returned within id_token - var userInfoHttpRequest = new HttpRequestMessage(HttpMethod.Get, openIdConfig.UserInfoURL); - userInfoHttpRequest.Headers.Add("Authorization", $"Bearer {userAccessToken}"); - var userInfoResult = await httpClient.SendAsync(userInfoHttpRequest).Result.Content.ReadAsStringAsync(); - var userInfo = JsonSerializer.Deserialize(userInfoResult); - if (!string.IsNullOrWhiteSpace(userInfo?.email ?? string.Empty)) - { - userEmailAddress = userInfo?.email ?? string.Empty; - } else - { - _logger.LogError($"OpenID Provider did not provide an email claim via UserInfo endpoint"); - } - } - else - { - var returnedClaims = parsedToken.Claims.Select(x => x.Type); - _logger.LogError($"OpenID Provider did not provide an email claim, claims returned: {string.Join(",", returnedClaims)}"); - } - if (!string.IsNullOrWhiteSpace(userEmailAddress)) - { - var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress }); - if (userData.Id != default) - { - AuthCookie authCookie = new AuthCookie - { - UserData = userData, - ExpiresOn = DateTime.Now.AddDays(1) - }; - var serializedCookie = JsonSerializer.Serialize(authCookie); - var encryptedCookie = _dataProtector.Protect(serializedCookie); - Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) }); - return new RedirectResult("/Home"); - } else - { - _logger.LogInformation($"User {userEmailAddress} tried to login via OpenID but is not a registered user in MotoVaultPro."); - return View("OpenIDRegistration", model: userEmailAddress); - } - } else - { - _logger.LogInformation("OpenID Provider did not provide a valid email address for the user"); - } - } else - { - _logger.LogInformation("OpenID Provider did not provide a valid id_token"); - if (!string.IsNullOrWhiteSpace(tokenResult)) - { - //if something was returned from the IdP but it's invalid, we want to log it as an error. - _logger.LogError($"Expected id_token, received {tokenResult}"); - } - } - } else - { - _logger.LogInformation("OpenID Provider did not provide a code."); - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return new RedirectResult("/Login"); - } - return new RedirectResult("/Login"); - } - public async Task RemoteAuthDebug(string code, string state = "") - { - List results = new List(); - try - { - if (!string.IsNullOrWhiteSpace(code)) - { - results.Add(OperationResponse.Succeed($"Received code from OpenID Provider: {code}")); - //received code from OIDC provider - //create http client to retrieve user token from OIDC - var httpClient = new HttpClient(); - var openIdConfig = _config.GetOpenIDConfig(); - //check if validate state is enabled. - if (openIdConfig.ValidateState) - { - var storedStateValue = Request.Cookies["OIDC_STATE"]; - if (!string.IsNullOrWhiteSpace(storedStateValue)) - { - Response.Cookies.Delete("OIDC_STATE"); - } - if (string.IsNullOrWhiteSpace(storedStateValue) || string.IsNullOrWhiteSpace(state) || storedStateValue != state) - { - results.Add(OperationResponse.Failed($"Failed State Validation - Expected: {storedStateValue} Received: {state}")); - } else - { - results.Add(OperationResponse.Succeed($"Passed State Validation - Expected: {storedStateValue} Received: {state}")); - } - } - var httpParams = new List> - { - new KeyValuePair("code", code), - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("client_id", openIdConfig.ClientId), - new KeyValuePair("client_secret", openIdConfig.ClientSecret), - new KeyValuePair("redirect_uri", openIdConfig.RedirectURL) - }; - if (openIdConfig.UsePKCE) - { - //retrieve stored challenge verifier - var storedVerifier = Request.Cookies["OIDC_VERIFIER"]; - if (!string.IsNullOrWhiteSpace(storedVerifier)) - { - httpParams.Add(new KeyValuePair("code_verifier", storedVerifier)); - Response.Cookies.Delete("OIDC_VERIFIER"); - } - } - var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL) - { - Content = new FormUrlEncodedContent(httpParams) - }; - var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync(); - var decodedToken = JsonSerializer.Deserialize(tokenResult); - var userJwt = decodedToken?.id_token ?? string.Empty; - var userAccessToken = decodedToken?.access_token ?? string.Empty; - if (!string.IsNullOrWhiteSpace(userJwt)) - { - results.Add(OperationResponse.Succeed($"Passed JWT Parsing - id_token: {userJwt}")); - //validate JWT token - var tokenParser = new JwtSecurityTokenHandler(); - var parsedToken = tokenParser.ReadJwtToken(userJwt); - var userEmailAddress = string.Empty; - if (parsedToken.Claims.Any(x => x.Type == "email")) - { - userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value; - results.Add(OperationResponse.Succeed($"Passed Claim Validation - email")); - } - else if (!string.IsNullOrWhiteSpace(openIdConfig.UserInfoURL) && !string.IsNullOrWhiteSpace(userAccessToken)) - { - //retrieve claims from userinfo endpoint if no email claims are returned within id_token - var userInfoHttpRequest = new HttpRequestMessage(HttpMethod.Get, openIdConfig.UserInfoURL); - userInfoHttpRequest.Headers.Add("Authorization", $"Bearer {userAccessToken}"); - var userInfoResult = await httpClient.SendAsync(userInfoHttpRequest).Result.Content.ReadAsStringAsync(); - var userInfo = JsonSerializer.Deserialize(userInfoResult); - if (!string.IsNullOrWhiteSpace(userInfo?.email ?? string.Empty)) - { - userEmailAddress = userInfo?.email ?? string.Empty; - results.Add(OperationResponse.Succeed($"Passed Claim Validation - Retrieved email via UserInfo endpoint")); - } else - { - results.Add(OperationResponse.Failed($"Failed Claim Validation - Unable to retrieve email via UserInfo endpoint: {openIdConfig.UserInfoURL} using access_token: {userAccessToken} - Received {userInfoResult}")); - } - } - else - { - var returnedClaims = parsedToken.Claims.Select(x => x.Type); - results.Add(OperationResponse.Failed($"Failed Claim Validation - Expected: email Received: {string.Join(",", returnedClaims)}")); - } - if (!string.IsNullOrWhiteSpace(userEmailAddress)) - { - var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress }); - if (userData.Id != default) - { - results.Add(OperationResponse.Succeed($"Passed User Validation - Email: {userEmailAddress} Username: {userData.UserName}")); - } - else - { - results.Add(OperationResponse.Succeed($"Passed Email Validation - Email: {userEmailAddress} User not registered")); - } - } - else - { - results.Add(OperationResponse.Failed($"Failed Email Validation - No email received from OpenID Provider")); - } - } - else - { - results.Add(OperationResponse.Failed($"Failed to parse JWT - Expected: id_token Received: {tokenResult}")); - } - } - else - { - results.Add(OperationResponse.Failed("No code received from OpenID Provider")); - } - } - catch (Exception ex) - { - results.Add(OperationResponse.Failed($"Exception: {ex.Message}")); - } - return View(results); - } - [HttpPost] - public IActionResult Login(LoginModel credentials) - { - if (string.IsNullOrWhiteSpace(credentials.UserName) || - string.IsNullOrWhiteSpace(credentials.Password)) - { - return Json(false); - } - //compare it against hashed credentials - try - { - var userData = _loginLogic.ValidateUserCredentials(credentials); - if (userData.Id != default) - { - AuthCookie authCookie = new AuthCookie - { - UserData = userData, - ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1) - }; - var serializedCookie = JsonSerializer.Serialize(authCookie); - var encryptedCookie = _dataProtector.Protect(serializedCookie); - Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) }); - return Json(true); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error on saving config file."); - } - return Json(false); - } - - [HttpPost] - public IActionResult Register(LoginModel credentials) - { - var result = _loginLogic.RegisterNewUser(credentials); - return Json(result); - } - [HttpPost] - public IActionResult RegisterOpenIdUser(LoginModel credentials) - { - var result = _loginLogic.RegisterOpenIdUser(credentials); - if (result.Success) - { - var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = credentials.EmailAddress }); - if (userData.Id != default) - { - AuthCookie authCookie = new AuthCookie - { - UserData = userData, - ExpiresOn = DateTime.Now.AddDays(1) - }; - var serializedCookie = JsonSerializer.Serialize(authCookie); - var encryptedCookie = _dataProtector.Protect(serializedCookie); - Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) }); - } - } - return Json(result); - } - [HttpPost] - public IActionResult SendRegistrationToken(LoginModel credentials) - { - var result = _loginLogic.SendRegistrationToken(credentials); - return Json(result); - } - [HttpPost] - public IActionResult RequestResetPassword(LoginModel credentials) - { - var result = _loginLogic.RequestResetPassword(credentials); - return Json(result); - } - [HttpPost] - public IActionResult PerformPasswordReset(LoginModel credentials) - { - var result = _loginLogic.ResetPasswordByUser(credentials); - return Json(result); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] //User must already be logged in as root user to do this. - [HttpPost] - public IActionResult CreateLoginCreds(LoginModel credentials) - { - try - { - var result = _loginLogic.CreateRootUserCredentials(credentials); - return Json(result); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error on saving config file."); - } - return Json(false); - } - [Authorize(Roles = nameof(UserData.IsRootUser))] - [HttpPost] - public IActionResult DestroyLoginCreds() - { - try - { - var result = _loginLogic.DeleteRootUserCredentials(); - //destroy any login cookies. - if (result) - { - Response.Cookies.Delete("ACCESS_TOKEN"); - } - return Json(result); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error on saving config file."); - } - return Json(false); - } - [Authorize] - [HttpPost] - public IActionResult LogOut() - { - Response.Cookies.Delete("ACCESS_TOKEN"); - var remoteAuthConfig = _config.GetOpenIDConfig(); - if (remoteAuthConfig.DisableRegularLogin && !string.IsNullOrWhiteSpace(remoteAuthConfig.LogOutURL)) - { - return Json(remoteAuthConfig.LogOutURL); - } - return Json("/Login"); - } - } -} diff --git a/Controllers/MigrationController.cs b/Controllers/MigrationController.cs deleted file mode 100644 index 5a88d86..0000000 --- a/Controllers/MigrationController.cs +++ /dev/null @@ -1,720 +0,0 @@ -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using LiteDB; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Npgsql; -using System.IO.Compression; -using JsonSerializer=System.Text.Json.JsonSerializer; - -namespace MotoVaultPro.Controllers -{ - [Authorize(Roles = nameof(UserData.IsRootUser))] - public class MigrationController : Controller - { - private IConfigHelper _configHelper; - private IFileHelper _fileHelper; - private readonly ILogger _logger; - public MigrationController(IConfigHelper configHelper, IFileHelper fileHelper, ILogger logger) - { - _configHelper = configHelper; - _fileHelper = fileHelper; - _logger = logger; - } - public IActionResult Index() - { - if (!string.IsNullOrWhiteSpace(_configHelper.GetServerPostgresConnection())) - { - return View(); - } else - { - return new RedirectResult("/Error/Unauthorized"); - } - } - private void InitializeTables(NpgsqlDataSource conn) - { - var cmds = new List - { - "CREATE SCHEMA IF NOT EXISTS app", - "CREATE TABLE IF NOT EXISTS app.vehicles (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.upgraderecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.servicerecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.gasrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.notes (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.odometerrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.reminderrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.planrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.planrecordtemplates (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.supplyrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.taxrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.userrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)", - "CREATE TABLE IF NOT EXISTS app.tokenrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)", - "CREATE TABLE IF NOT EXISTS app.userconfigrecords (id INT primary key, data jsonb not null)", - "CREATE TABLE IF NOT EXISTS app.useraccessrecords (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))", - "CREATE TABLE IF NOT EXISTS app.extrafields (id INT primary key, data jsonb not null)" - }; - foreach(string cmd in cmds) - { - using (var ctext = conn.CreateCommand(cmd)) - { - ctext.ExecuteNonQuery(); - } - } - } - public IActionResult Export() - { - if (string.IsNullOrWhiteSpace(_configHelper.GetServerPostgresConnection())) - { - return Json(OperationResponse.Failed("Postgres connection not set up")); - } - var tempFolder = $"temp/{Guid.NewGuid()}"; - var tempPath = $"{tempFolder}/cartracker.db"; - var fullFolderPath = _fileHelper.GetFullFilePath(tempFolder, false); - Directory.CreateDirectory(fullFolderPath); - var fullFileName = _fileHelper.GetFullFilePath(tempPath, false); - try - { - var pgDataSource = NpgsqlDataSource.Create(_configHelper.GetServerPostgresConnection()); - InitializeTables(pgDataSource); - //pull records - var vehicles = new List(); - var upgraderecords = new List(); - var servicerecords = new List(); - - var gasrecords = new List(); - var noterecords = new List(); - var odometerrecords = new List(); - var reminderrecords = new List(); - - var planrecords = new List(); - var planrecordtemplates = new List(); - var supplyrecords = new List(); - var taxrecords = new List(); - - var userrecords = new List(); - var tokenrecords = new List(); - var userconfigrecords = new List(); - var useraccessrecords = new List(); - - var extrafields = new List(); - #region "Part1" - string cmd = $"SELECT data FROM app.vehicles"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - Vehicle vehicle = JsonSerializer.Deserialize(reader["data"] as string); - vehicles.Add(vehicle); - } - } - foreach (var vehicle in vehicles) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("vehicles"); - table.Upsert(vehicle); - }; - } - cmd = $"SELECT data FROM app.upgraderecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - upgraderecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in upgraderecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("upgraderecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.servicerecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - servicerecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in servicerecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("servicerecords"); - table.Upsert(record); - }; - } - #endregion - #region "Part2" - cmd = $"SELECT data FROM app.gasrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - gasrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in gasrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("gasrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.notes"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - noterecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in noterecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("notes"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.odometerrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - odometerrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in odometerrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("odometerrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.reminderrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - reminderrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in reminderrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("reminderrecords"); - table.Upsert(record); - }; - } - #endregion - #region "Part3" - cmd = $"SELECT data FROM app.planrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - planrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in planrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("planrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.planrecordtemplates"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - planrecordtemplates.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in planrecordtemplates) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("planrecordtemplates"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.supplyrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - supplyrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in supplyrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("supplyrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.taxrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - taxrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in taxrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("taxrecords"); - table.Upsert(record); - }; - } - #endregion - #region "Part4" - cmd = $"SELECT id, username, emailaddress, password, isadmin FROM app.userrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UserData result = new UserData(); - result.Id = int.Parse(reader["id"].ToString()); - result.UserName = reader["username"].ToString(); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Password = reader["password"].ToString(); - result.IsAdmin = bool.Parse(reader["isadmin"].ToString()); - userrecords.Add(result); - } - } - foreach (var record in userrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("userrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT id, emailaddress, body FROM app.tokenrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - Token result = new Token(); - result.Id = int.Parse(reader["id"].ToString()); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Body = reader["body"].ToString(); - tokenrecords.Add(result); - } - } - foreach (var record in tokenrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("tokenrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT data FROM app.userconfigrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - userconfigrecords.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in userconfigrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("userconfigrecords"); - table.Upsert(record); - }; - } - cmd = $"SELECT userId, vehicleId FROM app.useraccessrecords"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UserAccess result = new UserAccess() - { - Id = new UserVehicle - { - UserId = int.Parse(reader["userId"].ToString()), - VehicleId = int.Parse(reader["vehicleId"].ToString()) - } - }; - useraccessrecords.Add(result); - } - } - foreach (var record in useraccessrecords) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("useraccessrecords"); - table.Upsert(record); - }; - } - #endregion - #region "Part5" - cmd = $"SELECT data FROM app.extrafields"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - extrafields.Add(JsonSerializer.Deserialize(reader["data"] as string)); - } - } - foreach (var record in extrafields) - { - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("extrafields"); - table.Upsert(record); - }; - } - #endregion - var destFilePath = $"{fullFolderPath}.zip"; - ZipFile.CreateFromDirectory(fullFolderPath, destFilePath); - return Json(OperationResponse.Succeed($"/{tempFolder}.zip")); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(OperationResponse.Failed()); - } - } - public IActionResult Import(string fileName) - { - if (string.IsNullOrWhiteSpace(_configHelper.GetServerPostgresConnection())) - { - return Json(OperationResponse.Failed("Postgres connection not set up")); - } - var fullFileName = _fileHelper.GetFullFilePath(fileName); - if (string.IsNullOrWhiteSpace(fullFileName)) - { - return Json(OperationResponse.Failed()); - } - try - { - var pgDataSource = NpgsqlDataSource.Create(_configHelper.GetServerPostgresConnection()); - InitializeTables(pgDataSource); - //pull records - var vehicles = new List(); - var upgraderecords = new List(); - var servicerecords = new List(); - - var gasrecords = new List(); - var noterecords = new List(); - var odometerrecords = new List(); - var reminderrecords = new List(); - - var planrecords = new List(); - var planrecordtemplates = new List(); - var supplyrecords = new List(); - var taxrecords = new List(); - - var userrecords = new List(); - var tokenrecords = new List(); - var userconfigrecords = new List(); - var useraccessrecords = new List(); - - var extrafields = new List(); - #region "Part1" - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("vehicles"); - vehicles = table.FindAll().ToList(); - }; - foreach(var vehicle in vehicles) - { - string cmd = $"INSERT INTO app.vehicles (id, data) VALUES(@id, CAST(@data AS jsonb)); SELECT setval('app.vehicles_id_seq', (SELECT MAX(id) from app.vehicles));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicle.Id); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(vehicle)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("servicerecords"); - servicerecords = table.FindAll().ToList(); - }; - foreach (var record in servicerecords) - { - string cmd = $"INSERT INTO app.servicerecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.servicerecords_id_seq', (SELECT MAX(id) from app.servicerecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("upgraderecords"); - upgraderecords = table.FindAll().ToList(); - }; - foreach (var record in upgraderecords) - { - string cmd = $"INSERT INTO app.upgraderecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.upgraderecords_id_seq', (SELECT MAX(id) from app.upgraderecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - #endregion - #region "Part2" - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("gasrecords"); - gasrecords = table.FindAll().ToList(); - }; - foreach (var record in gasrecords) - { - string cmd = $"INSERT INTO app.gasrecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.gasrecords_id_seq', (SELECT MAX(id) from app.gasrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("notes"); - noterecords = table.FindAll().ToList(); - }; - foreach (var record in noterecords) - { - string cmd = $"INSERT INTO app.notes (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.notes_id_seq', (SELECT MAX(id) from app.notes));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("odometerrecords"); - odometerrecords = table.FindAll().ToList(); - }; - foreach (var record in odometerrecords) - { - string cmd = $"INSERT INTO app.odometerrecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.odometerrecords_id_seq', (SELECT MAX(id) from app.odometerrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("reminderrecords"); - reminderrecords = table.FindAll().ToList(); - }; - foreach (var record in reminderrecords) - { - string cmd = $"INSERT INTO app.reminderrecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.reminderrecords_id_seq', (SELECT MAX(id) from app.reminderrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - #endregion - #region "Part3" - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("planrecords"); - planrecords = table.FindAll().ToList(); - }; - foreach (var record in planrecords) - { - string cmd = $"INSERT INTO app.planrecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.planrecords_id_seq', (SELECT MAX(id) from app.planrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("planrecordtemplates"); - planrecordtemplates = table.FindAll().ToList(); - }; - foreach (var record in planrecordtemplates) - { - string cmd = $"INSERT INTO app.planrecordtemplates (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.planrecordtemplates_id_seq', (SELECT MAX(id) from app.planrecordtemplates));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("supplyrecords"); - supplyrecords = table.FindAll().ToList(); - }; - foreach (var record in supplyrecords) - { - string cmd = $"INSERT INTO app.supplyrecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.supplyrecords_id_seq', (SELECT MAX(id) from app.supplyrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("taxrecords"); - taxrecords = table.FindAll().ToList(); - }; - foreach (var record in taxrecords) - { - string cmd = $"INSERT INTO app.taxrecords (id, vehicleId, data) VALUES(@id, @vehicleId, CAST(@data AS jsonb)); SELECT setval('app.taxrecords_id_seq', (SELECT MAX(id) from app.taxrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("vehicleId", record.VehicleId); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - #endregion - #region "Part4" - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("userrecords"); - userrecords = table.FindAll().ToList(); - }; - foreach (var record in userrecords) - { - string cmd = $"INSERT INTO app.userrecords (id, username, emailaddress, password, isadmin) VALUES(@id, @username, @emailaddress, @password, @isadmin); SELECT setval('app.userrecords_id_seq', (SELECT MAX(id) from app.userrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("username", record.UserName); - ctext.Parameters.AddWithValue("emailaddress", record.EmailAddress); - ctext.Parameters.AddWithValue("password", record.Password); - ctext.Parameters.AddWithValue("isadmin", record.IsAdmin); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("tokenrecords"); - tokenrecords = table.FindAll().ToList(); - }; - foreach (var record in tokenrecords) - { - string cmd = $"INSERT INTO app.tokenrecords (id, emailaddress, body) VALUES(@id, @emailaddress, @body); SELECT setval('app.tokenrecords_id_seq', (SELECT MAX(id) from app.tokenrecords));"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("emailaddress", record.EmailAddress); - ctext.Parameters.AddWithValue("body", record.Body); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("userconfigrecords"); - userconfigrecords = table.FindAll().ToList(); - }; - foreach (var record in userconfigrecords) - { - string cmd = $"INSERT INTO app.userconfigrecords (id, data) VALUES(@id, CAST(@data AS jsonb))"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("useraccessrecords"); - useraccessrecords = table.FindAll().ToList(); - }; - foreach (var record in useraccessrecords) - { - string cmd = $"INSERT INTO app.useraccessrecords (userId, vehicleId) VALUES(@userId, @vehicleId)"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("userId", record.Id.UserId); - ctext.Parameters.AddWithValue("vehicleId", record.Id.VehicleId); - ctext.ExecuteNonQuery(); - } - } - #endregion - #region "Part5" - using (var db = new LiteDatabase(fullFileName)) - { - var table = db.GetCollection("extrafields"); - extrafields = table.FindAll().ToList(); - }; - foreach (var record in extrafields) - { - string cmd = $"INSERT INTO app.extrafields (id, data) VALUES(@id, CAST(@data AS jsonb))"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - ctext.ExecuteNonQuery(); - } - } - #endregion - return Json(OperationResponse.Succeed("Data Imported Successfully")); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(OperationResponse.Failed()); - } - } - } -} diff --git a/Controllers/Vehicle/GasController.cs b/Controllers/Vehicle/GasController.cs deleted file mode 100644 index 3d3aca1..0000000 --- a/Controllers/Vehicle/GasController.cs +++ /dev/null @@ -1,217 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetGasRecordsByVehicleId(int vehicleId) - { - var result = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - //check if the user uses MPG or Liters per 100km. - var userConfig = _config.GetUserConfig(User); - bool useMPG = userConfig.UseMPG; - bool useUKMPG = userConfig.UseUKMPG; - var computedResults = _gasHelper.GetGasRecordViewModels(result, useMPG, useUKMPG); - if (userConfig.UseDescending) - { - computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList(); - } - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var vehicleIsElectric = vehicleData.IsElectric; - var vehicleUseHours = vehicleData.UseHours; - var viewModel = new GasRecordViewModelContainer() - { - UseKwh = vehicleIsElectric, - UseHours = vehicleUseHours, - GasRecords = computedResults - }; - return PartialView("Gas/_Gas", viewModel); - } - [HttpPost] - public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), gasRecord.VehicleId)) - { - return Json(false); - } - if (gasRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = DateTime.Parse(gasRecord.Date), - VehicleId = gasRecord.VehicleId, - Mileage = gasRecord.Mileage, - Notes = $"Auto Insert From Gas Record. {gasRecord.Notes}" - }); - } - gasRecord.Files = gasRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - var result = _gasRecordDataAccess.SaveGasRecordToVehicle(gasRecord.ToGasRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(gasRecord.ToGasRecord(), gasRecord.Id == default ? "gasrecord.add" : "gasrecord.update", User.Identity.Name)); - } - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetAddGasRecordPartialView(int vehicleId) - { - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var vehicleIsElectric = vehicleData.IsElectric; - var vehicleUseHours = vehicleData.UseHours; - return PartialView("Gas/_GasModal", new GasRecordInputContainer() { UseKwh = vehicleIsElectric, UseHours = vehicleUseHours, GasRecord = new GasRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields } }); - } - [HttpGet] - public IActionResult GetGasRecordForEditById(int gasRecordId) - { - var result = _gasRecordDataAccess.GetGasRecordById(gasRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - var convertedResult = new GasRecordInput - { - Id = result.Id, - Mileage = result.Mileage, - VehicleId = result.VehicleId, - Cost = result.Cost, - Date = result.Date.ToShortDateString(), - Files = result.Files, - Gallons = result.Gallons, - IsFillToFull = result.IsFillToFull, - MissedFuelUp = result.MissedFuelUp, - Notes = result.Notes, - Tags = result.Tags, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields) - }; - var vehicleData = _dataAccess.GetVehicleById(convertedResult.VehicleId); - var vehicleIsElectric = vehicleData.IsElectric; - var vehicleUseHours = vehicleData.UseHours; - var viewModel = new GasRecordInputContainer() - { - UseKwh = vehicleIsElectric, - UseHours = vehicleUseHours, - GasRecord = convertedResult - }; - return PartialView("Gas/_GasModal", viewModel); - } - private bool DeleteGasRecordWithChecks(int gasRecordId) - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(gasRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - var result = _gasRecordDataAccess.DeleteGasRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGasRecord(existingRecord, "gasrecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteGasRecordById(int gasRecordId) - { - var result = DeleteGasRecordWithChecks(gasRecordId); - return Json(result); - } - [HttpPost] - public IActionResult SaveUserGasTabPreferences(string gasUnit, string fuelMileageUnit) - { - var currentConfig = _config.GetUserConfig(User); - currentConfig.PreferredGasUnit = gasUnit; - currentConfig.PreferredGasMileageUnit = fuelMileageUnit; - var result = _config.SaveUserConfig(User, currentConfig); - return Json(result); - } - [HttpPost] - public IActionResult SaveSimpleFuelEntryPreference(bool useSimpleFuelEntry) - { - var currentConfig = _config.GetUserConfig(User); - currentConfig.UseSimpleFuelEntry = useSimpleFuelEntry; - var result = _config.SaveUserConfig(User, currentConfig); - return Json(result); - } - [HttpPost] - public IActionResult GetGasRecordsEditModal(List recordIds) - { - var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.GasRecord).ExtraFields; - return PartialView("Gas/_GasRecordsModal", new GasRecordEditModel { RecordIds = recordIds, EditRecord = new GasRecord { ExtraFields = extraFields } }); - } - [HttpPost] - public IActionResult SaveMultipleGasRecords(GasRecordEditModel editModel) - { - var dateIsEdited = editModel.EditRecord.Date != default; - var mileageIsEdited = editModel.EditRecord.Mileage != default; - var consumptionIsEdited = editModel.EditRecord.Gallons != default; - var costIsEdited = editModel.EditRecord.Cost != default; - var noteIsEdited = !string.IsNullOrWhiteSpace(editModel.EditRecord.Notes); - var tagsIsEdited = editModel.EditRecord.Tags.Any(); - var extraFieldIsEdited = editModel.EditRecord.ExtraFields.Any(); - //handle clear overrides - if (tagsIsEdited && editModel.EditRecord.Tags.Contains("---")) - { - editModel.EditRecord.Tags = new List(); - } - if (noteIsEdited && editModel.EditRecord.Notes == "---") - { - editModel.EditRecord.Notes = ""; - } - bool result = false; - foreach (int recordId in editModel.RecordIds) - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId); - if (dateIsEdited) - { - existingRecord.Date = editModel.EditRecord.Date; - } - if (consumptionIsEdited) - { - existingRecord.Gallons = editModel.EditRecord.Gallons; - } - if (costIsEdited) - { - existingRecord.Cost = editModel.EditRecord.Cost; - } - if (mileageIsEdited) - { - existingRecord.Mileage = editModel.EditRecord.Mileage; - } - if (noteIsEdited) - { - existingRecord.Notes = editModel.EditRecord.Notes; - } - if (tagsIsEdited) - { - existingRecord.Tags = editModel.EditRecord.Tags; - } - if (extraFieldIsEdited) - { - foreach (ExtraField extraField in editModel.EditRecord.ExtraFields) - { - if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name)) - { - var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name); - existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name); - existingRecord.ExtraFields.Insert(insertIndex, extraField); - } - else - { - existingRecord.ExtraFields.Add(extraField); - } - } - } - result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord); - } - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/ImportController.cs b/Controllers/Vehicle/ImportController.cs deleted file mode 100644 index d03ca13..0000000 --- a/Controllers/Vehicle/ImportController.cs +++ /dev/null @@ -1,614 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.MapProfile; -using MotoVaultPro.Models; -using CsvHelper; -using CsvHelper.Configuration; -using Microsoft.AspNetCore.Mvc; -using System.Globalization; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [HttpGet] - public IActionResult GetBulkImportModalPartialView(ImportMode mode) - { - return PartialView("_BulkDataImporter", mode); - } - [HttpGet] - public IActionResult GenerateCsvSample(ImportMode mode) - { - string uploadDirectory = "temp/"; - string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory); - if (!Directory.Exists(uploadPath)) - Directory.CreateDirectory(uploadPath); - var fileNameToExport = $"temp/{Guid.NewGuid()}.csv"; - var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false); - switch (mode) - { - case ImportMode.ServiceRecord: - case ImportMode.UpgradeRecord: - { - var exportData = new List { new GenericRecordExportModel - { - Date = DateTime.Now.ToShortDateString(), - Description = "Test", - Cost = 123.45M.ToString("C"), - Notes = "Test Note", - Odometer = 12345.ToString(), - Tags = "test1 test2" - } }; - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WriteGenericRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - } - break; - case ImportMode.GasRecord: - { - var exportData = new List { new GasRecordExportModel - { - Date = DateTime.Now.ToShortDateString(), - Odometer = 12345.ToString(), - FuelConsumed = 12.34M.ToString(), - Cost = 45.67M.ToString("C"), - IsFillToFull = true.ToString(), - MissedFuelUp = false.ToString(), - Notes = "Test Note", - Tags = "test1 test2" - } }; - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WriteGasRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - } - break; - case ImportMode.OdometerRecord: - { - var exportData = new List { new OdometerRecordExportModel - { - Date = DateTime.Now.ToShortDateString(), - InitialOdometer = 12345.ToString(), - Odometer = 12345.ToString(), - Notes = "Test Note", - Tags = "test1 test2" - } }; - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WriteOdometerRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - } - break; - case ImportMode.TaxRecord: - { - var exportData = new List { new TaxRecordExportModel - { - Date = DateTime.Now.ToShortDateString(), - Description = "Test", - Cost = 123.45M.ToString("C"), - Notes = "Test Note", - Tags = "test1 test2" - } }; - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WriteTaxRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - } - break; - case ImportMode.SupplyRecord: - { - var exportData = new List { new SupplyRecordExportModel - { - Date = DateTime.Now.ToShortDateString(), - PartNumber = "TEST-123456", - PartSupplier = "Test Supplier", - PartQuantity = 1.5M.ToString(), - Description = "Test", - Cost = 123.45M.ToString("C"), - Notes = "Test Note", - Tags = "test1 test2" - } }; - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WriteSupplyRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - } - break; - case ImportMode.PlanRecord: - { - var exportData = new List { new PlanRecordExportModel - { - DateCreated = DateTime.Now.ToString(), - DateModified = DateTime.Now.ToString(), - Description = "Test", - Type = ImportMode.ServiceRecord.ToString(), - Priority = PlanPriority.Normal.ToString(), - Progress = PlanProgress.Testing.ToString(), - Cost = 123.45M.ToString("C"), - Notes = "Test Note" - } }; - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WritePlanRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - } - break; - default: - return Json(OperationResponse.Failed("No parameters")); - } - try - { - var fileBytes = _fileHelper.GetFileBytes(fullExportFilePath, true); - if (fileBytes.Length > 0) - { - return File(fileBytes, "text/csv", $"{mode.ToString().ToLower()}sample.csv"); - } - else - { - return Json(OperationResponse.Failed("An error has occurred while generating CSV sample: file has zero bytes")); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(OperationResponse.Failed($"An error has occurred while generating CSV sample: {ex.Message}")); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode) - { - if (vehicleId == default && mode != ImportMode.SupplyRecord) - { - return Json(false); - } - string uploadDirectory = "temp/"; - string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", uploadDirectory); - if (!Directory.Exists(uploadPath)) - Directory.CreateDirectory(uploadPath); - var fileNameToExport = $"temp/{Guid.NewGuid()}.csv"; - var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false); - if (mode == ImportMode.ServiceRecord) - { - var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - if (vehicleRecords.Any()) - { - var exportData = vehicleRecords.Select(x => new GenericRecordExportModel - { - Date = x.Date.ToShortDateString(), - Description = x.Description, - Cost = x.Cost.ToString("C"), - Notes = x.Notes, - Odometer = x.Mileage.ToString(), - Tags = string.Join(" ", x.Tags), - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - //custom writer - StaticHelper.WriteGenericRecordExportModel(csv, exportData); - } - writer.Dispose(); - } - return Json($"/{fileNameToExport}"); - } - } - else if (mode == ImportMode.UpgradeRecord) - { - var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - if (vehicleRecords.Any()) - { - var exportData = vehicleRecords.Select(x => new GenericRecordExportModel - { - Date = x.Date.ToShortDateString(), - Description = x.Description, - Cost = x.Cost.ToString("C"), - Notes = x.Notes, - Odometer = x.Mileage.ToString(), - Tags = string.Join(" ", x.Tags), - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - StaticHelper.WriteGenericRecordExportModel(csv, exportData); - } - } - return Json($"/{fileNameToExport}"); - } - } - else if (mode == ImportMode.OdometerRecord) - { - var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - if (vehicleRecords.Any()) - { - var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel - { - Date = x.Date.ToShortDateString(), - Notes = x.Notes, - InitialOdometer = x.InitialMileage.ToString(), - Odometer = x.Mileage.ToString(), - Tags = string.Join(" ", x.Tags), - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - StaticHelper.WriteOdometerRecordExportModel(csv, exportData); - } - } - return Json($"/{fileNameToExport}"); - } - } - else if (mode == ImportMode.SupplyRecord) - { - var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId); - if (vehicleRecords.Any()) - { - var exportData = vehicleRecords.Select(x => new SupplyRecordExportModel - { - Date = x.Date.ToShortDateString(), - Description = x.Description, - Cost = x.Cost.ToString("C"), - PartNumber = x.PartNumber, - PartQuantity = x.Quantity.ToString(), - PartSupplier = x.PartSupplier, - Notes = x.Notes, - Tags = string.Join(" ", x.Tags), - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - StaticHelper.WriteSupplyRecordExportModel(csv, exportData); - } - } - return Json($"/{fileNameToExport}"); - } - } - else if (mode == ImportMode.TaxRecord) - { - var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - if (vehicleRecords.Any()) - { - var exportData = vehicleRecords.Select(x => new TaxRecordExportModel - { - Date = x.Date.ToShortDateString(), - Description = x.Description, - Cost = x.Cost.ToString("C"), - Notes = x.Notes, - Tags = string.Join(" ", x.Tags), - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - StaticHelper.WriteTaxRecordExportModel(csv, exportData); - } - } - return Json($"/{fileNameToExport}"); - } - } - else if (mode == ImportMode.PlanRecord) - { - var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); - if (vehicleRecords.Any()) - { - var exportData = vehicleRecords.Select(x => new PlanRecordExportModel - { - DateCreated = x.DateCreated.ToString("G"), - DateModified = x.DateModified.ToString("G"), - Description = x.Description, - Cost = x.Cost.ToString("C"), - Type = x.ImportMode.ToString(), - Priority = x.Priority.ToString(), - Progress = x.Progress.ToString(), - Notes = x.Notes, - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - StaticHelper.WritePlanRecordExportModel(csv, exportData); - } - } - return Json($"/{fileNameToExport}"); - } - } - else if (mode == ImportMode.GasRecord) - { - var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - bool useMPG = _config.GetUserConfig(User).UseMPG; - bool useUKMPG = _config.GetUserConfig(User).UseUKMPG; - var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG); - var exportData = convertedRecords.Select(x => new GasRecordExportModel - { - Date = x.Date.ToString(), - Cost = x.Cost.ToString(), - FuelConsumed = x.Gallons.ToString(), - FuelEconomy = x.MilesPerGallon.ToString(), - Odometer = x.Mileage.ToString(), - IsFillToFull = x.IsFillToFull.ToString(), - MissedFuelUp = x.MissedFuelUp.ToString(), - Notes = x.Notes, - Tags = string.Join(" ", x.Tags), - ExtraFields = x.ExtraFields - }); - using (var writer = new StreamWriter(fullExportFilePath)) - { - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - StaticHelper.WriteGasRecordExportModel(csv, exportData); - } - } - return Json($"/{fileNameToExport}"); - } - return Json(false); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName) - { - if (vehicleId == default && mode != ImportMode.SupplyRecord) - { - return Json(false); - } - if (string.IsNullOrWhiteSpace(fileName)) - { - return Json(false); - } - var fullFileName = _fileHelper.GetFullFilePath(fileName); - if (string.IsNullOrWhiteSpace(fullFileName)) - { - return Json(false); - } - try - { - using (var reader = new StreamReader(fullFileName)) - { - var config = new CsvConfiguration(CultureInfo.InvariantCulture); - config.MissingFieldFound = null; - config.HeaderValidated = null; - config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); }; - using (var csv = new CsvReader(reader, config)) - { - csv.Context.RegisterClassMap(); - var records = csv.GetRecords().ToList(); - if (records.Any()) - { - var requiredExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)mode).ExtraFields.Where(x => x.IsRequired).Select(y => y.Name); - foreach (ImportModel importModel in records) - { - var parsedDate = DateTime.Now.Date; - if (!string.IsNullOrWhiteSpace(importModel.Date)) - { - parsedDate = DateTime.Parse(importModel.Date); - } - else if (!string.IsNullOrWhiteSpace(importModel.Day) && !string.IsNullOrWhiteSpace(importModel.Month) && !string.IsNullOrWhiteSpace(importModel.Year)) - { - parsedDate = new DateTime(int.Parse(importModel.Year), int.Parse(importModel.Month), int.Parse(importModel.Day)); - } - if (mode == ImportMode.GasRecord) - { - //convert to gas model. - var convertedRecord = new GasRecord() - { - VehicleId = vehicleId, - Date = parsedDate, - Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)), - Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any), - Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, - Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price)) - { - //cost was not given but price is. - //fuelly sometimes exports CSVs without total cost. - var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any); - convertedRecord.Cost = convertedRecord.Gallons * parsedPrice; - } - else - { - convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any); - } - if (string.IsNullOrWhiteSpace(importModel.IsFillToFull) && !string.IsNullOrWhiteSpace(importModel.PartialFuelUp)) - { - var parsedBool = importModel.PartialFuelUp.Trim() == "1"; - convertedRecord.IsFillToFull = !parsedBool; - } - else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull)) - { - var possibleFillToFullValues = new List { "1", "true", "full" }; - var parsedBool = possibleFillToFullValues.Contains(importModel.IsFillToFull.Trim().ToLower()); - convertedRecord.IsFillToFull = parsedBool; - } - if (!string.IsNullOrWhiteSpace(importModel.MissedFuelUp)) - { - var possibleMissedFuelUpValues = new List { "1", "true" }; - var parsedBool = possibleMissedFuelUpValues.Contains(importModel.MissedFuelUp.Trim().ToLower()); - convertedRecord.MissedFuelUp = parsedBool; - } - //insert record into db, check to make sure fuelconsumed is not zero so we don't get a divide by zero error. - if (convertedRecord.Gallons > 0) - { - _gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord); - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = convertedRecord.Date, - VehicleId = convertedRecord.VehicleId, - Mileage = convertedRecord.Mileage, - Notes = $"Auto Insert From Gas Record via CSV Import. {convertedRecord.Notes}" - }); - } - } - } - else if (mode == ImportMode.ServiceRecord) - { - var convertedRecord = new ServiceRecord() - { - VehicleId = vehicleId, - Date = parsedDate, - Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)), - Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {parsedDate.ToShortDateString()}" : importModel.Description, - Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, - Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), - Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - _serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord); - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = convertedRecord.Date, - VehicleId = convertedRecord.VehicleId, - Mileage = convertedRecord.Mileage, - Notes = $"Auto Insert From Service Record via CSV Import. {convertedRecord.Notes}" - }); - } - } - else if (mode == ImportMode.OdometerRecord) - { - var convertedRecord = new OdometerRecord() - { - VehicleId = vehicleId, - Date = parsedDate, - InitialMileage = string.IsNullOrWhiteSpace(importModel.InitialOdometer) ? 0 : decimal.ToInt32(decimal.Parse(importModel.InitialOdometer, NumberStyles.Any)), - Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)), - Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, - Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - _odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord); - } - else if (mode == ImportMode.PlanRecord) - { - var progressIsEnum = Enum.TryParse(importModel.Progress, out PlanProgress parsedProgress); - var typeIsEnum = Enum.TryParse(importModel.Type, out ImportMode parsedType); - var priorityIsEnum = Enum.TryParse(importModel.Priority, out PlanPriority parsedPriority); - var convertedRecord = new PlanRecord() - { - VehicleId = vehicleId, - DateCreated = DateTime.Parse(importModel.DateCreated), - DateModified = DateTime.Parse(importModel.DateModified), - Progress = parsedProgress, - ImportMode = parsedType, - Priority = parsedPriority, - Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Plan Record on {importModel.DateCreated}" : importModel.Description, - Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, - Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - _planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord); - } - else if (mode == ImportMode.UpgradeRecord) - { - var convertedRecord = new UpgradeRecord() - { - VehicleId = vehicleId, - Date = parsedDate, - Mileage = decimal.ToInt32(decimal.Parse(importModel.Odometer, NumberStyles.Any)), - Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {parsedDate.ToShortDateString()}" : importModel.Description, - Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, - Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), - Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord); - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = convertedRecord.Date, - VehicleId = convertedRecord.VehicleId, - Mileage = convertedRecord.Mileage, - Notes = $"Auto Insert From Upgrade Record via CSV Import. {convertedRecord.Notes}" - }); - } - } - else if (mode == ImportMode.SupplyRecord) - { - var convertedRecord = new SupplyRecord() - { - VehicleId = vehicleId, - Date = parsedDate, - PartNumber = importModel.PartNumber, - PartSupplier = importModel.PartSupplier, - Quantity = decimal.Parse(importModel.PartQuantity, NumberStyles.Any), - Description = importModel.Description, - Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), - Notes = importModel.Notes, - Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - _supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord); - } - else if (mode == ImportMode.TaxRecord) - { - var convertedRecord = new TaxRecord() - { - VehicleId = vehicleId, - Date = parsedDate, - Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {parsedDate.ToShortDateString()}" : importModel.Description, - Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes, - Cost = decimal.Parse(importModel.Cost, NumberStyles.Any), - Tags = string.IsNullOrWhiteSpace(importModel.Tags) ? [] : importModel.Tags.Split(" ").ToList(), - ExtraFields = importModel.ExtraFields.Any() ? importModel.ExtraFields.Select(x => new ExtraField { Name = x.Key, Value = x.Value, IsRequired = requiredExtraFields.Contains(x.Key) }).ToList() : new List() - }; - _taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord); - } - } - } - } - } - return Json(true); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error Occurred While Bulk Inserting"); - return Json(false); - } - } - } -} diff --git a/Controllers/Vehicle/NoteController.cs b/Controllers/Vehicle/NoteController.cs deleted file mode 100644 index dabddaf..0000000 --- a/Controllers/Vehicle/NoteController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetNotesByVehicleId(int vehicleId) - { - var result = _noteDataAccess.GetNotesByVehicleId(vehicleId); - result = result.OrderByDescending(x => x.Pinned).ThenBy(x => x.Description).ToList(); - return PartialView("Note/_Notes", result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetPinnedNotesByVehicleId(int vehicleId) - { - var result = _noteDataAccess.GetNotesByVehicleId(vehicleId); - result = result.Where(x => x.Pinned).ToList(); - return Json(result); - } - [HttpPost] - public IActionResult SaveNoteToVehicleId(Note note) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), note.VehicleId)) - { - return Json(false); - } - note.Files = note.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - bool isCreate = note.Id == default; //needed here since Notes don't use an input object. - var result = _noteDataAccess.SaveNoteToVehicle(note); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromNoteRecord(note, isCreate ? "noterecord.add" : "noterecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpGet] - public IActionResult GetAddNotePartialView() - { - var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.NoteRecord).ExtraFields; - return PartialView("Note/_NoteModal", new Note() { ExtraFields = extraFields }); - } - [HttpGet] - public IActionResult GetNoteForEditById(int noteId) - { - var result = _noteDataAccess.GetNoteById(noteId); - result.ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.NoteRecord).ExtraFields); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - return PartialView("Note/_NoteModal", result); - } - private bool DeleteNoteWithChecks(int noteId) - { - var existingRecord = _noteDataAccess.GetNoteById(noteId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - var result = _noteDataAccess.DeleteNoteById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromNoteRecord(existingRecord, "noterecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteNoteById(int noteId) - { - var result = DeleteNoteWithChecks(noteId); - return Json(result); - } - [HttpPost] - public IActionResult PinNotes(List noteIds, bool isToggle = false, bool pinStatus = false) - { - var result = false; - foreach (int noteId in noteIds) - { - var existingNote = _noteDataAccess.GetNoteById(noteId); - if (isToggle) - { - existingNote.Pinned = !existingNote.Pinned; - } - else - { - existingNote.Pinned = pinStatus; - } - result = _noteDataAccess.SaveNoteToVehicle(existingNote); - } - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/OdometerController.cs b/Controllers/Vehicle/OdometerController.cs deleted file mode 100644 index 46741aa..0000000 --- a/Controllers/Vehicle/OdometerController.cs +++ /dev/null @@ -1,176 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult ForceRecalculateDistanceByVehicleId(int vehicleId) - { - var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - result = _odometerLogic.AutoConvertOdometerRecord(result); - return Json(result.Any()); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetOdometerRecordsByVehicleId(int vehicleId) - { - var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - //determine if conversion is needed. - if (result.All(x => x.InitialMileage == default)) - { - result = _odometerLogic.AutoConvertOdometerRecord(result); - } - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList(); - } - else - { - result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList(); - } - return PartialView("Odometer/_OdometerRecords", result); - } - [HttpPost] - public IActionResult SaveOdometerRecordToVehicleId(OdometerRecordInput odometerRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), odometerRecord.VehicleId)) - { - return Json(false); - } - //move files from temp. - odometerRecord.Files = odometerRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - var result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord.ToOdometerRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(odometerRecord.ToOdometerRecord(), odometerRecord.Id == default ? "odometerrecord.add" : "odometerrecord.update", User.Identity.Name)); - } - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetAddOdometerRecordPartialView(int vehicleId) - { - return PartialView("Odometer/_OdometerRecordModal", new OdometerRecordInput() { InitialMileage = _odometerLogic.GetLastOdometerRecordMileage(vehicleId, new List()), ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields }); - } - [HttpPost] - public IActionResult GetOdometerRecordsEditModal(List recordIds) - { - var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields; - return PartialView("Odometer/_OdometerRecordsModal", new OdometerRecordEditModel { RecordIds = recordIds, EditRecord = new OdometerRecord { ExtraFields = extraFields } }); - } - [HttpPost] - public IActionResult SaveMultipleOdometerRecords(OdometerRecordEditModel editModel) - { - var dateIsEdited = editModel.EditRecord.Date != default; - var initialMileageIsEdited = editModel.EditRecord.InitialMileage != default; - var mileageIsEdited = editModel.EditRecord.Mileage != default; - var noteIsEdited = !string.IsNullOrWhiteSpace(editModel.EditRecord.Notes); - var tagsIsEdited = editModel.EditRecord.Tags.Any(); - var extraFieldIsEdited = editModel.EditRecord.ExtraFields.Any(); - //handle clear overrides - if (tagsIsEdited && editModel.EditRecord.Tags.Contains("---")) - { - editModel.EditRecord.Tags = new List(); - } - if (noteIsEdited && editModel.EditRecord.Notes == "---") - { - editModel.EditRecord.Notes = ""; - } - bool result = false; - foreach (int recordId in editModel.RecordIds) - { - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId); - if (dateIsEdited) - { - existingRecord.Date = editModel.EditRecord.Date; - } - if (initialMileageIsEdited) - { - existingRecord.InitialMileage = editModel.EditRecord.InitialMileage; - } - if (mileageIsEdited) - { - existingRecord.Mileage = editModel.EditRecord.Mileage; - } - if (noteIsEdited) - { - existingRecord.Notes = editModel.EditRecord.Notes; - } - if (tagsIsEdited) - { - existingRecord.Tags = editModel.EditRecord.Tags; - } - if (extraFieldIsEdited) - { - foreach (ExtraField extraField in editModel.EditRecord.ExtraFields) - { - if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name)) - { - var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name); - existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name); - existingRecord.ExtraFields.Insert(insertIndex, extraField); - } - else - { - existingRecord.ExtraFields.Add(extraField); - } - } - } - result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord); - } - return Json(result); - } - [HttpGet] - public IActionResult GetOdometerRecordForEditById(int odometerRecordId) - { - var result = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - //convert to Input object. - var convertedResult = new OdometerRecordInput - { - Id = result.Id, - Date = result.Date.ToShortDateString(), - InitialMileage = result.InitialMileage, - Mileage = result.Mileage, - Notes = result.Notes, - VehicleId = result.VehicleId, - Files = result.Files, - Tags = result.Tags, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.OdometerRecord).ExtraFields) - }; - return PartialView("Odometer/_OdometerRecordModal", convertedResult); - } - private bool DeleteOdometerRecordWithChecks(int odometerRecordId) - { - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - var result = _odometerRecordDataAccess.DeleteOdometerRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromOdometerRecord(existingRecord, "odometerrecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteOdometerRecordById(int odometerRecordId) - { - var result = DeleteOdometerRecordWithChecks(odometerRecordId); - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/PlanController.cs b/Controllers/Vehicle/PlanController.cs deleted file mode 100644 index ed5395c..0000000 --- a/Controllers/Vehicle/PlanController.cs +++ /dev/null @@ -1,308 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetPlanRecordsByVehicleId(int vehicleId) - { - var result = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); - return PartialView("Plan/_PlanRecords", result); - } - [HttpPost] - public IActionResult SavePlanRecordToVehicleId(PlanRecordInput planRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), planRecord.VehicleId)) - { - return Json(false); - } - //populate createdDate - if (planRecord.Id == default) - { - planRecord.DateCreated = DateTime.Now.ToString("G"); - } - planRecord.DateModified = DateTime.Now.ToString("G"); - //move files from temp. - planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - if (planRecord.Supplies.Any()) - { - planRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(planRecord.Supplies, DateTime.Parse(planRecord.DateCreated), planRecord.Description)); - if (planRecord.CopySuppliesAttachment) - { - planRecord.Files.AddRange(GetSuppliesAttachments(planRecord.Supplies)); - } - } - if (planRecord.DeletedRequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(planRecord.DeletedRequisitionHistory, planRecord.Description); - } - var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord.ToPlanRecord(), planRecord.Id == default ? "planrecord.add" : "planrecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpPost] - public IActionResult SavePlanRecordTemplateToVehicleId(PlanRecordInput planRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), planRecord.VehicleId)) - { - return Json(OperationResponse.Failed("Access Denied")); - } - //check if template name already taken. - var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(planRecord.VehicleId).Where(x => x.Description == planRecord.Description).Any(); - if (planRecord.Id == default && existingRecord) - { - return Json(OperationResponse.Failed("A template with that description already exists for this vehicle")); - } - planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - var result = _planRecordTemplateDataAccess.SavePlanRecordTemplateToVehicle(planRecord); - return Json(OperationResponse.Conditional(result, "Template Added", string.Empty)); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetPlanRecordTemplatesForVehicleId(int vehicleId) - { - var result = _planRecordTemplateDataAccess.GetPlanRecordTemplatesByVehicleId(vehicleId); - return PartialView("Plan/_PlanRecordTemplateModal", result); - } - [HttpPost] - public IActionResult DeletePlanRecordTemplateById(int planRecordTemplateId) - { - var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId); - if (existingRecord.Id == default) - { - return Json(false); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return Json(false); - } - var result = _planRecordTemplateDataAccess.DeletePlanRecordTemplateById(planRecordTemplateId); - return Json(result); - } - [HttpGet] - public IActionResult OrderPlanSupplies(int planRecordTemplateId) - { - var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId); - if (existingRecord.Id == default) - { - return Json(OperationResponse.Failed("Unable to find template")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return Json(OperationResponse.Failed("Access Denied")); - } - if (existingRecord.Supplies.Any()) - { - var suppliesToOrder = CheckSupplyRecordsAvailability(existingRecord.Supplies); - return PartialView("Plan/_PlanOrderSupplies", suppliesToOrder); - } - else - { - return Json(OperationResponse.Failed("Template has No Supplies")); - } - } - [HttpPost] - public IActionResult ConvertPlanRecordTemplateToPlanRecord(int planRecordTemplateId) - { - var existingRecord = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId); - if (existingRecord.Id == default) - { - return Json(OperationResponse.Failed("Unable to find template")); - } - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return Json(OperationResponse.Failed("Access Denied")); - } - if (existingRecord.Supplies.Any()) - { - //check if all supplies are available - var supplyAvailability = CheckSupplyRecordsAvailability(existingRecord.Supplies); - if (supplyAvailability.Any(x => x.Missing)) - { - return Json(OperationResponse.Failed("Missing Supplies, Please Delete This Template and Recreate It.")); - } - else if (supplyAvailability.Any(x => x.Insufficient)) - { - return Json(OperationResponse.Failed("Insufficient Supplies")); - } - } - if (existingRecord.ReminderRecordId != default) - { - //check if reminder still exists and is still recurring. - var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(existingRecord.ReminderRecordId); - if (existingReminder is null || existingReminder.Id == default || !existingReminder.IsRecurring) - { - return Json(OperationResponse.Failed("Missing or Non-recurring Reminder, Please Delete This Template and Recreate It.")); - } - } - //populate createdDate - existingRecord.DateCreated = DateTime.Now.ToString("G"); - existingRecord.DateModified = DateTime.Now.ToString("G"); - existingRecord.Id = default; - if (existingRecord.Supplies.Any()) - { - existingRecord.RequisitionHistory = RequisitionSupplyRecordsByUsage(existingRecord.Supplies, DateTime.Parse(existingRecord.DateCreated), existingRecord.Description); - if (existingRecord.CopySuppliesAttachment) - { - existingRecord.Files.AddRange(GetSuppliesAttachments(existingRecord.Supplies)); - } - } - var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord.ToPlanRecord()); - return Json(OperationResponse.Conditional(result, "Plan Record Added", string.Empty)); - } - [HttpGet] - public IActionResult GetAddPlanRecordPartialView() - { - return PartialView("Plan/_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields }); - } - [HttpPost] - public IActionResult GetAddPlanRecordPartialView(PlanRecordInput? planModel) - { - if (planModel is not null) - { - planModel.ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields; - return PartialView("Plan/_PlanRecordModal", planModel); - } - return PartialView("Plan/_PlanRecordModal", new PlanRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields }); - } - [HttpPost] - public IActionResult UpdatePlanRecordProgress(int planRecordId, PlanProgress planProgress, int odometer = 0) - { - if (planRecordId == default) - { - return Json(false); - } - var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return Json(false); - } - existingRecord.Progress = planProgress; - existingRecord.DateModified = DateTime.Now; - var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord); - if (planProgress == PlanProgress.Done) - { - if (_config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = DateTime.Now.Date, - VehicleId = existingRecord.VehicleId, - Mileage = odometer, - Notes = $"Auto Insert From Plan Record: {existingRecord.Description}", - ExtraFields = existingRecord.ExtraFields - }); - } - //convert plan record to service/upgrade/repair record. - if (existingRecord.ImportMode == ImportMode.ServiceRecord) - { - var newRecord = new ServiceRecord() - { - VehicleId = existingRecord.VehicleId, - Date = DateTime.Now.Date, - Mileage = odometer, - Description = existingRecord.Description, - Cost = existingRecord.Cost, - Notes = existingRecord.Notes, - Files = existingRecord.Files, - RequisitionHistory = existingRecord.RequisitionHistory, - ExtraFields = existingRecord.ExtraFields - }; - _serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord); - } - else if (existingRecord.ImportMode == ImportMode.UpgradeRecord) - { - var newRecord = new UpgradeRecord() - { - VehicleId = existingRecord.VehicleId, - Date = DateTime.Now.Date, - Mileage = odometer, - Description = existingRecord.Description, - Cost = existingRecord.Cost, - Notes = existingRecord.Notes, - Files = existingRecord.Files, - RequisitionHistory = existingRecord.RequisitionHistory, - ExtraFields = existingRecord.ExtraFields - }; - _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(newRecord); - } - //push back any reminders - if (existingRecord.ReminderRecordId != default) - { - PushbackRecurringReminderRecordWithChecks(existingRecord.ReminderRecordId, DateTime.Now, odometer); - } - } - return Json(result); - } - [HttpGet] - public IActionResult GetPlanRecordTemplateForEditById(int planRecordTemplateId) - { - var result = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId); - return PartialView("Plan/_PlanRecordTemplateEditModal", result); - } - [HttpGet] - public IActionResult GetPlanRecordForEditById(int planRecordId) - { - var result = _planRecordDataAccess.GetPlanRecordById(planRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - //convert to Input object. - var convertedResult = new PlanRecordInput - { - Id = result.Id, - Description = result.Description, - DateCreated = result.DateCreated.ToString("G"), - DateModified = result.DateModified.ToString("G"), - ImportMode = result.ImportMode, - Priority = result.Priority, - Progress = result.Progress, - Cost = result.Cost, - Notes = result.Notes, - VehicleId = result.VehicleId, - Files = result.Files, - RequisitionHistory = result.RequisitionHistory, - ReminderRecordId = result.ReminderRecordId, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.PlanRecord).ExtraFields) - }; - return PartialView("Plan/_PlanRecordModal", convertedResult); - } - [HttpPost] - public IActionResult DeletePlanRecordById(int planRecordId) - { - var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return Json(false); - } - //restore any requisitioned supplies if it has not been converted to other record types. - if (existingRecord.RequisitionHistory.Any() && existingRecord.Progress != PlanProgress.Done) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); - } - var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete", User.Identity.Name)); - } - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/ReminderController.cs b/Controllers/Vehicle/ReminderController.cs deleted file mode 100644 index c6d7f9e..0000000 --- a/Controllers/Vehicle/ReminderController.cs +++ /dev/null @@ -1,187 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - private List GetRemindersAndUrgency(int vehicleId, DateTime dateCompare) - { - var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId); - var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); - List results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare); - return results; - } - private bool GetAndUpdateVehicleUrgentOrPastDueReminders(int vehicleId) - { - var result = GetRemindersAndUrgency(vehicleId, DateTime.Now); - //check if user wants auto-refresh past-due reminders - if (_config.GetUserConfig(User).EnableAutoReminderRefresh) - { - //check for past due reminders that are eligible for recurring. - var pastDueAndRecurring = result.Where(x => x.Urgency == ReminderUrgency.PastDue && x.IsRecurring); - if (pastDueAndRecurring.Any()) - { - foreach (ReminderRecordViewModel reminderRecord in pastDueAndRecurring) - { - //update based on recurring intervals. - //pull reminderRecord based on ID - var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecord.Id); - existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder, null, null); - //save to db. - _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder); - //set urgency to not urgent so it gets excluded in count. - reminderRecord.Urgency = ReminderUrgency.NotUrgent; - } - } - } - //check for very urgent or past due reminders that were not eligible for recurring. - var pastDueAndUrgentReminders = result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue); - if (pastDueAndUrgentReminders.Any()) - { - return true; - } - return false; - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId) - { - var result = GetAndUpdateVehicleUrgentOrPastDueReminders(vehicleId); - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetReminderRecordsByVehicleId(int vehicleId) - { - var result = GetRemindersAndUrgency(vehicleId, DateTime.Now); - result = result.OrderByDescending(x => x.Urgency).ToList(); - return PartialView("Reminder/_ReminderRecords", result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetRecurringReminderRecordsByVehicleId(int vehicleId) - { - var result = GetRemindersAndUrgency(vehicleId, DateTime.Now); - result.RemoveAll(x => !x.IsRecurring); - result = result.OrderByDescending(x => x.Urgency).ThenBy(x => x.Description).ToList(); - return PartialView("_RecurringReminderSelector", result); - } - [HttpPost] - public IActionResult PushbackRecurringReminderRecord(int reminderRecordId) - { - var result = PushbackRecurringReminderRecordWithChecks(reminderRecordId, null, null); - return Json(result); - } - private bool PushbackRecurringReminderRecordWithChecks(int reminderRecordId, DateTime? currentDate, int? currentMileage) - { - try - { - var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId); - if (existingReminder is not null && existingReminder.Id != default && existingReminder.IsRecurring) - { - existingReminder = _reminderHelper.GetUpdatedRecurringReminderRecord(existingReminder, currentDate, currentMileage); - //save to db. - var reminderUpdateResult = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder); - if (!reminderUpdateResult) - { - _logger.LogError("Unable to update reminder either because the reminder no longer exists or is no longer recurring"); - return false; - } - return true; - } - else - { - _logger.LogError("Unable to update reminder because it no longer exists."); - return false; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - [HttpPost] - public IActionResult SaveReminderRecordToVehicleId(ReminderRecordInput reminderRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), reminderRecord.VehicleId)) - { - return Json(false); - } - var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord.ToReminderRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(reminderRecord.ToReminderRecord(), reminderRecord.Id == default ? "reminderrecord.add" : "reminderrecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpPost] - public IActionResult GetAddReminderRecordPartialView(ReminderRecordInput? reminderModel) - { - if (reminderModel is not null) - { - return PartialView("Reminder/_ReminderRecordModal", reminderModel); - } - else - { - return PartialView("Reminder/_ReminderRecordModal", new ReminderRecordInput()); - } - } - [HttpGet] - public IActionResult GetReminderRecordForEditById(int reminderRecordId) - { - var result = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - //convert to Input object. - var convertedResult = new ReminderRecordInput - { - Id = result.Id, - Date = result.Date.ToShortDateString(), - Description = result.Description, - Notes = result.Notes, - VehicleId = result.VehicleId, - Mileage = result.Mileage, - Metric = result.Metric, - IsRecurring = result.IsRecurring, - UseCustomThresholds = result.UseCustomThresholds, - CustomThresholds = result.CustomThresholds, - ReminderMileageInterval = result.ReminderMileageInterval, - ReminderMonthInterval = result.ReminderMonthInterval, - CustomMileageInterval = result.CustomMileageInterval, - CustomMonthInterval = result.CustomMonthInterval, - CustomMonthIntervalUnit = result.CustomMonthIntervalUnit, - Tags = result.Tags - }; - return PartialView("Reminder/_ReminderRecordModal", convertedResult); - } - private bool DeleteReminderRecordWithChecks(int reminderRecordId) - { - var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - var result = _reminderRecordDataAccess.DeleteReminderRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteReminderRecordById(int reminderRecordId) - { - var result = DeleteReminderRecordWithChecks(reminderRecordId); - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/ReportController.cs b/Controllers/Vehicle/ReportController.cs deleted file mode 100644 index a70e321..0000000 --- a/Controllers/Vehicle/ReportController.cs +++ /dev/null @@ -1,708 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; -using System.Globalization; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetReportPartialView(int vehicleId) - { - //get records - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId); - var serviceRecords = vehicleRecords.ServiceRecords; - var gasRecords = vehicleRecords.GasRecords; - var taxRecords = vehicleRecords.TaxRecords; - var upgradeRecords = vehicleRecords.UpgradeRecords; - var odometerRecords = vehicleRecords.OdometerRecords; - var userConfig = _config.GetUserConfig(User); - var viewModel = new ReportViewModel() { ReportHeaderForVehicle = new ReportHeader() }; - //check if custom widgets are configured - viewModel.CustomWidgetsConfigured = _fileHelper.WidgetsExist(); - //get totalCostMakeUp - viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle - { - ServiceRecordSum = serviceRecords.Sum(x => x.Cost), - GasRecordSum = gasRecords.Sum(x => x.Cost), - TaxRecordSum = taxRecords.Sum(x => x.Cost), - UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost) - }; - //get costbymonth - List allCosts = StaticHelper.GetBaseLineCosts(); - allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0)); - allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0)); - allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, 0)); - allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, 0)); - allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, 0)); - viewModel.CostForVehicleByMonth = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth - { - MonthName = x.Key.MonthName, - Cost = x.Sum(y => y.Cost), - DistanceTraveled = x.Max(y => y.DistanceTraveled) - }).ToList(); - - //set available metrics - var visibleTabs = userConfig.VisibleTabs; - if (visibleTabs.Contains(ImportMode.OdometerRecord) || odometerRecords.Any()) - { - viewModel.AvailableMetrics.Add(ImportMode.OdometerRecord); - } - if (visibleTabs.Contains(ImportMode.ServiceRecord) || serviceRecords.Any()) - { - viewModel.AvailableMetrics.Add(ImportMode.ServiceRecord); - } - if (visibleTabs.Contains(ImportMode.UpgradeRecord) || upgradeRecords.Any()) - { - viewModel.AvailableMetrics.Add(ImportMode.UpgradeRecord); - } - if (visibleTabs.Contains(ImportMode.GasRecord) || gasRecords.Any()) - { - viewModel.AvailableMetrics.Add(ImportMode.GasRecord); - } - if (visibleTabs.Contains(ImportMode.TaxRecord) || taxRecords.Any()) - { - viewModel.AvailableMetrics.Add(ImportMode.TaxRecord); - } - - //get reminders - var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now); - viewModel.ReminderMakeUpForVehicle = new ReminderMakeUpForVehicle - { - NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(), - UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(), - VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(), - PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count() - }; - //populate year dropdown. - var numbersArray = new List(); - if (serviceRecords.Any()) - { - numbersArray.Add(serviceRecords.Min(x => x.Date.Year)); - } - if (gasRecords.Any()) - { - numbersArray.Add(gasRecords.Min(x => x.Date.Year)); - } - if (upgradeRecords.Any()) - { - numbersArray.Add(upgradeRecords.Min(x => x.Date.Year)); - } - if (odometerRecords.Any()) - { - numbersArray.Add(odometerRecords.Min(x => x.Date.Year)); - } - var minYear = numbersArray.Any() ? numbersArray.Min() : DateTime.Now.AddYears(-5).Year; - var yearDifference = DateTime.Now.Year - minYear + 1; - for (int i = 0; i < yearDifference; i++) - { - viewModel.Years.Add(DateTime.Now.AddYears(i * -1).Year); - } - //get collaborators - var collaborators = _userLogic.GetCollaboratorsForVehicle(vehicleId); - viewModel.Collaborators = collaborators; - //get MPG per month. - var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG); - string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit; - var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG); - var averageMPG = _gasHelper.GetAverageGasMileage(mileageData, userConfig.UseMPG); - mileageData.RemoveAll(x => x.MilesPerGallon == default); - bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l"; - var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName(); - monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - Cost = x.Average(y => y.MilesPerGallon) - })); - monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = x.Sum(y => y.Cost) - }).ToList(); - if (invertedFuelMileageUnit) - { - foreach (CostForVehicleByMonth monthMileage in monthlyMileageData) - { - if (monthMileage.Cost != default) - { - monthMileage.Cost = 100 / monthMileage.Cost; - } - } - var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any); - if (newAverageMPG != 0) - { - newAverageMPG = 100 / newAverageMPG; - } - averageMPG = newAverageMPG.ToString("F"); - } - var mpgViewModel = new MPGForVehicleByMonth { - CostData = monthlyMileageData, - Unit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit, - SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList() - }; - viewModel.FuelMileageForVehicleByMonth = mpgViewModel; - //report header - - var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords); - var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords); - - viewModel.ReportHeaderForVehicle.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords); - viewModel.ReportHeaderForVehicle.AverageMPG = $"{averageMPG} {mpgViewModel.Unit}"; - viewModel.ReportHeaderForVehicle.MaxOdometer = maxMileage; - viewModel.ReportHeaderForVehicle.DistanceTraveled = maxMileage - minMileage; - return PartialView("_Report", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetCollaboratorsForVehicle(int vehicleId) - { - var result = _userLogic.GetCollaboratorsForVehicle(vehicleId); - return PartialView("_Collaborators", result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult AddCollaboratorsToVehicle(int vehicleId, string username) - { - var result = _userLogic.AddCollaboratorToVehicle(vehicleId, username); - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult DeleteCollaboratorFromVehicle(int userId, int vehicleId) - { - var result = _userLogic.DeleteCollaboratorFromVehicle(userId, vehicleId); - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult GetSummaryForVehicle(int vehicleId, int year = 0) - { - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId); - - var serviceRecords = vehicleRecords.ServiceRecords; - var gasRecords = vehicleRecords.GasRecords; - var taxRecords = vehicleRecords.TaxRecords; - var upgradeRecords = vehicleRecords.UpgradeRecords; - var odometerRecords = vehicleRecords.OdometerRecords; - - if (year != default) - { - serviceRecords.RemoveAll(x => x.Date.Year != year); - gasRecords.RemoveAll(x => x.Date.Year != year); - taxRecords.RemoveAll(x => x.Date.Year != year); - upgradeRecords.RemoveAll(x => x.Date.Year != year); - odometerRecords.RemoveAll(x => x.Date.Year != year); - } - - var userConfig = _config.GetUserConfig(User); - - var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG); - string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit; - var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG); - var averageMPG = _gasHelper.GetAverageGasMileage(mileageData, userConfig.UseMPG); - bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l"; - - if (invertedFuelMileageUnit) - { - var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any); - if (newAverageMPG != 0) - { - newAverageMPG = 100 / newAverageMPG; - } - averageMPG = newAverageMPG.ToString("F"); - } - - var mpgUnit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit; - - var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords); - var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords); - - var viewModel = new ReportHeader() - { - TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords), - AverageMPG = $"{averageMPG} {mpgUnit}", - MaxOdometer = maxMileage, - DistanceTraveled = maxMileage - minMileage - }; - - return PartialView("_ReportHeader", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetCostMakeUpForVehicle(int vehicleId, int year = 0) - { - var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - if (year != default) - { - serviceRecords.RemoveAll(x => x.Date.Year != year); - gasRecords.RemoveAll(x => x.Date.Year != year); - taxRecords.RemoveAll(x => x.Date.Year != year); - upgradeRecords.RemoveAll(x => x.Date.Year != year); - } - var viewModel = new CostMakeUpForVehicle - { - ServiceRecordSum = serviceRecords.Sum(x => x.Cost), - GasRecordSum = gasRecords.Sum(x => x.Cost), - TaxRecordSum = taxRecords.Sum(x => x.Cost), - UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost) - }; - return PartialView("_CostMakeUpReport", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetCostTableForVehicle(int vehicleId, int year = 0) - { - var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId); - var serviceRecords = vehicleRecords.ServiceRecords; - var gasRecords = vehicleRecords.GasRecords; - var taxRecords = vehicleRecords.TaxRecords; - var upgradeRecords = vehicleRecords.UpgradeRecords; - var odometerRecords = vehicleRecords.OdometerRecords; - if (year != default) - { - serviceRecords.RemoveAll(x => x.Date.Year != year); - gasRecords.RemoveAll(x => x.Date.Year != year); - taxRecords.RemoveAll(x => x.Date.Year != year); - upgradeRecords.RemoveAll(x => x.Date.Year != year); - odometerRecords.RemoveAll(x => x.Date.Year != year); - } - var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords); - var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords); - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var userConfig = _config.GetUserConfig(User); - var totalDistanceTraveled = maxMileage - minMileage; - var totalDays = _vehicleLogic.GetOwnershipDays(vehicleData.PurchaseDate, vehicleData.SoldDate, year, serviceRecords, gasRecords, upgradeRecords, odometerRecords, taxRecords); - var viewModel = new CostTableForVehicle - { - ServiceRecordSum = serviceRecords.Sum(x => x.Cost), - GasRecordSum = gasRecords.Sum(x => x.Cost), - TaxRecordSum = taxRecords.Sum(x => x.Cost), - UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost), - TotalDistance = totalDistanceTraveled, - DistanceUnit = vehicleData.UseHours ? "Cost Per Hour" : userConfig.UseMPG ? "Cost Per Mile" : "Cost Per Kilometer", - NumberOfDays = totalDays - }; - return PartialView("_CostTableReport", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - public IActionResult GetReminderMakeUpByVehicle(int vehicleId, int daysToAdd) - { - var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd)); - var viewModel = new ReminderMakeUpForVehicle - { - NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(), - UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(), - VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(), - PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count() - }; - return PartialView("_ReminderMakeUpReport", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult GetVehicleAttachments(int vehicleId, List exportTabs) - { - List attachmentData = new List(); - if (exportTabs.Contains(ImportMode.ServiceRecord)) - { - var records = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Files.Any()); - attachmentData.AddRange(records.Select(x => new GenericReportModel - { - DataType = ImportMode.ServiceRecord, - Date = x.Date, - Odometer = x.Mileage, - Files = x.Files - })); - } - if (exportTabs.Contains(ImportMode.UpgradeRecord)) - { - var records = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId).Where(x => x.Files.Any()); - attachmentData.AddRange(records.Select(x => new GenericReportModel - { - DataType = ImportMode.UpgradeRecord, - Date = x.Date, - Odometer = x.Mileage, - Files = x.Files - })); - } - if (exportTabs.Contains(ImportMode.GasRecord)) - { - var records = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId).Where(x => x.Files.Any()); - attachmentData.AddRange(records.Select(x => new GenericReportModel - { - DataType = ImportMode.GasRecord, - Date = x.Date, - Odometer = x.Mileage, - Files = x.Files - })); - } - if (exportTabs.Contains(ImportMode.TaxRecord)) - { - var records = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId).Where(x => x.Files.Any()); - attachmentData.AddRange(records.Select(x => new GenericReportModel - { - DataType = ImportMode.TaxRecord, - Date = x.Date, - Odometer = 0, - Files = x.Files - })); - } - if (exportTabs.Contains(ImportMode.OdometerRecord)) - { - var records = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId).Where(x => x.Files.Any()); - attachmentData.AddRange(records.Select(x => new GenericReportModel - { - DataType = ImportMode.OdometerRecord, - Date = x.Date, - Odometer = x.Mileage, - Files = x.Files - })); - } - if (exportTabs.Contains(ImportMode.NoteRecord)) - { - var records = _noteDataAccess.GetNotesByVehicleId(vehicleId).Where(x => x.Files.Any()); - attachmentData.AddRange(records.Select(x => new GenericReportModel - { - DataType = ImportMode.NoteRecord, - Date = DateTime.Now, - Odometer = 0, - Files = x.Files - })); - } - if (attachmentData.Any()) - { - attachmentData = attachmentData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList(); - var result = _fileHelper.MakeAttachmentsExport(attachmentData); - if (string.IsNullOrWhiteSpace(result)) - { - return Json(OperationResponse.Failed()); - } - return Json(OperationResponse.Succeed(result)); - } - else - { - return Json(OperationResponse.Failed("No Attachments Found")); - } - } - public IActionResult GetReportParameters() - { - var viewModel = new ReportParameter() { - VisibleColumns = new List { - nameof(GenericReportModel.DataType), - nameof(GenericReportModel.Date), - nameof(GenericReportModel.Odometer), - nameof(GenericReportModel.Description), - nameof(GenericReportModel.Cost), - nameof(GenericReportModel.Notes) - } - }; - //get all extra fields from service records, repairs, upgrades, and tax records. - var recordTypes = new List() { 0, 1, 3, 4 }; - var extraFields = new List(); - foreach(int recordType in recordTypes) - { - extraFields.AddRange(_extraFieldDataAccess.GetExtraFieldsById(recordType).ExtraFields.Select(x => x.Name)); - } - viewModel.ExtraFields = extraFields.Distinct().ToList(); - - return PartialView("_ReportParameters", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - public IActionResult GetVehicleHistory(int vehicleId, ReportParameter reportParameter) - { - var vehicleHistory = new VehicleHistoryViewModel(); - vehicleHistory.ReportParameters = reportParameter; - vehicleHistory.VehicleData = _dataAccess.GetVehicleById(vehicleId); - var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId); - bool useMPG = _config.GetUserConfig(User).UseMPG; - bool useUKMPG = _config.GetUserConfig(User).UseUKMPG; - var gasViewModels = _gasHelper.GetGasRecordViewModels(vehicleRecords.GasRecords, useMPG, useUKMPG); - //filter by tags - if (reportParameter.Tags.Any()) - { - if (reportParameter.TagFilter == TagFilter.Exclude) - { - vehicleRecords.OdometerRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.ServiceRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.UpgradeRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.TaxRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y))); - gasViewModels.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.GasRecords.RemoveAll(x => x.Tags.Any(y => reportParameter.Tags.Contains(y))); - } - else if (reportParameter.TagFilter == TagFilter.IncludeOnly) - { - vehicleRecords.OdometerRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.ServiceRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.UpgradeRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.TaxRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y))); - gasViewModels.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y))); - vehicleRecords.GasRecords.RemoveAll(x => !x.Tags.Any(y => reportParameter.Tags.Contains(y))); - } - } - //filter by date range. - if (reportParameter.FilterByDateRange && !string.IsNullOrWhiteSpace(reportParameter.StartDate) && !string.IsNullOrWhiteSpace(reportParameter.EndDate)) - { - var startDate = DateTime.Parse(reportParameter.StartDate).Date; - var endDate = DateTime.Parse(reportParameter.EndDate).Date; - //validate date range - if (endDate >= startDate) //allow for same day. - { - vehicleHistory.StartDate = reportParameter.StartDate; - vehicleHistory.EndDate = reportParameter.EndDate; - //remove all records with dates after the end date and dates before the start date. - vehicleRecords.OdometerRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate); - vehicleRecords.ServiceRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate); - vehicleRecords.UpgradeRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate); - vehicleRecords.TaxRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate); - gasViewModels.RemoveAll(x => DateTime.Parse(x.Date).Date > endDate || DateTime.Parse(x.Date).Date < startDate); - vehicleRecords.GasRecords.RemoveAll(x => x.Date.Date > endDate || x.Date.Date < startDate); - } - } - var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords); - vehicleHistory.Odometer = maxMileage.ToString("N0"); - var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords); - var distanceTraveled = maxMileage - minMileage; - if (!string.IsNullOrWhiteSpace(vehicleHistory.VehicleData.PurchaseDate)) - { - var endDate = vehicleHistory.VehicleData.SoldDate; - int daysOwned = 0; - if (string.IsNullOrWhiteSpace(endDate)) - { - endDate = DateTime.Now.ToShortDateString(); - } - try - { - daysOwned = (DateTime.Parse(endDate) - DateTime.Parse(vehicleHistory.VehicleData.PurchaseDate)).Days; - vehicleHistory.DaysOwned = daysOwned.ToString("N0"); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - vehicleHistory.DaysOwned = string.Empty; - } - //calculate depreciation - var totalDepreciation = vehicleHistory.VehicleData.PurchasePrice - vehicleHistory.VehicleData.SoldPrice; - //we only calculate depreciation if a sold price is provided. - if (totalDepreciation != default && vehicleHistory.VehicleData.SoldPrice != default) - { - vehicleHistory.TotalDepreciation = totalDepreciation; - if (daysOwned != default) - { - vehicleHistory.DepreciationPerDay = Math.Abs(totalDepreciation / daysOwned); - } - if (distanceTraveled != default) - { - vehicleHistory.DepreciationPerMile = Math.Abs(totalDepreciation / distanceTraveled); - } - } - } - List reportData = new List(); - string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit; - vehicleHistory.DistanceUnit = vehicleHistory.VehicleData.UseHours ? "h" : useMPG ? "mi." : "km"; - vehicleHistory.TotalGasCost = gasViewModels.Sum(x => x.Cost); - vehicleHistory.TotalCost = vehicleRecords.ServiceRecords.Sum(x => x.Cost) + vehicleRecords.UpgradeRecords.Sum(x => x.Cost) + vehicleRecords.TaxRecords.Sum(x => x.Cost); - if (distanceTraveled != default) - { - vehicleHistory.DistanceTraveled = distanceTraveled.ToString("N0"); - vehicleHistory.TotalCostPerMile = vehicleHistory.TotalCost / distanceTraveled; - vehicleHistory.TotalGasCostPerMile = vehicleHistory.TotalGasCost / distanceTraveled; - } - var averageMPG = "0"; - if (gasViewModels.Any()) - { - averageMPG = _gasHelper.GetAverageGasMileage(gasViewModels, useMPG); - } - var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleHistory.VehicleData.IsElectric, vehicleHistory.VehicleData.UseHours, useMPG, useUKMPG); - if (fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l") - { - //conversion needed. - var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any); - if (newAverageMPG != 0) - { - newAverageMPG = 100 / newAverageMPG; - } - averageMPG = newAverageMPG.ToString("F"); - fuelEconomyMileageUnit = preferredFuelMileageUnit; - } - vehicleHistory.MPG = $"{averageMPG} {fuelEconomyMileageUnit}"; - //insert servicerecords - reportData.AddRange(vehicleRecords.ServiceRecords.Select(x => new GenericReportModel - { - Date = x.Date, - Odometer = x.Mileage, - Description = x.Description, - Notes = x.Notes, - Cost = x.Cost, - DataType = ImportMode.ServiceRecord, - ExtraFields = x.ExtraFields, - RequisitionHistory = x.RequisitionHistory - })); - //repair records - reportData.AddRange(vehicleRecords.UpgradeRecords.Select(x => new GenericReportModel - { - Date = x.Date, - Odometer = x.Mileage, - Description = x.Description, - Notes = x.Notes, - Cost = x.Cost, - DataType = ImportMode.UpgradeRecord, - ExtraFields = x.ExtraFields, - RequisitionHistory = x.RequisitionHistory - })); - reportData.AddRange(vehicleRecords.TaxRecords.Select(x => new GenericReportModel - { - Date = x.Date, - Odometer = 0, - Description = x.Description, - Notes = x.Notes, - Cost = x.Cost, - DataType = ImportMode.TaxRecord, - ExtraFields = x.ExtraFields - })); - vehicleHistory.VehicleHistory = reportData.OrderBy(x => x.Date).ThenBy(x => x.Odometer).ToList(); - return PartialView("_VehicleHistory", vehicleHistory); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult GetMonthMPGByVehicle(int vehicleId, int year = 0) - { - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - var userConfig = _config.GetUserConfig(User); - string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit; - var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG); - bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l"; - var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG); - if (year != 0) - { - mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year); - } - mileageData.RemoveAll(x => x.MilesPerGallon == default); - var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName(); - monthlyMileageData.AddRange(mileageData.GroupBy(x => x.MonthId).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - Cost = x.Average(y => y.MilesPerGallon) - })); - monthlyMileageData = monthlyMileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = x.Sum(y => y.Cost) - }).ToList(); - if (invertedFuelMileageUnit) - { - foreach (CostForVehicleByMonth monthMileage in monthlyMileageData) - { - if (monthMileage.Cost != default) - { - monthMileage.Cost = 100 / monthMileage.Cost; - } - } - } - var mpgViewModel = new MPGForVehicleByMonth - { - CostData = monthlyMileageData, - Unit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit, - SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList() - }; - return PartialView("_MPGByMonthReport", mpgViewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult GetCostByMonthByVehicle(int vehicleId, List selectedMetrics, int year = 0) - { - List allCosts = StaticHelper.GetBaseLineCosts(); - if (selectedMetrics.Contains(ImportMode.ServiceRecord)) - { - var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, year)); - } - if (selectedMetrics.Contains(ImportMode.UpgradeRecord)) - { - var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, year)); - } - if (selectedMetrics.Contains(ImportMode.GasRecord)) - { - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year)); - } - if (selectedMetrics.Contains(ImportMode.TaxRecord)) - { - var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year)); - } - if (selectedMetrics.Contains(ImportMode.OdometerRecord)) - { - var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, year)); - } - var groupedRecord = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth - { - MonthName = x.Key.MonthName, - Cost = x.Sum(y => y.Cost), - DistanceTraveled = x.Max(y => y.DistanceTraveled) - }).ToList(); - return PartialView("_GasCostByMonthReport", groupedRecord); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult GetCostByMonthAndYearByVehicle(int vehicleId, List selectedMetrics, int year = 0) - { - List allCosts = StaticHelper.GetBaseLineCosts(); - if (selectedMetrics.Contains(ImportMode.ServiceRecord)) - { - var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, year, true)); - } - if (selectedMetrics.Contains(ImportMode.UpgradeRecord)) - { - var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, year, true)); - } - if (selectedMetrics.Contains(ImportMode.GasRecord)) - { - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year, true)); - } - if (selectedMetrics.Contains(ImportMode.TaxRecord)) - { - var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year, true)); - } - if (selectedMetrics.Contains(ImportMode.OdometerRecord)) - { - var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - allCosts.AddRange(_reportHelper.GetOdometerRecordSum(odometerRecords, year, true)); - } - var groupedRecord = allCosts.GroupBy(x => new { x.MonthName, x.MonthId, x.Year }).OrderByDescending(x=>x.Key.Year).Select(x => new CostForVehicleByMonth - { - Year = x.Key.Year, - MonthName = x.Key.MonthName, - Cost = x.Sum(y => y.Cost), - DistanceTraveled = x.Max(y => y.DistanceTraveled), - MonthId = x.Key.MonthId - }).ToList(); - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - var userConfig = _config.GetUserConfig(User); - var viewModel = new CostDistanceTableForVehicle { CostData = groupedRecord }; - viewModel.DistanceUnit = vehicleData.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km"; - return PartialView("_CostDistanceTableReport", viewModel); - } - [HttpGet] - public IActionResult GetAdditionalWidgets() - { - var widgets = _fileHelper.GetWidgets(); - return PartialView("_ReportWidgets", widgets); - } - } -} diff --git a/Controllers/Vehicle/ServiceController.cs b/Controllers/Vehicle/ServiceController.cs deleted file mode 100644 index d4ba42c..0000000 --- a/Controllers/Vehicle/ServiceController.cs +++ /dev/null @@ -1,131 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetServiceRecordsByVehicleId(int vehicleId) - { - var result = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList(); - } - else - { - result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList(); - } - return PartialView("Service/_ServiceRecords", result); - } - [HttpPost] - public IActionResult SaveServiceRecordToVehicleId(ServiceRecordInput serviceRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), serviceRecord.VehicleId)) - { - return Json(false); - } - if (serviceRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = DateTime.Parse(serviceRecord.Date), - VehicleId = serviceRecord.VehicleId, - Mileage = serviceRecord.Mileage, - Notes = $"Auto Insert From Service Record: {serviceRecord.Description}" - }); - } - //move files from temp. - serviceRecord.Files = serviceRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - if (serviceRecord.Supplies.Any()) - { - serviceRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(serviceRecord.Supplies, DateTime.Parse(serviceRecord.Date), serviceRecord.Description)); - if (serviceRecord.CopySuppliesAttachment) - { - serviceRecord.Files.AddRange(GetSuppliesAttachments(serviceRecord.Supplies)); - } - } - if (serviceRecord.DeletedRequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(serviceRecord.DeletedRequisitionHistory, serviceRecord.Description); - } - //push back any reminders - if (serviceRecord.ReminderRecordId.Any()) - { - foreach (int reminderRecordId in serviceRecord.ReminderRecordId) - { - PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(serviceRecord.Date), serviceRecord.Mileage); - } - } - var result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(serviceRecord.ToServiceRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(serviceRecord.ToServiceRecord(), serviceRecord.Id == default ? "servicerecord.add" : "servicerecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpGet] - public IActionResult GetAddServiceRecordPartialView() - { - return PartialView("Service/_ServiceRecordModal", new ServiceRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.ServiceRecord).ExtraFields }); - } - [HttpGet] - public IActionResult GetServiceRecordForEditById(int serviceRecordId) - { - var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - //convert to Input object. - var convertedResult = new ServiceRecordInput - { - Id = result.Id, - Cost = result.Cost, - Date = result.Date.ToShortDateString(), - Description = result.Description, - Mileage = result.Mileage, - Notes = result.Notes, - VehicleId = result.VehicleId, - Files = result.Files, - Tags = result.Tags, - RequisitionHistory = result.RequisitionHistory, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.ServiceRecord).ExtraFields) - }; - return PartialView("Service/_ServiceRecordModal", convertedResult); - } - private bool DeleteServiceRecordWithChecks(int serviceRecordId) - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - //restore any requisitioned supplies. - if (existingRecord.RequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); - } - var result = _serviceRecordDataAccess.DeleteServiceRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "servicerecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteServiceRecordById(int serviceRecordId) - { - var result = DeleteServiceRecordWithChecks(serviceRecordId); - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/SupplyController.cs b/Controllers/Vehicle/SupplyController.cs deleted file mode 100644 index 00487dd..0000000 --- a/Controllers/Vehicle/SupplyController.cs +++ /dev/null @@ -1,215 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - private List CheckSupplyRecordsAvailability(List supplyUsage) - { - //returns empty string if all supplies are available - var result = new List(); - foreach (SupplyUsage supply in supplyUsage) - { - //get supply record. - var supplyData = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId); - if (supplyData == null) - { - result.Add(new SupplyAvailability { Missing = true }); - } - else - { - result.Add(new SupplyAvailability { Missing = false, Description = supplyData.Description, Required = supply.Quantity, InStock = supplyData.Quantity }); - } - } - return result; - } - private List GetSuppliesAttachments(List supplyUsage) - { - List results = new List(); - foreach (SupplyUsage supply in supplyUsage) - { - var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId); - results.AddRange(result.Files); - } - return results; - } - private List RequisitionSupplyRecordsByUsage(List supplyUsage, DateTime dateRequisitioned, string usageDescription) - { - List results = new List(); - foreach (SupplyUsage supply in supplyUsage) - { - //get supply record. - var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.SupplyId); - var unitCost = (result.Quantity != 0) ? result.Cost / result.Quantity : 0; - //deduct quantity used. - result.Quantity -= supply.Quantity; - //deduct cost. - result.Cost -= (supply.Quantity * unitCost); - //check decimal places to ensure that it always has a max of 3 decimal places. - var roundedDecimal = decimal.Round(result.Cost, 3); - if (roundedDecimal != result.Cost) - { - //Too many decimals - result.Cost = roundedDecimal; - } - //create new requisitionrrecord - var requisitionRecord = new SupplyUsageHistory - { - Id = supply.SupplyId, - Date = dateRequisitioned, - Description = usageDescription, - Quantity = supply.Quantity, - Cost = (supply.Quantity * unitCost) - }; - result.RequisitionHistory.Add(requisitionRecord); - //save - _supplyRecordDataAccess.SaveSupplyRecordToVehicle(result); - requisitionRecord.Description = result.Description; //change the name of the description for plan/service/repair/upgrade records - requisitionRecord.PartNumber = result.PartNumber; //populate part number if not displayed in supplies modal. - results.Add(requisitionRecord); - } - return results; - } - - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetSupplyRecordsByVehicleId(int vehicleId) - { - var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId); - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - result = result.OrderByDescending(x => x.Date).ToList(); - } - else - { - result = result.OrderBy(x => x.Date).ToList(); - } - return PartialView("Supply/_SupplyRecords", result); - } - [HttpGet] - public IActionResult GetSupplyRecordsForPlanRecordTemplate(int planRecordTemplateId) - { - var viewModel = new SupplyUsageViewModel(); - var planRecordTemplate = _planRecordTemplateDataAccess.GetPlanRecordTemplateById(planRecordTemplateId); - if (planRecordTemplate != default && planRecordTemplate.VehicleId != default) - { - var supplies = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(planRecordTemplate.VehicleId); - if (_config.GetServerEnableShopSupplies()) - { - supplies.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies - } - supplies.RemoveAll(x => x.Quantity <= 0); - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - supplies = supplies.OrderByDescending(x => x.Date).ToList(); - } - else - { - supplies = supplies.OrderBy(x => x.Date).ToList(); - } - viewModel.Supplies = supplies; - viewModel.Usage = planRecordTemplate.Supplies; - } - return PartialView("Supply/_SupplyUsage", viewModel); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetSupplyRecordsForRecordsByVehicleId(int vehicleId) - { - var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId); - if (_config.GetServerEnableShopSupplies()) - { - result.AddRange(_supplyRecordDataAccess.GetSupplyRecordsByVehicleId(0)); // add shop supplies - } - result.RemoveAll(x => x.Quantity <= 0); - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - result = result.OrderByDescending(x => x.Date).ToList(); - } - else - { - result = result.OrderBy(x => x.Date).ToList(); - } - var viewModel = new SupplyUsageViewModel - { - Supplies = result - }; - return PartialView("Supply/_SupplyUsage", viewModel); - } - [HttpPost] - public IActionResult SaveSupplyRecordToVehicleId(SupplyRecordInput supplyRecord) - { - //move files from temp. - supplyRecord.Files = supplyRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - var result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(supplyRecord.ToSupplyRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromSupplyRecord(supplyRecord.ToSupplyRecord(), supplyRecord.Id == default ? "supplyrecord.add" : "supplyrecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpGet] - public IActionResult GetAddSupplyRecordPartialView() - { - return PartialView("Supply/_SupplyRecordModal", new SupplyRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields }); - } - [HttpGet] - public IActionResult GetSupplyRecordForEditById(int supplyRecordId) - { - var result = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId); - if (result.RequisitionHistory.Any()) - { - //requisition history when viewed through the supply is always immutable. - result.RequisitionHistory = result.RequisitionHistory.Select(x => new SupplyUsageHistory { Id = default, Cost = x.Cost, Description = x.Description, Date = x.Date, PartNumber = x.PartNumber, Quantity = x.Quantity }).ToList(); - } - //convert to Input object. - var convertedResult = new SupplyRecordInput - { - Id = result.Id, - Cost = result.Cost, - Date = result.Date.ToShortDateString(), - Description = result.Description, - PartNumber = result.PartNumber, - Quantity = result.Quantity, - PartSupplier = result.PartSupplier, - Notes = result.Notes, - VehicleId = result.VehicleId, - Files = result.Files, - Tags = result.Tags, - RequisitionHistory = result.RequisitionHistory, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.SupplyRecord).ExtraFields) - }; - return PartialView("Supply/_SupplyRecordModal", convertedResult); - } - private bool DeleteSupplyRecordWithChecks(int supplyRecordId) - { - var existingRecord = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId); - if (existingRecord.VehicleId != default) - { - //security check only if not editing shop supply. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - } - var result = _supplyRecordDataAccess.DeleteSupplyRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromSupplyRecord(existingRecord, "supplyrecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteSupplyRecordById(int supplyRecordId) - { - var result = DeleteSupplyRecordWithChecks(supplyRecordId); - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/TaxController.cs b/Controllers/Vehicle/TaxController.cs deleted file mode 100644 index 44a675f..0000000 --- a/Controllers/Vehicle/TaxController.cs +++ /dev/null @@ -1,122 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetTaxRecordsByVehicleId(int vehicleId) - { - var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - result = result.OrderByDescending(x => x.Date).ToList(); - } - else - { - result = result.OrderBy(x => x.Date).ToList(); - } - return PartialView("Tax/_TaxRecords", result); - } - - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult CheckRecurringTaxRecords(int vehicleId) - { - try - { - var result = _vehicleLogic.UpdateRecurringTaxes(vehicleId); - return Json(result); - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(false); - } - } - [HttpPost] - public IActionResult SaveTaxRecordToVehicleId(TaxRecordInput taxRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), taxRecord.VehicleId)) - { - return Json(false); - } - //move files from temp. - taxRecord.Files = taxRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - //push back any reminders - if (taxRecord.ReminderRecordId.Any()) - { - foreach (int reminderRecordId in taxRecord.ReminderRecordId) - { - PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(taxRecord.Date), null); - } - } - var result = _taxRecordDataAccess.SaveTaxRecordToVehicle(taxRecord.ToTaxRecord()); - _vehicleLogic.UpdateRecurringTaxes(taxRecord.VehicleId); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(taxRecord.ToTaxRecord(), taxRecord.Id == default ? "taxrecord.add" : "taxrecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpGet] - public IActionResult GetAddTaxRecordPartialView() - { - return PartialView("Tax/_TaxRecordModal", new TaxRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields }); - } - [HttpGet] - public IActionResult GetTaxRecordForEditById(int taxRecordId) - { - var result = _taxRecordDataAccess.GetTaxRecordById(taxRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - //convert to Input object. - var convertedResult = new TaxRecordInput - { - Id = result.Id, - Cost = result.Cost, - Date = result.Date.ToShortDateString(), - Description = result.Description, - Notes = result.Notes, - VehicleId = result.VehicleId, - IsRecurring = result.IsRecurring, - RecurringInterval = result.RecurringInterval, - CustomMonthInterval = result.CustomMonthInterval, - CustomMonthIntervalUnit = result.CustomMonthIntervalUnit, - Files = result.Files, - Tags = result.Tags, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.TaxRecord).ExtraFields) - }; - return PartialView("Tax/_TaxRecordModal", convertedResult); - } - private bool DeleteTaxRecordWithChecks(int taxRecordId) - { - var existingRecord = _taxRecordDataAccess.GetTaxRecordById(taxRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - var result = _taxRecordDataAccess.DeleteTaxRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromTaxRecord(existingRecord, "taxrecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteTaxRecordById(int taxRecordId) - { - var result = DeleteTaxRecordWithChecks(taxRecordId); - return Json(result); - } - } -} diff --git a/Controllers/Vehicle/UpgradeController.cs b/Controllers/Vehicle/UpgradeController.cs deleted file mode 100644 index 97cd3bb..0000000 --- a/Controllers/Vehicle/UpgradeController.cs +++ /dev/null @@ -1,131 +0,0 @@ -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; - -namespace MotoVaultPro.Controllers -{ - public partial class VehicleController - { - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId) - { - var result = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - bool _useDescending = _config.GetUserConfig(User).UseDescending; - if (_useDescending) - { - result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList(); - } - else - { - result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList(); - } - return PartialView("Upgrade/_UpgradeRecords", result); - } - [HttpPost] - public IActionResult SaveUpgradeRecordToVehicleId(UpgradeRecordInput upgradeRecord) - { - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), upgradeRecord.VehicleId)) - { - return Json(false); - } - if (upgradeRecord.Id == default && _config.GetUserConfig(User).EnableAutoOdometerInsert) - { - _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = DateTime.Parse(upgradeRecord.Date), - VehicleId = upgradeRecord.VehicleId, - Mileage = upgradeRecord.Mileage, - Notes = $"Auto Insert From Upgrade Record: {upgradeRecord.Description}" - }); - } - //move files from temp. - upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); - if (upgradeRecord.Supplies.Any()) - { - upgradeRecord.RequisitionHistory.AddRange(RequisitionSupplyRecordsByUsage(upgradeRecord.Supplies, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Description)); - if (upgradeRecord.CopySuppliesAttachment) - { - upgradeRecord.Files.AddRange(GetSuppliesAttachments(upgradeRecord.Supplies)); - } - } - if (upgradeRecord.DeletedRequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(upgradeRecord.DeletedRequisitionHistory, upgradeRecord.Description); - } - //push back any reminders - if (upgradeRecord.ReminderRecordId.Any()) - { - foreach (int reminderRecordId in upgradeRecord.ReminderRecordId) - { - PushbackRecurringReminderRecordWithChecks(reminderRecordId, DateTime.Parse(upgradeRecord.Date), upgradeRecord.Mileage); - } - } - var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord()); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(upgradeRecord.ToUpgradeRecord(), upgradeRecord.Id == default ? "upgraderecord.add" : "upgraderecord.update", User.Identity.Name)); - } - return Json(result); - } - [HttpGet] - public IActionResult GetAddUpgradeRecordPartialView() - { - return PartialView("Upgrade/_UpgradeRecordModal", new UpgradeRecordInput() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.UpgradeRecord).ExtraFields }); - } - [HttpGet] - public IActionResult GetUpgradeRecordForEditById(int upgradeRecordId) - { - var result = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), result.VehicleId)) - { - return Redirect("/Error/Unauthorized"); - } - //convert to Input object. - var convertedResult = new UpgradeRecordInput - { - Id = result.Id, - Cost = result.Cost, - Date = result.Date.ToShortDateString(), - Description = result.Description, - Mileage = result.Mileage, - Notes = result.Notes, - VehicleId = result.VehicleId, - Files = result.Files, - Tags = result.Tags, - RequisitionHistory = result.RequisitionHistory, - ExtraFields = StaticHelper.AddExtraFields(result.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.UpgradeRecord).ExtraFields) - }; - return PartialView("Upgrade/_UpgradeRecordModal", convertedResult); - } - private bool DeleteUpgradeRecordWithChecks(int upgradeRecordId) - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId); - //security check. - if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) - { - return false; - } - //restore any requisitioned supplies. - if (existingRecord.RequisitionHistory.Any()) - { - _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); - } - var result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(existingRecord.Id); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromGenericRecord(existingRecord, "upgraderecord.delete", User.Identity.Name)); - } - return result; - } - [HttpPost] - public IActionResult DeleteUpgradeRecordById(int upgradeRecordId) - { - var result = DeleteUpgradeRecordWithChecks(upgradeRecordId); - return Json(result); - } - } -} diff --git a/Controllers/VehicleController.cs b/Controllers/VehicleController.cs deleted file mode 100644 index 9321b43..0000000 --- a/Controllers/VehicleController.cs +++ /dev/null @@ -1,1090 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Filter; -using MotoVaultPro.Helper; -using MotoVaultPro.Logic; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.Globalization; -using System.Security.Claims; -using System.Text.Json; - -namespace MotoVaultPro.Controllers -{ - [Authorize] - public partial class VehicleController : Controller - { - private readonly ILogger _logger; - private readonly IVehicleDataAccess _dataAccess; - private readonly INoteDataAccess _noteDataAccess; - private readonly IServiceRecordDataAccess _serviceRecordDataAccess; - private readonly IGasRecordDataAccess _gasRecordDataAccess; - private readonly ITaxRecordDataAccess _taxRecordDataAccess; - private readonly IReminderRecordDataAccess _reminderRecordDataAccess; - private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; - private readonly ISupplyRecordDataAccess _supplyRecordDataAccess; - private readonly IPlanRecordDataAccess _planRecordDataAccess; - private readonly IPlanRecordTemplateDataAccess _planRecordTemplateDataAccess; - private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; - private readonly IWebHostEnvironment _webEnv; - private readonly IConfigHelper _config; - private readonly IFileHelper _fileHelper; - private readonly IGasHelper _gasHelper; - private readonly IReminderHelper _reminderHelper; - private readonly IReportHelper _reportHelper; - private readonly IUserLogic _userLogic; - private readonly IOdometerLogic _odometerLogic; - private readonly IVehicleLogic _vehicleLogic; - private readonly IExtraFieldDataAccess _extraFieldDataAccess; - - public VehicleController(ILogger logger, - IFileHelper fileHelper, - IGasHelper gasHelper, - IReminderHelper reminderHelper, - IReportHelper reportHelper, - IVehicleDataAccess dataAccess, - INoteDataAccess noteDataAccess, - IServiceRecordDataAccess serviceRecordDataAccess, - IGasRecordDataAccess gasRecordDataAccess, - ITaxRecordDataAccess taxRecordDataAccess, - IReminderRecordDataAccess reminderRecordDataAccess, - IUpgradeRecordDataAccess upgradeRecordDataAccess, - ISupplyRecordDataAccess supplyRecordDataAccess, - IPlanRecordDataAccess planRecordDataAccess, - IPlanRecordTemplateDataAccess planRecordTemplateDataAccess, - IOdometerRecordDataAccess odometerRecordDataAccess, - IExtraFieldDataAccess extraFieldDataAccess, - IUserLogic userLogic, - IOdometerLogic odometerLogic, - IVehicleLogic vehicleLogic, - IWebHostEnvironment webEnv, - IConfigHelper config) - { - _logger = logger; - _dataAccess = dataAccess; - _noteDataAccess = noteDataAccess; - _fileHelper = fileHelper; - _gasHelper = gasHelper; - _reminderHelper = reminderHelper; - _reportHelper = reportHelper; - _serviceRecordDataAccess = serviceRecordDataAccess; - _gasRecordDataAccess = gasRecordDataAccess; - _taxRecordDataAccess = taxRecordDataAccess; - _reminderRecordDataAccess = reminderRecordDataAccess; - _upgradeRecordDataAccess = upgradeRecordDataAccess; - _supplyRecordDataAccess = supplyRecordDataAccess; - _planRecordDataAccess = planRecordDataAccess; - _planRecordTemplateDataAccess = planRecordTemplateDataAccess; - _odometerRecordDataAccess = odometerRecordDataAccess; - _extraFieldDataAccess = extraFieldDataAccess; - _userLogic = userLogic; - _odometerLogic = odometerLogic; - _vehicleLogic = vehicleLogic; - _webEnv = webEnv; - _config = config; - } - private int GetUserID() - { - return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult Index(int vehicleId) - { - var data = _dataAccess.GetVehicleById(vehicleId); - return View(data); - } - [HttpGet] - public IActionResult AddVehiclePartialView() - { - return PartialView("_VehicleModal", new Vehicle() { ExtraFields = _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.VehicleRecord).ExtraFields }); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpGet] - public IActionResult GetEditVehiclePartialViewById(int vehicleId) - { - var data = _dataAccess.GetVehicleById(vehicleId); - data.ExtraFields = StaticHelper.AddExtraFields(data.ExtraFields, _extraFieldDataAccess.GetExtraFieldsById((int)ImportMode.VehicleRecord).ExtraFields); - return PartialView("_VehicleModal", data); - } - [HttpPost] - public IActionResult SaveVehicle(Vehicle vehicleInput) - { - try - { - bool isNewAddition = vehicleInput.Id == default; - if (!isNewAddition) - { - if (!_userLogic.UserCanEditVehicle(GetUserID(), vehicleInput.Id)) - { - return View("401"); - } - } - //move image from temp folder to images folder. - vehicleInput.ImageLocation = _fileHelper.MoveFileFromTemp(vehicleInput.ImageLocation, "images/"); - //save vehicle. - var result = _dataAccess.SaveVehicle(vehicleInput); - if (isNewAddition) - { - _userLogic.AddUserAccessToVehicle(GetUserID(), vehicleInput.Id); - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Created Vehicle {vehicleInput.Year} {vehicleInput.Make} {vehicleInput.Model}({StaticHelper.GetVehicleIdentifier(vehicleInput)})", "vehicle.add", User.Identity.Name, vehicleInput.Id.ToString())); - } - else - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Updated Vehicle {vehicleInput.Year} {vehicleInput.Make} {vehicleInput.Model}({StaticHelper.GetVehicleIdentifier(vehicleInput)})", "vehicle.update", User.Identity.Name, vehicleInput.Id.ToString())); - } - return Json(result); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error Saving Vehicle"); - return Json(false); - } - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult DeleteVehicle(int vehicleId) - { - //Delete all service records, gas records, notes, etc. - var result = _gasRecordDataAccess.DeleteAllGasRecordsByVehicleId(vehicleId) && - _serviceRecordDataAccess.DeleteAllServiceRecordsByVehicleId(vehicleId) && - _taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) && - _noteDataAccess.DeleteAllNotesByVehicleId(vehicleId) && - _reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) && - _upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) && - _planRecordDataAccess.DeleteAllPlanRecordsByVehicleId(vehicleId) && - _planRecordTemplateDataAccess.DeleteAllPlanRecordTemplatesByVehicleId(vehicleId) && - _supplyRecordDataAccess.DeleteAllSupplyRecordsByVehicleId(vehicleId) && - _odometerRecordDataAccess.DeleteAllOdometerRecordsByVehicleId(vehicleId) && - _userLogic.DeleteAllAccessToVehicle(vehicleId) && - _dataAccess.DeleteVehicle(vehicleId); - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic(string.Empty, "vehicle.delete", User.Identity.Name, vehicleId.ToString())); - } - return Json(result); - } - [HttpPost] - public IActionResult DuplicateVehicleCollaborators(int sourceVehicleId, int destVehicleId) - { - try - { - //retrieve collaborators for both source and destination vehicle id. - if (_userLogic.UserCanEditVehicle(GetUserID(), sourceVehicleId) && _userLogic.UserCanEditVehicle(GetUserID(), destVehicleId)) - { - var sourceCollaborators = _userLogic.GetCollaboratorsForVehicle(sourceVehicleId).Select(x => x.UserVehicle.UserId).ToList(); - var destCollaborators = _userLogic.GetCollaboratorsForVehicle(destVehicleId).Select(x => x.UserVehicle.UserId).ToList(); - sourceCollaborators.RemoveAll(x => destCollaborators.Contains(x)); - if (sourceCollaborators.Any()) - { - foreach (int collaboratorId in sourceCollaborators) - { - _userLogic.AddUserAccessToVehicle(collaboratorId, destVehicleId); - } - } - else - { - return Json(OperationResponse.Failed("Both vehicles already have identical collaborators")); - } - } - return Json(OperationResponse.Succeed("Collaborators Copied")); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(OperationResponse.Failed()); - } - } - - #region "Shared Methods" - [HttpPost] - public IActionResult GetFilesPendingUpload(List uploadedFiles) - { - var filesPendingUpload = uploadedFiles.Where(x => x.IsPending).ToList(); - return PartialView("_FilesToUpload", filesPendingUpload); - } - [HttpPost] - [TypeFilter(typeof(CollaboratorFilter))] - public IActionResult SearchRecords(int vehicleId, string searchQuery, bool caseSensitive) - { - List searchResults = new List(); - if (string.IsNullOrWhiteSpace(searchQuery)) - { - return Json(searchResults); - } - if (!caseSensitive) - { - searchQuery = searchQuery.ToLower(); - } - foreach (ImportMode visibleTab in _config.GetUserConfig(User).VisibleTabs) - { - switch (visibleTab) - { - case ImportMode.ServiceRecord: - { - var results = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ServiceRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ServiceRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - } - break; - case ImportMode.UpgradeRecord: - { - var results = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.UpgradeRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.UpgradeRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - } - break; - case ImportMode.TaxRecord: - { - var results = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.TaxRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.TaxRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - } - break; - case ImportMode.SupplyRecord: - { - var results = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.SupplyRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.SupplyRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" })); - } - } - break; - case ImportMode.PlanRecord: - { - var results = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.PlanRecord, Description = $"{x.DateCreated.ToShortDateString()} - {x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.PlanRecord, Description = $"{x.DateCreated.ToShortDateString()} - {x.Description}" })); - } - } - break; - case ImportMode.OdometerRecord: - { - var results = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.OdometerRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.OdometerRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" })); - } - } - break; - case ImportMode.GasRecord: - { - var results = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.GasRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.GasRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" })); - } - } - break; - case ImportMode.NoteRecord: - { - var results = _noteDataAccess.GetNotesByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.NoteRecord, Description = $"{x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.NoteRecord, Description = $"{x.Description}" })); - } - } - break; - case ImportMode.ReminderRecord: - { - var results = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); - if (caseSensitive) - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ReminderRecord, Description = $"{x.Description}" })); - } - else - { - searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ReminderRecord, Description = $"{x.Description}" })); - } - } - break; - } - } - return PartialView("_GlobalSearchResult", searchResults); - } - [TypeFilter(typeof(CollaboratorFilter))] - public IActionResult GetMaxMileage(int vehicleId) - { - var result = _vehicleLogic.GetMaxMileage(vehicleId); - return Json(result); - } - public IActionResult MoveRecord(int recordId, ImportMode source, ImportMode destination) - { - var genericRecord = new GenericRecord(); - bool result = false; - //get - switch (source) - { - case ImportMode.ServiceRecord: - genericRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - break; - case ImportMode.UpgradeRecord: - genericRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - break; - } - //save - switch (destination) - { - case ImportMode.ServiceRecord: - result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(StaticHelper.GenericToServiceRecord(genericRecord)); - break; - case ImportMode.UpgradeRecord: - result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(StaticHelper.GenericToUpgradeRecord(genericRecord)); - break; - } - //delete - if (result) - { - switch (source) - { - case ImportMode.ServiceRecord: - _serviceRecordDataAccess.DeleteServiceRecordById(recordId); - break; - case ImportMode.UpgradeRecord: - _upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId); - break; - } - } - return Json(result); - } - public IActionResult MoveRecords(List recordIds, ImportMode source, ImportMode destination) - { - var genericRecord = new GenericRecord(); - bool result = false; - foreach (int recordId in recordIds) - { - //get - switch (source) - { - case ImportMode.ServiceRecord: - genericRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - break; - case ImportMode.UpgradeRecord: - genericRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - break; - } - //save - switch (destination) - { - case ImportMode.ServiceRecord: - result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(StaticHelper.GenericToServiceRecord(genericRecord)); - break; - case ImportMode.UpgradeRecord: - result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(StaticHelper.GenericToUpgradeRecord(genericRecord)); - break; - } - //delete - if (result) - { - switch (source) - { - case ImportMode.ServiceRecord: - _serviceRecordDataAccess.DeleteServiceRecordById(recordId); - break; - case ImportMode.UpgradeRecord: - _upgradeRecordDataAccess.DeleteUpgradeRecordById(recordId); - break; - } - } - } - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Moved multiple {source.ToString()} to {destination.ToString()} - Ids: {string.Join(",", recordIds)}", "bulk.move", User.Identity.Name, string.Empty)); - } - return Json(result); - } - public IActionResult DeleteRecords(List recordIds, ImportMode importMode) - { - bool result = false; - foreach (int recordId in recordIds) - { - switch (importMode) - { - case ImportMode.ServiceRecord: - result = DeleteServiceRecordWithChecks(recordId); - break; - case ImportMode.UpgradeRecord: - result = DeleteUpgradeRecordWithChecks(recordId); - break; - case ImportMode.GasRecord: - result = DeleteGasRecordWithChecks(recordId); - break; - case ImportMode.TaxRecord: - result = DeleteTaxRecordWithChecks(recordId); - break; - case ImportMode.SupplyRecord: - result = DeleteSupplyRecordWithChecks(recordId); - break; - case ImportMode.NoteRecord: - result = DeleteNoteWithChecks(recordId); - break; - case ImportMode.OdometerRecord: - result = DeleteOdometerRecordWithChecks(recordId); - break; - case ImportMode.ReminderRecord: - result = DeleteReminderRecordWithChecks(recordId); - break; - } - } - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Deleted multiple {importMode.ToString()} - Ids: {string.Join(", ", recordIds)}", "bulk.delete", User.Identity.Name, string.Empty)); - } - return Json(result); - } - [TypeFilter(typeof(CollaboratorFilter))] - [HttpPost] - public IActionResult AdjustRecordsOdometer(List recordIds, int vehicleId, ImportMode importMode) - { - bool result = false; - //get vehicle's odometer adjustments - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - foreach (int recordId in recordIds) - { - switch (importMode) - { - case ImportMode.ServiceRecord: - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - existingRecord.Mileage += int.Parse(vehicleData.OdometerDifference); - existingRecord.Mileage = decimal.ToInt32(existingRecord.Mileage * decimal.Parse(vehicleData.OdometerMultiplier, NumberStyles.Any)); - result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord); - } - break; - case ImportMode.UpgradeRecord: - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - existingRecord.Mileage += int.Parse(vehicleData.OdometerDifference); - existingRecord.Mileage = decimal.ToInt32(existingRecord.Mileage * decimal.Parse(vehicleData.OdometerMultiplier, NumberStyles.Any)); - result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord); - } - break; - case ImportMode.GasRecord: - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId); - existingRecord.Mileage += int.Parse(vehicleData.OdometerDifference); - existingRecord.Mileage = decimal.ToInt32(existingRecord.Mileage * decimal.Parse(vehicleData.OdometerMultiplier, NumberStyles.Any)); - result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord); - } - break; - case ImportMode.OdometerRecord: - { - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId); - existingRecord.Mileage += int.Parse(vehicleData.OdometerDifference); - existingRecord.Mileage = decimal.ToInt32(existingRecord.Mileage * decimal.Parse(vehicleData.OdometerMultiplier, NumberStyles.Any)); - result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord); - } - break; - } - } - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Adjusted odometer for multiple {importMode.ToString()} - Ids: {string.Join(",", recordIds)}", "bulk.odometer.adjust", User.Identity.Name, string.Empty)); - } - return Json(result); - } - [HttpPost] - public IActionResult DuplicateRecords(List recordIds, ImportMode importMode) - { - bool result = false; - foreach (int recordId in recordIds) - { - switch (importMode) - { - case ImportMode.ServiceRecord: - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - existingRecord.Id = default; - existingRecord.RequisitionHistory = new List(); - result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord); - } - break; - case ImportMode.UpgradeRecord: - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - existingRecord.Id = default; - existingRecord.RequisitionHistory = new List(); - result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord); - } - break; - case ImportMode.GasRecord: - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId); - existingRecord.Id = default; - result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord); - } - break; - case ImportMode.TaxRecord: - { - var existingRecord = _taxRecordDataAccess.GetTaxRecordById(recordId); - existingRecord.Id = default; - result = _taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord); - } - break; - case ImportMode.SupplyRecord: - { - var existingRecord = _supplyRecordDataAccess.GetSupplyRecordById(recordId); - existingRecord.Id = default; - existingRecord.RequisitionHistory = new List(); - result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(existingRecord); - } - break; - case ImportMode.NoteRecord: - { - var existingRecord = _noteDataAccess.GetNoteById(recordId); - existingRecord.Id = default; - result = _noteDataAccess.SaveNoteToVehicle(existingRecord); - } - break; - case ImportMode.OdometerRecord: - { - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId); - existingRecord.Id = default; - result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord); - } - break; - case ImportMode.ReminderRecord: - { - var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(recordId); - existingRecord.Id = default; - result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord); - } - break; - case ImportMode.PlanRecord: - { - var existingRecord = _planRecordDataAccess.GetPlanRecordById(recordId); - existingRecord.Id = default; - existingRecord.ReminderRecordId = default; - existingRecord.RequisitionHistory = new List(); - result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord); - } - break; - } - } - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Duplicated multiple {importMode.ToString()} - Ids: {string.Join(",", recordIds)}", "bulk.duplicate", User.Identity.Name, string.Empty)); - } - return Json(result); - } - [HttpPost] - public IActionResult DuplicateRecordsToOtherVehicles(List recordIds, List vehicleIds, ImportMode importMode) - { - bool result = false; - if (!recordIds.Any() || !vehicleIds.Any()) - { - return Json(result); - } - foreach (int recordId in recordIds) - { - switch (importMode) - { - case ImportMode.ServiceRecord: - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - existingRecord.Id = default; - existingRecord.RequisitionHistory = new List(); - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.UpgradeRecord: - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - existingRecord.Id = default; - existingRecord.RequisitionHistory = new List(); - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.GasRecord: - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId); - existingRecord.Id = default; - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _gasRecordDataAccess.SaveGasRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.TaxRecord: - { - var existingRecord = _taxRecordDataAccess.GetTaxRecordById(recordId); - existingRecord.Id = default; - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _taxRecordDataAccess.SaveTaxRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.SupplyRecord: - { - var existingRecord = _supplyRecordDataAccess.GetSupplyRecordById(recordId); - existingRecord.Id = default; - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.NoteRecord: - { - var existingRecord = _noteDataAccess.GetNoteById(recordId); - existingRecord.Id = default; - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _noteDataAccess.SaveNoteToVehicle(existingRecord); - } - } - break; - case ImportMode.OdometerRecord: - { - var existingRecord = _odometerRecordDataAccess.GetOdometerRecordById(recordId); - existingRecord.Id = default; - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.ReminderRecord: - { - var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(recordId); - existingRecord.Id = default; - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord); - } - } - break; - case ImportMode.PlanRecord: - { - var existingRecord = _planRecordDataAccess.GetPlanRecordById(recordId); - existingRecord.Id = default; - existingRecord.ReminderRecordId = default; - existingRecord.RequisitionHistory = new List(); - foreach (int vehicleId in vehicleIds) - { - existingRecord.VehicleId = vehicleId; - result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord); - } - } - break; - } - } - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Duplicated multiple {importMode.ToString()} - Ids: {string.Join(",", recordIds)} - to Vehicle Ids: {string.Join(",", vehicleIds)}", "bulk.duplicate.to.vehicles", User.Identity.Name, string.Join(",", vehicleIds))); - } - return Json(result); - } - [HttpPost] - public IActionResult BulkCreateOdometerRecords(List recordIds, ImportMode importMode) - { - bool result = false; - foreach (int recordId in recordIds) - { - switch (importMode) - { - case ImportMode.ServiceRecord: - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - result = _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = existingRecord.Date, - VehicleId = existingRecord.VehicleId, - Mileage = existingRecord.Mileage, - Notes = $"Auto Insert From Service Record: {existingRecord.Description}" - }); - } - break; - case ImportMode.UpgradeRecord: - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - result = _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = existingRecord.Date, - VehicleId = existingRecord.VehicleId, - Mileage = existingRecord.Mileage, - Notes = $"Auto Insert From Upgrade Record: {existingRecord.Description}" - }); - } - break; - case ImportMode.GasRecord: - { - var existingRecord = _gasRecordDataAccess.GetGasRecordById(recordId); - result = _odometerLogic.AutoInsertOdometerRecord(new OdometerRecord - { - Date = existingRecord.Date, - VehicleId = existingRecord.VehicleId, - Mileage = existingRecord.Mileage, - Notes = $"Auto Insert From Gas Record. {existingRecord.Notes}" - }); - } - break; - } - } - if (result) - { - StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic($"Created Odometer Records based on {importMode.ToString()} - Ids: {string.Join(",", recordIds)}", "bulk.odometer.insert", User.Identity.Name, string.Empty)); - } - return Json(result); - } - [HttpPost] - public IActionResult GetGenericRecordModal(List recordIds, ImportMode dataType) - { - var extraFields = _extraFieldDataAccess.GetExtraFieldsById((int)dataType).ExtraFields; - return PartialView("_GenericRecordModal", new GenericRecordEditModel() { DataType = dataType, RecordIds = recordIds, EditRecord = new GenericRecord { ExtraFields = extraFields } }); - } - [HttpPost] - public IActionResult EditMultipleRecords(GenericRecordEditModel genericRecordEditModel) - { - var dateIsEdited = genericRecordEditModel.EditRecord.Date != default; - var descriptionIsEdited = !string.IsNullOrWhiteSpace(genericRecordEditModel.EditRecord.Description); - var mileageIsEdited = genericRecordEditModel.EditRecord.Mileage != default; - var costIsEdited = genericRecordEditModel.EditRecord.Cost != default; - var noteIsEdited = !string.IsNullOrWhiteSpace(genericRecordEditModel.EditRecord.Notes); - var tagsIsEdited = genericRecordEditModel.EditRecord.Tags.Any(); - var extraFieldIsEdited = genericRecordEditModel.EditRecord.ExtraFields.Any(); - //handle clear overrides - if (tagsIsEdited && genericRecordEditModel.EditRecord.Tags.Contains("---")) - { - genericRecordEditModel.EditRecord.Tags = new List(); - } - if (noteIsEdited && genericRecordEditModel.EditRecord.Notes == "---") - { - genericRecordEditModel.EditRecord.Notes = ""; - } - bool result = false; - foreach (int recordId in genericRecordEditModel.RecordIds) - { - switch (genericRecordEditModel.DataType) - { - case ImportMode.ServiceRecord: - { - var existingRecord = _serviceRecordDataAccess.GetServiceRecordById(recordId); - if (dateIsEdited) - { - existingRecord.Date = genericRecordEditModel.EditRecord.Date; - } - if (descriptionIsEdited) - { - existingRecord.Description = genericRecordEditModel.EditRecord.Description; - } - if (mileageIsEdited) - { - existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage; - } - if (costIsEdited) - { - existingRecord.Cost = genericRecordEditModel.EditRecord.Cost; - } - if (noteIsEdited) - { - existingRecord.Notes = genericRecordEditModel.EditRecord.Notes; - } - if (tagsIsEdited) - { - existingRecord.Tags = genericRecordEditModel.EditRecord.Tags; - } - if (extraFieldIsEdited) - { - foreach (ExtraField extraField in genericRecordEditModel.EditRecord.ExtraFields) - { - if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name)) - { - var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name); - existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name); - existingRecord.ExtraFields.Insert(insertIndex, extraField); - } - else - { - existingRecord.ExtraFields.Add(extraField); - } - } - } - result = _serviceRecordDataAccess.SaveServiceRecordToVehicle(existingRecord); - } - break; - case ImportMode.UpgradeRecord: - { - var existingRecord = _upgradeRecordDataAccess.GetUpgradeRecordById(recordId); - if (dateIsEdited) - { - existingRecord.Date = genericRecordEditModel.EditRecord.Date; - } - if (descriptionIsEdited) - { - existingRecord.Description = genericRecordEditModel.EditRecord.Description; - } - if (mileageIsEdited) - { - existingRecord.Mileage = genericRecordEditModel.EditRecord.Mileage; - } - if (costIsEdited) - { - existingRecord.Cost = genericRecordEditModel.EditRecord.Cost; - } - if (noteIsEdited) - { - existingRecord.Notes = genericRecordEditModel.EditRecord.Notes; - } - if (tagsIsEdited) - { - existingRecord.Tags = genericRecordEditModel.EditRecord.Tags; - } - if (extraFieldIsEdited) - { - foreach (ExtraField extraField in genericRecordEditModel.EditRecord.ExtraFields) - { - if (existingRecord.ExtraFields.Any(x => x.Name == extraField.Name)) - { - var insertIndex = existingRecord.ExtraFields.FindIndex(x => x.Name == extraField.Name); - existingRecord.ExtraFields.RemoveAll(x => x.Name == extraField.Name); - existingRecord.ExtraFields.Insert(insertIndex, extraField); - } - else - { - existingRecord.ExtraFields.Add(extraField); - } - } - } - result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(existingRecord); - } - break; - } - } - return Json(result); - } - [HttpPost] - public IActionResult PrintRecordStickers(int vehicleId, List recordIds, ImportMode importMode) - { - bool result = false; - if (!recordIds.Any()) - { - return Json(result); - } - var stickerViewModel = new StickerViewModel() { RecordType = importMode }; - if (vehicleId != default) - { - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - if (vehicleData != null && vehicleData.Id != default) - { - stickerViewModel.VehicleData = vehicleData; - } - } - - int recordsAdded = 0; - switch (importMode) - { - case ImportMode.ServiceRecord: - { - foreach (int recordId in recordIds) - { - stickerViewModel.GenericRecords.Add(_serviceRecordDataAccess.GetServiceRecordById(recordId)); - recordsAdded++; - } - - } - break; - case ImportMode.UpgradeRecord: - { - foreach (int recordId in recordIds) - { - stickerViewModel.GenericRecords.Add(_upgradeRecordDataAccess.GetUpgradeRecordById(recordId)); - recordsAdded++; - } - } - break; - case ImportMode.GasRecord: - { - foreach (int recordId in recordIds) - { - var record = _gasRecordDataAccess.GetGasRecordById(recordId); - stickerViewModel.GenericRecords.Add(new GenericRecord - { - Cost = record.Cost, - Date = record.Date, - Notes = record.Notes, - Mileage = record.Mileage, - ExtraFields = record.ExtraFields - }); - recordsAdded++; - } - - } - break; - case ImportMode.TaxRecord: - { - foreach (int recordId in recordIds) - { - var record = _taxRecordDataAccess.GetTaxRecordById(recordId); - stickerViewModel.GenericRecords.Add(new GenericRecord - { - Description = record.Description, - Cost = record.Cost, - Notes = record.Notes, - Date = record.Date, - ExtraFields = record.ExtraFields - }); - recordsAdded++; - } - } - break; - case ImportMode.SupplyRecord: - { - foreach (int recordId in recordIds) - { - var record = _supplyRecordDataAccess.GetSupplyRecordById(recordId); - stickerViewModel.SupplyRecords.Add(record); - recordsAdded++; - } - } - break; - case ImportMode.NoteRecord: - { - foreach (int recordId in recordIds) - { - var record = _noteDataAccess.GetNoteById(recordId); - stickerViewModel.GenericRecords.Add(new GenericRecord - { - Description = record.Description, - Notes = record.NoteText - }); - recordsAdded++; - } - - } - break; - case ImportMode.OdometerRecord: - { - foreach (int recordId in recordIds) - { - var record = _odometerRecordDataAccess.GetOdometerRecordById(recordId); - stickerViewModel.GenericRecords.Add(new GenericRecord - { - Date = record.Date, - Mileage = record.Mileage, - Notes = record.Notes, - ExtraFields = record.ExtraFields - }); - recordsAdded++; - } - - } - break; - case ImportMode.ReminderRecord: - { - foreach (int recordId in recordIds) - { - stickerViewModel.ReminderRecords.Add(_reminderRecordDataAccess.GetReminderRecordById(recordId)); - recordsAdded++; - } - - } - break; - case ImportMode.PlanRecord: - { - foreach (int recordId in recordIds) - { - var record = _planRecordDataAccess.GetPlanRecordById(recordId); - stickerViewModel.GenericRecords.Add(new GenericRecord - { - Description = record.Description, - Cost = record.Cost, - Notes = record.Notes, - Date = record.DateModified, - ExtraFields = record.ExtraFields, - RequisitionHistory = record.RequisitionHistory - }); - recordsAdded++; - } - } - break; - } - if (recordsAdded > 0) - { - return PartialView("_Stickers", stickerViewModel); - } - return Json(result); - } - [HttpPost] - public IActionResult SaveUserColumnPreferences(UserColumnPreference columnPreference) - { - try - { - var userConfig = _config.GetUserConfig(User); - var existingUserColumnPreference = userConfig.UserColumnPreferences.Where(x => x.Tab == columnPreference.Tab); - if (existingUserColumnPreference.Any()) - { - var existingPreference = existingUserColumnPreference.Single(); - existingPreference.VisibleColumns = columnPreference.VisibleColumns; - existingPreference.ColumnOrder = columnPreference.ColumnOrder; - } - else - { - userConfig.UserColumnPreferences.Add(columnPreference); - } - var result = _config.SaveUserConfig(User, userConfig); - return Json(result); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Json(false); - } - } - #endregion - } -} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e6b9fee..0000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env -WORKDIR /App - -COPY . ./ -ARG TARGETARCH -RUN dotnet restore -a $TARGETARCH -RUN dotnet publish -a $TARGETARCH -c Release -o out - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 -WORKDIR /App -COPY --from=build-env /App/out . -EXPOSE 8080 -CMD ["./MotoVaultPro"] diff --git a/Enum/DashboardMetric.cs b/Enum/DashboardMetric.cs deleted file mode 100644 index 38ee91f..0000000 --- a/Enum/DashboardMetric.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum DashboardMetric - { - Default = 0, - TotalCost = 1, - CostPerMile = 2 - } -} diff --git a/Enum/ExtraFieldType.cs b/Enum/ExtraFieldType.cs deleted file mode 100644 index 1e770f3..0000000 --- a/Enum/ExtraFieldType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ExtraFieldType - { - Text = 0, - Number = 1, - Decimal = 2, - Date = 3, - Time = 4, - Location = 5 - } -} diff --git a/Enum/ImportMode.cs b/Enum/ImportMode.cs deleted file mode 100644 index a154170..0000000 --- a/Enum/ImportMode.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ImportMode - { - ServiceRecord = 0, - GasRecord = 2, - TaxRecord = 3, - UpgradeRecord = 4, - ReminderRecord = 5, - NoteRecord = 6, - SupplyRecord = 7, - Dashboard = 8, - PlanRecord = 9, - OdometerRecord = 10, - VehicleRecord = 11 - } -} diff --git a/Enum/KioskMode.cs b/Enum/KioskMode.cs deleted file mode 100644 index c8ab4c9..0000000 --- a/Enum/KioskMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum KioskMode - { - Vehicle = 0, - Plan = 1, - Reminder = 2, - Cycle = 3 - } -} diff --git a/Enum/PlanPriority.cs b/Enum/PlanPriority.cs deleted file mode 100644 index ccf04b4..0000000 --- a/Enum/PlanPriority.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum PlanPriority - { - Critical = 0, - Normal = 1, - Low = 2 - } -} diff --git a/Enum/PlanProgress.cs b/Enum/PlanProgress.cs deleted file mode 100644 index db32273..0000000 --- a/Enum/PlanProgress.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum PlanProgress - { - Backlog = 0, - InProgress = 1, - Testing = 2, - Done = 3 - } -} diff --git a/Enum/ReminderIntervalUnit.cs b/Enum/ReminderIntervalUnit.cs deleted file mode 100644 index 611c579..0000000 --- a/Enum/ReminderIntervalUnit.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ReminderIntervalUnit - { - Months = 1, - Days = 2 - } -} diff --git a/Enum/ReminderMetric.cs b/Enum/ReminderMetric.cs deleted file mode 100644 index 294b271..0000000 --- a/Enum/ReminderMetric.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ReminderMetric - { - Date = 0, - Odometer = 1, - Both = 2 - } -} diff --git a/Enum/ReminderMileageInterval.cs b/Enum/ReminderMileageInterval.cs deleted file mode 100644 index e29b91d..0000000 --- a/Enum/ReminderMileageInterval.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ReminderMileageInterval - { - Other = 0, - FiftyMiles = 50, - OneHundredMiles = 100, - FiveHundredMiles = 500, - OneThousandMiles = 1000, - ThreeThousandMiles = 3000, - FourThousandMiles = 4000, - FiveThousandMiles = 5000, - SevenThousandFiveHundredMiles = 7500, - TenThousandMiles = 10000, - FifteenThousandMiles = 15000, - TwentyThousandMiles = 20000, - ThirtyThousandMiles = 30000, - FortyThousandMiles = 40000, - FiftyThousandMiles = 50000, - SixtyThousandMiles = 60000, - OneHundredThousandMiles = 100000, - OneHundredFiftyThousandMiles = 150000 - } -} diff --git a/Enum/ReminderMonthInterval.cs b/Enum/ReminderMonthInterval.cs deleted file mode 100644 index f8f765a..0000000 --- a/Enum/ReminderMonthInterval.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ReminderMonthInterval - { - Other = 0, - OneMonth = 1, - ThreeMonths = 3, - SixMonths = 6, - OneYear = 12, - TwoYears = 24, - ThreeYears = 36, - FiveYears = 60 - } -} diff --git a/Enum/ReminderUrgency.cs b/Enum/ReminderUrgency.cs deleted file mode 100644 index 2807ade..0000000 --- a/Enum/ReminderUrgency.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum ReminderUrgency - { - NotUrgent = 0, - Urgent = 1, - VeryUrgent = 2, - PastDue = 3 - } -} diff --git a/Enum/TagFilter.cs b/Enum/TagFilter.cs deleted file mode 100644 index fd95b4a..0000000 --- a/Enum/TagFilter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public enum TagFilter - { - Exclude = 0, - IncludeOnly = 1 - } -} diff --git a/External/Implementations/Litedb/ExtraFieldDataAccess.cs b/External/Implementations/Litedb/ExtraFieldDataAccess.cs deleted file mode 100644 index 9ba3d37..0000000 --- a/External/Implementations/Litedb/ExtraFieldDataAccess.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Implementations -{ - public class ExtraFieldDataAccess : IExtraFieldDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "extrafields"; - public ExtraFieldDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public RecordExtraField GetExtraFieldsById(int importMode) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(importMode) ?? new RecordExtraField(); - } - public bool SaveExtraFields(RecordExtraField record) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(record); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/GasRecordDataAccess.cs b/External/Implementations/Litedb/GasRecordDataAccess.cs deleted file mode 100644 index 2506320..0000000 --- a/External/Implementations/Litedb/GasRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class GasRecordDataAccess : IGasRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "gasrecords"; - public GasRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetGasRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var gasRecords = table.Find(Query.EQ(nameof(GasRecord.VehicleId), vehicleId)); - return gasRecords.ToList() ?? new List(); - } - public GasRecord GetGasRecordById(int gasRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(gasRecordId); - } - public bool DeleteGasRecordById(int gasRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(gasRecordId); - db.Checkpoint(); - return true; - } - public bool SaveGasRecordToVehicle(GasRecord gasRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(gasRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllGasRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var gasRecords = table.DeleteMany(Query.EQ(nameof(GasRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/NoteDataAccess.cs b/External/Implementations/Litedb/NoteDataAccess.cs deleted file mode 100644 index 0023c82..0000000 --- a/External/Implementations/Litedb/NoteDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class NoteDataAccess : INoteDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "notes"; - public NoteDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetNotesByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var noteToReturn = table.Find(Query.EQ(nameof(Note.VehicleId), vehicleId)); - return noteToReturn.ToList() ?? new List(); - } - public Note GetNoteById(int noteId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(noteId); - } - public bool SaveNoteToVehicle(Note note) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(note); - db.Checkpoint(); - return true; - } - public bool DeleteNoteById(int noteId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(noteId); - db.Checkpoint(); - return true; - } - public bool DeleteAllNotesByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var notes = table.DeleteMany(Query.EQ(nameof(Note.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/OdometerRecordDataAccess.cs b/External/Implementations/Litedb/OdometerRecordDataAccess.cs deleted file mode 100644 index 3653308..0000000 --- a/External/Implementations/Litedb/OdometerRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class OdometerRecordDataAccess : IOdometerRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "odometerrecords"; - public OdometerRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetOdometerRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var odometerRecords = table.Find(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId)); - return odometerRecords.ToList() ?? new List(); - } - public OdometerRecord GetOdometerRecordById(int odometerRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(odometerRecordId); - } - public bool DeleteOdometerRecordById(int odometerRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(odometerRecordId); - db.Checkpoint(); - return true; - } - public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(odometerRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var odometerRecords = table.DeleteMany(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/PlanRecordDataAccess.cs b/External/Implementations/Litedb/PlanRecordDataAccess.cs deleted file mode 100644 index d51b40f..0000000 --- a/External/Implementations/Litedb/PlanRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class PlanRecordDataAccess : IPlanRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "planrecords"; - public PlanRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetPlanRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var planRecords = table.Find(Query.EQ(nameof(PlanRecord.VehicleId), vehicleId)); - return planRecords.ToList() ?? new List(); - } - public PlanRecord GetPlanRecordById(int planRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(planRecordId); - } - public bool DeletePlanRecordById(int planRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(planRecordId); - db.Checkpoint(); - return true; - } - public bool SavePlanRecordToVehicle(PlanRecord planRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(planRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllPlanRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var planRecords = table.DeleteMany(Query.EQ(nameof(PlanRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/PlanRecordTemplateDataAccess.cs b/External/Implementations/Litedb/PlanRecordTemplateDataAccess.cs deleted file mode 100644 index 35b3389..0000000 --- a/External/Implementations/Litedb/PlanRecordTemplateDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class PlanRecordTemplateDataAccess : IPlanRecordTemplateDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "planrecordtemplates"; - public PlanRecordTemplateDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetPlanRecordTemplatesByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var planRecords = table.Find(Query.EQ(nameof(PlanRecordInput.VehicleId), vehicleId)); - return planRecords.ToList() ?? new List(); - } - public PlanRecordInput GetPlanRecordTemplateById(int planRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(planRecordId); - } - public bool DeletePlanRecordTemplateById(int planRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(planRecordId); - db.Checkpoint(); - return true; - } - public bool SavePlanRecordTemplateToVehicle(PlanRecordInput planRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(planRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllPlanRecordTemplatesByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var planRecords = table.DeleteMany(Query.EQ(nameof(PlanRecordInput.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/ReminderRecordDataAccess.cs b/External/Implementations/Litedb/ReminderRecordDataAccess.cs deleted file mode 100644 index 7b5f567..0000000 --- a/External/Implementations/Litedb/ReminderRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class ReminderRecordDataAccess : IReminderRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "reminderrecords"; - public ReminderRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetReminderRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var reminderRecords = table.Find(Query.EQ(nameof(ReminderRecord.VehicleId), vehicleId)); - return reminderRecords.ToList() ?? new List(); - } - public ReminderRecord GetReminderRecordById(int reminderRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(reminderRecordId); - } - public bool DeleteReminderRecordById(int reminderRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(reminderRecordId); - db.Checkpoint(); - return true; - } - public bool SaveReminderRecordToVehicle(ReminderRecord reminderRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(reminderRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllReminderRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var reminderRecords = table.DeleteMany(Query.EQ(nameof(ReminderRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/ServiceRecordDataAccess.cs b/External/Implementations/Litedb/ServiceRecordDataAccess.cs deleted file mode 100644 index bdd291d..0000000 --- a/External/Implementations/Litedb/ServiceRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class ServiceRecordDataAccess : IServiceRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "servicerecords"; - public ServiceRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetServiceRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var serviceRecords = table.Find(Query.EQ(nameof(ServiceRecord.VehicleId), vehicleId)); - return serviceRecords.ToList() ?? new List(); - } - public ServiceRecord GetServiceRecordById(int serviceRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(serviceRecordId); - } - public bool DeleteServiceRecordById(int serviceRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(serviceRecordId); - db.Checkpoint(); - return true; - } - public bool SaveServiceRecordToVehicle(ServiceRecord serviceRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(serviceRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllServiceRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var serviceRecords = table.DeleteMany(Query.EQ(nameof(ServiceRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/SupplyRecordDataAccess.cs b/External/Implementations/Litedb/SupplyRecordDataAccess.cs deleted file mode 100644 index 6fdb27e..0000000 --- a/External/Implementations/Litedb/SupplyRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class SupplyRecordDataAccess : ISupplyRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "supplyrecords"; - public SupplyRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetSupplyRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var supplyRecords = table.Find(Query.EQ(nameof(SupplyRecord.VehicleId), vehicleId)); - return supplyRecords.ToList() ?? new List(); - } - public SupplyRecord GetSupplyRecordById(int supplyRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(supplyRecordId); - } - public bool DeleteSupplyRecordById(int supplyRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(supplyRecordId); - db.Checkpoint(); - return true; - } - public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(supplyRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var supplyRecords = table.DeleteMany(Query.EQ(nameof(SupplyRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/TaxRecordDataAccess.cs b/External/Implementations/Litedb/TaxRecordDataAccess.cs deleted file mode 100644 index c6f4235..0000000 --- a/External/Implementations/Litedb/TaxRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class TaxRecordDataAccess : ITaxRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "taxrecords"; - public TaxRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetTaxRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var taxRecords = table.Find(Query.EQ(nameof(TaxRecord.VehicleId), vehicleId)); - return taxRecords.ToList() ?? new List(); - } - public TaxRecord GetTaxRecordById(int taxRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(taxRecordId); - } - public bool DeleteTaxRecordById(int taxRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(taxRecordId); - db.Checkpoint(); - return true; - } - public bool SaveTaxRecordToVehicle(TaxRecord taxRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(taxRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllTaxRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var taxRecords = table.DeleteMany(Query.EQ(nameof(TaxRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/TokenRecordDataAccess.cs b/External/Implementations/Litedb/TokenRecordDataAccess.cs deleted file mode 100644 index c3cbdcc..0000000 --- a/External/Implementations/Litedb/TokenRecordDataAccess.cs +++ /dev/null @@ -1,53 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class TokenRecordDataAccess : ITokenRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "tokenrecords"; - public TokenRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetTokens() - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindAll().ToList(); - } - public Token GetTokenRecordByBody(string tokenBody) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var tokenRecord = table.FindOne(Query.EQ(nameof(Token.Body), tokenBody)); - return tokenRecord ?? new Token(); - } - public Token GetTokenRecordByEmailAddress(string emailAddress) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var tokenRecord = table.FindOne(Query.EQ(nameof(Token.EmailAddress), emailAddress)); - return tokenRecord ?? new Token(); - } - public bool CreateNewToken(Token token) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Insert(token); - db.Checkpoint(); - return true; - } - public bool DeleteToken(int tokenId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(tokenId); - db.Checkpoint(); - return true; - } - } -} \ No newline at end of file diff --git a/External/Implementations/Litedb/UpgradeRecordDataAccess.cs b/External/Implementations/Litedb/UpgradeRecordDataAccess.cs deleted file mode 100644 index 086a44c..0000000 --- a/External/Implementations/Litedb/UpgradeRecordDataAccess.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using LiteDB; - -namespace MotoVaultPro.External.Implementations -{ - public class UpgradeRecordDataAccess : IUpgradeRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - public UpgradeRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - private static string tableName = "upgraderecords"; - public List GetUpgradeRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var upgradeRecords = table.Find(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId)); - return upgradeRecords.ToList() ?? new List(); - } - public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(upgradeRecordId); - } - public bool DeleteUpgradeRecordById(int upgradeRecordId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(upgradeRecordId); - db.Checkpoint(); - return true; - } - public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(upgradeRecord); - db.Checkpoint(); - return true; - } - public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var upgradeRecords = table.DeleteMany(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId)); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/UserAccessDataAcces.cs b/External/Implementations/Litedb/UserAccessDataAcces.cs deleted file mode 100644 index 404e0c9..0000000 --- a/External/Implementations/Litedb/UserAccessDataAcces.cs +++ /dev/null @@ -1,81 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Implementations -{ - public class UserAccessDataAccess : IUserAccessDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "useraccessrecords"; - public UserAccessDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - /// - /// Gets a list of vehicles user have access to. - /// - /// - /// - public List GetUserAccessByUserId(int userId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.Find(x => x.Id.UserId == userId).ToList(); - } - public UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.Find(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId).FirstOrDefault(); - } - public List GetUserAccessByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.Find(x => x.Id.VehicleId == vehicleId).ToList(); - } - public bool SaveUserAccess(UserAccess userAccess) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(userAccess); - db.Checkpoint(); - return true; - } - public bool DeleteUserAccess(int userId, int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.DeleteMany(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId); - db.Checkpoint(); - return true; - } - /// - /// Delete all access records when a vehicle is deleted. - /// - /// - /// - public bool DeleteAllAccessRecordsByVehicleId(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.DeleteMany(x => x.Id.VehicleId == vehicleId); - db.Checkpoint(); - return true; - } - /// - /// Delee all access records when a user is deleted. - /// - /// - /// - public bool DeleteAllAccessRecordsByUserId(int userId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.DeleteMany(x => x.Id.UserId == userId); - db.Checkpoint(); - return true; - } - } -} \ No newline at end of file diff --git a/External/Implementations/Litedb/UserConfigDataAccess.cs b/External/Implementations/Litedb/UserConfigDataAccess.cs deleted file mode 100644 index 0bd53ad..0000000 --- a/External/Implementations/Litedb/UserConfigDataAccess.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using MotoVaultPro.Helper; - -namespace MotoVaultPro.External.Implementations -{ - public class UserConfigDataAccess : IUserConfigDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "userconfigrecords"; - public UserConfigDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public UserConfigData GetUserConfig(int userId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(userId); - } - public bool SaveUserConfig(UserConfigData userConfigData) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(userConfigData); - db.Checkpoint(); - return true; - } - public bool DeleteUserConfig(int userId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(userId); - db.Checkpoint(); - return true; - } - } -} diff --git a/External/Implementations/Litedb/UserRecordDataAccess.cs b/External/Implementations/Litedb/UserRecordDataAccess.cs deleted file mode 100644 index 8159aa7..0000000 --- a/External/Implementations/Litedb/UserRecordDataAccess.cs +++ /dev/null @@ -1,60 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using LiteDB; -using MotoVaultPro.Helper; - -namespace MotoVaultPro.External.Implementations -{ - public class UserRecordDataAccess : IUserRecordDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "userrecords"; - public UserRecordDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public List GetUsers() - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindAll().ToList(); - } - public UserData GetUserRecordByUserName(string userName) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var userRecord = table.FindOne(Query.EQ(nameof(UserData.UserName), userName)); - return userRecord ?? new UserData(); - } - public UserData GetUserRecordByEmailAddress(string emailAddress) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var userRecord = table.FindOne(Query.EQ(nameof(UserData.EmailAddress), emailAddress)); - return userRecord ?? new UserData(); - } - public UserData GetUserRecordById(int userId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var userRecord = table.FindById(userId); - return userRecord ?? new UserData(); - } - public bool SaveUserRecord(UserData userRecord) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Upsert(userRecord); - db.Checkpoint(); - return true; - } - public bool DeleteUserRecord(int userId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - table.Delete(userId); - db.Checkpoint(); - return true; - } - } -} \ No newline at end of file diff --git a/External/Implementations/Litedb/VehicleDataAccess.cs b/External/Implementations/Litedb/VehicleDataAccess.cs deleted file mode 100644 index f8bd00d..0000000 --- a/External/Implementations/Litedb/VehicleDataAccess.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Implementations -{ - public class VehicleDataAccess : IVehicleDataAccess - { - private ILiteDBHelper _liteDB { get; set; } - private static string tableName = "vehicles"; - public VehicleDataAccess(ILiteDBHelper liteDB) - { - _liteDB = liteDB; - } - public bool SaveVehicle(Vehicle vehicle) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var result = table.Upsert(vehicle); - db.Checkpoint(); - return true; - } - public bool DeleteVehicle(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - var result = table.Delete(vehicleId); - db.Checkpoint(); - return result; - } - public List GetVehicles() - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindAll().ToList(); - } - public Vehicle GetVehicleById(int vehicleId) - { - var db = _liteDB.GetLiteDB(); - var table = db.GetCollection(tableName); - return table.FindById(vehicleId); - } - } -} diff --git a/External/Implementations/Postgres/ExtraFieldDataAccess.cs b/External/Implementations/Postgres/ExtraFieldDataAccess.cs deleted file mode 100644 index cdb13d9..0000000 --- a/External/Implementations/Postgres/ExtraFieldDataAccess.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGExtraFieldDataAccess : IExtraFieldDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "extrafields"; - public PGExtraFieldDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public RecordExtraField GetExtraFieldsById(int importMode) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var results = new RecordExtraField(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", importMode); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - RecordExtraField result = JsonSerializer.Deserialize(reader["data"] as string); - results = result; - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new RecordExtraField(); - } - } - public bool SaveExtraFields(RecordExtraField record) - { - try - { - var existingRecord = GetExtraFieldsById(record.Id); - string cmd = $"INSERT INTO app.{tableName} (id, data) VALUES(@id, CAST(@data AS jsonb)) ON CONFLICT(id) DO UPDATE SET data = CAST(@data AS jsonb)"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", record.Id); - ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record)); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/GasRecordDataAccess.cs b/External/Implementations/Postgres/GasRecordDataAccess.cs deleted file mode 100644 index e705c54..0000000 --- a/External/Implementations/Postgres/GasRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGGasRecordDataAccess: IGasRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "gasrecords"; - public PGGasRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetGasRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - GasRecord gasRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(gasRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public GasRecord GetGasRecordById(int gasRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new GasRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", gasRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - GasRecord gasRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = gasRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new GasRecord(); - } - } - public bool DeleteGasRecordById(int gasRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", gasRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveGasRecordToVehicle(GasRecord gasRecord) - { - try - { - if (gasRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", gasRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - gasRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (gasRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(gasRecord); - ctextU.Parameters.AddWithValue("id", gasRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return gasRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(gasRecord); - ctext.Parameters.AddWithValue("id", gasRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllGasRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/NoteDataAccess.cs b/External/Implementations/Postgres/NoteDataAccess.cs deleted file mode 100644 index 47f647b..0000000 --- a/External/Implementations/Postgres/NoteDataAccess.cs +++ /dev/null @@ -1,154 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGNoteDataAccess: INoteDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "notes"; - public PGNoteDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetNotesByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - Note note = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(note); - } - } - return results; - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public Note GetNoteById(int noteId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new Note(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", noteId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - Note note = JsonSerializer.Deserialize(reader["data"] as string); - result = note; - } - } - return result; - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Note(); - } - } - public bool SaveNoteToVehicle(Note note) - { - try - { - if (note.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", note.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - note.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (note.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(note); - ctextU.Parameters.AddWithValue("id", note.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return note.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(note); - ctext.Parameters.AddWithValue("id", note.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteNoteById(int noteId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", noteId); - return ctext.ExecuteNonQuery() > 0; - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllNotesByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/OdometerRecordDataAccess.cs b/External/Implementations/Postgres/OdometerRecordDataAccess.cs deleted file mode 100644 index bc38142..0000000 --- a/External/Implementations/Postgres/OdometerRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGOdometerRecordDataAccess : IOdometerRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "odometerrecords"; - public PGOdometerRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetOdometerRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - OdometerRecord odometerRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(odometerRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public OdometerRecord GetOdometerRecordById(int odometerRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new OdometerRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", odometerRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - OdometerRecord odometerRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = odometerRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new OdometerRecord(); - } - } - public bool DeleteOdometerRecordById(int odometerRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", odometerRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord) - { - try - { - if (odometerRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", odometerRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - odometerRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (odometerRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(odometerRecord); - ctextU.Parameters.AddWithValue("id", odometerRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return odometerRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(odometerRecord); - ctext.Parameters.AddWithValue("id", odometerRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/PlanRecordDataAccess.cs b/External/Implementations/Postgres/PlanRecordDataAccess.cs deleted file mode 100644 index 7c27583..0000000 --- a/External/Implementations/Postgres/PlanRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGPlanRecordDataAccess : IPlanRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "planrecords"; - public PGPlanRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetPlanRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - PlanRecord planRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(planRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public PlanRecord GetPlanRecordById(int planRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new PlanRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", planRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - PlanRecord planRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = planRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new PlanRecord(); - } - } - public bool DeletePlanRecordById(int planRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", planRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SavePlanRecordToVehicle(PlanRecord planRecord) - { - try - { - if (planRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", planRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - planRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (planRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(planRecord); - ctextU.Parameters.AddWithValue("id", planRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return planRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(planRecord); - ctext.Parameters.AddWithValue("id", planRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllPlanRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/PlanRecordTemplateDataAccess.cs b/External/Implementations/Postgres/PlanRecordTemplateDataAccess.cs deleted file mode 100644 index 33ca3a7..0000000 --- a/External/Implementations/Postgres/PlanRecordTemplateDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGPlanRecordTemplateDataAccess : IPlanRecordTemplateDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "planrecordtemplates"; - public PGPlanRecordTemplateDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetPlanRecordTemplatesByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - PlanRecordInput planRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(planRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public PlanRecordInput GetPlanRecordTemplateById(int planRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new PlanRecordInput(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", planRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - PlanRecordInput planRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = planRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new PlanRecordInput(); - } - } - public bool DeletePlanRecordTemplateById(int planRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", planRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SavePlanRecordTemplateToVehicle(PlanRecordInput planRecord) - { - try - { - if (planRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", planRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - planRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (planRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(planRecord); - ctextU.Parameters.AddWithValue("id", planRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return planRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(planRecord); - ctext.Parameters.AddWithValue("id", planRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllPlanRecordTemplatesByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/ReminderRecordDataAccess.cs b/External/Implementations/Postgres/ReminderRecordDataAccess.cs deleted file mode 100644 index b811927..0000000 --- a/External/Implementations/Postgres/ReminderRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGReminderRecordDataAccess : IReminderRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "reminderrecords"; - public PGReminderRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetReminderRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - ReminderRecord reminderRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(reminderRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public ReminderRecord GetReminderRecordById(int reminderRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new ReminderRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", reminderRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - ReminderRecord reminderRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = reminderRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new ReminderRecord(); - } - } - public bool DeleteReminderRecordById(int reminderRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", reminderRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveReminderRecordToVehicle(ReminderRecord reminderRecord) - { - try - { - if (reminderRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", reminderRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - reminderRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (reminderRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(reminderRecord); - ctextU.Parameters.AddWithValue("id", reminderRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return reminderRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(reminderRecord); - ctext.Parameters.AddWithValue("id", reminderRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllReminderRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/ServiceRecordDataAccess.cs b/External/Implementations/Postgres/ServiceRecordDataAccess.cs deleted file mode 100644 index 040d5f1..0000000 --- a/External/Implementations/Postgres/ServiceRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGServiceRecordDataAccess: IServiceRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "servicerecords"; - public PGServiceRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetServiceRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - ServiceRecord serviceRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(serviceRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public ServiceRecord GetServiceRecordById(int serviceRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new ServiceRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", serviceRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - ServiceRecord serviceRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = serviceRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new ServiceRecord(); - } - } - public bool DeleteServiceRecordById(int serviceRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", serviceRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveServiceRecordToVehicle(ServiceRecord serviceRecord) - { - try - { - if (serviceRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", serviceRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - serviceRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (serviceRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(serviceRecord); - ctextU.Parameters.AddWithValue("id", serviceRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return serviceRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(serviceRecord); - ctext.Parameters.AddWithValue("id", serviceRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllServiceRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/SupplyRecordDataAccess.cs b/External/Implementations/Postgres/SupplyRecordDataAccess.cs deleted file mode 100644 index 2a1a1ff..0000000 --- a/External/Implementations/Postgres/SupplyRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGSupplyRecordDataAccess : ISupplyRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "supplyrecords"; - public PGSupplyRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetSupplyRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - SupplyRecord supplyRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(supplyRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public SupplyRecord GetSupplyRecordById(int supplyRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new SupplyRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", supplyRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - SupplyRecord supplyRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = supplyRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new SupplyRecord(); - } - } - public bool DeleteSupplyRecordById(int supplyRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", supplyRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord) - { - try - { - if (supplyRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", supplyRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - supplyRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (supplyRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(supplyRecord); - ctextU.Parameters.AddWithValue("id", supplyRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return supplyRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(supplyRecord); - ctext.Parameters.AddWithValue("id", supplyRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/TaxRecordDataAccess.cs b/External/Implementations/Postgres/TaxRecordDataAccess.cs deleted file mode 100644 index e393c5f..0000000 --- a/External/Implementations/Postgres/TaxRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGTaxRecordDataAccess : ITaxRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "taxrecords"; - public PGTaxRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetTaxRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - TaxRecord taxRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(taxRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public TaxRecord GetTaxRecordById(int taxRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new TaxRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", taxRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - TaxRecord taxRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = taxRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new TaxRecord(); - } - } - public bool DeleteTaxRecordById(int taxRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", taxRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveTaxRecordToVehicle(TaxRecord taxRecord) - { - try - { - if (taxRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", taxRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - taxRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (taxRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(taxRecord); - ctextU.Parameters.AddWithValue("id", taxRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return taxRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(taxRecord); - ctext.Parameters.AddWithValue("id", taxRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllTaxRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/TokenRecordDataAccess.cs b/External/Implementations/Postgres/TokenRecordDataAccess.cs deleted file mode 100644 index 855a619..0000000 --- a/External/Implementations/Postgres/TokenRecordDataAccess.cs +++ /dev/null @@ -1,143 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; - -namespace MotoVaultPro.External.Implementations -{ - public class PGTokenRecordDataAccess : ITokenRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "tokenrecords"; - public PGTokenRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, body TEXT not null, emailaddress TEXT not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetTokens() - { - try - { - string cmd = $"SELECT id, emailaddress, body FROM app.{tableName}"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - Token result = new Token(); - result.Id = int.Parse(reader["id"].ToString()); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Body = reader["body"].ToString(); - results.Add(result); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public Token GetTokenRecordByBody(string tokenBody) - { - try - { - string cmd = $"SELECT id, emailaddress, body FROM app.{tableName} WHERE body = @body"; - var result = new Token(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("body", tokenBody); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - result.Id = int.Parse(reader["id"].ToString()); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Body = reader["body"].ToString(); - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Token(); - } - } - public Token GetTokenRecordByEmailAddress(string emailAddress) - { - try - { - string cmd = $"SELECT id, emailaddress, body FROM app.{tableName} WHERE emailaddress = @emailaddress"; - var result = new Token(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("emailaddress", emailAddress); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - result.Id = int.Parse(reader["id"].ToString()); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Body = reader["body"].ToString(); - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Token(); - } - } - public bool CreateNewToken(Token token) - { - try - { - string cmd = $"INSERT INTO app.{tableName} (emailaddress, body) VALUES(@emailaddress, @body) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("emailaddress", token.EmailAddress); - ctext.Parameters.AddWithValue("body", token.Body); - token.Id = Convert.ToInt32(ctext.ExecuteScalar()); - return token.Id != default; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteToken(int tokenId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", tokenId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} \ No newline at end of file diff --git a/External/Implementations/Postgres/UpgradeRecordDataAccess.cs b/External/Implementations/Postgres/UpgradeRecordDataAccess.cs deleted file mode 100644 index 4f87f7c..0000000 --- a/External/Implementations/Postgres/UpgradeRecordDataAccess.cs +++ /dev/null @@ -1,160 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGUpgradeRecordDataAccess : IUpgradeRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "upgraderecords"; - public PGUpgradeRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetUpgradeRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UpgradeRecord upgradeRecord = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(upgradeRecord); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - var result = new UpgradeRecord(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", upgradeRecordId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UpgradeRecord upgradeRecord = JsonSerializer.Deserialize(reader["data"] as string); - result = upgradeRecord; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new UpgradeRecord(); - } - } - public bool DeleteUpgradeRecordById(int upgradeRecordId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", upgradeRecordId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord) - { - try - { - if (upgradeRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (vehicleId, data) VALUES(@vehicleId, CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", upgradeRecord.VehicleId); - ctext.Parameters.AddWithValue("data", "{}"); - upgradeRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (upgradeRecord.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(upgradeRecord); - ctextU.Parameters.AddWithValue("id", upgradeRecord.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return upgradeRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(upgradeRecord); - ctext.Parameters.AddWithValue("id", upgradeRecord.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/UserAccessDataAccess.cs b/External/Implementations/Postgres/UserAccessDataAccess.cs deleted file mode 100644 index f3c335c..0000000 --- a/External/Implementations/Postgres/UserAccessDataAccess.cs +++ /dev/null @@ -1,212 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; - -namespace MotoVaultPro.External.Implementations -{ - public class PGUserAccessDataAccess : IUserAccessDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "useraccessrecords"; - public PGUserAccessDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - /// - /// Gets a list of vehicles user have access to. - /// - /// - /// - public List GetUserAccessByUserId(int userId) - { - try - { - string cmd = $"SELECT userId, vehicleId FROM app.{tableName} WHERE userId = @userId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("userId", userId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UserAccess result = new UserAccess() - { - Id = new UserVehicle - { - UserId = int.Parse(reader["userId"].ToString()), - VehicleId = int.Parse(reader["vehicleId"].ToString()) - } - }; - results.Add(result); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId) - { - try - { - string cmd = $"SELECT userId, vehicleId FROM app.{tableName} WHERE userId = @userId AND vehicleId = @vehicleId"; - UserAccess result = null; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("userId", userId); - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - result = new UserAccess() - { - Id = new UserVehicle - { - UserId = int.Parse(reader["userId"].ToString()), - VehicleId = int.Parse(reader["vehicleId"].ToString()) - } - }; - return result; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new UserAccess(); - } - } - public List GetUserAccessByVehicleId(int vehicleId) - { - try - { - string cmd = $"SELECT userId, vehicleId FROM app.{tableName} WHERE vehicleId = @vehicleId"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UserAccess result = new UserAccess() - { - Id = new UserVehicle - { - UserId = int.Parse(reader["userId"].ToString()), - VehicleId = int.Parse(reader["vehicleId"].ToString()) - } - }; - results.Add(result); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public bool SaveUserAccess(UserAccess userAccess) - { - try - { - string cmd = $"INSERT INTO app.{tableName} (userId, vehicleId) VALUES(@userId, @vehicleId)"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("userId", userAccess.Id.UserId); - ctext.Parameters.AddWithValue("vehicleId", userAccess.Id.VehicleId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteUserAccess(int userId, int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE userId = @userId AND vehicleId = @vehicleId"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("userId", userId); - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - /// - /// Delete all access records when a vehicle is deleted. - /// - /// - /// - public bool DeleteAllAccessRecordsByVehicleId(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE vehicleId = @vehicleId"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("vehicleId", vehicleId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - /// - /// Delee all access records when a user is deleted. - /// - /// - /// - public bool DeleteAllAccessRecordsByUserId(int userId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE userId = @userId"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("userId", userId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} \ No newline at end of file diff --git a/External/Implementations/Postgres/UserConfigDataAccess.cs b/External/Implementations/Postgres/UserConfigDataAccess.cs deleted file mode 100644 index 6235f55..0000000 --- a/External/Implementations/Postgres/UserConfigDataAccess.cs +++ /dev/null @@ -1,108 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGUserConfigDataAccess: IUserConfigDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "userconfigrecords"; - public PGUserConfigDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT primary key, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public UserConfigData GetUserConfig(int userId) - { - try - { - string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id"; - UserConfigData result = null; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", userId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UserConfigData userConfig = JsonSerializer.Deserialize(reader["data"] as string); - result = userConfig; - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new UserConfigData(); - } - } - public bool SaveUserConfig(UserConfigData userConfigData) - { - var existingRecord = GetUserConfig(userConfigData.Id); - try - { - if (existingRecord == null) - { - string cmd = $"INSERT INTO app.{tableName} (id, data) VALUES(@id, CAST(@data AS jsonb))"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(userConfigData); - ctext.Parameters.AddWithValue("id", userConfigData.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(userConfigData); - ctext.Parameters.AddWithValue("id", userConfigData.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteUserConfig(int userId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", userId); - ctext.ExecuteNonQuery(); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/External/Implementations/Postgres/UserRecordDataAccess.cs b/External/Implementations/Postgres/UserRecordDataAccess.cs deleted file mode 100644 index a9941e4..0000000 --- a/External/Implementations/Postgres/UserRecordDataAccess.cs +++ /dev/null @@ -1,194 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; - -namespace MotoVaultPro.External.Implementations -{ - public class PGUserRecordDataAccess : IUserRecordDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "userrecords"; - public PGUserRecordDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, username TEXT not null, emailaddress TEXT not null, password TEXT not null, isadmin BOOLEAN)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public List GetUsers() - { - try - { - string cmd = $"SELECT id, username, emailaddress, password, isadmin FROM app.{tableName}"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - UserData result = new UserData(); - result.Id = int.Parse(reader["id"].ToString()); - result.UserName = reader["username"].ToString(); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Password = reader["password"].ToString(); - result.IsAdmin = bool.Parse(reader["isadmin"].ToString()); - results.Add(result); - } - } - return results; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public UserData GetUserRecordByUserName(string userName) - { - try - { - string cmd = $"SELECT id, username, emailaddress, password, isadmin FROM app.{tableName} WHERE username = @username"; - var result = new UserData(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("username", userName); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - result.Id = int.Parse(reader["id"].ToString()); - result.UserName = reader["username"].ToString(); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Password = reader["password"].ToString(); - result.IsAdmin = bool.Parse(reader["isadmin"].ToString()); - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new UserData(); - } - } - public UserData GetUserRecordByEmailAddress(string emailAddress) - { - try - { - string cmd = $"SELECT id, username, emailaddress, password, isadmin FROM app.{tableName} WHERE emailaddress = @emailaddress"; - var result = new UserData(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("emailaddress", emailAddress); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - result.Id = int.Parse(reader["id"].ToString()); - result.UserName = reader["username"].ToString(); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Password = reader["password"].ToString(); - result.IsAdmin = bool.Parse(reader["isadmin"].ToString()); - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new UserData(); - } - } - public UserData GetUserRecordById(int userId) - { - try - { - string cmd = $"SELECT id, username, emailaddress, password, isadmin FROM app.{tableName} WHERE id = @id"; - var result = new UserData(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", userId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - result.Id = int.Parse(reader["id"].ToString()); - result.UserName = reader["username"].ToString(); - result.EmailAddress = reader["emailaddress"].ToString(); - result.Password = reader["password"].ToString(); - result.IsAdmin = bool.Parse(reader["isadmin"].ToString()); - } - } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new UserData(); - } - } - public bool SaveUserRecord(UserData userRecord) - { - try - { - if (userRecord.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (username, emailaddress, password, isadmin) VALUES(@username, @emailaddress, @password, @isadmin) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("username", userRecord.UserName); - ctext.Parameters.AddWithValue("emailaddress", userRecord.EmailAddress); - ctext.Parameters.AddWithValue("password", userRecord.Password); - ctext.Parameters.AddWithValue("isadmin", userRecord.IsAdmin); - userRecord.Id = Convert.ToInt32(ctext.ExecuteScalar()); - return userRecord.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET username = @username, emailaddress = @emailaddress, password = @password, isadmin = @isadmin WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", userRecord.Id); - ctext.Parameters.AddWithValue("username", userRecord.UserName); - ctext.Parameters.AddWithValue("emailaddress", userRecord.EmailAddress); - ctext.Parameters.AddWithValue("password", userRecord.Password); - ctext.Parameters.AddWithValue("isadmin", userRecord.IsAdmin); - return ctext.ExecuteNonQuery() > 0; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteUserRecord(int userId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", userId); - return ctext.ExecuteNonQuery() > 0; - } - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} \ No newline at end of file diff --git a/External/Implementations/Postgres/VehicleDataAccess.cs b/External/Implementations/Postgres/VehicleDataAccess.cs deleted file mode 100644 index 374058a..0000000 --- a/External/Implementations/Postgres/VehicleDataAccess.cs +++ /dev/null @@ -1,138 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Npgsql; -using System.Text.Json; - -namespace MotoVaultPro.External.Implementations -{ - public class PGVehicleDataAccess: IVehicleDataAccess - { - private NpgsqlDataSource pgDataSource; - private readonly ILogger _logger; - private static string tableName = "vehicles"; - public PGVehicleDataAccess(IConfiguration config, ILogger logger) - { - pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); - _logger = logger; - try - { - //create table if not exist. - string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, data jsonb not null)"; - using (var ctext = pgDataSource.CreateCommand(initCMD)) - { - ctext.ExecuteNonQuery(); - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - } - } - public bool SaveVehicle(Vehicle vehicle) - { - try - { - if (string.IsNullOrWhiteSpace(vehicle.ImageLocation)) - { - vehicle.ImageLocation = "/defaults/noimage.png"; - } - if (vehicle.Id == default) - { - string cmd = $"INSERT INTO app.{tableName} (data) VALUES(CAST(@data AS jsonb)) RETURNING id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("data", "{}"); - vehicle.Id = Convert.ToInt32(ctext.ExecuteScalar()); - //update json data - if (vehicle.Id != default) - { - string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctextU = pgDataSource.CreateCommand(cmdU)) - { - var serializedData = JsonSerializer.Serialize(vehicle); - ctextU.Parameters.AddWithValue("id", vehicle.Id); - ctextU.Parameters.AddWithValue("data", serializedData); - return ctextU.ExecuteNonQuery() > 0; - } - } - return vehicle.Id != default; - } - } - else - { - string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - var serializedData = JsonSerializer.Serialize(vehicle); - ctext.Parameters.AddWithValue("id", vehicle.Id); - ctext.Parameters.AddWithValue("data", serializedData); - return ctext.ExecuteNonQuery() > 0; - } - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - } - return false; - } - public bool DeleteVehicle(int vehicleId) - { - try - { - string cmd = $"DELETE FROM app.{tableName} WHERE id = @id"; - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - return ctext.ExecuteNonQuery() > 0; - } - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public List GetVehicles() - { - try - { - string cmd = $"SELECT id, data FROM app.{tableName} ORDER BY id ASC"; - var results = new List(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - Vehicle vehicle = JsonSerializer.Deserialize(reader["data"] as string); - results.Add(vehicle); - } - } - return results; - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return new List(); - } - } - public Vehicle GetVehicleById(int vehicleId) - { - try - { - string cmd = $"SELECT id, data FROM app.{tableName} WHERE id = @id"; - Vehicle vehicle = new Vehicle(); - using (var ctext = pgDataSource.CreateCommand(cmd)) - { - ctext.Parameters.AddWithValue("id", vehicleId); - using (NpgsqlDataReader reader = ctext.ExecuteReader()) - while (reader.Read()) - { - vehicle = JsonSerializer.Deserialize(reader["data"] as string); - } - } - return vehicle; - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Vehicle(); - } - } - } -} diff --git a/External/Interfaces/IExtraFieldDataAccess.cs b/External/Interfaces/IExtraFieldDataAccess.cs deleted file mode 100644 index 69d4ab9..0000000 --- a/External/Interfaces/IExtraFieldDataAccess.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IExtraFieldDataAccess - { - public RecordExtraField GetExtraFieldsById(int importMode); - public bool SaveExtraFields(RecordExtraField record); - } -} diff --git a/External/Interfaces/IGasRecordDataAccess.cs b/External/Interfaces/IGasRecordDataAccess.cs deleted file mode 100644 index b24f345..0000000 --- a/External/Interfaces/IGasRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IGasRecordDataAccess - { - public List GetGasRecordsByVehicleId(int vehicleId); - public GasRecord GetGasRecordById(int gasRecordId); - public bool DeleteGasRecordById(int gasRecordId); - public bool SaveGasRecordToVehicle(GasRecord gasRecord); - public bool DeleteAllGasRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/INoteDataAccess.cs b/External/Interfaces/INoteDataAccess.cs deleted file mode 100644 index 49fdc8f..0000000 --- a/External/Interfaces/INoteDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface INoteDataAccess - { - public List GetNotesByVehicleId(int vehicleId); - public Note GetNoteById(int noteId); - public bool SaveNoteToVehicle(Note note); - public bool DeleteNoteById(int noteId); - public bool DeleteAllNotesByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/IOdometerRecordDataAccess.cs b/External/Interfaces/IOdometerRecordDataAccess.cs deleted file mode 100644 index 00b48fa..0000000 --- a/External/Interfaces/IOdometerRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IOdometerRecordDataAccess - { - public List GetOdometerRecordsByVehicleId(int vehicleId); - public OdometerRecord GetOdometerRecordById(int odometerRecordId); - public bool DeleteOdometerRecordById(int odometerRecordId); - public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord); - public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/IPlanRecordDataAccess.cs b/External/Interfaces/IPlanRecordDataAccess.cs deleted file mode 100644 index 9d8180d..0000000 --- a/External/Interfaces/IPlanRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IPlanRecordDataAccess - { - public List GetPlanRecordsByVehicleId(int vehicleId); - public PlanRecord GetPlanRecordById(int planRecordId); - public bool DeletePlanRecordById(int planRecordId); - public bool SavePlanRecordToVehicle(PlanRecord planRecord); - public bool DeleteAllPlanRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/IPlanRecordTemplateDataAccess.cs b/External/Interfaces/IPlanRecordTemplateDataAccess.cs deleted file mode 100644 index f1be692..0000000 --- a/External/Interfaces/IPlanRecordTemplateDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IPlanRecordTemplateDataAccess - { - public List GetPlanRecordTemplatesByVehicleId(int vehicleId); - public PlanRecordInput GetPlanRecordTemplateById(int planRecordId); - public bool DeletePlanRecordTemplateById(int planRecordId); - public bool SavePlanRecordTemplateToVehicle(PlanRecordInput planRecord); - public bool DeleteAllPlanRecordTemplatesByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/IReminderRecordDataAccess.cs b/External/Interfaces/IReminderRecordDataAccess.cs deleted file mode 100644 index 54bbea9..0000000 --- a/External/Interfaces/IReminderRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IReminderRecordDataAccess - { - public List GetReminderRecordsByVehicleId(int vehicleId); - public ReminderRecord GetReminderRecordById(int reminderRecordId); - public bool DeleteReminderRecordById(int reminderRecordId); - public bool SaveReminderRecordToVehicle(ReminderRecord reminderRecord); - public bool DeleteAllReminderRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/IServiceRecordDataAccess.cs b/External/Interfaces/IServiceRecordDataAccess.cs deleted file mode 100644 index c882c05..0000000 --- a/External/Interfaces/IServiceRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IServiceRecordDataAccess - { - public List GetServiceRecordsByVehicleId(int vehicleId); - public ServiceRecord GetServiceRecordById(int serviceRecordId); - public bool DeleteServiceRecordById(int serviceRecordId); - public bool SaveServiceRecordToVehicle(ServiceRecord serviceRecord); - public bool DeleteAllServiceRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/ISupplyRecordDataAccess.cs b/External/Interfaces/ISupplyRecordDataAccess.cs deleted file mode 100644 index 3fa471c..0000000 --- a/External/Interfaces/ISupplyRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface ISupplyRecordDataAccess - { - public List GetSupplyRecordsByVehicleId(int vehicleId); - public SupplyRecord GetSupplyRecordById(int supplyRecordId); - public bool DeleteSupplyRecordById(int supplyRecordId); - public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord); - public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/ITaxRecordDataAccess.cs b/External/Interfaces/ITaxRecordDataAccess.cs deleted file mode 100644 index 471bc71..0000000 --- a/External/Interfaces/ITaxRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface ITaxRecordDataAccess - { - public List GetTaxRecordsByVehicleId(int vehicleId); - public TaxRecord GetTaxRecordById(int taxRecordId); - public bool DeleteTaxRecordById(int taxRecordId); - public bool SaveTaxRecordToVehicle(TaxRecord taxRecord); - public bool DeleteAllTaxRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/ITokenRecordDataAccess.cs b/External/Interfaces/ITokenRecordDataAccess.cs deleted file mode 100644 index 630fccb..0000000 --- a/External/Interfaces/ITokenRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface ITokenRecordDataAccess - { - public List GetTokens(); - public Token GetTokenRecordByBody(string tokenBody); - public Token GetTokenRecordByEmailAddress(string emailAddress); - public bool CreateNewToken(Token token); - public bool DeleteToken(int tokenId); - } -} diff --git a/External/Interfaces/IUpgradeRecordDataAccess.cs b/External/Interfaces/IUpgradeRecordDataAccess.cs deleted file mode 100644 index 5e161dd..0000000 --- a/External/Interfaces/IUpgradeRecordDataAccess.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IUpgradeRecordDataAccess - { - public List GetUpgradeRecordsByVehicleId(int vehicleId); - public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId); - public bool DeleteUpgradeRecordById(int upgradeRecordId); - public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord); - public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId); - } -} diff --git a/External/Interfaces/IUserAccessDataAccess.cs b/External/Interfaces/IUserAccessDataAccess.cs deleted file mode 100644 index eda9694..0000000 --- a/External/Interfaces/IUserAccessDataAccess.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IUserAccessDataAccess - { - List GetUserAccessByUserId(int userId); - UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId); - List GetUserAccessByVehicleId(int vehicleId); - bool SaveUserAccess(UserAccess userAccess); - bool DeleteUserAccess(int userId, int vehicleId); - bool DeleteAllAccessRecordsByVehicleId(int vehicleId); - bool DeleteAllAccessRecordsByUserId(int userId); - } -} diff --git a/External/Interfaces/IUserConfigDataAccess.cs b/External/Interfaces/IUserConfigDataAccess.cs deleted file mode 100644 index b2a0cd7..0000000 --- a/External/Interfaces/IUserConfigDataAccess.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IUserConfigDataAccess - { - public UserConfigData GetUserConfig(int userId); - public bool SaveUserConfig(UserConfigData userConfigData); - public bool DeleteUserConfig(int userId); - } -} diff --git a/External/Interfaces/IUserRecordDataAccess.cs b/External/Interfaces/IUserRecordDataAccess.cs deleted file mode 100644 index 41ad7d4..0000000 --- a/External/Interfaces/IUserRecordDataAccess.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IUserRecordDataAccess - { - public List GetUsers(); - public UserData GetUserRecordByUserName(string userName); - public UserData GetUserRecordByEmailAddress(string emailAddress); - public UserData GetUserRecordById(int userId); - public bool SaveUserRecord(UserData userRecord); - public bool DeleteUserRecord(int userId); - } -} \ No newline at end of file diff --git a/External/Interfaces/IVehicleDataAccess.cs b/External/Interfaces/IVehicleDataAccess.cs deleted file mode 100644 index ec0d87f..0000000 --- a/External/Interfaces/IVehicleDataAccess.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.External.Interfaces -{ - public interface IVehicleDataAccess - { - public bool SaveVehicle(Vehicle vehicle); - public bool DeleteVehicle(int vehicleId); - public List GetVehicles(); - public Vehicle GetVehicleById(int vehicleId); - } -} diff --git a/Filter/CollaboratorFilter.cs b/Filter/CollaboratorFilter.cs deleted file mode 100644 index ce3bf65..0000000 --- a/Filter/CollaboratorFilter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MotoVaultPro.Helper; -using MotoVaultPro.Logic; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using System.Security.Claims; - -namespace MotoVaultPro.Filter -{ - public class CollaboratorFilter: ActionFilterAttribute - { - private readonly IUserLogic _userLogic; - private readonly IConfigHelper _config; - public CollaboratorFilter(IUserLogic userLogic, IConfigHelper config) { - _userLogic = userLogic; - _config = config; - } - public override void OnActionExecuting(ActionExecutingContext filterContext) - { - if (!filterContext.HttpContext.User.IsInRole(nameof(UserData.IsRootUser))) - { - if (filterContext.ActionArguments.ContainsKey("vehicleId")) - { - var vehicleId = int.Parse(filterContext.ActionArguments["vehicleId"].ToString()); - if (vehicleId != default) - { - var userId = int.Parse(filterContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)); - if (!_userLogic.UserCanEditVehicle(userId, vehicleId)) - { - filterContext.Result = new RedirectResult("/Error/Unauthorized"); - } - } - else - { - var shopSupplyEndpoints = new List { "ImportToVehicleIdFromCsv", "GetSupplyRecordsByVehicleId", "ExportFromVehicleToCsv" }; - if (shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()) && !_config.GetServerEnableShopSupplies()) - { - //user trying to access shop supplies but shop supplies is not enabled by root user. - filterContext.Result = new RedirectResult("/Error/Unauthorized"); - } - else if (!shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString())) - { - //user trying to access any other endpoints using 0 as vehicle id. - filterContext.Result = new RedirectResult("/Error/Unauthorized"); - } - } - } else - { - filterContext.Result = new RedirectResult("/Error/Unauthorized"); - } - } - } - } -} diff --git a/Helper/ConfigHelper.cs b/Helper/ConfigHelper.cs deleted file mode 100644 index ebaff68..0000000 --- a/Helper/ConfigHelper.cs +++ /dev/null @@ -1,391 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; -using Microsoft.Extensions.Caching.Memory; -using System.Security.Claims; -using System.Text.Json; - -namespace MotoVaultPro.Helper -{ - public interface IConfigHelper - { - OpenIDConfig GetOpenIDConfig(); - ReminderUrgencyConfig GetReminderUrgencyConfig(); - MailConfig GetMailConfig(); - UserConfig GetUserConfig(ClaimsPrincipal user); - bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData); - bool SaveServerConfig(ServerConfig serverConfig); - bool AuthenticateRootUser(string username, string password); - bool AuthenticateRootUserOIDC(string email); - string GetWebHookUrl(); - bool GetCustomWidgetsEnabled(); - string GetMOTD(); - string GetLogoUrl(); - string GetSmallLogoUrl(); - string GetServerLanguage(); - bool GetServerDisabledRegistration(); - bool GetServerEnableShopSupplies(); - bool GetServerAuthEnabled(); - bool GetEnableRootUserOIDC(); - string GetServerPostgresConnection(); - string GetAllowedFileUploadExtensions(); - string GetServerDomain(); - bool DeleteUserConfig(int userId); - bool GetInvariantApi(); - bool GetServerOpenRegistration(); - string GetDefaultReminderEmail(); - } - public class ConfigHelper : IConfigHelper - { - private readonly IConfiguration _config; - private readonly IUserConfigDataAccess _userConfig; - private readonly ILogger _logger; - private IMemoryCache _cache; - public ConfigHelper(IConfiguration serverConfig, - IUserConfigDataAccess userConfig, - IMemoryCache memoryCache, - ILogger logger) - { - _config = serverConfig; - _userConfig = userConfig; - _cache = memoryCache; - _logger = logger; - } - public string GetWebHookUrl() - { - var webhook = CheckString("LUBELOGGER_WEBHOOK"); - return webhook; - } - public bool GetCustomWidgetsEnabled() - { - return CheckBool(CheckString("LUBELOGGER_CUSTOM_WIDGETS")); - } - public bool GetInvariantApi() - { - return CheckBool(CheckString("LUBELOGGER_INVARIANT_API")); - } - public string GetMOTD() - { - var motd = CheckString("LUBELOGGER_MOTD"); - return motd; - } - public string GetServerDomain() - { - var domain = CheckString("LUBELOGGER_DOMAIN"); - return domain; - } - public bool GetServerOpenRegistration() - { - return CheckBool(CheckString("LUBELOGGER_OPEN_REGISTRATION")); - } - public bool GetServerAuthEnabled() - { - return CheckBool(CheckString(nameof(UserConfig.EnableAuth))); - } - public OpenIDConfig GetOpenIDConfig() - { - OpenIDConfig openIdConfig = _config.GetSection("OpenIDConfig").Get() ?? new OpenIDConfig(); - return openIdConfig; - } - public ReminderUrgencyConfig GetReminderUrgencyConfig() - { - ReminderUrgencyConfig reminderUrgencyConfig = _config.GetSection("ReminderUrgencyConfig").Get() ?? new ReminderUrgencyConfig(); - return reminderUrgencyConfig; - } - public MailConfig GetMailConfig() - { - MailConfig mailConfig = _config.GetSection("MailConfig").Get() ?? new MailConfig(); - return mailConfig; - } - public string GetLogoUrl() - { - var logoUrl = CheckString("LUBELOGGER_LOGO_URL", StaticHelper.DefaultLogoPath); - return logoUrl; - } - public string GetSmallLogoUrl() - { - var logoUrl = CheckString("LUBELOGGER_LOGO_SMALL_URL", StaticHelper.DefaultSmallLogoPath); - return logoUrl; - } - public string GetDefaultReminderEmail() - { - var reminderEmail = CheckString(nameof(ServerConfig.DefaultReminderEmail)); - return reminderEmail; - } - public string GetAllowedFileUploadExtensions() - { - var allowedFileExtensions = CheckString("LUBELOGGER_ALLOWED_FILE_EXTENSIONS", StaticHelper.DefaultAllowedFileExtensions); - return allowedFileExtensions; - } - public bool AuthenticateRootUser(string username, string password) - { - var rootUsername = CheckString(nameof(UserConfig.UserNameHash)); - var rootPassword = CheckString(nameof(UserConfig.UserPasswordHash)); - if (string.IsNullOrWhiteSpace(rootUsername) || string.IsNullOrWhiteSpace(rootPassword)) - { - return false; - } - return username == rootUsername && password == rootPassword; - } - public bool AuthenticateRootUserOIDC(string email) - { - var rootEmail = CheckString(nameof(ServerConfig.DefaultReminderEmail)); - var rootUserOIDC = CheckBool(CheckString(nameof(ServerConfig.EnableRootUserOIDC))); - if (!rootUserOIDC || string.IsNullOrWhiteSpace(rootEmail)) - { - return false; - } - return email == rootEmail; - } - public bool GetEnableRootUserOIDC() - { - var rootUserOIDC = CheckBool(CheckString(nameof(ServerConfig.EnableRootUserOIDC))); - return rootUserOIDC; - } - public string GetServerLanguage() - { - var serverLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"); - return serverLanguage; - } - public bool GetServerDisabledRegistration() - { - var registrationDisabled = CheckBool(CheckString(nameof(ServerConfig.DisableRegistration))); - return registrationDisabled; - } - public string GetServerPostgresConnection() - { - var postgresConnection = CheckString("POSTGRES_CONNECTION"); - return postgresConnection; - } - public bool GetServerEnableShopSupplies() - { - return CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))); - } - public bool SaveServerConfig(ServerConfig serverConfig) - { - //nullify default values - if (string.IsNullOrWhiteSpace(serverConfig.PostgresConnection)) - { - serverConfig.PostgresConnection = null; - } - if (serverConfig.AllowedFileExtensions == StaticHelper.DefaultAllowedFileExtensions || string.IsNullOrWhiteSpace(serverConfig.AllowedFileExtensions)) - { - serverConfig.AllowedFileExtensions = null; - } - if (serverConfig.CustomLogoURL == StaticHelper.DefaultLogoPath || string.IsNullOrWhiteSpace(serverConfig.CustomLogoURL)) - { - serverConfig.CustomLogoURL = null; - } - if (serverConfig.CustomSmallLogoURL == StaticHelper.DefaultSmallLogoPath || string.IsNullOrWhiteSpace(serverConfig.CustomSmallLogoURL)) - { - serverConfig.CustomSmallLogoURL = null; - } - if (string.IsNullOrWhiteSpace(serverConfig.MessageOfTheDay)) - { - serverConfig.MessageOfTheDay = null; - } - if (string.IsNullOrWhiteSpace(serverConfig.WebHookURL)) - { - serverConfig.WebHookURL = null; - } - if (string.IsNullOrWhiteSpace(serverConfig.ServerURL)) - { - serverConfig.ServerURL = null; - } - if (serverConfig.CustomWidgetsEnabled.HasValue && !serverConfig.CustomWidgetsEnabled.Value) - { - serverConfig.CustomWidgetsEnabled = null; - } - if (serverConfig.InvariantAPIEnabled.HasValue && !serverConfig.InvariantAPIEnabled.Value) - { - serverConfig.InvariantAPIEnabled = null; - } - if (string.IsNullOrWhiteSpace(serverConfig.SMTPConfig?.EmailServer ?? string.Empty)) - { - serverConfig.SMTPConfig = null; - } - if (string.IsNullOrWhiteSpace(serverConfig.OIDCConfig?.Name ?? string.Empty)) - { - serverConfig.OIDCConfig = null; - } - if (serverConfig.OpenRegistration.HasValue && !serverConfig.OpenRegistration.Value) - { - serverConfig.OpenRegistration = null; - } - if (serverConfig.DisableRegistration.HasValue && !serverConfig.DisableRegistration.Value) - { - serverConfig.DisableRegistration = null; - } - if (string.IsNullOrWhiteSpace(serverConfig.DefaultReminderEmail)) - { - serverConfig.DefaultReminderEmail = null; - } - if (serverConfig.EnableRootUserOIDC.HasValue && !serverConfig.EnableRootUserOIDC.Value) - { - serverConfig.EnableRootUserOIDC = null; - } - try - { - File.WriteAllText(StaticHelper.ServerConfigPath, JsonSerializer.Serialize(serverConfig)); - return true; - } catch (Exception ex) - { - _logger.LogWarning(ex.Message); - return false; - } - } - public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData) - { - var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier); - int userId = 0; - if (storedUserId != null) - { - userId = int.Parse(storedUserId); - } - bool isRootUser = user.IsInRole(nameof(UserData.IsRootUser)) || userId == -1; - if (isRootUser) - { - try - { - if (!File.Exists(StaticHelper.UserConfigPath)) - { - //if file doesn't exist it might be because it's running on a mounted volume in docker. - File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(new UserConfig())); - } - var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath); - configData.EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)] ?? "false"); - configData.UserNameHash = _config[nameof(UserConfig.UserNameHash)] ?? string.Empty; - configData.UserPasswordHash = _config[nameof(UserConfig.UserPasswordHash)] ?? string.Empty; - File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(configData)); - _cache.Set($"userConfig_{userId}", configData); - return true; - } - catch (Exception ex) - { - _logger.LogWarning(ex.Message); - return false; - } - } else - { - var userConfig = new UserConfigData() - { - Id = userId, - UserConfig = configData - }; - var result = _userConfig.SaveUserConfig(userConfig); - _cache.Set($"userConfig_{userId}", configData); - return result; - } - } - public bool DeleteUserConfig(int userId) - { - _cache.Remove($"userConfig_{userId}"); - var result = _userConfig.DeleteUserConfig(userId); - return result; - } - private bool CheckBool(string value, bool defaultValue = false) - { - try - { - if (string.IsNullOrWhiteSpace(value)) - { - return defaultValue; - } - else if (bool.TryParse(value, out bool result)) - { - return result; - } - else - { - return defaultValue; - } - } catch (Exception ex) - { - _logger.LogWarning($"ConfigHelper Warning: You might be missing keys in appsettings.json, Message: ${ex.Message}"); - return defaultValue; - } - } - private string CheckString(string configName, string defaultValue = "") - { - try - { - var configValue = _config[configName] ?? defaultValue; - return configValue; - } catch(Exception ex) - { - _logger.LogWarning($"ConfigHelper Warning: You might be missing keys in appsettings.json, Message: ${ex.Message}"); - return defaultValue; - } - } - public UserConfig GetUserConfig(ClaimsPrincipal user) - { - var serverConfig = new UserConfig - { - EnableCsvImports = CheckBool(CheckString(nameof(UserConfig.EnableCsvImports)), true), - UseDarkMode = CheckBool(CheckString(nameof(UserConfig.UseDarkMode))), - UseSystemColorMode = CheckBool(CheckString(nameof(UserConfig.UseSystemColorMode)), true), - UseMPG = CheckBool(CheckString(nameof(UserConfig.UseMPG)), true), - UseDescending = CheckBool(CheckString(nameof(UserConfig.UseDescending))), - EnableAuth = CheckBool(CheckString(nameof(UserConfig.EnableAuth))), - HideZero = CheckBool(CheckString(nameof(UserConfig.HideZero))), - AutomaticDecimalFormat = CheckBool(CheckString(nameof(UserConfig.AutomaticDecimalFormat))), - UseUKMPG = CheckBool(CheckString(nameof(UserConfig.UseUKMPG))), - UseMarkDownOnSavedNotes = CheckBool(CheckString(nameof(UserConfig.UseMarkDownOnSavedNotes))), - UseThreeDecimalGasCost = CheckBool(CheckString(nameof(UserConfig.UseThreeDecimalGasCost)), true), - UseThreeDecimalGasConsumption = CheckBool(CheckString(nameof(UserConfig.UseThreeDecimalGasConsumption)), true), - EnableAutoReminderRefresh = CheckBool(CheckString(nameof(UserConfig.EnableAutoReminderRefresh))), - EnableAutoOdometerInsert = CheckBool(CheckString(nameof(UserConfig.EnableAutoOdometerInsert))), - PreferredGasMileageUnit = CheckString(nameof(UserConfig.PreferredGasMileageUnit)), - PreferredGasUnit = CheckString(nameof(UserConfig.PreferredGasUnit)), - UseUnitForFuelCost = CheckBool(CheckString(nameof(UserConfig.UseUnitForFuelCost))), - UseSimpleFuelEntry = CheckBool(CheckString(nameof(UserConfig.UseSimpleFuelEntry))), - UserLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"), - HideSoldVehicles = CheckBool(CheckString(nameof(UserConfig.HideSoldVehicles))), - EnableShopSupplies = CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))), - ShowCalendar = CheckBool(CheckString(nameof(UserConfig.ShowCalendar))), - EnableExtraFieldColumns = CheckBool(CheckString(nameof(UserConfig.EnableExtraFieldColumns))), - VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get>() ?? new UserConfig().VisibleTabs, - TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get>() ?? new UserConfig().TabOrder, - UserColumnPreferences = _config.GetSection(nameof(UserConfig.UserColumnPreferences)).Get>() ?? new List(), - DefaultTab = (ImportMode)int.Parse(CheckString(nameof(UserConfig.DefaultTab), "8")), - ShowVehicleThumbnail = CheckBool(CheckString(nameof(UserConfig.ShowVehicleThumbnail))) - }; - int userId = 0; - if (user != null) - { - var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier); - if (storedUserId != null) - { - userId = int.Parse(storedUserId); - } - } else - { - return serverConfig; - } - return _cache.GetOrCreate($"userConfig_{userId}", entry => - { - entry.SlidingExpiration = TimeSpan.FromHours(1); - if (!user.Identity.IsAuthenticated) - { - return serverConfig; - } - bool isRootUser = user.IsInRole(nameof(UserData.IsRootUser)) || userId == -1; - if (isRootUser) - { - return serverConfig; - } - else - { - var result = _userConfig.GetUserConfig(userId); - if (result == null) - { - return serverConfig; - } - else - { - return result.UserConfig; - } - } - }); - } - } -} diff --git a/Helper/FileHelper.cs b/Helper/FileHelper.cs deleted file mode 100644 index 09c3526..0000000 --- a/Helper/FileHelper.cs +++ /dev/null @@ -1,479 +0,0 @@ -using MotoVaultPro.Models; -using System.IO.Compression; - -namespace MotoVaultPro.Helper -{ - public interface IFileHelper - { - string GetFullFilePath(string currentFilePath, bool mustExist = true); - byte[] GetFileBytes(string fullFilePath, bool deleteFile = false); - string MoveFileFromTemp(string currentFilePath, string newFolder); - bool RenameFile(string currentFilePath, string newName); - bool DeleteFile(string currentFilePath); - string MakeBackup(); - bool RestoreBackup(string fileName, bool clearExisting = false); - string MakeAttachmentsExport(List exportData); - List GetLanguages(); - int ClearTempFolder(); - int ClearUnlinkedThumbnails(List linkedImages); - int ClearUnlinkedDocuments(List linkedDocuments); - string GetWidgets(); - bool WidgetsExist(); - bool SaveWidgets(string widgetsData); - bool DeleteWidgets(); - } - public class FileHelper : IFileHelper - { - private readonly IWebHostEnvironment _webEnv; - private readonly ILogger _logger; - private ILiteDBHelper _liteDB; - public FileHelper(IWebHostEnvironment webEnv, ILogger logger, ILiteDBHelper liteDB) - { - _webEnv = webEnv; - _logger = logger; - _liteDB = liteDB; - } - public List GetLanguages() - { - var languagePath = Path.Combine(_webEnv.ContentRootPath, "data", "translations"); - var defaultList = new List() { "en_US" }; - if (Directory.Exists(languagePath)) - { - var listOfLanguages = Directory.GetFiles(languagePath); - if (listOfLanguages.Any()) - { - defaultList.AddRange(listOfLanguages.Select(x => Path.GetFileNameWithoutExtension(x))); - } - } - return defaultList; - } - public bool RenameFile(string currentFilePath, string newName) - { - var fullFilePath = GetFullFilePath(currentFilePath); - if (!string.IsNullOrWhiteSpace(fullFilePath)) - { - try - { - var originalFileName = Path.GetFileNameWithoutExtension(fullFilePath); - var newFilePath = fullFilePath.Replace(originalFileName, newName); - File.Move(fullFilePath, newFilePath); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - return false; - } - public string GetFullFilePath(string currentFilePath, bool mustExist = true) - { - if (currentFilePath.StartsWith("/")) - { - currentFilePath = currentFilePath.Substring(1); - } - string oldFilePath = currentFilePath.StartsWith("defaults/") ? Path.Combine(_webEnv.WebRootPath, currentFilePath) : Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath); - if (File.Exists(oldFilePath)) - { - return oldFilePath; - } - else if (!mustExist) - { - return oldFilePath; - } - { - return string.Empty; - } - } - public byte[] GetFileBytes(string fullFilePath, bool deleteFile = false) - { - if (File.Exists(fullFilePath)) - { - var fileBytes = File.ReadAllBytes(fullFilePath); - if (deleteFile) - { - File.Delete(fullFilePath); - } - return fileBytes; - } - return Array.Empty(); - } - public bool RestoreBackup(string fileName, bool clearExisting = false) - { - var fullFilePath = GetFullFilePath(fileName); - if (string.IsNullOrWhiteSpace(fullFilePath)) - { - return false; - } - try - { - var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{Guid.NewGuid()}"); - if (!Directory.Exists(tempPath)) - Directory.CreateDirectory(tempPath); - //extract zip file - ZipFile.ExtractToDirectory(fullFilePath, tempPath); - //copy over images and documents. - var imagePath = Path.Combine(tempPath, "images"); - var documentPath = Path.Combine(tempPath, "documents"); - var translationPath = Path.Combine(tempPath, "translations"); - var dataPath = Path.Combine(tempPath, StaticHelper.DbName); - var widgetPath = Path.Combine(tempPath, StaticHelper.AdditionalWidgetsPath); - var configPath = Path.Combine(tempPath, StaticHelper.LegacyUserConfigPath); - var serverConfigPath = Path.Combine(tempPath, StaticHelper.LegacyServerConfigPath); - if (Directory.Exists(imagePath)) - { - var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "images"); - if (!Directory.Exists(existingPath)) - { - Directory.CreateDirectory(existingPath); - } - else if (clearExisting) - { - var filesToDelete = Directory.GetFiles(existingPath); - foreach (string file in filesToDelete) - { - File.Delete(file); - } - } - //copy each files from temp folder to newPath - var filesToUpload = Directory.GetFiles(imagePath); - foreach (string file in filesToUpload) - { - File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true); - } - } - if (Directory.Exists(documentPath)) - { - var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "documents"); - if (!Directory.Exists(existingPath)) - { - Directory.CreateDirectory(existingPath); - } - else if (clearExisting) - { - var filesToDelete = Directory.GetFiles(existingPath); - foreach (string file in filesToDelete) - { - File.Delete(file); - } - } - //copy each files from temp folder to newPath - var filesToUpload = Directory.GetFiles(documentPath); - foreach (string file in filesToUpload) - { - File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true); - } - } - if (Directory.Exists(translationPath)) - { - var existingPath = Path.Combine(_webEnv.ContentRootPath, "data", "translations"); - if (!Directory.Exists(existingPath)) - { - Directory.CreateDirectory(existingPath); - } - else if (clearExisting) - { - var filesToDelete = Directory.GetFiles(existingPath); - foreach (string file in filesToDelete) - { - File.Delete(file); - } - } - //copy each files from temp folder to newPath - var filesToUpload = Directory.GetFiles(translationPath); - foreach (string file in filesToUpload) - { - File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true); - } - } - if (File.Exists(dataPath)) - { - //Relinquish current DB file lock - _liteDB.DisposeLiteDB(); - //data path will always exist as it is created on startup if not. - File.Move(dataPath, StaticHelper.DbName, true); - } - if (File.Exists(widgetPath)) - { - File.Move(widgetPath, StaticHelper.AdditionalWidgetsPath, true); - } - if (File.Exists(configPath)) - { - //check if config folder exists. - if (!Directory.Exists("data/config")) - { - Directory.CreateDirectory("data/config"); - } - File.Move(configPath, StaticHelper.UserConfigPath, true); - } - if (File.Exists(serverConfigPath)) - { - //check if config folder exists. - if (!Directory.Exists("data/config")) - { - Directory.CreateDirectory("data/config"); - } - File.Move(serverConfigPath, StaticHelper.ServerConfigPath, true); - } - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}"); - return false; - } - } - public string MakeAttachmentsExport(List exportData) - { - var folderName = Guid.NewGuid(); - var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{folderName}"); - if (!Directory.Exists(tempPath)) - Directory.CreateDirectory(tempPath); - int fileIndex = 0; - foreach (GenericReportModel reportModel in exportData) - { - foreach (UploadedFiles file in reportModel.Files) - { - var fileToCopy = GetFullFilePath(file.Location); - var destFileName = $"{tempPath}/{fileIndex}_{reportModel.DataType}_{reportModel.Date.ToString("yyyy-MM-dd")}_{file.Name}{Path.GetExtension(file.Location)}"; - File.Copy(fileToCopy, destFileName); - fileIndex++; - } - } - var destFilePath = $"{tempPath}.zip"; - ZipFile.CreateFromDirectory(tempPath, destFilePath); - //delete temp directory - Directory.Delete(tempPath, true); - var zipFileName = $"/temp/{folderName}.zip"; - return zipFileName; - } - public string MakeBackup() - { - var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}"; - var tempPath = Path.Combine(_webEnv.ContentRootPath, "data", $"temp/{folderName}"); - var imagePath = Path.Combine(_webEnv.ContentRootPath, "data", "images"); - var documentPath = Path.Combine(_webEnv.ContentRootPath, "data", "documents"); - var translationPath = Path.Combine(_webEnv.ContentRootPath, "data", "translations"); - var dataPath = StaticHelper.DbName; - var widgetPath = StaticHelper.AdditionalWidgetsPath; - var configPath = StaticHelper.UserConfigPath; - var serverConfigPath = StaticHelper.ServerConfigPath; - if (!Directory.Exists(tempPath)) - Directory.CreateDirectory(tempPath); - if (Directory.Exists(imagePath)) - { - var files = Directory.GetFiles(imagePath); - foreach (var file in files) - { - var newPath = Path.Combine(tempPath, "images"); - Directory.CreateDirectory(newPath); - File.Copy(file, $"{newPath}/{Path.GetFileName(file)}"); - } - } - if (Directory.Exists(documentPath)) - { - var files = Directory.GetFiles(documentPath); - foreach (var file in files) - { - var newPath = Path.Combine(tempPath, "documents"); - Directory.CreateDirectory(newPath); - File.Copy(file, $"{newPath}/{Path.GetFileName(file)}"); - } - } - if (Directory.Exists(translationPath)) - { - var files = Directory.GetFiles(translationPath); - foreach(var file in files) - { - var newPath = Path.Combine(tempPath, "translations"); - Directory.CreateDirectory(newPath); - File.Copy(file, $"{newPath}/{Path.GetFileName(file)}"); - } - } - if (File.Exists(dataPath)) - { - var newPath = Path.Combine(tempPath, "data"); - Directory.CreateDirectory(newPath); - File.Copy(dataPath, $"{newPath}/{Path.GetFileName(dataPath)}"); - } - if (File.Exists(widgetPath)) - { - var newPath = Path.Combine(tempPath, "data"); - Directory.CreateDirectory(newPath); - File.Copy(widgetPath, $"{newPath}/{Path.GetFileName(widgetPath)}"); - } - if (File.Exists(configPath)) - { - var newPath = Path.Combine(tempPath, "config"); - Directory.CreateDirectory(newPath); - File.Copy(configPath, $"{newPath}/{Path.GetFileName(configPath)}"); - } - if (File.Exists(serverConfigPath)) - { - var newPath = Path.Combine(tempPath, "config"); - Directory.CreateDirectory(newPath); - File.Copy(serverConfigPath, $"{newPath}/{Path.GetFileName(serverConfigPath)}"); - } - var destFilePath = $"{tempPath}.zip"; - ZipFile.CreateFromDirectory(tempPath, destFilePath); - //delete temp directory - Directory.Delete(tempPath, true); - return $"/temp/{folderName}.zip"; - } - public string MoveFileFromTemp(string currentFilePath, string newFolder) - { - string tempPath = "temp/"; - if (string.IsNullOrWhiteSpace(currentFilePath) || !currentFilePath.StartsWith("/temp/")) //file is not in temp directory. - { - return currentFilePath; - } - if (currentFilePath.StartsWith("/")) - { - currentFilePath = currentFilePath.Substring(1); - } - string uploadPath = Path.Combine(_webEnv.ContentRootPath, "data", newFolder); - string oldFilePath = Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath); - if (!Directory.Exists(uploadPath)) - Directory.CreateDirectory(uploadPath); - string newFileUploadPath = oldFilePath.Replace(tempPath, newFolder); - if (File.Exists(oldFilePath)) - { - File.Move(oldFilePath, newFileUploadPath); - } - string newFilePathToReturn = "/" + currentFilePath.Replace(tempPath, newFolder); - return newFilePathToReturn; - } - public bool DeleteFile(string currentFilePath) - { - if (currentFilePath.StartsWith("/")) - { - currentFilePath = currentFilePath.Substring(1); - } - string filePath = Path.Combine(_webEnv.ContentRootPath, "data", currentFilePath); - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - if (!File.Exists(filePath)) //verify file no longer exists. - { - return true; - } - else - { - return false; - } - } - public int ClearTempFolder() - { - int filesDeleted = 0; - var tempPath = GetFullFilePath("temp", false); - if (Directory.Exists(tempPath)) - { - //delete files - var files = Directory.GetFiles(tempPath); - foreach (var file in files) - { - File.Delete(file); - filesDeleted++; - } - //delete folders - var folders = Directory.GetDirectories(tempPath); - foreach(var folder in folders) - { - Directory.Delete(folder, true); - filesDeleted++; - } - } - return filesDeleted; - } - public int ClearUnlinkedThumbnails(List linkedImages) - { - int filesDeleted = 0; - var imagePath = GetFullFilePath("images", false); - if (Directory.Exists(imagePath)) - { - var files = Directory.GetFiles(imagePath); - foreach(var file in files) - { - if (!linkedImages.Contains(Path.GetFileName(file))) - { - File.Delete(file); - filesDeleted++; - } - } - } - return filesDeleted; - } - public int ClearUnlinkedDocuments(List linkedDocuments) - { - int filesDeleted = 0; - var documentPath = GetFullFilePath("documents", false); - if (Directory.Exists(documentPath)) - { - var files = Directory.GetFiles(documentPath); - foreach (var file in files) - { - if (!linkedDocuments.Contains(Path.GetFileName(file))) - { - File.Delete(file); - filesDeleted++; - } - } - } - return filesDeleted; - } - public string GetWidgets() - { - if (File.Exists(StaticHelper.AdditionalWidgetsPath)) - { - try - { - //read file - var widgets = File.ReadAllText(StaticHelper.AdditionalWidgetsPath); - return widgets; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return string.Empty; - } - } - return string.Empty; - } - public bool WidgetsExist() - { - return File.Exists(StaticHelper.AdditionalWidgetsPath); - } - public bool SaveWidgets(string widgetsData) - { - try - { - //Delete Widgets if exists - DeleteWidgets(); - File.WriteAllText(StaticHelper.AdditionalWidgetsPath, widgetsData); - return true; - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - public bool DeleteWidgets() - { - try - { - if (File.Exists(StaticHelper.AdditionalWidgetsPath)) - { - File.Delete(StaticHelper.AdditionalWidgetsPath); - } - return true; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } -} diff --git a/Helper/GasHelper.cs b/Helper/GasHelper.cs deleted file mode 100644 index 8b63c11..0000000 --- a/Helper/GasHelper.cs +++ /dev/null @@ -1,146 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.Helper -{ - public interface IGasHelper - { - List GetGasRecordViewModels(List result, bool useMPG, bool useUKMPG); - string GetAverageGasMileage(List results, bool useMPG); - } - public class GasHelper : IGasHelper - { - public string GetAverageGasMileage(List results, bool useMPG) - { - var recordsToCalculate = results.Where(x => x.IncludeInAverage); - if (recordsToCalculate.Any()) - { - try - { - var totalMileage = recordsToCalculate.Sum(x => x.DeltaMileage); - var totalGallons = recordsToCalculate.Sum(x => x.Gallons); - var averageGasMileage = totalMileage / totalGallons; - if (!useMPG && averageGasMileage > 0) - { - averageGasMileage = 100 / averageGasMileage; - } - return averageGasMileage.ToString("F"); - } catch (Exception ex) - { - return "0"; - } - } - return "0"; - } - public List GetGasRecordViewModels(List result, bool useMPG, bool useUKMPG) - { - //need to order by to get correct results - result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList(); - var computedResults = new List(); - int previousMileage = 0; - decimal unFactoredConsumption = 0.00M; - int unFactoredMileage = 0; - //perform computation. - for (int i = 0; i < result.Count; i++) - { - var currentObject = result[i]; - decimal convertedConsumption; - if (useUKMPG && useMPG) - { - //if we're using UK MPG and the user wants imperial calculation insteace of l/100km - //if UK MPG is selected then the gas consumption are stored in liters but need to convert into UK gallons for computation. - convertedConsumption = currentObject.Gallons / 4.546M; - } - else - { - convertedConsumption = currentObject.Gallons; - } - if (i > 0) - { - var deltaMileage = currentObject.Mileage - previousMileage; - if (deltaMileage < 0) - { - deltaMileage = 0; - } - var gasRecordViewModel = new GasRecordViewModel() - { - Id = currentObject.Id, - VehicleId = currentObject.VehicleId, - MonthId = currentObject.Date.Month, - Date = currentObject.Date.ToShortDateString(), - Mileage = currentObject.Mileage, - Gallons = convertedConsumption, - Cost = currentObject.Cost, - DeltaMileage = deltaMileage, - CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0, - IsFillToFull = currentObject.IsFillToFull, - MissedFuelUp = currentObject.MissedFuelUp, - Notes = currentObject.Notes, - Tags = currentObject.Tags, - ExtraFields = currentObject.ExtraFields, - Files = currentObject.Files - }; - if (currentObject.MissedFuelUp) - { - //if they missed a fuel up, we skip MPG calculation. - gasRecordViewModel.MilesPerGallon = 0; - //reset unFactored vars for missed fuel up because the numbers wont be reliable. - unFactoredConsumption = 0; - unFactoredMileage = 0; - } - else if (currentObject.IsFillToFull && currentObject.Mileage != default) - { - //if user filled to full and an odometer is provided, otherwise we will defer calculations - if (convertedConsumption > 0.00M && deltaMileage > 0) - { - try - { - gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption)); - } - catch (Exception ex) - { - gasRecordViewModel.MilesPerGallon = 0; - } - } - //reset unFactored vars - unFactoredConsumption = 0; - unFactoredMileage = 0; - } - else - { - unFactoredConsumption += convertedConsumption; - unFactoredMileage += deltaMileage; - gasRecordViewModel.MilesPerGallon = 0; - } - computedResults.Add(gasRecordViewModel); - } - else - { - computedResults.Add(new GasRecordViewModel() - { - Id = currentObject.Id, - VehicleId = currentObject.VehicleId, - MonthId = currentObject.Date.Month, - Date = currentObject.Date.ToShortDateString(), - Mileage = currentObject.Mileage, - Gallons = convertedConsumption, - Cost = currentObject.Cost, - DeltaMileage = 0, - MilesPerGallon = 0, - CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0, - IsFillToFull = currentObject.IsFillToFull, - MissedFuelUp = currentObject.MissedFuelUp, - Notes = currentObject.Notes, - Tags = currentObject.Tags, - ExtraFields = currentObject.ExtraFields, - Files = currentObject.Files - }); - } - if (currentObject.Mileage != default) - { - previousMileage = currentObject.Mileage; - } - } - return computedResults; - } - } -} diff --git a/Helper/LiteDBHelper.cs b/Helper/LiteDBHelper.cs deleted file mode 100644 index 4c41622..0000000 --- a/Helper/LiteDBHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -using LiteDB; - -namespace MotoVaultPro.Helper; - -public interface ILiteDBHelper -{ - LiteDatabase GetLiteDB(); - void DisposeLiteDB(); -} -public class LiteDBHelper: ILiteDBHelper -{ - public LiteDatabase db { get; set; } - public LiteDBHelper() - { - if (db == null) - { - db = new LiteDatabase(StaticHelper.DbName); - } - } - public LiteDatabase GetLiteDB() - { - if (db == null) - { - db = new LiteDatabase(StaticHelper.DbName); - } - return db; - } - public void DisposeLiteDB() - { - if (db != null) - { - db.Dispose(); - db = null; - } - } -} diff --git a/Helper/MailHelper.cs b/Helper/MailHelper.cs deleted file mode 100644 index 537f7e9..0000000 --- a/Helper/MailHelper.cs +++ /dev/null @@ -1,259 +0,0 @@ -using MotoVaultPro.Models; -using MimeKit; -using MailKit.Net.Smtp; -using MailKit.Security; - -namespace MotoVaultPro.Helper -{ - public interface IMailHelper - { - OperationResponse NotifyUserForRegistration(string emailAddress, string token); - OperationResponse NotifyUserForPasswordReset(string emailAddress, string token); - OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token); - OperationResponse NotifyUserForReminders(Vehicle vehicle, List emailAddresses, List reminders); - OperationResponse SendTestEmail(string emailAddress, MailConfig testMailConfig); - } - public class MailHelper : IMailHelper - { - private readonly MailConfig mailConfig; - private readonly string serverLanguage; - private readonly string serverDomain; - private readonly IFileHelper _fileHelper; - private readonly ITranslationHelper _translator; - private readonly ILogger _logger; - public MailHelper( - IConfigHelper config, - IFileHelper fileHelper, - ITranslationHelper translationHelper, - ILogger logger - ) { - //load mailConfig from Configuration - mailConfig = config.GetMailConfig(); - serverLanguage = config.GetServerLanguage(); - serverDomain = config.GetServerDomain(); - _fileHelper = fileHelper; - _translator = translationHelper; - _logger = logger; - } - public OperationResponse NotifyUserForRegistration(string emailAddress, string token) - { - if (string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - return OperationResponse.Failed("SMTP Server Not Setup"); - } - if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) { - return OperationResponse.Failed("Email Address or Token is invalid"); - } - string emailSubject = _translator.Translate(serverLanguage, "Your Registration Token for MotoVaultPro"); - string tokenHtml = token; - if (!string.IsNullOrWhiteSpace(serverDomain)) - { - string cleanedURL = serverDomain.EndsWith('/') ? serverDomain.TrimEnd('/') : serverDomain; - //construct registration URL. - tokenHtml = $"{token}"; - } - string emailBody = $"{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please complete your registration for MotoVaultPro using the token")}: {tokenHtml}"; - var result = SendEmail(new List { emailAddress }, emailSubject, emailBody); - if (result) - { - return OperationResponse.Succeed("Email Sent!"); - } else - { - return OperationResponse.Failed(); - } - } - public OperationResponse NotifyUserForPasswordReset(string emailAddress, string token) - { - if (string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - return OperationResponse.Failed("SMTP Server Not Setup"); - } - if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) - { - return OperationResponse.Failed("Email Address or Token is invalid"); - } - string emailSubject = _translator.Translate(serverLanguage, "Your Password Reset Token for MotoVaultPro"); - string tokenHtml = token; - if (!string.IsNullOrWhiteSpace(serverDomain)) - { - string cleanedURL = serverDomain.EndsWith('/') ? serverDomain.TrimEnd('/') : serverDomain; - //construct registration URL. - tokenHtml = $"{token}"; - } - string emailBody = $"{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please reset your password for MotoVaultPro using the token")}: {tokenHtml}"; - var result = SendEmail(new List { emailAddress }, emailSubject, emailBody); - if (result) - { - return OperationResponse.Succeed("Email Sent!"); - } - else - { - return OperationResponse.Failed(); - } - } - public OperationResponse SendTestEmail(string emailAddress, MailConfig testMailConfig) - { - if (string.IsNullOrWhiteSpace(testMailConfig.EmailServer)) - { - return OperationResponse.Failed("SMTP Server Not Setup"); - } - if (string.IsNullOrWhiteSpace(emailAddress)) - { - return OperationResponse.Failed("Email Address or Token is invalid"); - } - string emailSubject = _translator.Translate(serverLanguage, "Test Email from MotoVaultPro"); - string emailBody = _translator.Translate(serverLanguage, "If you are seeing this email it means your SMTP configuration is functioning correctly"); - var result = SendEmail(testMailConfig, new List { emailAddress }, emailSubject, emailBody); - if (result) - { - return OperationResponse.Succeed("Email Sent!"); - } - else - { - return OperationResponse.Failed(); - } - } - public OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token) - { - if (string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - return OperationResponse.Failed("SMTP Server Not Setup"); - } - if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) - { - return OperationResponse.Failed("Email Address or Token is invalid"); - } - string emailSubject = _translator.Translate(serverLanguage, "Your User Account Update Token for MotoVaultPro"); - string emailBody = $"{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please update your account for MotoVaultPro using the token")}: {token}"; - var result = SendEmail(new List { emailAddress}, emailSubject, emailBody); - if (result) - { - return OperationResponse.Succeed("Email Sent!"); - } - else - { - return OperationResponse.Failed(); - } - } - public OperationResponse NotifyUserForReminders(Vehicle vehicle, List emailAddresses, List reminders) - { - if (string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - return OperationResponse.Failed("SMTP Server Not Setup"); - } - if (!emailAddresses.Any()) - { - return OperationResponse.Failed("No recipients could be found"); - } - if (!reminders.Any()) - { - return OperationResponse.Failed("No reminders could be found"); - } - //get email template, this file has to exist since it's a static file. - var emailTemplatePath = _fileHelper.GetFullFilePath(StaticHelper.ReminderEmailTemplate); - string emailSubject = $"{_translator.Translate(serverLanguage, "Vehicle Reminders From MotoVaultPro")} - {DateTime.Now.ToShortDateString()}"; - //construct html table. - string emailBody = File.ReadAllText(emailTemplatePath); - emailBody = emailBody.Replace("{VehicleInformation}", $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)}"); - string tableHeader = $"{_translator.Translate(serverLanguage, "Urgency")}{_translator.Translate(serverLanguage, "Description")}{_translator.Translate(serverLanguage, "Due")}"; - string tableBody = ""; - foreach(ReminderRecordViewModel reminder in reminders) - { - var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date.ToShortDateString()} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}"; - tableBody += $"{_translator.Translate(serverLanguage, StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency))}{reminder.Description}{dueOn}"; - } - emailBody = emailBody.Replace("{TableHeader}", tableHeader).Replace("{TableBody}", tableBody); - try - { - var result = SendEmail(emailAddresses, emailSubject, emailBody); - if (result) - { - return OperationResponse.Succeed("Email Sent!"); - } else - { - return OperationResponse.Failed(); - } - } catch (Exception ex) - { - return OperationResponse.Failed(ex.Message); - } - } - private bool SendEmail(List emailTo, string emailSubject, string emailBody) { - string from = mailConfig.EmailFrom; - var server = mailConfig.EmailServer; - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(from, from)); - foreach(string emailRecipient in emailTo) - { - message.To.Add(new MailboxAddress(emailRecipient, emailRecipient)); - } - message.Subject = emailSubject; - - var builder = new BodyBuilder(); - - builder.HtmlBody = emailBody; - - message.Body = builder.ToMessageBody(); - - using (var client = new SmtpClient()) - { - client.Connect(server, mailConfig.Port, SecureSocketOptions.Auto); - //perform authentication if either username or password is provided. - //do not perform authentication if neither are provided. - if (!string.IsNullOrWhiteSpace(mailConfig.Username) || !string.IsNullOrWhiteSpace(mailConfig.Password)) { - client.Authenticate(mailConfig.Username, mailConfig.Password); - } - try - { - client.Send(message); - client.Disconnect(true); - return true; - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } - private bool SendEmail(MailConfig testMailConfig, List emailTo, string emailSubject, string emailBody) - { - string from = testMailConfig.EmailFrom; - var server = testMailConfig.EmailServer; - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(from, from)); - foreach (string emailRecipient in emailTo) - { - message.To.Add(new MailboxAddress(emailRecipient, emailRecipient)); - } - message.Subject = emailSubject; - - var builder = new BodyBuilder(); - - builder.HtmlBody = emailBody; - - message.Body = builder.ToMessageBody(); - - using (var client = new SmtpClient()) - { - client.Connect(server, testMailConfig.Port, SecureSocketOptions.Auto); - //perform authentication if either username or password is provided. - //do not perform authentication if neither are provided. - if (!string.IsNullOrWhiteSpace(testMailConfig.Username) || !string.IsNullOrWhiteSpace(testMailConfig.Password)) - { - client.Authenticate(testMailConfig.Username, testMailConfig.Password); - } - try - { - client.Send(message); - client.Disconnect(true); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return false; - } - } - } - } -} diff --git a/Helper/ReminderHelper.cs b/Helper/ReminderHelper.cs deleted file mode 100644 index ee46023..0000000 --- a/Helper/ReminderHelper.cs +++ /dev/null @@ -1,175 +0,0 @@ -using MotoVaultPro.Models; - -namespace MotoVaultPro.Helper -{ - public interface IReminderHelper - { - ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder, DateTime? currentDate, int? currentMileage); - List GetReminderRecordViewModels(List reminders, int currentMileage, DateTime dateCompare); - } - public class ReminderHelper: IReminderHelper - { - private readonly IConfigHelper _config; - public ReminderHelper(IConfigHelper config) - { - _config = config; - } - public ReminderRecord GetUpdatedRecurringReminderRecord(ReminderRecord existingReminder, DateTime? currentDate, int? currentMileage) - { - var newDate = currentDate ?? existingReminder.Date; - var newMileage = currentMileage ?? existingReminder.Mileage; - if (existingReminder.Metric == ReminderMetric.Both) - { - if (existingReminder.ReminderMonthInterval != ReminderMonthInterval.Other) - { - existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval); - } else - { - if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Months) - { - existingReminder.Date = newDate.Date.AddMonths(existingReminder.CustomMonthInterval); - } - else if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Days) - { - existingReminder.Date = newDate.Date.AddDays(existingReminder.CustomMonthInterval); - } - } - - if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other) - { - existingReminder.Mileage = newMileage + (int)existingReminder.ReminderMileageInterval; - } - else - { - existingReminder.Mileage = newMileage + existingReminder.CustomMileageInterval; - } - } - else if (existingReminder.Metric == ReminderMetric.Odometer) - { - if (existingReminder.ReminderMileageInterval != ReminderMileageInterval.Other) - { - existingReminder.Mileage = newMileage + (int)existingReminder.ReminderMileageInterval; - } else - { - existingReminder.Mileage = newMileage + existingReminder.CustomMileageInterval; - } - } - else if (existingReminder.Metric == ReminderMetric.Date) - { - if (existingReminder.ReminderMonthInterval != ReminderMonthInterval.Other) - { - existingReminder.Date = newDate.AddMonths((int)existingReminder.ReminderMonthInterval); - } - else - { - if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Months) - { - existingReminder.Date = newDate.AddMonths(existingReminder.CustomMonthInterval); - } - else if (existingReminder.CustomMonthIntervalUnit == ReminderIntervalUnit.Days) - { - existingReminder.Date = newDate.AddDays(existingReminder.CustomMonthInterval); - } - } - } - return existingReminder; - } - public List GetReminderRecordViewModels(List reminders, int currentMileage, DateTime dateCompare) - { - List reminderViewModels = new List(); - var reminderUrgencyConfig = _config.GetReminderUrgencyConfig(); - foreach (var reminder in reminders) - { - if (reminder.UseCustomThresholds) - { - reminderUrgencyConfig = reminder.CustomThresholds; - } - var reminderViewModel = new ReminderRecordViewModel() - { - Id = reminder.Id, - VehicleId = reminder.VehicleId, - Date = reminder.Date, - Mileage = reminder.Mileage, - Description = reminder.Description, - Notes = reminder.Notes, - Metric = reminder.Metric, - UserMetric = reminder.Metric, - IsRecurring = reminder.IsRecurring, - Tags = reminder.Tags - }; - if (reminder.Metric == ReminderMetric.Both) - { - if (reminder.Date < dateCompare) - { - reminderViewModel.Urgency = ReminderUrgency.PastDue; - reminderViewModel.Metric = ReminderMetric.Date; - } - else if (reminder.Mileage < currentMileage) - { - reminderViewModel.Urgency = ReminderUrgency.PastDue; - reminderViewModel.Metric = ReminderMetric.Odometer; - } - else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.VeryUrgentDays)) - { - //if less than a week from today or less than 50 miles from current mileage then very urgent. - reminderViewModel.Urgency = ReminderUrgency.VeryUrgent; - //have to specify by which metric this reminder is urgent. - reminderViewModel.Metric = ReminderMetric.Date; - } - else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.VeryUrgentDistance) - { - reminderViewModel.Urgency = ReminderUrgency.VeryUrgent; - reminderViewModel.Metric = ReminderMetric.Odometer; - } - else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.UrgentDays)) - { - reminderViewModel.Urgency = ReminderUrgency.Urgent; - reminderViewModel.Metric = ReminderMetric.Date; - } - else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.UrgentDistance) - { - reminderViewModel.Urgency = ReminderUrgency.Urgent; - reminderViewModel.Metric = ReminderMetric.Odometer; - } - reminderViewModel.DueDays = (reminder.Date - dateCompare).Days; - reminderViewModel.DueMileage = reminder.Mileage - currentMileage; - } - else if (reminder.Metric == ReminderMetric.Date) - { - if (reminder.Date < dateCompare) - { - reminderViewModel.Urgency = ReminderUrgency.PastDue; - } - else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.VeryUrgentDays)) - { - reminderViewModel.Urgency = ReminderUrgency.VeryUrgent; - } - else if (reminder.Date < dateCompare.AddDays(reminderUrgencyConfig.UrgentDays)) - { - reminderViewModel.Urgency = ReminderUrgency.Urgent; - } - reminderViewModel.DueDays = (reminder.Date - dateCompare).Days; - } - else if (reminder.Metric == ReminderMetric.Odometer) - { - if (reminder.Mileage < currentMileage) - { - reminderViewModel.Urgency = ReminderUrgency.PastDue; - reminderViewModel.Metric = ReminderMetric.Odometer; - } - else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.VeryUrgentDistance) - { - reminderViewModel.Urgency = ReminderUrgency.VeryUrgent; - } - else if (reminder.Mileage < currentMileage + reminderUrgencyConfig.UrgentDistance) - { - reminderViewModel.Urgency = ReminderUrgency.Urgent; - } - reminderViewModel.DueMileage = reminder.Mileage - currentMileage; - } - reminderViewModels.Add(reminderViewModel); - } - return reminderViewModels; - } - } -} diff --git a/Helper/ReportHelper.cs b/Helper/ReportHelper.cs deleted file mode 100644 index 8abbda3..0000000 --- a/Helper/ReportHelper.cs +++ /dev/null @@ -1,144 +0,0 @@ -using MotoVaultPro.Models; -using System.Globalization; - -namespace MotoVaultPro.Helper -{ - public interface IReportHelper - { - IEnumerable GetOdometerRecordSum(List odometerRecords, int year = 0, bool sortIntoYear = false); - IEnumerable GetServiceRecordSum(List serviceRecords, int year = 0, bool sortIntoYear = false); - IEnumerable GetUpgradeRecordSum(List upgradeRecords, int year = 0, bool sortIntoYear = false); - IEnumerable GetGasRecordSum(List gasRecords, int year = 0, bool sortIntoYear = false); - IEnumerable GetTaxRecordSum(List taxRecords, int year = 0, bool sortIntoYear = false); - } - public class ReportHelper: IReportHelper - { - public IEnumerable GetOdometerRecordSum(List odometerRecords, int year = 0, bool sortIntoYear = false) - { - if (year != default) - { - odometerRecords.RemoveAll(x => x.Date.Year != year); - } - if (sortIntoYear) - { - return odometerRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key.Month, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month), - Year = x.Key.Year, - Cost = 0, - DistanceTraveled = x.Sum(y => y.DistanceTraveled) - }); - } else - { - return odometerRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = 0, - DistanceTraveled = x.Sum(y => y.DistanceTraveled) - }); - } - } - public IEnumerable GetServiceRecordSum(List serviceRecords, int year = 0, bool sortIntoYear = false) - { - if (year != default) - { - serviceRecords.RemoveAll(x => x.Date.Year != year); - } - if (sortIntoYear) - { - return serviceRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key.Month, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month), - Year = x.Key.Year, - Cost = x.Sum(y => y.Cost) - }); - } else - { - return serviceRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = x.Sum(y => y.Cost) - }); - } - } - public IEnumerable GetUpgradeRecordSum(List upgradeRecords, int year = 0, bool sortIntoYear = false) - { - if (year != default) - { - upgradeRecords.RemoveAll(x => x.Date.Year != year); - } - if (sortIntoYear) - { - return upgradeRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key.Month, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month), - Year = x.Key.Year, - Cost = x.Sum(y => y.Cost) - }); - } else - { - return upgradeRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = x.Sum(y => y.Cost) - }); - } - } - public IEnumerable GetGasRecordSum(List gasRecords, int year = 0, bool sortIntoYear = false) - { - if (year != default) - { - gasRecords.RemoveAll(x => x.Date.Year != year); - } - if (sortIntoYear) - { - return gasRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key.Month, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month), - Year = x.Key.Year, - Cost = x.Sum(y => y.Cost) - }); - } else - { - return gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = x.Sum(y => y.Cost) - }); - } - } - public IEnumerable GetTaxRecordSum(List taxRecords, int year = 0, bool sortIntoYear = false) - { - if (year != default) - { - taxRecords.RemoveAll(x => x.Date.Year != year); - } - if (sortIntoYear) - { - return taxRecords.GroupBy(x => new { x.Date.Month, x.Date.Year }).OrderBy(x => x.Key.Month).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key.Month, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key.Month), - Year = x.Key.Year, - Cost = x.Sum(y => y.Cost) - }); - } else - { - return taxRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth - { - MonthId = x.Key, - MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key), - Cost = x.Sum(y => y.Cost) - }); - } - } - } -} diff --git a/Helper/StaticHelper.cs b/Helper/StaticHelper.cs deleted file mode 100644 index 469a103..0000000 --- a/Helper/StaticHelper.cs +++ /dev/null @@ -1,855 +0,0 @@ -using MotoVaultPro.Models; -using CsvHelper; -using System.Globalization; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -namespace MotoVaultPro.Helper -{ - /// - /// helper method for static vars - /// - public static class StaticHelper - { - public const string VersionNumber = "1.0.0"; - public const string DbName = "data/cartracker.db"; - public const string UserConfigPath = "data/config/userConfig.json"; - public const string ServerConfigPath = "data/config/serverConfig.json"; - public const string LegacyUserConfigPath = "config/userConfig.json"; - public const string LegacyServerConfigPath = "config/serverConfig.json"; - public const string AdditionalWidgetsPath = "data/widgets.html"; - public const string DefaultLogoPath = "/defaults/motovaultpro_logo.png"; - public const string DefaultSmallLogoPath = "/defaults/motovaultpro_logo_small.png"; - public const string GenericErrorMessage = "An error occurred, please try again later"; - public const string ReminderEmailTemplate = "defaults/reminderemailtemplate.txt"; - public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx"; - public const string SponsorsPath = "https://ericgullickson.github.io/ericgullickson/sponsors.json"; - public const string TranslationPath = "https://ericgullickson.github.io/motovaultpro_translations"; - public const string ReleasePath = "https://api.github.com/repos/ericgullickson/motovaultpro/releases/latest"; - public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json"; - public const string ReportNote = "Report generated by MotoVaultPro, a Free and Open Source Vehicle Maintenance Tracker - MotoVaultPro.com"; - public static string GetTitleCaseReminderUrgency(ReminderUrgency input) - { - switch (input) - { - case ReminderUrgency.NotUrgent: - return "Not Urgent"; - case ReminderUrgency.VeryUrgent: - return "Very Urgent"; - case ReminderUrgency.PastDue: - return "Past Due"; - default: - return input.ToString(); - } - } - public static string GetTitleCaseReminderUrgency(string input) - { - switch (input) - { - case "NotUrgent": - return "Not Urgent"; - case "VeryUrgent": - return "Very Urgent"; - case "PastDue": - return "Past Due"; - default: - return input; - } - } - public static string GetReminderUrgencyColor(ReminderUrgency input) - { - switch (input) - { - case ReminderUrgency.NotUrgent: - return "text-bg-success"; - case ReminderUrgency.VeryUrgent: - return "text-bg-danger"; - case ReminderUrgency.PastDue: - return "text-bg-secondary"; - default: - return "text-bg-warning"; - } - } - - public static string GetPlanRecordColor(PlanPriority input) - { - switch (input) - { - case PlanPriority.Critical: - return "text-bg-danger"; - case PlanPriority.Normal: - return "text-bg-primary"; - case PlanPriority.Low: - return "text-bg-info"; - default: - return "text-bg-primary"; - } - } - - public static string GetPlanRecordProgress(PlanProgress input) - { - switch (input) - { - case PlanProgress.Backlog: - return "Planned"; - case PlanProgress.InProgress: - return "Doing"; - case PlanProgress.Testing: - return "Testing"; - case PlanProgress.Done: - return "Done"; - default: - return input.ToString(); - } - } - - public static string TruncateStrings(string input, int maxLength = 25) - { - if (string.IsNullOrWhiteSpace(input)) - { - return string.Empty; - } - if (input.Length > maxLength) - { - return (input.Substring(0, maxLength) + "..."); - } - else - { - return input; - } - } - public static string DefaultActiveTab(UserConfig userConfig, ImportMode tab) - { - var defaultTab = userConfig.DefaultTab; - var visibleTabs = userConfig.VisibleTabs; - if (visibleTabs.Contains(tab) && tab == defaultTab) - { - return "active"; - } - else if (!visibleTabs.Contains(tab)) - { - return "d-none"; - } - return ""; - } - public static string DefaultActiveTabContent(UserConfig userConfig, ImportMode tab) - { - var defaultTab = userConfig.DefaultTab; - if (tab == defaultTab) - { - return "show active"; - } - return ""; - } - public static string DefaultTabSelected(UserConfig userConfig, ImportMode tab) - { - var defaultTab = userConfig.DefaultTab; - var visibleTabs = userConfig.VisibleTabs; - if (!visibleTabs.Contains(tab)) - { - return "disabled"; - } - else if (tab == defaultTab) - { - return "selected"; - } - return ""; - } - public static List GetBaseLineCosts() - { - return new List() - { - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(1), MonthId = 1, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(2), MonthId = 2, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(3), MonthId = 3, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(4), MonthId = 4, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(5), MonthId = 5, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(6), MonthId = 6, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(7), MonthId = 7, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(8), MonthId = 8, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(9), MonthId = 9, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(10), MonthId = 10, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(11), MonthId = 11, Cost = 0M}, - new CostForVehicleByMonth {MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(12), MonthId = 12, Cost = 0M} - }; - } - public static List GetBaseLineCostsNoMonthName() - { - return new List() - { - new CostForVehicleByMonth { MonthId = 1, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 2, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 3, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 4, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 5, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 6, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 7, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 8, Cost = 0M}, - new CostForVehicleByMonth {MonthId = 9, Cost = 0M}, - new CostForVehicleByMonth { MonthId = 10, Cost = 0M}, - new CostForVehicleByMonth { MonthId = 11, Cost = 0M}, - new CostForVehicleByMonth { MonthId = 12, Cost = 0M} - }; - } - public static List GetBarChartColors() - { - return new List { "#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f" }; - } - - public static ServiceRecord GenericToServiceRecord(GenericRecord input) - { - return new ServiceRecord - { - VehicleId = input.VehicleId, - Date = input.Date, - Description = input.Description, - Cost = input.Cost, - Mileage = input.Mileage, - Files = input.Files, - Notes = input.Notes, - Tags = input.Tags, - ExtraFields = input.ExtraFields, - RequisitionHistory = input.RequisitionHistory - }; - } - public static UpgradeRecord GenericToUpgradeRecord(GenericRecord input) - { - return new UpgradeRecord - { - VehicleId = input.VehicleId, - Date = input.Date, - Description = input.Description, - Cost = input.Cost, - Mileage = input.Mileage, - Files = input.Files, - Notes = input.Notes, - Tags = input.Tags, - ExtraFields = input.ExtraFields, - RequisitionHistory = input.RequisitionHistory - }; - } - - public static List AddExtraFields(List recordExtraFields, List templateExtraFields) - { - if (!templateExtraFields.Any()) - { - return new List(); - } - if (!recordExtraFields.Any()) - { - return templateExtraFields; - } - var fieldNames = templateExtraFields.Select(x => x.Name); - //remove fields that are no longer present in template. - recordExtraFields.RemoveAll(x => !fieldNames.Contains(x.Name)); - if (!recordExtraFields.Any()) - { - return templateExtraFields; - } - var recordFieldNames = recordExtraFields.Select(x => x.Name); - //update isrequired setting - foreach (ExtraField extraField in recordExtraFields) - { - var firstMatchingField = templateExtraFields.First(x => x.Name == extraField.Name); - extraField.IsRequired = firstMatchingField.IsRequired; - extraField.FieldType = firstMatchingField.FieldType; - } - //append extra fields - foreach (ExtraField extraField in templateExtraFields) - { - if (!recordFieldNames.Contains(extraField.Name)) - { - recordExtraFields.Add(extraField); - } - } - //re-order extra fields - recordExtraFields = recordExtraFields.OrderBy(x => templateExtraFields.FindIndex(y => y.Name == x.Name)).ToList(); - return recordExtraFields; - } - - public static string GetFuelEconomyUnit(bool useKwh, bool useHours, bool useMPG, bool useUKMPG) - { - string fuelEconomyUnit; - if (useKwh) - { - var distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km"); - fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}"; - } - else if (useMPG && useUKMPG) - { - fuelEconomyUnit = useHours ? "h/g" : "mpg"; - } - else if (useUKMPG) - { - fuelEconomyUnit = useHours ? "l/100h" : "l/100mi."; - } - else - { - fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km"); - } - return fuelEconomyUnit; - } - public static long GetEpochFromDateTime(DateTime date) - { - return new DateTimeOffset(date).ToUnixTimeMilliseconds(); - } - public static void InitMessage(IConfiguration config) - { - Console.WriteLine($"MotoVaultPro {VersionNumber}"); - Console.WriteLine("Website: https://motovaultpro.com"); - Console.WriteLine("Documentation: https://docs.motovaultpro.com"); - Console.WriteLine("GitHub: https://github.com/ericgullickson/motovaultpro"); - var mailConfig = config.GetSection("MailConfig").Get(); - if (mailConfig != null && !string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - Console.WriteLine($"SMTP Configured for {mailConfig.EmailServer}"); - } - else - { - Console.WriteLine("SMTP Not Configured"); - } - var motd = config["LUBELOGGER_MOTD"] ?? "Not Configured"; - Console.WriteLine($"Message Of The Day: {motd}"); - if (string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.Name)) - { - Console.WriteLine("WARNING: No Locale or Culture Configured for MotoVaultPro, Check Environment Variables"); - } - //Create folders if they don't exist. - if (!Directory.Exists("data")) - { - Directory.CreateDirectory("data"); - Console.WriteLine("Created data directory"); - } - if (!Directory.Exists("data/images")) - { - Console.WriteLine("Created images directory"); - Directory.CreateDirectory("data/images"); - } - if (!Directory.Exists("data/documents")) - { - Directory.CreateDirectory("data/documents"); - Console.WriteLine("Created documents directory"); - } - if (!Directory.Exists("data/translations")) - { - Directory.CreateDirectory("data/translations"); - Console.WriteLine("Created translations directory"); - } - if (!Directory.Exists("data/temp")) - { - Directory.CreateDirectory("data/temp"); - Console.WriteLine("Created translations directory"); - } - if (!Directory.Exists("data/config")) - { - Directory.CreateDirectory("data/config"); - Console.WriteLine("Created config directory"); - } - } - public static void CheckMigration(string webRootPath, string webContentPath) - { - //check if current working directory differs from content root. - if (Directory.GetCurrentDirectory() != webContentPath) - { - Console.WriteLine("WARNING: The Working Directory differs from the Web Content Path"); - Console.WriteLine($"Working Directory: {Directory.GetCurrentDirectory()}"); - Console.WriteLine($"Web Content Path: {webContentPath}"); - } - //migrates all user-uploaded files from webroot to new data folder - //images - var imagePath = Path.Combine(webRootPath, "images"); - var docsPath = Path.Combine(webRootPath, "documents"); - var translationPath = Path.Combine(webRootPath, "translations"); - var tempPath = Path.Combine(webRootPath, "temp"); - if (File.Exists(LegacyUserConfigPath)) - { - File.Move(LegacyUserConfigPath, UserConfigPath, true); - } - if (Directory.Exists(imagePath)) - { - foreach (string fileToMove in Directory.GetFiles(imagePath)) - { - var newFilePath = $"data/images/{Path.GetFileName(fileToMove)}"; - File.Move(fileToMove, newFilePath, true); - Console.WriteLine($"Migrated Image: {Path.GetFileName(fileToMove)}"); - } - } - if (Directory.Exists(docsPath)) - { - foreach (string fileToMove in Directory.GetFiles(docsPath)) - { - var newFilePath = $"data/documents/{Path.GetFileName(fileToMove)}"; - File.Move(fileToMove, newFilePath, true); - Console.WriteLine($"Migrated Document: {Path.GetFileName(fileToMove)}"); - } - } - if (Directory.Exists(translationPath)) - { - foreach (string fileToMove in Directory.GetFiles(translationPath)) - { - var newFilePath = $"data/translations/{Path.GetFileName(fileToMove)}"; - File.Move(fileToMove, newFilePath, true); - Console.WriteLine($"Migrated Translation: {Path.GetFileName(fileToMove)}"); - } - } - if (Directory.Exists(tempPath)) - { - foreach (string fileToMove in Directory.GetFiles(tempPath)) - { - var newFilePath = $"data/temp/{Path.GetFileName(fileToMove)}"; - File.Move(fileToMove, newFilePath, true); - Console.WriteLine($"Migrated Temp File: {Path.GetFileName(fileToMove)}"); - } - } - } - public static async void NotifyAsync(string webhookURL, WebHookPayload webHookPayload) - { - if (string.IsNullOrWhiteSpace(webhookURL)) - { - return; - } - var httpClient = new HttpClient(); - if (webhookURL.StartsWith("discord://")) - { - webhookURL = webhookURL.Replace("discord://", "https://"); //cleanurl - //format to discord - httpClient.PostAsJsonAsync(webhookURL, DiscordWebHook.FromWebHookPayload(webHookPayload)); - } - else - { - httpClient.PostAsJsonAsync(webhookURL, webHookPayload); - } - } - public static string GetImportModeIcon(ImportMode importMode) - { - switch (importMode) - { - case ImportMode.ServiceRecord: - return "bi-card-checklist"; - case ImportMode.UpgradeRecord: - return "bi-wrench-adjustable"; - case ImportMode.TaxRecord: - return "bi-currency-dollar"; - case ImportMode.SupplyRecord: - return "bi-shop"; - case ImportMode.PlanRecord: - return "bi-bar-chart-steps"; - case ImportMode.OdometerRecord: - return "bi-speedometer"; - case ImportMode.GasRecord: - return "bi-fuel-pump"; - case ImportMode.NoteRecord: - return "bi-journal-bookmark"; - case ImportMode.ReminderRecord: - return "bi-bell"; - default: - return "bi-file-bar-graph"; - } - } - public static string GetVehicleIdentifier(Vehicle vehicle) - { - if (vehicle.VehicleIdentifier == "LicensePlate") - { - return vehicle.LicensePlate; - } - else - { - if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier)) - { - return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value; - } - else - { - return "N/A"; - } - } - } - public static string GetVehicleIdentifier(VehicleViewModel vehicle) - { - if (vehicle.VehicleIdentifier == "LicensePlate") - { - return vehicle.LicensePlate; - } - else - { - if (vehicle.ExtraFields.Any(x => x.Name == vehicle.VehicleIdentifier)) - { - return vehicle.ExtraFields?.FirstOrDefault(x => x.Name == vehicle.VehicleIdentifier)?.Value; - } - else - { - return "N/A"; - } - } - } - //Translations - public static string GetTranslationDownloadPath(string continent, string name) - { - if (string.IsNullOrWhiteSpace(continent) || string.IsNullOrWhiteSpace(name)) - { - return string.Empty; - } - else - { - switch (continent) - { - case "NorthAmerica": - continent = "North America"; - break; - case "SouthAmerica": - continent = "South America"; - break; - } - return $"{TranslationPath}/{continent}/{name}.json"; - } - } - public static string GetTranslationName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - return string.Empty; - } - else - { - try - { - string cleanedName = name.Contains("_") ? name.Replace("_", "-") : name; - string displayName = CultureInfo.GetCultureInfo(cleanedName).DisplayName; - if (string.IsNullOrWhiteSpace(displayName)) - { - return name; - } - else - { - return displayName; - } - } - catch (Exception ex) - { - return name; - } - } - } - //CSV Write Methods - public static void WriteGenericRecordExportModel(CsvWriter _csv, IEnumerable genericRecords) - { - var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct(); - //write headers - _csv.WriteField(nameof(GenericRecordExportModel.Date)); - _csv.WriteField(nameof(GenericRecordExportModel.Description)); - _csv.WriteField(nameof(GenericRecordExportModel.Cost)); - _csv.WriteField(nameof(GenericRecordExportModel.Notes)); - _csv.WriteField(nameof(GenericRecordExportModel.Odometer)); - _csv.WriteField(nameof(GenericRecordExportModel.Tags)); - foreach (string extraHeader in extraHeaders) - { - _csv.WriteField($"extrafield_{extraHeader}"); - } - _csv.NextRecord(); - foreach (GenericRecordExportModel genericRecord in genericRecords) - { - _csv.WriteField(genericRecord.Date); - _csv.WriteField(genericRecord.Description); - _csv.WriteField(genericRecord.Cost); - _csv.WriteField(genericRecord.Notes); - _csv.WriteField(genericRecord.Odometer); - _csv.WriteField(genericRecord.Tags); - foreach (string extraHeader in extraHeaders) - { - var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault(); - _csv.WriteField(extraField != null ? extraField.Value : string.Empty); - } - _csv.NextRecord(); - } - } - public static void WriteOdometerRecordExportModel(CsvWriter _csv, IEnumerable genericRecords) - { - var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct(); - //write headers - _csv.WriteField(nameof(OdometerRecordExportModel.Date)); - _csv.WriteField(nameof(OdometerRecordExportModel.InitialOdometer)); - _csv.WriteField(nameof(OdometerRecordExportModel.Odometer)); - _csv.WriteField(nameof(OdometerRecordExportModel.Notes)); - _csv.WriteField(nameof(OdometerRecordExportModel.Tags)); - foreach (string extraHeader in extraHeaders) - { - _csv.WriteField($"extrafield_{extraHeader}"); - } - _csv.NextRecord(); - foreach (OdometerRecordExportModel genericRecord in genericRecords) - { - _csv.WriteField(genericRecord.Date); - _csv.WriteField(genericRecord.InitialOdometer); - _csv.WriteField(genericRecord.Odometer); - _csv.WriteField(genericRecord.Notes); - _csv.WriteField(genericRecord.Tags); - foreach (string extraHeader in extraHeaders) - { - var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault(); - _csv.WriteField(extraField != null ? extraField.Value : string.Empty); - } - _csv.NextRecord(); - } - } - public static void WriteTaxRecordExportModel(CsvWriter _csv, IEnumerable genericRecords) - { - var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct(); - //write headers - _csv.WriteField(nameof(TaxRecordExportModel.Date)); - _csv.WriteField(nameof(TaxRecordExportModel.Description)); - _csv.WriteField(nameof(TaxRecordExportModel.Cost)); - _csv.WriteField(nameof(TaxRecordExportModel.Notes)); - _csv.WriteField(nameof(TaxRecordExportModel.Tags)); - foreach (string extraHeader in extraHeaders) - { - _csv.WriteField($"extrafield_{extraHeader}"); - } - _csv.NextRecord(); - foreach (TaxRecordExportModel genericRecord in genericRecords) - { - _csv.WriteField(genericRecord.Date); - _csv.WriteField(genericRecord.Description); - _csv.WriteField(genericRecord.Cost); - _csv.WriteField(genericRecord.Notes); - _csv.WriteField(genericRecord.Tags); - foreach (string extraHeader in extraHeaders) - { - var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault(); - _csv.WriteField(extraField != null ? extraField.Value : string.Empty); - } - _csv.NextRecord(); - } - } - public static void WriteSupplyRecordExportModel(CsvWriter _csv, IEnumerable genericRecords) - { - var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct(); - //write headers - _csv.WriteField(nameof(SupplyRecordExportModel.Date)); - _csv.WriteField(nameof(SupplyRecordExportModel.PartNumber)); - _csv.WriteField(nameof(SupplyRecordExportModel.PartSupplier)); - _csv.WriteField(nameof(SupplyRecordExportModel.PartQuantity)); - _csv.WriteField(nameof(SupplyRecordExportModel.Description)); - _csv.WriteField(nameof(SupplyRecordExportModel.Notes)); - _csv.WriteField(nameof(SupplyRecordExportModel.Cost)); - _csv.WriteField(nameof(SupplyRecordExportModel.Tags)); - foreach (string extraHeader in extraHeaders) - { - _csv.WriteField($"extrafield_{extraHeader}"); - } - _csv.NextRecord(); - foreach (SupplyRecordExportModel genericRecord in genericRecords) - { - _csv.WriteField(genericRecord.Date); - _csv.WriteField(genericRecord.PartNumber); - _csv.WriteField(genericRecord.PartSupplier); - _csv.WriteField(genericRecord.PartQuantity); - _csv.WriteField(genericRecord.Description); - _csv.WriteField(genericRecord.Notes); - _csv.WriteField(genericRecord.Cost); - _csv.WriteField(genericRecord.Tags); - foreach (string extraHeader in extraHeaders) - { - var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault(); - _csv.WriteField(extraField != null ? extraField.Value : string.Empty); - } - _csv.NextRecord(); - } - } - public static void WritePlanRecordExportModel(CsvWriter _csv, IEnumerable genericRecords) - { - var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct(); - //write headers - _csv.WriteField(nameof(PlanRecordExportModel.DateCreated)); - _csv.WriteField(nameof(PlanRecordExportModel.DateModified)); - _csv.WriteField(nameof(PlanRecordExportModel.Description)); - _csv.WriteField(nameof(PlanRecordExportModel.Notes)); - _csv.WriteField(nameof(PlanRecordExportModel.Type)); - _csv.WriteField(nameof(PlanRecordExportModel.Priority)); - _csv.WriteField(nameof(PlanRecordExportModel.Progress)); - _csv.WriteField(nameof(PlanRecordExportModel.Cost)); - foreach (string extraHeader in extraHeaders) - { - _csv.WriteField($"extrafield_{extraHeader}"); - } - _csv.NextRecord(); - foreach (PlanRecordExportModel genericRecord in genericRecords) - { - _csv.WriteField(genericRecord.DateCreated); - _csv.WriteField(genericRecord.DateModified); - _csv.WriteField(genericRecord.Description); - _csv.WriteField(genericRecord.Notes); - _csv.WriteField(genericRecord.Type); - _csv.WriteField(genericRecord.Priority); - _csv.WriteField(genericRecord.Progress); - _csv.WriteField(genericRecord.Cost); - foreach (string extraHeader in extraHeaders) - { - var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault(); - _csv.WriteField(extraField != null ? extraField.Value : string.Empty); - } - _csv.NextRecord(); - } - } - public static string HideZeroCost(string input, bool hideZero, string decorations = "") - { - if (input == 0M.ToString("C2") && hideZero) - { - return "---"; - } - else - { - return string.IsNullOrWhiteSpace(decorations) ? input : $"{input}{decorations}"; - } - } - public static string HideZeroCost(decimal input, bool hideZero, string decorations = "") - { - if (input == default && hideZero) - { - return "---"; - } - else - { - return string.IsNullOrWhiteSpace(decorations) ? input.ToString("C2") : $"{input.ToString("C2")}{decorations}"; - } - } - public static string GetIconByFileExtension(string fileLocation) - { - var fileExt = Path.GetExtension(fileLocation); - if (!fileLocation.StartsWith("/documents") && !fileLocation.StartsWith("documents") && !fileLocation.StartsWith("/temp") && !fileLocation.StartsWith("temp")) - { - return "bi-link-45deg"; - } - switch (fileExt) - { - case ".pdf": - return "bi-file-earmark-pdf"; - case ".zip": - case ".7z": - case ".rar": - return "bi-file-earmark-zip"; - case ".png": - case ".jpg": - case ".jpeg": - return "bi-file-earmark-image"; - case ".xls": - case ".xlsx": - case ".xlsm": - case ".ods": - case ".csv": - return "bi-file-earmark-spreadsheet"; - case ".docx": - case ".odt": - case ".rtf": - return "bi-file-earmark-richtext"; - default: - return "bi-file-earmark"; - } - } - public static JsonSerializerOptions GetInvariantOption() - { - var serializerOption = new JsonSerializerOptions(); - serializerOption.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - serializerOption.Converters.Add(new InvariantConverter()); - return serializerOption; - } - public static void WriteGasRecordExportModel(CsvWriter _csv, IEnumerable genericRecords) - { - var extraHeaders = genericRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct(); - //write headers - _csv.WriteField(nameof(GasRecordExportModel.Date)); - _csv.WriteField(nameof(GasRecordExportModel.Odometer)); - _csv.WriteField(nameof(GasRecordExportModel.FuelConsumed)); - _csv.WriteField(nameof(GasRecordExportModel.Cost)); - _csv.WriteField(nameof(GasRecordExportModel.FuelEconomy)); - _csv.WriteField(nameof(GasRecordExportModel.IsFillToFull)); - _csv.WriteField(nameof(GasRecordExportModel.MissedFuelUp)); - _csv.WriteField(nameof(GasRecordExportModel.Notes)); - _csv.WriteField(nameof(GasRecordExportModel.Tags)); - foreach (string extraHeader in extraHeaders) - { - _csv.WriteField($"extrafield_{extraHeader}"); - } - _csv.NextRecord(); - foreach (GasRecordExportModel genericRecord in genericRecords) - { - _csv.WriteField(genericRecord.Date); - _csv.WriteField(genericRecord.Odometer); - _csv.WriteField(genericRecord.FuelConsumed); - _csv.WriteField(genericRecord.Cost); - _csv.WriteField(genericRecord.FuelEconomy); - _csv.WriteField(genericRecord.IsFillToFull); - _csv.WriteField(genericRecord.MissedFuelUp); - _csv.WriteField(genericRecord.Notes); - _csv.WriteField(genericRecord.Tags); - foreach (string extraHeader in extraHeaders) - { - var extraField = genericRecord.ExtraFields.Where(x => x.Name == extraHeader).FirstOrDefault(); - _csv.WriteField(extraField != null ? extraField.Value : string.Empty); - } - _csv.NextRecord(); - } - } - public static byte[] RemindersToCalendar(List reminders) - { - //converts reminders to iCal file - StringBuilder sb = new StringBuilder(); - //start the calendar item - sb.AppendLine("BEGIN:VCALENDAR"); - sb.AppendLine("VERSION:2.0"); - sb.AppendLine("PRODID:motovaultpro.com"); - sb.AppendLine("CALSCALE:GREGORIAN"); - sb.AppendLine("METHOD:PUBLISH"); - - //create events. - foreach(ReminderRecordViewModel reminder in reminders) - { - var dtStart = reminder.Date.Date.ToString("yyyyMMddTHHmm00"); - var dtEnd = reminder.Date.Date.AddDays(1).AddMilliseconds(-1).ToString("yyyyMMddTHHmm00"); - var calendarUID = new Guid(MD5.HashData(Encoding.UTF8.GetBytes($"{dtStart}_{reminder.Description}"))); - sb.AppendLine("BEGIN:VEVENT"); - sb.AppendLine("DTSTAMP:" + DateTime.Now.ToString("yyyyMMddTHHmm00")); - sb.AppendLine("UID:" + calendarUID); - sb.AppendLine("DTSTART:" + dtStart); - sb.AppendLine("DTEND:" + dtEnd); - sb.AppendLine($"SUMMARY:{reminder.Description}"); - sb.AppendLine($"DESCRIPTION:{reminder.Description}"); - switch (reminder.Urgency) - { - case ReminderUrgency.NotUrgent: - sb.AppendLine("PRIORITY:3"); - break; - case ReminderUrgency.Urgent: - sb.AppendLine("PRIORITY:2"); - break; - case ReminderUrgency.VeryUrgent: - sb.AppendLine("PRIORITY:1"); - break; - case ReminderUrgency.PastDue: - sb.AppendLine("PRIORITY:1"); - break; - } - sb.AppendLine("END:VEVENT"); - } - - //end calendar item - sb.AppendLine("END:VCALENDAR"); - string calendarContent = sb.ToString(); - return Encoding.UTF8.GetBytes(calendarContent); - } - public static decimal CalculateNiceStepSize(decimal min, decimal max, int desiredTicks) - { - double range = Convert.ToDouble(max - min); - double roughStep = range / desiredTicks; - double exponent = Math.Floor(Math.Log10(roughStep)); - double stepPower = Math.Pow(10, exponent); - double normalizedStep = roughStep / stepPower; - - // Choose the closest nice interval (1, 2, or 5) - double[] niceSteps = { 1, 2, 5 }; - double goodNormalizedStep = niceSteps.OrderBy(s => Math.Abs(s - normalizedStep)).First(); - - return Convert.ToDecimal(goodNormalizedStep * stepPower); - } - } -} diff --git a/Helper/TranslationHelper.cs b/Helper/TranslationHelper.cs deleted file mode 100644 index d03c369..0000000 --- a/Helper/TranslationHelper.cs +++ /dev/null @@ -1,197 +0,0 @@ -using MotoVaultPro.Models; -using Microsoft.Extensions.Caching.Memory; -using System.Text.Json; - -namespace MotoVaultPro.Helper -{ - public interface ITranslationHelper - { - string Translate(string userLanguage, string text); - Dictionary GetTranslations(string userLanguage); - OperationResponse SaveTranslation(string userLanguage, Dictionary translations); - string ExportTranslation(Dictionary translations); - } - public class TranslationHelper : ITranslationHelper - { - private readonly IFileHelper _fileHelper; - private readonly IConfiguration _config; - private readonly ILogger _logger; - private IMemoryCache _cache; - public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache, ILogger logger) - { - _fileHelper = fileHelper; - _config = config; - _cache = memoryCache; - _logger = logger; - } - public string Translate(string userLanguage, string text) - { - bool create = bool.Parse(_config["LUBELOGGER_TRANSLATOR"] ?? "false"); - //transform input text into key. - string translationKey = text.Replace(" ", "_"); - var translationFilePath = userLanguage == "en_US" ? _fileHelper.GetFullFilePath($"/defaults/en_US.json") : _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false); - var dictionary = _cache.GetOrCreate>($"lang_{userLanguage}", entry => - { - entry.SlidingExpiration = TimeSpan.FromHours(1); - if (File.Exists(translationFilePath)) - { - try - { - var translationFile = File.ReadAllText(translationFilePath); - var translationDictionary = JsonSerializer.Deserialize>(translationFile); - return translationDictionary ?? new Dictionary(); - } catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Dictionary(); - } - } - else - { - _logger.LogError($"Could not find translation file for {userLanguage}"); - return new Dictionary(); - } - }); - if (dictionary != null && dictionary.ContainsKey(translationKey)) - { - return dictionary[translationKey]; - } - else if (create && File.Exists(translationFilePath)) - { - //create entry - dictionary.Add(translationKey, text); - _logger.LogInformation($"Translation key added to {userLanguage} for {translationKey} with value {text}"); - File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary)); - return text; - } - return text; - } - private Dictionary GetDefaultTranslation() - { - //this method always returns en_US translation. - var translationFilePath = _fileHelper.GetFullFilePath($"/defaults/en_US.json"); - if (!string.IsNullOrWhiteSpace(translationFilePath)) - { - //file exists. - try - { - var translationFile = File.ReadAllText(translationFilePath); - var translationDictionary = JsonSerializer.Deserialize>(translationFile); - return translationDictionary ?? new Dictionary(); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Dictionary(); - } - } - _logger.LogError($"Could not find translation file for en_US"); - return new Dictionary(); - } - public Dictionary GetTranslations(string userLanguage) - { - var defaultTranslation = GetDefaultTranslation(); - if (userLanguage == "en_US") - { - return defaultTranslation; - } - var translationFilePath = _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json"); - if (!string.IsNullOrWhiteSpace(translationFilePath)) - { - //file exists. - try - { - var translationFile = File.ReadAllText(translationFilePath); - var translationDictionary = JsonSerializer.Deserialize>(translationFile); - if (translationDictionary != null) - { - foreach(var translation in translationDictionary) - { - if (defaultTranslation.ContainsKey(translation.Key)) - { - defaultTranslation[translation.Key] = translation.Value; - } - } - } - return defaultTranslation ?? new Dictionary(); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return new Dictionary(); - } - } - _logger.LogError($"Could not find translation file for {userLanguage}"); - return new Dictionary(); - } - public OperationResponse SaveTranslation(string userLanguage, Dictionary translations) - { - bool create = bool.Parse(_config["LUBELOGGER_TRANSLATOR"] ?? "false"); - bool isDefaultLanguage = userLanguage == "en_US"; - if (isDefaultLanguage && !create) - { - return OperationResponse.Failed("The translation file name en_US is reserved."); - } - if (string.IsNullOrWhiteSpace(userLanguage)) - { - return OperationResponse.Failed("File name is not provided."); - } - if (!translations.Any()) - { - return OperationResponse.Failed("Translation has no data."); - } - var translationFilePath = isDefaultLanguage ? _fileHelper.GetFullFilePath($"/defaults/en_US.json") : _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false); - try - { - if (File.Exists(translationFilePath)) - { - //write to file - File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations)); - _cache.Remove($"lang_{userLanguage}"); //clear out cache, force a reload from file. - } else - { - //check if directory exists first. - var translationDirectory = _fileHelper.GetFullFilePath("translations/", false); - if (!Directory.Exists(translationDirectory)) - { - Directory.CreateDirectory(translationDirectory); - } - //write to file - File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations)); - } - return OperationResponse.Succeed("Translation Updated"); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return OperationResponse.Failed(); - } - } - public string ExportTranslation(Dictionary translations) - { - try - { - var tempFileName = $"/temp/{Guid.NewGuid()}.json"; - string uploadDirectory = _fileHelper.GetFullFilePath("temp/", false); - if (!Directory.Exists(uploadDirectory)) - { - Directory.CreateDirectory(uploadDirectory); - } - var saveFilePath = _fileHelper.GetFullFilePath(tempFileName, false); - //standardize translation format for export only. - Dictionary sortedTranslations = new Dictionary(); - foreach (var translation in translations.OrderBy(x => x.Key)) - { - sortedTranslations.Add(translation.Key, translation.Value); - }; - File.WriteAllText(saveFilePath, JsonSerializer.Serialize(sortedTranslations, new JsonSerializerOptions { WriteIndented = true })); - return tempFileName; - } - catch(Exception ex) - { - _logger.LogError(ex.Message); - return string.Empty; - } - } - } -} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c36ff84..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Hargata Softworks - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Logic/LoginLogic.cs b/Logic/LoginLogic.cs deleted file mode 100644 index 99ecc06..0000000 --- a/Logic/LoginLogic.cs +++ /dev/null @@ -1,510 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.IdentityModel.Tokens; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -namespace MotoVaultPro.Logic -{ - public interface ILoginLogic - { - bool MakeUserAdmin(int userId, bool isAdmin); - OperationResponse GenerateUserToken(string emailAddress, bool autoNotify); - bool DeleteUserToken(int tokenId); - bool DeleteUser(int userId); - OperationResponse RegisterOpenIdUser(LoginModel credentials); - OperationResponse UpdateUserDetails(int userId, LoginModel credentials); - OperationResponse RegisterNewUser(LoginModel credentials); - OperationResponse RequestResetPassword(LoginModel credentials); - OperationResponse ResetPasswordByUser(LoginModel credentials); - OperationResponse ResetUserPassword(LoginModel credentials); - OperationResponse SendRegistrationToken(LoginModel credentials); - UserData ValidateUserCredentials(LoginModel credentials); - UserData ValidateOpenIDUser(LoginModel credentials); - bool CheckIfUserIsValid(int userId); - bool CreateRootUserCredentials(LoginModel credentials); - bool DeleteRootUserCredentials(); - bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset); - List GetAllUsers(); - List GetAllTokens(); - KeyValuePair GetPKCEChallengeCode(); - } - public class LoginLogic : ILoginLogic - { - private readonly IUserRecordDataAccess _userData; - private readonly ITokenRecordDataAccess _tokenData; - private readonly IMailHelper _mailHelper; - private readonly IConfigHelper _configHelper; - private IMemoryCache _cache; - public LoginLogic(IUserRecordDataAccess userData, - ITokenRecordDataAccess tokenData, - IMailHelper mailHelper, - IConfigHelper configHelper, - IMemoryCache memoryCache) - { - _userData = userData; - _tokenData = tokenData; - _mailHelper = mailHelper; - _configHelper = configHelper; - _cache = memoryCache; - } - public bool CheckIfUserIsValid(int userId) - { - if (userId == -1) - { - return true; - } - var result = _userData.GetUserRecordById(userId); - if (result == null) - { - return false; - } else - { - return result.Id != 0; - } - } - public OperationResponse UpdateUserDetails(int userId, LoginModel credentials) - { - //get current user details - var existingUser = _userData.GetUserRecordById(userId); - if (existingUser.Id == default) - { - return OperationResponse.Failed("Invalid user"); - } - //validate user token - var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token); - if (existingToken.Id == default || existingToken.EmailAddress != existingUser.EmailAddress) - { - return OperationResponse.Failed("Invalid Token"); - } - if (!string.IsNullOrWhiteSpace(credentials.UserName) && existingUser.UserName != credentials.UserName) - { - //check if new username is already taken. - var existingUserWithUserName = _userData.GetUserRecordByUserName(credentials.UserName); - if (existingUserWithUserName.Id != default) - { - return OperationResponse.Failed("Username already taken"); - } - existingUser.UserName = credentials.UserName; - } - if (!string.IsNullOrWhiteSpace(credentials.EmailAddress) && existingUser.EmailAddress != credentials.EmailAddress) - { - //check if email address already exists - var existingUserWithEmailAddress = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress); - if (existingUserWithEmailAddress.Id != default) - { - return OperationResponse.Failed("A user with that email already exists"); - } - existingUser.EmailAddress = credentials.EmailAddress; - } - if (!string.IsNullOrWhiteSpace(credentials.Password)) - { - //update password - existingUser.Password = GetHash(credentials.Password); - } - //delete token - _tokenData.DeleteToken(existingToken.Id); - var result = _userData.SaveUserRecord(existingUser); - return OperationResponse.Conditional(result, "User Updated", string.Empty); - } - public OperationResponse RegisterOpenIdUser(LoginModel credentials) - { - //validate their token. - var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token); - if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress) - { - return OperationResponse.Failed("Invalid Token"); - } - if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName)) - { - return OperationResponse.Failed("Username cannot be blank"); - } - var existingUser = _userData.GetUserRecordByUserName(credentials.UserName); - if (existingUser.Id != default) - { - return OperationResponse.Failed("Username already taken"); - } - var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress); - if (existingUserWithEmail.Id != default) - { - return OperationResponse.Failed("A user with that email already exists"); - } - _tokenData.DeleteToken(existingToken.Id); - var newUser = new UserData() - { - UserName = credentials.UserName, - Password = GetHash(NewToken()), //generate a password for OpenID User - EmailAddress = credentials.EmailAddress - }; - var result = _userData.SaveUserRecord(newUser); - if (result) - { - return OperationResponse.Succeed("You will be logged in briefly."); - } - else - { - return OperationResponse.Failed("Something went wrong, please try again later."); - } - } - //handles user registration - public OperationResponse RegisterNewUser(LoginModel credentials) - { - //validate their token. - var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token); - if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress) - { - return OperationResponse.Failed("Invalid Token"); - } - //token is valid, check if username and password is acceptable and that username is unique. - if (string.IsNullOrWhiteSpace(credentials.EmailAddress) || string.IsNullOrWhiteSpace(credentials.UserName) || string.IsNullOrWhiteSpace(credentials.Password)) - { - return OperationResponse.Failed("Neither username nor password can be blank"); - } - var existingUser = _userData.GetUserRecordByUserName(credentials.UserName); - if (existingUser.Id != default) - { - return OperationResponse.Failed("Username already taken"); - } - var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress); - if (existingUserWithEmail.Id != default) - { - return OperationResponse.Failed("A user with that email already exists"); - } - //username is unique then we delete the token and create the user. - _tokenData.DeleteToken(existingToken.Id); - var newUser = new UserData() - { - UserName = credentials.UserName, - Password = GetHash(credentials.Password), - EmailAddress = credentials.EmailAddress - }; - var result = _userData.SaveUserRecord(newUser); - if (result) - { - return OperationResponse.Succeed("You will be redirected to the login page briefly."); - } - else - { - return OperationResponse.Failed(); - } - } - public OperationResponse SendRegistrationToken(LoginModel credentials) - { - if (_configHelper.GetServerOpenRegistration()) - { - return GenerateUserToken(credentials.EmailAddress, true); - } else - { - return OperationResponse.Failed("Open Registration Disabled"); - } - } - /// - /// Generates a token and notifies user via email so they can reset their password. - /// - /// - /// - public OperationResponse RequestResetPassword(LoginModel credentials) - { - var existingUser = _userData.GetUserRecordByUserName(credentials.UserName); - if (existingUser.Id != default) - { - //user exists, generate a token and send email. - GenerateTokenForEmailAddress(existingUser.EmailAddress, true); - } - //for security purposes we want to always return true for this method. - //otherwise someone can spam the reset password method to sniff out users. - return OperationResponse.Succeed("If your user exists in the system you should receive an email shortly with instructions on how to proceed."); - } - public OperationResponse ResetPasswordByUser(LoginModel credentials) - { - var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token); - if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress) - { - return OperationResponse.Failed("Invalid Token"); - } - if (string.IsNullOrWhiteSpace(credentials.Password)) - { - return OperationResponse.Failed("New Password cannot be blank"); - } - //if token is valid. - var existingUser = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress); - if (existingUser.Id == default) - { - return OperationResponse.Failed("Unable to locate user"); - } - existingUser.Password = GetHash(credentials.Password); - var result = _userData.SaveUserRecord(existingUser); - //delete token - _tokenData.DeleteToken(existingToken.Id); - if (result) - { - return OperationResponse.Succeed("Password resetted, you will be redirected to login page shortly."); - } else - { - return OperationResponse.Failed(); - } - } - /// - /// Returns an empty user if can't auth against neither root nor db user. - /// - /// credentials from login page - /// - public UserData ValidateUserCredentials(LoginModel credentials) - { - if (UserIsRoot(credentials)) - { - return GetRootUserData(credentials.UserName); - } - else - { - //authenticate via DB. - var result = _userData.GetUserRecordByUserName(credentials.UserName); - if (GetHash(credentials.Password) == result.Password) - { - result.Password = string.Empty; - return result; - } - else - { - return new UserData(); - } - } - } - public UserData ValidateOpenIDUser(LoginModel credentials) - { - //validate for root user - var isRootUser = _configHelper.AuthenticateRootUserOIDC(credentials.EmailAddress); - if (isRootUser) - { - return GetRootUserData(credentials.EmailAddress); - } - - var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress); - if (result.Id != default) - { - result.Password = string.Empty; - return result; - } - else - { - return new UserData(); - } - } - #region "Admin Functions" - public bool MakeUserAdmin(int userId, bool isAdmin) - { - var user = _userData.GetUserRecordById(userId); - if (user == default) - { - return false; - } - user.IsAdmin = isAdmin; - var result = _userData.SaveUserRecord(user); - return result; - } - public List GetAllUsers() - { - var result = _userData.GetUsers(); - return result; - } - public List GetAllTokens() - { - var result = _tokenData.GetTokens(); - return result; - } - public OperationResponse GenerateUserToken(string emailAddress, bool autoNotify) - { - //check if email address already has a token attached to it. - var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress); - if (existingToken.Id != default) - { - if (autoNotify) //re-send email - { - var notificationResult = _mailHelper.NotifyUserForRegistration(emailAddress, existingToken.Body); - if (notificationResult.Success) - { - return OperationResponse.Failed($"There is an existing token tied to {emailAddress}, a new email has been sent out"); - } else - { - return notificationResult; - } - } - return OperationResponse.Failed($"There is an existing token tied to {emailAddress}"); - } - var token = new Token() - { - Body = NewToken(), - EmailAddress = emailAddress - }; - var result = _tokenData.CreateNewToken(token); - if (result && autoNotify) - { - result = _mailHelper.NotifyUserForRegistration(emailAddress, token.Body).Success; - if (!result) - { - return OperationResponse.Failed("Token Generated, but Email failed to send, please check your SMTP settings."); - } - } - if (result) - { - return OperationResponse.Succeed("Token Generated!"); - } - else - { - return OperationResponse.Failed(); - } - } - public bool DeleteUserToken(int tokenId) - { - var result = _tokenData.DeleteToken(tokenId); - return result; - } - public bool DeleteUser(int userId) - { - var result = _userData.DeleteUserRecord(userId); - return result; - } - public OperationResponse ResetUserPassword(LoginModel credentials) - { - //user might have forgotten their password. - var existingUser = _userData.GetUserRecordByUserName(credentials.UserName); - if (existingUser.Id == default) - { - return OperationResponse.Failed("Unable to find user"); - } - var newPassword = Guid.NewGuid().ToString().Substring(0, 8); - existingUser.Password = GetHash(newPassword); - var result = _userData.SaveUserRecord(existingUser); - if (result) - { - return OperationResponse.Succeed(newPassword); - } - else - { - return OperationResponse.Failed(); - } - } - #endregion - #region "Root User" - public bool CreateRootUserCredentials(LoginModel credentials) - { - //check if file exists - if (File.Exists(StaticHelper.UserConfigPath)) - { - var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath); - var existingUserConfig = JsonSerializer.Deserialize(configFileContents); - if (existingUserConfig is not null) - { - //create hashes of the login credentials. - var hashedUserName = GetHash(credentials.UserName); - var hashedPassword = GetHash(credentials.Password); - //copy over settings that are off limits on the settings page. - existingUserConfig.EnableAuth = true; - existingUserConfig.UserNameHash = hashedUserName; - existingUserConfig.UserPasswordHash = hashedPassword; - } - File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig)); - } else - { - var newUserConfig = new UserConfig() - { - EnableAuth = true, - UserNameHash = GetHash(credentials.UserName), - UserPasswordHash = GetHash(credentials.Password) - }; - File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(newUserConfig)); - } - _cache.Remove("userConfig_-1"); - return true; - } - public bool DeleteRootUserCredentials() - { - var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath); - var existingUserConfig = JsonSerializer.Deserialize(configFileContents); - if (existingUserConfig is not null) - { - //copy over settings that are off limits on the settings page. - existingUserConfig.EnableAuth = false; - existingUserConfig.UserNameHash = string.Empty; - existingUserConfig.UserPasswordHash = string.Empty; - } - //clear out the cached config for the root user. - _cache.Remove("userConfig_-1"); - File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig)); - return true; - } - private bool UserIsRoot(LoginModel credentials) - { - var hashedUserName = GetHash(credentials.UserName); - var hashedPassword = GetHash(credentials.Password); - return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword); - } - private UserData GetRootUserData(string username) - { - return new UserData() - { - Id = -1, - UserName = username, - IsAdmin = true, - IsRootUser = true, - EmailAddress = string.Empty - }; - } - #endregion - private static string GetHash(string value) - { - StringBuilder Sb = new StringBuilder(); - - using (var hash = SHA256.Create()) - { - Encoding enc = Encoding.UTF8; - byte[] result = hash.ComputeHash(enc.GetBytes(value)); - - foreach (byte b in result) - Sb.Append(b.ToString("x2")); - } - - return Sb.ToString(); - } - private string NewToken() - { - return Guid.NewGuid().ToString().Substring(0, 8); - } - public KeyValuePair GetPKCEChallengeCode() - { - var verifierCode = Base64UrlEncoder.Encode(Guid.NewGuid().ToString().Replace("-", "")); - var verifierBytes = Encoding.UTF8.GetBytes(verifierCode); - var hashedCode = SHA256.Create().ComputeHash(verifierBytes); - var encodedChallengeCode = Base64UrlEncoder.Encode(hashedCode); - return new KeyValuePair(verifierCode, encodedChallengeCode); - } - public bool GenerateTokenForEmailAddress(string emailAddress, bool isPasswordReset) - { - bool result = false; - //check if there is already a token tied to this email address. - var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress); - if (existingToken.Id == default) - { - //no token, generate one and send. - var token = new Token() - { - Body = NewToken(), - EmailAddress = emailAddress - }; - result = _tokenData.CreateNewToken(token); - if (result) - { - result = isPasswordReset ? _mailHelper.NotifyUserForPasswordReset(emailAddress, token.Body).Success : _mailHelper.NotifyUserForAccountUpdate(emailAddress, token.Body).Success; - } - } else - { - //token exists, send it again. - result = isPasswordReset ? _mailHelper.NotifyUserForPasswordReset(emailAddress, existingToken.Body).Success : _mailHelper.NotifyUserForAccountUpdate(emailAddress, existingToken.Body).Success; - } - return result; - } - } -} diff --git a/Logic/OdometerLogic.cs b/Logic/OdometerLogic.cs deleted file mode 100644 index e51929c..0000000 --- a/Logic/OdometerLogic.cs +++ /dev/null @@ -1,71 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Models; - -namespace MotoVaultPro.Logic -{ - public interface IOdometerLogic - { - int GetLastOdometerRecordMileage(int vehicleId, List odometerRecords); - bool AutoInsertOdometerRecord(OdometerRecord odometer); - List AutoConvertOdometerRecord(List odometerRecords); - } - public class OdometerLogic: IOdometerLogic - { - private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; - private readonly ILogger _logger; - public OdometerLogic(IOdometerRecordDataAccess odometerRecordDataAccess, ILogger logger) - { - _odometerRecordDataAccess = odometerRecordDataAccess; - _logger = logger; - } - public int GetLastOdometerRecordMileage(int vehicleId, List odometerRecords) - { - if (!odometerRecords.Any()) - { - odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - } - if (!odometerRecords.Any()) - { - //no existing odometer records for this vehicle. - return 0; - } - return odometerRecords.Max(x => x.Mileage); - } - public bool AutoInsertOdometerRecord(OdometerRecord odometer) - { - if (odometer.Mileage == default) - { - return false; - } - var lastReportedMileage = GetLastOdometerRecordMileage(odometer.VehicleId, new List()); - odometer.InitialMileage = lastReportedMileage != default ? lastReportedMileage : odometer.Mileage; - - var result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometer); - return result; - } - public List AutoConvertOdometerRecord(List odometerRecords) - { - //perform ordering - odometerRecords = odometerRecords.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList(); - int previousMileage = 0; - for (int i = 0; i < odometerRecords.Count; i++) - { - var currentObject = odometerRecords[i]; - if (previousMileage == default) - { - //first record - currentObject.InitialMileage = currentObject.Mileage; - } - else - { - //subsequent records - currentObject.InitialMileage = previousMileage; - } - //save to db. - _odometerRecordDataAccess.SaveOdometerRecordToVehicle(currentObject); - previousMileage = currentObject.Mileage; - } - return odometerRecords; - } - } -} diff --git a/Logic/UserLogic.cs b/Logic/UserLogic.cs deleted file mode 100644 index 094da0d..0000000 --- a/Logic/UserLogic.cs +++ /dev/null @@ -1,123 +0,0 @@ -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; - -namespace MotoVaultPro.Logic -{ - public interface IUserLogic - { - List GetCollaboratorsForVehicle(int vehicleId); - bool AddUserAccessToVehicle(int userId, int vehicleId); - bool DeleteCollaboratorFromVehicle(int userId, int vehicleId); - OperationResponse AddCollaboratorToVehicle(int vehicleId, string username); - List FilterUserVehicles(List results, int userId); - bool UserCanEditVehicle(int userId, int vehicleId); - bool DeleteAllAccessToVehicle(int vehicleId); - bool DeleteAllAccessToUser(int userId); - } - public class UserLogic: IUserLogic - { - private readonly IUserAccessDataAccess _userAccess; - private readonly IUserRecordDataAccess _userData; - public UserLogic(IUserAccessDataAccess userAccess, - IUserRecordDataAccess userData) { - _userAccess = userAccess; - _userData = userData; - } - public List GetCollaboratorsForVehicle(int vehicleId) - { - var result = _userAccess.GetUserAccessByVehicleId(vehicleId); - var convertedResult = new List(); - //convert useraccess to usercollaborator - foreach(UserAccess userAccess in result) - { - var userCollaborator = new UserCollaborator - { - UserName = _userData.GetUserRecordById(userAccess.Id.UserId).UserName, - UserVehicle = userAccess.Id - }; - convertedResult.Add(userCollaborator); - } - return convertedResult; - } - public OperationResponse AddCollaboratorToVehicle(int vehicleId, string username) - { - //try to find existing user. - var existingUser = _userData.GetUserRecordByUserName(username); - if (existingUser.Id != default) - { - //user exists. - //check if user is already a collaborator - var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(existingUser.Id, vehicleId); - if (userAccess != null) - { - return OperationResponse.Failed("User is already a collaborator"); - } - var result = AddUserAccessToVehicle(existingUser.Id, vehicleId); - if (result) - { - return OperationResponse.Succeed("Collaborator Added"); - } - return OperationResponse.Failed(); - } - return OperationResponse.Failed($"Unable to find user {username} in the system"); - } - public bool DeleteCollaboratorFromVehicle(int userId, int vehicleId) - { - var result = _userAccess.DeleteUserAccess(userId, vehicleId); - return result; - } - public bool AddUserAccessToVehicle(int userId, int vehicleId) - { - if (userId == -1) - { - return true; - } - var userVehicle = new UserVehicle { UserId = userId, VehicleId = vehicleId }; - var userAccess = new UserAccess { Id = userVehicle }; - var result = _userAccess.SaveUserAccess(userAccess); - return result; - } - public List FilterUserVehicles(List results, int userId) - { - //user is root user. - if (userId == -1) - { - return results; - } - var accessibleVehicles = _userAccess.GetUserAccessByUserId(userId); - if (accessibleVehicles.Any()) - { - var vehicleIds = accessibleVehicles.Select(x => x.Id.VehicleId); - return results.Where(x => vehicleIds.Contains(x.Id)).ToList(); - } - else - { - return new List(); - } - } - public bool UserCanEditVehicle(int userId, int vehicleId) - { - if (userId == -1) - { - return true; - } - var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(userId, vehicleId); - if (userAccess != null) - { - return true; - } - return false; - } - public bool DeleteAllAccessToVehicle(int vehicleId) - { - var result = _userAccess.DeleteAllAccessRecordsByVehicleId(vehicleId); - return result; - } - public bool DeleteAllAccessToUser(int userId) - { - var result = _userAccess.DeleteAllAccessRecordsByUserId(userId); - return result; - } - } -} diff --git a/Logic/VehicleLogic.cs b/Logic/VehicleLogic.cs deleted file mode 100644 index e1cada0..0000000 --- a/Logic/VehicleLogic.cs +++ /dev/null @@ -1,421 +0,0 @@ -using MotoVaultPro.Controllers; -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Models; - -namespace MotoVaultPro.Logic -{ - public interface IVehicleLogic - { - VehicleRecords GetVehicleRecords(int vehicleId); - decimal GetVehicleTotalCost(VehicleRecords vehicleRecords); - int GetMaxMileage(int vehicleId); - int GetMaxMileage(VehicleRecords vehicleRecords); - int GetMinMileage(int vehicleId); - int GetMinMileage(VehicleRecords vehicleRecords); - int GetOwnershipDays(string purchaseDate, string soldDate, int year, List serviceRecords, List gasRecords, List upgradeRecords, List odometerRecords, List taxRecords); - bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage); - List GetVehicleInfo(List vehicles); - List GetReminders(List vehicles, bool isCalendar); - List GetPlans(List vehicles, bool excludeDone); - bool UpdateRecurringTaxes(int vehicleId); - void RestoreSupplyRecordsByUsage(List supplyUsage, string usageDescription); - } - public class VehicleLogic: IVehicleLogic - { - private readonly IServiceRecordDataAccess _serviceRecordDataAccess; - private readonly IGasRecordDataAccess _gasRecordDataAccess; - private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; - private readonly ITaxRecordDataAccess _taxRecordDataAccess; - private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; - private readonly IReminderRecordDataAccess _reminderRecordDataAccess; - private readonly IPlanRecordDataAccess _planRecordDataAccess; - private readonly IReminderHelper _reminderHelper; - private readonly IVehicleDataAccess _dataAccess; - private readonly ISupplyRecordDataAccess _supplyRecordDataAccess; - private readonly ILogger _logger; - - public VehicleLogic( - IServiceRecordDataAccess serviceRecordDataAccess, - IGasRecordDataAccess gasRecordDataAccess, - IUpgradeRecordDataAccess upgradeRecordDataAccess, - ITaxRecordDataAccess taxRecordDataAccess, - IOdometerRecordDataAccess odometerRecordDataAccess, - IReminderRecordDataAccess reminderRecordDataAccess, - IPlanRecordDataAccess planRecordDataAccess, - IReminderHelper reminderHelper, - IVehicleDataAccess dataAccess, - ISupplyRecordDataAccess supplyRecordDataAccess, - ILogger logger - ) { - _serviceRecordDataAccess = serviceRecordDataAccess; - _gasRecordDataAccess = gasRecordDataAccess; - _upgradeRecordDataAccess = upgradeRecordDataAccess; - _taxRecordDataAccess = taxRecordDataAccess; - _odometerRecordDataAccess = odometerRecordDataAccess; - _planRecordDataAccess = planRecordDataAccess; - _reminderRecordDataAccess = reminderRecordDataAccess; - _reminderHelper = reminderHelper; - _dataAccess = dataAccess; - _supplyRecordDataAccess = supplyRecordDataAccess; - _logger = logger; - } - public VehicleRecords GetVehicleRecords(int vehicleId) - { - return new VehicleRecords - { - ServiceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId), - GasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId), - TaxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId), - UpgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId), - OdometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId), - }; - } - public decimal GetVehicleTotalCost(VehicleRecords vehicleRecords) - { - var serviceRecordSum = vehicleRecords.ServiceRecords.Sum(x => x.Cost); - var upgradeRecordSum = vehicleRecords.UpgradeRecords.Sum(x => x.Cost); - var taxRecordSum = vehicleRecords.TaxRecords.Sum(x => x.Cost); - var gasRecordSum = vehicleRecords.GasRecords.Sum(x => x.Cost); - return serviceRecordSum + upgradeRecordSum + taxRecordSum + gasRecordSum; - } - public int GetMaxMileage(int vehicleId) - { - var numbersArray = new List(); - var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId); - if (serviceRecords.Any()) - { - numbersArray.Add(serviceRecords.Max(x => x.Mileage)); - } - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId); - if (gasRecords.Any()) - { - numbersArray.Add(gasRecords.Max(x => x.Mileage)); - } - var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId); - if (upgradeRecords.Any()) - { - numbersArray.Add(upgradeRecords.Max(x => x.Mileage)); - } - var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); - if (odometerRecords.Any()) - { - numbersArray.Add(odometerRecords.Max(x => x.Mileage)); - } - return numbersArray.Any() ? numbersArray.Max() : 0; - } - public int GetMaxMileage(VehicleRecords vehicleRecords) - { - var numbersArray = new List(); - if (vehicleRecords.ServiceRecords.Any()) - { - numbersArray.Add(vehicleRecords.ServiceRecords.Max(x => x.Mileage)); - } - if (vehicleRecords.GasRecords.Any()) - { - numbersArray.Add(vehicleRecords.GasRecords.Max(x => x.Mileage)); - } - if (vehicleRecords.UpgradeRecords.Any()) - { - numbersArray.Add(vehicleRecords.UpgradeRecords.Max(x => x.Mileage)); - } - if (vehicleRecords.OdometerRecords.Any()) - { - numbersArray.Add(vehicleRecords.OdometerRecords.Max(x => x.Mileage)); - } - return numbersArray.Any() ? numbersArray.Max() : 0; - } - public int GetMinMileage(int vehicleId) - { - var numbersArray = new List(); - var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default); - if (serviceRecords.Any()) - { - numbersArray.Add(serviceRecords.Min(x => x.Mileage)); - } - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default); - if (gasRecords.Any()) - { - numbersArray.Add(gasRecords.Min(x => x.Mileage)); - } - var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default); - if (upgradeRecords.Any()) - { - numbersArray.Add(upgradeRecords.Min(x => x.Mileage)); - } - var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId).Where(x => x.Mileage != default); - if (odometerRecords.Any()) - { - numbersArray.Add(odometerRecords.Min(x => x.Mileage)); - } - return numbersArray.Any() ? numbersArray.Min() : 0; - } - public int GetMinMileage(VehicleRecords vehicleRecords) - { - var numbersArray = new List(); - var _serviceRecords = vehicleRecords.ServiceRecords.Where(x => x.Mileage != default).ToList(); - if (_serviceRecords.Any()) - { - numbersArray.Add(_serviceRecords.Min(x => x.Mileage)); - } - var _gasRecords = vehicleRecords.GasRecords.Where(x => x.Mileage != default).ToList(); - if (_gasRecords.Any()) - { - numbersArray.Add(_gasRecords.Min(x => x.Mileage)); - } - var _upgradeRecords = vehicleRecords.UpgradeRecords.Where(x => x.Mileage != default).ToList(); - if (_upgradeRecords.Any()) - { - numbersArray.Add(_upgradeRecords.Min(x => x.Mileage)); - } - var _odometerRecords = vehicleRecords.OdometerRecords.Where(x => x.Mileage != default).ToList(); - if (_odometerRecords.Any()) - { - numbersArray.Add(_odometerRecords.Min(x => x.Mileage)); - } - return numbersArray.Any() ? numbersArray.Min() : 0; - } - public int GetOwnershipDays(string purchaseDate, string soldDate, int year, List serviceRecords, List gasRecords, List upgradeRecords, List odometerRecords, List taxRecords) - { - var startDate = DateTime.Now; - var endDate = DateTime.Now; - bool usePurchaseDate = false; - bool useSoldDate = false; - if (!string.IsNullOrWhiteSpace(soldDate) && DateTime.TryParse(soldDate, out DateTime vehicleSoldDate)) - { - if (year == default || year >= vehicleSoldDate.Year) //All Time is selected or the selected year is greater or equal to the year the vehicle is sold - { - endDate = vehicleSoldDate; //cap end date to vehicle sold date. - useSoldDate = true; - } - } - if (!string.IsNullOrWhiteSpace(purchaseDate) && DateTime.TryParse(purchaseDate, out DateTime vehiclePurchaseDate)) - { - if (year == default || year <= vehiclePurchaseDate.Year) //All Time is selected or the selected year is less or equal to the year the vehicle is purchased - { - startDate = vehiclePurchaseDate; //cap start date to vehicle purchase date - usePurchaseDate = true; - } - } - if (year != default) - { - var calendarYearStart = new DateTime(year, 1, 1); - var calendarYearEnd = new DateTime(year + 1, 1, 1); - if (!useSoldDate) - { - endDate = endDate > calendarYearEnd ? calendarYearEnd : endDate; - } - if (!usePurchaseDate) - { - startDate = startDate > calendarYearStart ? calendarYearStart : startDate; - } - var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays); - return timeElapsed; - } - var dateArray = new List() { startDate }; - dateArray.AddRange(serviceRecords.Select(x => x.Date)); - dateArray.AddRange(gasRecords.Select(x => x.Date)); - dateArray.AddRange(upgradeRecords.Select(x => x.Date)); - dateArray.AddRange(odometerRecords.Select(x => x.Date)); - dateArray.AddRange(taxRecords.Select(x => x.Date)); - if (dateArray.Any()) - { - startDate = dateArray.Min(); - var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays); - return timeElapsed; - } else - { - return 1; - } - } - public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage) - { - var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId); - var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now); - return results.Any(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue); - } - - public List GetVehicleInfo(List vehicles) - { - List apiResult = new List(); - - foreach (Vehicle vehicle in vehicles) - { - var currentMileage = GetMaxMileage(vehicle.Id); - var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id); - var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now); - - var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id); - var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id); - var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id); - var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id); - var planRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id); - - var resultToAdd = new VehicleInfo() - { - VehicleData = vehicle, - LastReportedOdometer = currentMileage, - ServiceRecordCount = serviceRecords.Count(), - ServiceRecordCost = serviceRecords.Sum(x => x.Cost), - UpgradeRecordCount = upgradeRecords.Count(), - UpgradeRecordCost = upgradeRecords.Sum(x => x.Cost), - GasRecordCount = gasRecords.Count(), - GasRecordCost = gasRecords.Sum(x => x.Cost), - TaxRecordCount = taxRecords.Count(), - TaxRecordCost = taxRecords.Sum(x => x.Cost), - VeryUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.VeryUrgent), - PastDueReminderCount = results.Count(x => x.Urgency == ReminderUrgency.PastDue), - UrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.Urgent), - NotUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.NotUrgent), - PlanRecordBackLogCount = planRecords.Count(x => x.Progress == PlanProgress.Backlog), - PlanRecordInProgressCount = planRecords.Count(x => x.Progress == PlanProgress.InProgress), - PlanRecordTestingCount = planRecords.Count(x => x.Progress == PlanProgress.Testing), - PlanRecordDoneCount = planRecords.Count(x => x.Progress == PlanProgress.Done) - }; - //set next reminder - if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date)) - { - resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderAPIExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), UserMetric = x.UserMetric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), DueDays = x.DueDays.ToString(), DueDistance = x.DueMileage.ToString(), Tags = string.Join(' ', x.Tags) }).First(); - } - else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage)) - { - resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderAPIExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), UserMetric = x.UserMetric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), DueDays = x.DueDays.ToString(), DueDistance = x.DueMileage.ToString(), Tags = string.Join(' ', x.Tags) }).First(); - } - apiResult.Add(resultToAdd); - } - return apiResult; - } - public List GetReminders(List vehicles, bool isCalendar) - { - List reminders = new List(); - foreach (Vehicle vehicle in vehicles) - { - var vehicleReminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id); - if (isCalendar) - { - vehicleReminders.RemoveAll(x => x.Metric == ReminderMetric.Odometer); - //we don't care about mileages so we can basically fake the current vehicle mileage. - } - if (vehicleReminders.Any()) - { - var vehicleMileage = isCalendar ? 0 : GetMaxMileage(vehicle.Id); - var reminderUrgency = _reminderHelper.GetReminderRecordViewModels(vehicleReminders, vehicleMileage, DateTime.Now); - reminderUrgency = reminderUrgency.Select(x => new ReminderRecordViewModel { Id = x.Id, Metric = x.Metric, Date = x.Date, Notes = x.Notes, Mileage = x.Mileage, Urgency = x.Urgency, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)} - {x.Description}" }).ToList(); - reminders.AddRange(reminderUrgency); - } - } - return reminders.OrderByDescending(x=>x.Urgency).ToList(); - } - public List GetPlans(List vehicles, bool excludeDone) - { - List plans = new List(); - foreach (Vehicle vehicle in vehicles) - { - var vehiclePlans = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id); - if (excludeDone) - { - vehiclePlans.RemoveAll(x => x.Progress == PlanProgress.Done); - } - if (vehiclePlans.Any()) - { - var convertedPlans = vehiclePlans.Select(x => new PlanRecord { ImportMode = x.ImportMode, Priority = x.Priority, Progress = x.Progress, Notes = x.Notes, RequisitionHistory = x.RequisitionHistory, Description = $"{vehicle.Year} {vehicle.Make} {vehicle.Model} #{StaticHelper.GetVehicleIdentifier(vehicle)} - {x.Description}" }); - plans.AddRange(convertedPlans); - } - } - return plans.OrderBy(x => x.Priority).ThenBy(x=>x.Progress).ToList(); - } - public bool UpdateRecurringTaxes(int vehicleId) - { - var vehicleData = _dataAccess.GetVehicleById(vehicleId); - if (!string.IsNullOrWhiteSpace(vehicleData.SoldDate)) - { - return false; - } - bool RecurringTaxIsOutdated(TaxRecord taxRecord) - { - var monthInterval = taxRecord.RecurringInterval != ReminderMonthInterval.Other ? (int)taxRecord.RecurringInterval : taxRecord.CustomMonthInterval; - bool addDays = taxRecord.RecurringInterval == ReminderMonthInterval.Other && taxRecord.CustomMonthIntervalUnit == ReminderIntervalUnit.Days; - return addDays ? DateTime.Now > taxRecord.Date.AddDays(monthInterval) : DateTime.Now > taxRecord.Date.AddMonths(monthInterval); - } - var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId); - var outdatedRecurringFees = result.Where(x => x.IsRecurring && RecurringTaxIsOutdated(x)); - if (outdatedRecurringFees.Any()) - { - var success = false; - foreach (TaxRecord recurringFee in outdatedRecurringFees) - { - var monthInterval = recurringFee.RecurringInterval != ReminderMonthInterval.Other ? (int)recurringFee.RecurringInterval : recurringFee.CustomMonthInterval; - bool isOutdated = true; - bool addDays = recurringFee.RecurringInterval == ReminderMonthInterval.Other && recurringFee.CustomMonthIntervalUnit == ReminderIntervalUnit.Days; - //update the original outdated tax record - recurringFee.IsRecurring = false; - _taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee); - //month multiplier for severely outdated monthly tax records. - int monthMultiplier = 1; - var originalDate = recurringFee.Date; - while (isOutdated) - { - try - { - var nextDate = addDays ? originalDate.AddDays(monthInterval * monthMultiplier) : originalDate.AddMonths(monthInterval * monthMultiplier); - monthMultiplier++; - var nextnextDate = addDays ? originalDate.AddDays(monthInterval * monthMultiplier) : originalDate.AddMonths(monthInterval * monthMultiplier); - recurringFee.Date = nextDate; - recurringFee.Id = default; //new record - recurringFee.IsRecurring = DateTime.Now <= nextnextDate; - _taxRecordDataAccess.SaveTaxRecordToVehicle(recurringFee); - isOutdated = !recurringFee.IsRecurring; - success = true; - } - catch (Exception) - { - isOutdated = false; //break out of loop if something broke. - success = false; - } - } - } - return success; - } - return false; //no outdated recurring tax records. - } - public void RestoreSupplyRecordsByUsage(List supplyUsage, string usageDescription) - { - foreach (SupplyUsageHistory supply in supplyUsage) - { - try - { - if (supply.Id == default) - { - continue; //no id, skip current supply. - } - var result = _supplyRecordDataAccess.GetSupplyRecordById(supply.Id); - if (result != null && result.Id != default) - { - //supply exists, re-add the quantity and cost - result.Quantity += supply.Quantity; - result.Cost += supply.Cost; - var requisitionRecord = new SupplyUsageHistory - { - Id = supply.Id, - Date = DateTime.Now.Date, - Description = $"Restored from {usageDescription}", - Quantity = supply.Quantity, - Cost = supply.Cost - }; - result.RequisitionHistory.Add(requisitionRecord); - //save - _supplyRecordDataAccess.SaveSupplyRecordToVehicle(result); - } - else - { - _logger.LogError($"Unable to find supply with id {supply.Id}"); - } - } - catch (Exception ex) - { - _logger.LogError($"Error restoring supply with id {supply.Id} : {ex.Message}"); - } - } - } - } -} diff --git a/MOTOVAULTPRO.md b/MOTOVAULTPRO.md deleted file mode 100644 index b4fc5b6..0000000 --- a/MOTOVAULTPRO.md +++ /dev/null @@ -1,138 +0,0 @@ -Technology Stack - -Frontend: React 18 with TypeScript -Backend: Node.js 20 + Express + TypeScript -Database: PostgreSQL 15 -Cache: Redis 7 -File Storage: MinIO (S3-compatible) -Authentication: Auth0 -Container Runtime: Docker -Orchestration: Kubernetes (Nutanix NKP) -GitOps: FluxCD + Kustomize -APIs: NHTSA vPIC (free), Google Maps Platform - -Project Structure -motovaultpro/ -├── docker-compose.yml # Local development environment -├── .env.example # Environment variables template -├── README.md # Project documentation -├── Makefile # Common commands -│ -├── backend/ -│ ├── package.json -│ ├── tsconfig.json -│ ├── Dockerfile -│ ├── src/ -│ │ ├── index.ts # Express server entry point -│ │ ├── app.ts # Express app configuration -│ │ ├── config/ -│ │ │ ├── database.ts # PostgreSQL connection -│ │ │ ├── redis.ts # Redis connection -│ │ │ ├── auth0.ts # Auth0 middleware -│ │ │ └── minio.ts # MinIO client setup -│ │ ├── routes/ -│ │ │ ├── index.ts # Route aggregator -│ │ │ ├── auth.routes.ts # Authentication endpoints -│ │ │ ├── vehicles.routes.ts # Vehicle CRUD + VIN decode -│ │ │ ├── fuel.routes.ts # Fuel log endpoints -│ │ │ ├── maintenance.routes.ts -│ │ │ └── stations.routes.ts # Google Maps integration -│ │ ├── controllers/ -│ │ │ ├── vehicles.controller.ts -│ │ │ ├── fuel.controller.ts -│ │ │ ├── maintenance.controller.ts -│ │ │ └── stations.controller.ts -│ │ ├── services/ -│ │ │ ├── vpic.service.ts # NHTSA vPIC integration -│ │ │ ├── googlemaps.service.ts -│ │ │ ├── cache.service.ts # Redis caching logic -│ │ │ └── storage.service.ts # MinIO operations -│ │ ├── models/ -│ │ │ ├── user.model.ts -│ │ │ ├── vehicle.model.ts -│ │ │ ├── fuel-log.model.ts -│ │ │ └── maintenance-log.model.ts -│ │ ├── middleware/ -│ │ │ ├── auth.middleware.ts -│ │ │ ├── validation.middleware.ts -│ │ │ └── error.middleware.ts -│ │ ├── utils/ -│ │ │ ├── logger.ts # Structured logging -│ │ │ └── validators.ts # Input validation schemas -│ │ └── migrations/ -│ │ ├── 001_initial_schema.sql -│ │ ├── 002_vin_cache.sql -│ │ └── 003_stored_procedures.sql -│ -├── frontend/ -│ ├── package.json -│ ├── tsconfig.json -│ ├── Dockerfile -│ ├── vite.config.ts -│ ├── index.html -│ ├── src/ -│ │ ├── main.tsx -│ │ ├── App.tsx -│ │ ├── api/ -│ │ │ ├── client.ts # Axios instance -│ │ │ ├── vehicles.api.ts -│ │ │ ├── fuel.api.ts -│ │ │ └── stations.api.ts -│ │ ├── components/ -│ │ │ ├── Layout/ -│ │ │ ├── VehicleForm/ -│ │ │ ├── VINDecoder/ -│ │ │ ├── FuelLogForm/ -│ │ │ ├── StationPicker/ # Google Maps component -│ │ │ └── common/ -│ │ ├── pages/ -│ │ │ ├── Dashboard.tsx -│ │ │ ├── Vehicles.tsx -│ │ │ ├── FuelLogs.tsx -│ │ │ └── Maintenance.tsx -│ │ ├── hooks/ -│ │ │ ├── useAuth.ts -│ │ │ ├── useVehicles.ts -│ │ │ └── useGoogleMaps.ts -│ │ ├── store/ -│ │ │ └── index.ts # Zustand or Redux -│ │ └── types/ -│ │ └── index.ts -│ -├── k8s/ # GitOps configurations -│ ├── base/ -│ │ ├── namespace.yaml -│ │ ├── backend/ -│ │ │ ├── deployment.yaml -│ │ │ ├── service.yaml -│ │ │ └── configmap.yaml -│ │ ├── frontend/ -│ │ │ ├── deployment.yaml -│ │ │ └── service.yaml -│ │ ├── postgres/ -│ │ │ ├── statefulset.yaml -│ │ │ ├── service.yaml -│ │ │ ├── pvc.yaml -│ │ │ └── init-scripts.yaml -│ │ ├── redis/ -│ │ │ ├── statefulset.yaml -│ │ │ └── service.yaml -│ │ ├── minio/ -│ │ │ ├── statefulset.yaml -│ │ │ ├── service.yaml -│ │ │ └── pvc.yaml -│ │ └── ingress.yaml -│ │ -│ └── overlays/ -│ ├── development/ -│ │ ├── kustomization.yaml -│ │ └── patches/ -│ └── production/ -│ ├── kustomization.yaml -│ ├── sealed-secrets/ -│ └── patches/ -│ -└── .github/ - └── workflows/ - ├── ci.yml # Test and build - └── cd.yml # Deploy via GitOps \ No newline at end of file diff --git a/MapProfile/ImportMappers.cs b/MapProfile/ImportMappers.cs deleted file mode 100644 index bf20217..0000000 --- a/MapProfile/ImportMappers.cs +++ /dev/null @@ -1,47 +0,0 @@ -using MotoVaultPro.Models; -using CsvHelper.Configuration; - -namespace MotoVaultPro.MapProfile -{ - public class ImportMapper: ClassMap - { - public ImportMapper() - { - Map(m => m.Date).Name(["date", "fuelup_date"]); - Map(m => m.Day).Name(["day"]); - Map(m => m.Month).Name(["month"]); - Map(m => m.Year).Name(["year"]); - Map(m => m.DateCreated).Name(["datecreated"]); - Map(m => m.DateModified).Name(["datemodified"]); - Map(m => m.InitialOdometer).Name(["initialodometer"]); - Map(m => m.Odometer).Name(["odometer", "odo"]); - Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fuelconsumed", "qty"]); - Map(m => m.Cost).Name(["cost", "total cost", "totalcost", "total price"]); - Map(m => m.Notes).Name("notes", "note"); - Map(m => m.Price).Name(["price"]); - Map(m => m.PartialFuelUp).Name(["partial_fuelup", "partial tank", "partial_fill"]); - Map(m => m.IsFillToFull).Name(["isfilltofull", "filled up"]); - Map(m => m.Description).Name(["description"]); - Map(m => m.MissedFuelUp).Name(["missed_fuelup", "missedfuelup", "missed fill up", "missed_fill"]); - Map(m => m.PartSupplier).Name(["partsupplier"]); - Map(m => m.PartQuantity).Name(["partquantity"]); - Map(m => m.PartNumber).Name(["partnumber"]); - Map(m => m.Progress).Name(["progress"]); - Map(m => m.Type).Name(["type"]); - Map(m => m.Priority).Name(["priority"]); - Map(m => m.Tags).Name(["tags"]); - Map(m => m.ExtraFields).Convert(row => - { - var attributes = new Dictionary(); - foreach (var header in row.Row.HeaderRecord) - { - if (header.ToLower().StartsWith("extrafield_")) - { - attributes.Add(header.Substring(11), row.Row.GetField(header)); - } - } - return attributes; - }); - } - } -} diff --git a/Middleware/Authen.cs b/Middleware/Authen.cs deleted file mode 100644 index 447d9cb..0000000 --- a/Middleware/Authen.cs +++ /dev/null @@ -1,186 +0,0 @@ -using MotoVaultPro.Logic; -using MotoVaultPro.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Options; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using System.Text.Json; - -namespace MotoVaultPro.Middleware -{ - public class Authen : AuthenticationHandler - { - private IHttpContextAccessor _httpContext; - private IDataProtector _dataProtector; - private ILoginLogic _loginLogic; - private bool enableAuth; - public Authen( - IOptionsMonitor options, - UrlEncoder encoder, - ILoggerFactory logger, - IConfiguration configuration, - ILoginLogic loginLogic, - IDataProtectionProvider securityProvider, - IHttpContextAccessor httpContext) : base(options, logger, encoder) - { - _httpContext = httpContext; - _dataProtector = securityProvider.CreateProtector("login"); - _loginLogic = loginLogic; - enableAuth = bool.Parse(configuration["EnableAuth"] ?? "false"); - } - protected override async Task HandleAuthenticateAsync() - { - if (!enableAuth) - { - //generate a fake user ticket to go with it lol. - var appIdentity = new ClaimsIdentity("Custom"); - var userIdentity = new List - { - new(ClaimTypes.Name, "admin"), - new(ClaimTypes.NameIdentifier, "-1"), - new(ClaimTypes.Role, nameof(UserData.IsRootUser)) - }; - appIdentity.AddClaims(userIdentity); - AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name); - return AuthenticateResult.Success(ticket); - } - else - { - //auth is enabled by user, we will have to authenticate the user via a ticket retrieved from the auth cookie. - var access_token = _httpContext.HttpContext.Request.Cookies["ACCESS_TOKEN"]; - //auth using Basic Auth for API. - var request_header = _httpContext.HttpContext.Request.Headers["Authorization"]; - if (string.IsNullOrWhiteSpace(access_token) && string.IsNullOrWhiteSpace(request_header)) - { - return AuthenticateResult.Fail("Cookie is invalid or does not exist."); - } - else if (!string.IsNullOrWhiteSpace(request_header)) - { - var cleanedHeader = request_header.ToString().Replace("Basic ", "").Trim(); - byte[] data = Convert.FromBase64String(cleanedHeader); - string decodedString = Encoding.UTF8.GetString(data); - var splitString = decodedString.Split(":"); - if (splitString.Count() != 2) - { - return AuthenticateResult.Fail("Invalid credentials"); - } - else - { - var userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] }); - if (userData.Id != default) - { - var appIdentity = new ClaimsIdentity("Custom"); - var userIdentity = new List - { - new(ClaimTypes.Name, splitString[0]), - new(ClaimTypes.NameIdentifier, userData.Id.ToString()), - new(ClaimTypes.Email, userData.EmailAddress), - new(ClaimTypes.Role, "APIAuth") - }; - if (userData.IsAdmin) - { - userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin))); - } - if (userData.IsRootUser) - { - userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser))); - } - appIdentity.AddClaims(userIdentity); - AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name); - return AuthenticateResult.Success(ticket); - } - } - } - else if (!string.IsNullOrWhiteSpace(access_token)) - { - try - { - //decrypt the access token. - var decryptedCookie = _dataProtector.Unprotect(access_token); - AuthCookie authCookie = JsonSerializer.Deserialize(decryptedCookie); - if (authCookie != null) - { - //validate auth cookie - if (authCookie.ExpiresOn < DateTime.Now) - { - //if cookie is expired - return AuthenticateResult.Fail("Expired credentials"); - } - else if (authCookie.UserData is null || authCookie.UserData.Id == default || string.IsNullOrWhiteSpace(authCookie.UserData.UserName)) - { - return AuthenticateResult.Fail("Corrupted credentials"); - } - else - { - if (!_loginLogic.CheckIfUserIsValid(authCookie.UserData.Id)) - { - return AuthenticateResult.Fail("Cookie points to non-existant user."); - } - //validate if user is still valid - var appIdentity = new ClaimsIdentity("Custom"); - var userIdentity = new List - { - new(ClaimTypes.Name, authCookie.UserData.UserName), - new(ClaimTypes.NameIdentifier, authCookie.UserData.Id.ToString()), - new(ClaimTypes.Email, authCookie.UserData.EmailAddress), - new(ClaimTypes.Role, "CookieAuth") - }; - if (authCookie.UserData.IsAdmin) - { - userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin))); - } - if (authCookie.UserData.IsRootUser) - { - userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser))); - } - appIdentity.AddClaims(userIdentity); - AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name); - return AuthenticateResult.Success(ticket); - } - } - } - catch (Exception ex) - { - return AuthenticateResult.Fail("Corrupted credentials"); - } - } - return AuthenticateResult.Fail("Invalid credentials"); - } - } - protected override Task HandleChallengeAsync(AuthenticationProperties properties) - { - if (Request.RouteValues.TryGetValue("controller", out object value)) - { - if (value.ToString().ToLower() == "api") - { - Response.StatusCode = 401; - Response.Headers.Append("WWW-Authenticate", "Basic"); - return Task.CompletedTask; - } - } - if (Request.Path.Value == "/Vehicle/Index" && Request.QueryString.HasValue) - { - Response.Redirect($"/Login/Index?redirectURL={Request.Path.Value}{Request.QueryString.Value}"); - } else - { - Response.Redirect("/Login/Index"); - } - return Task.CompletedTask; - } - protected override Task HandleForbiddenAsync(AuthenticationProperties properties) - { - if (Request.RouteValues.TryGetValue("controller", out object value)) - { - if (value.ToString().ToLower() == "api") - { - Response.StatusCode = 403; - return Task.CompletedTask; - } - } - Response.Redirect("/Error/Unauthorized"); - return Task.CompletedTask; - } - } -} diff --git a/Models/API/MethodParameter.cs b/Models/API/MethodParameter.cs deleted file mode 100644 index 1440f5c..0000000 --- a/Models/API/MethodParameter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MotoVaultPro.Models -{ - public class MethodParameter - { - public int Id { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string StartDate { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string EndDate { get; set; } - public string Tags { get; set; } - public bool UseMPG { get; set; } - public bool UseUKMPG { get; set; } - } -} diff --git a/Models/API/ReleaseVersion.cs b/Models/API/ReleaseVersion.cs deleted file mode 100644 index 9e3ba90..0000000 --- a/Models/API/ReleaseVersion.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MotoVaultPro.Models -{ - /// - /// For deserializing GitHub response for latest version - /// - public class ReleaseResponse - { - public string tag_name { get; set; } - } - /// - /// For returning the version numbers via API. - /// - public class ReleaseVersion - { - public string CurrentVersion { get; set; } - public string LatestVersion { get; set; } - } -} diff --git a/Models/API/TypeConverter.cs b/Models/API/TypeConverter.cs deleted file mode 100644 index 03d9229..0000000 --- a/Models/API/TypeConverter.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MotoVaultPro.Models -{ - public class DummyType - { - - } - class InvariantConverter : JsonConverter - { - public override void Write(Utf8JsonWriter writer, DummyType value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - public override DummyType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - } - class FromDateOptional: JsonConverter - { - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var tokenType = reader.TokenType; - if (tokenType == JsonTokenType.String) - { - return reader.GetString(); - } - else if (tokenType == JsonTokenType.Number) - { - if (reader.TryGetInt64(out long intInput)) - { - return DateTimeOffset.FromUnixTimeSeconds(intInput).Date.ToShortDateString(); - } - } - return reader.GetString(); - } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - if (options.Converters.Any(x => x.Type == typeof(DummyType))) - { - writer.WriteStringValue(DateTime.Parse(value).ToString("yyyy-MM-dd")); - } - else - { - writer.WriteStringValue(value); - } - } - } - class FromDecimalOptional : JsonConverter - { - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var tokenType = reader.TokenType; - if (tokenType == JsonTokenType.String) - { - return reader.GetString(); - } - else if (tokenType == JsonTokenType.Number) { - if (reader.TryGetDecimal(out decimal decimalInput)) - { - return decimalInput.ToString(); - } - } - return reader.GetString(); - } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - if (options.Converters.Any(x=>x.Type == typeof(DummyType))) - { - writer.WriteNumberValue(decimal.Parse(value)); - } else - { - writer.WriteStringValue(value); - } - } - } - class FromIntOptional : JsonConverter - { - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var tokenType = reader.TokenType; - if (tokenType == JsonTokenType.String) - { - return reader.GetString(); - } - else if (tokenType == JsonTokenType.Number) - { - if (reader.TryGetInt32(out int intInput)) - { - return intInput.ToString(); - } - } - return reader.GetString(); - } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - if (options.Converters.Any(x => x.Type == typeof(DummyType))) - { - writer.WriteNumberValue(int.Parse(value)); - } - else - { - writer.WriteStringValue(value); - } - } - } - class FromBoolOptional : JsonConverter - { - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var tokenType = reader.TokenType; - switch (tokenType) - { - case JsonTokenType.String: - return reader.GetString(); - case JsonTokenType.True: - return "True"; - case JsonTokenType.False: - return "False"; - default: - return reader.GetString(); - } - } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - if (options.Converters.Any(x => x.Type == typeof(DummyType))) - { - writer.WriteBooleanValue(bool.Parse(value)); - } - else - { - writer.WriteStringValue(value); - } - } - } -} diff --git a/Models/API/VehicleInfo.cs b/Models/API/VehicleInfo.cs deleted file mode 100644 index 5e5f56d..0000000 --- a/Models/API/VehicleInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class VehicleInfo - { - public Vehicle VehicleData { get; set; } = new Vehicle(); - public int VeryUrgentReminderCount { get; set; } - public int UrgentReminderCount { get; set;} - public int NotUrgentReminderCount { get; set; } - public int PastDueReminderCount { get; set; } - public ReminderAPIExportModel NextReminder { get; set; } - public int ServiceRecordCount { get; set; } - public decimal ServiceRecordCost { get; set; } - public int UpgradeRecordCount { get; set; } - public decimal UpgradeRecordCost { get; set; } - public int TaxRecordCount { get; set; } - public decimal TaxRecordCost { get; set; } - public int GasRecordCount { get; set; } - public decimal GasRecordCost { get; set; } - public int LastReportedOdometer { get; set; } - public int PlanRecordBackLogCount { get; set; } - public int PlanRecordInProgressCount { get; set; } - public int PlanRecordTestingCount { get; set; } - public int PlanRecordDoneCount { get; set; } - } -} diff --git a/Models/Admin/AdminViewModel.cs b/Models/Admin/AdminViewModel.cs deleted file mode 100644 index 87edb19..0000000 --- a/Models/Admin/AdminViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class AdminViewModel - { - public List Users { get; set; } - public List Tokens { get; set; } - } -} diff --git a/Models/Configuration/MailConfig.cs b/Models/Configuration/MailConfig.cs deleted file mode 100644 index 4f8c261..0000000 --- a/Models/Configuration/MailConfig.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class MailConfig - { - public string EmailServer { get; set; } - public string EmailFrom { get; set; } - public int Port { get; set; } - public string Username { get; set; } - public string Password { get; set; } - } -} diff --git a/Models/ErrorViewModel.cs b/Models/ErrorViewModel.cs deleted file mode 100644 index 2a8bf0c..0000000 --- a/Models/ErrorViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ErrorViewModel - { - public string? RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} diff --git a/Models/GasRecord/GasRecord.cs b/Models/GasRecord/GasRecord.cs deleted file mode 100644 index d01556c..0000000 --- a/Models/GasRecord/GasRecord.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GasRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - public DateTime Date { get; set; } - /// - /// American moment - /// - public int Mileage { get; set; } - /// - /// Wtf is a kilometer? - /// - public decimal Gallons { get; set; } - public decimal Cost { get; set; } - public bool IsFillToFull { get; set; } = true; - public bool MissedFuelUp { get; set; } = false; - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - } -} diff --git a/Models/GasRecord/GasRecordEditModel.cs b/Models/GasRecord/GasRecordEditModel.cs deleted file mode 100644 index 7086e19..0000000 --- a/Models/GasRecord/GasRecordEditModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GasRecordEditModel - { - public List RecordIds { get; set; } = new List(); - public GasRecord EditRecord { get; set; } = new GasRecord(); - } -} diff --git a/Models/GasRecord/GasRecordInput.cs b/Models/GasRecord/GasRecordInput.cs deleted file mode 100644 index aeae52a..0000000 --- a/Models/GasRecord/GasRecordInput.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GasRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public string Date { get; set; } = DateTime.Now.ToShortDateString(); - /// - /// American moment - /// - public int Mileage { get; set; } - /// - /// Wtf is a kilometer? - /// - public decimal Gallons { get; set; } - public decimal Cost { get; set; } - public bool IsFillToFull { get; set; } = true; - public bool MissedFuelUp { get; set; } = false; - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public GasRecord ToGasRecord() { return new GasRecord { - Id = Id, - Cost = Cost, - Date = DateTime.Parse(Date), - Gallons = Gallons, - Mileage = Mileage, - VehicleId = VehicleId, - Files = Files, - IsFillToFull = IsFillToFull, - MissedFuelUp = MissedFuelUp, - Notes = Notes, - Tags = Tags, - ExtraFields = ExtraFields - }; } - } -} diff --git a/Models/GasRecord/GasRecordInputContainer.cs b/Models/GasRecord/GasRecordInputContainer.cs deleted file mode 100644 index 3092bbc..0000000 --- a/Models/GasRecord/GasRecordInputContainer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GasRecordInputContainer - { - public bool UseKwh { get; set; } - public bool UseHours { get; set; } - public GasRecordInput GasRecord { get; set; } - } -} diff --git a/Models/GasRecord/GasRecordViewModel.cs b/Models/GasRecord/GasRecordViewModel.cs deleted file mode 100644 index ccd5802..0000000 --- a/Models/GasRecord/GasRecordViewModel.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GasRecordViewModel - { - public int Id { get; set; } - public int VehicleId { get; set; } - public int MonthId { get; set; } - public string Date { get; set; } - /// - /// American moment - /// - public int Mileage { get; set; } - /// - /// Wtf is a kilometer? - /// - public decimal Gallons { get; set; } - public decimal Cost { get; set; } - public int DeltaMileage { get; set; } - public decimal MilesPerGallon { get; set; } - public decimal CostPerGallon { get; set; } - public bool IsFillToFull { get; set; } - public bool MissedFuelUp { get; set; } - public string Notes { get; set; } - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public List Files { get; set; } = new List(); - public bool IncludeInAverage { get { return MilesPerGallon > 0 || (!IsFillToFull && !MissedFuelUp) || (Mileage == default && !MissedFuelUp); } } - } -} diff --git a/Models/GasRecord/GasRecordViewModelContainer.cs b/Models/GasRecord/GasRecordViewModelContainer.cs deleted file mode 100644 index cbed9a8..0000000 --- a/Models/GasRecord/GasRecordViewModelContainer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GasRecordViewModelContainer - { - public bool UseKwh { get; set; } - public bool UseHours { get; set; } - public List GasRecords { get; set; } = new List(); - } -} diff --git a/Models/GenericRecordEditModel.cs b/Models/GenericRecordEditModel.cs deleted file mode 100644 index a33c5d6..0000000 --- a/Models/GenericRecordEditModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GenericRecordEditModel - { - public ImportMode DataType { get; set; } - public List RecordIds { get; set; } = new List(); - public GenericRecord EditRecord { get; set; } = new GenericRecord(); - } -} diff --git a/Models/Kiosk/KioskViewModel.cs b/Models/Kiosk/KioskViewModel.cs deleted file mode 100644 index e15cd01..0000000 --- a/Models/Kiosk/KioskViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class KioskViewModel - { - /// - /// List of vehicle ids to exclude from Kiosk Dashboard - /// - public List Exclusions { get; set; } = new List(); - /// - /// Whether to retrieve data for vehicle, plans, or reminder view. - /// - public KioskMode KioskMode { get; set; } = KioskMode.Vehicle; - } -} diff --git a/Models/Login/AuthCookie.cs b/Models/Login/AuthCookie.cs deleted file mode 100644 index 5cb318f..0000000 --- a/Models/Login/AuthCookie.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class AuthCookie - { - public UserData UserData { get; set; } - public DateTime ExpiresOn { get; set; } - } -} diff --git a/Models/Login/LoginModel.cs b/Models/Login/LoginModel.cs deleted file mode 100644 index 37110df..0000000 --- a/Models/Login/LoginModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class LoginModel - { - public string UserName { get; set; } - public string Password { get; set; } - public string EmailAddress { get; set; } - public string Token { get; set; } - public bool IsPersistent { get; set; } = false; - } -} diff --git a/Models/Login/Token.cs b/Models/Login/Token.cs deleted file mode 100644 index cef8779..0000000 --- a/Models/Login/Token.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class Token - { - public int Id { get; set; } - public string Body { get; set; } - public string EmailAddress { get; set; } - } -} diff --git a/Models/Note/Note.cs b/Models/Note/Note.cs deleted file mode 100644 index 3d1d10a..0000000 --- a/Models/Note/Note.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class Note - { - public int Id { get; set; } - public int VehicleId { get; set; } - public string Description { get; set; } - public string NoteText { get; set; } - public bool Pinned { get; set; } - public List Tags { get; set; } = new List(); - public List Files { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - } -} diff --git a/Models/OIDC/OpenIDConfig.cs b/Models/OIDC/OpenIDConfig.cs deleted file mode 100644 index 7520715..0000000 --- a/Models/OIDC/OpenIDConfig.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class OpenIDConfig - { - public string Name { get; set; } - public string ClientId { get; set; } - public string ClientSecret { get; set; } - public string AuthURL { get; set; } - public string TokenURL { get; set; } - public string RedirectURL { get; set; } - public string Scope { get; set; } = "openid email"; - public string State { get; set; } - public string CodeChallenge { get; set; } - public bool ValidateState { get; set; } = false; - public bool DisableRegularLogin { get; set; } = false; - public bool UsePKCE { get; set; } = false; - public string LogOutURL { get; set; } = ""; - public string UserInfoURL { get; set; } = ""; - public string RemoteAuthURL { get { - var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}"; - if (UsePKCE) - { - redirectUrl += $"&code_challenge={CodeChallenge}&code_challenge_method=S256"; - } - return redirectUrl; - } } - } -} diff --git a/Models/OIDC/OpenIDResult.cs b/Models/OIDC/OpenIDResult.cs deleted file mode 100644 index 9f662d8..0000000 --- a/Models/OIDC/OpenIDResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class OpenIDResult - { - public string id_token { get; set; } - public string access_token { get; set; } - } -} diff --git a/Models/OIDC/OpenIDUserInfo.cs b/Models/OIDC/OpenIDUserInfo.cs deleted file mode 100644 index 0f95e0f..0000000 --- a/Models/OIDC/OpenIDUserInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class OpenIDUserInfo - { - public string email { get; set; } = ""; - } -} diff --git a/Models/OdometerRecord/OdometerRecord.cs b/Models/OdometerRecord/OdometerRecord.cs deleted file mode 100644 index 5376b78..0000000 --- a/Models/OdometerRecord/OdometerRecord.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class OdometerRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - public DateTime Date { get; set; } - public int InitialMileage { get; set; } - public int Mileage { get; set; } - public int DistanceTraveled { get { return Mileage - InitialMileage; } } - public string Notes { get; set; } - public List Tags { get; set; } = new List(); - public List Files { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - } -} diff --git a/Models/OdometerRecord/OdometerRecordEditModel.cs b/Models/OdometerRecord/OdometerRecordEditModel.cs deleted file mode 100644 index 89fe0f6..0000000 --- a/Models/OdometerRecord/OdometerRecordEditModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class OdometerRecordEditModel - { - public List RecordIds { get; set; } = new List(); - public OdometerRecord EditRecord { get; set; } = new OdometerRecord(); - } -} diff --git a/Models/OdometerRecord/OdometerRecordInput.cs b/Models/OdometerRecord/OdometerRecordInput.cs deleted file mode 100644 index 250789a..0000000 --- a/Models/OdometerRecord/OdometerRecordInput.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class OdometerRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public string Date { get; set; } = DateTime.Now.ToShortDateString(); - public int InitialMileage { get; set; } - public int Mileage { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files, Tags = Tags, ExtraFields = ExtraFields, InitialMileage = InitialMileage }; } - } -} diff --git a/Models/OperationResponse.cs b/Models/OperationResponse.cs deleted file mode 100644 index 33c4001..0000000 --- a/Models/OperationResponse.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MotoVaultPro.Helper; - -namespace MotoVaultPro.Models -{ - public class OperationResponseBase - { - public bool Success { get; set; } - public string Message { get; set; } - } - public class OperationResponse: OperationResponseBase - { - public static OperationResponse Succeed(string message = "") - { - return new OperationResponse { Success = true, Message = message }; - } - public static OperationResponse Failed(string message = "") - { - if (string.IsNullOrWhiteSpace(message)) - { - message = StaticHelper.GenericErrorMessage; - } - return new OperationResponse { Success = false, Message = message}; - } - public static OperationResponse Conditional(bool result, string successMessage = "", string errorMessage = "") - { - if (string.IsNullOrWhiteSpace(errorMessage)) - { - errorMessage = StaticHelper.GenericErrorMessage; - } - return new OperationResponse { Success = result, Message = result ? successMessage : errorMessage }; - } - } -} diff --git a/Models/PlanRecord/PlanRecord.cs b/Models/PlanRecord/PlanRecord.cs deleted file mode 100644 index 198aa0e..0000000 --- a/Models/PlanRecord/PlanRecord.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class PlanRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - public int ReminderRecordId { get; set; } - public DateTime DateCreated { get; set; } - public DateTime DateModified { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public ImportMode ImportMode { get; set; } - public PlanPriority Priority { get; set; } - public PlanProgress Progress { get; set; } - public decimal Cost { get; set; } - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - } -} diff --git a/Models/PlanRecord/PlanRecordInput.cs b/Models/PlanRecord/PlanRecordInput.cs deleted file mode 100644 index 143d8e5..0000000 --- a/Models/PlanRecord/PlanRecordInput.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class PlanRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public int ReminderRecordId { get; set; } - public string DateCreated { get; set; } = DateTime.Now.ToShortDateString(); - public string DateModified { get; set; } = DateTime.Now.ToShortDateString(); - public string Description { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Supplies { get; set; } = new List(); - public ImportMode ImportMode { get; set; } - public PlanPriority Priority { get; set; } - public PlanProgress Progress { get; set; } - public decimal Cost { get; set; } - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - public List DeletedRequisitionHistory { get; set; } = new List(); - public bool CopySuppliesAttachment { get; set; } = false; - public PlanRecord ToPlanRecord() { return new PlanRecord { - Id = Id, - VehicleId = VehicleId, - ReminderRecordId = ReminderRecordId, - DateCreated = DateTime.Parse(DateCreated), - DateModified = DateTime.Parse(DateModified), - Description = Description, - Notes = Notes, - Files = Files, - ImportMode = ImportMode, - Cost = Cost, - Priority = Priority, - Progress = Progress, - ExtraFields = ExtraFields, - RequisitionHistory = RequisitionHistory - }; } - /// - /// only used to hide view template button on plan create modal. - /// - public bool CreatedFromReminder { get; set; } - } -} diff --git a/Models/Reminder/ReminderConfig.cs b/Models/Reminder/ReminderConfig.cs deleted file mode 100644 index 4830e57..0000000 --- a/Models/Reminder/ReminderConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReminderUrgencyConfig - { - public int UrgentDays { get; set; } = 30; - public int VeryUrgentDays { get; set; } = 7; - public int UrgentDistance { get; set; } = 100; - public int VeryUrgentDistance { get; set; } = 50; - } -} diff --git a/Models/Reminder/ReminderRecord.cs b/Models/Reminder/ReminderRecord.cs deleted file mode 100644 index a7d0ef4..0000000 --- a/Models/Reminder/ReminderRecord.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReminderRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - public DateTime Date { get; set; } - public int Mileage { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public bool IsRecurring { get; set; } = false; - public bool UseCustomThresholds { get; set; } = false; - public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig(); - public int CustomMileageInterval { get; set; } = 0; - public int CustomMonthInterval { get; set; } = 0; - public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months; - public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles; - public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear; - public ReminderMetric Metric { get; set; } = ReminderMetric.Date; - public List Tags { get; set; } = new List(); - } -} diff --git a/Models/Reminder/ReminderRecordInput.cs b/Models/Reminder/ReminderRecordInput.cs deleted file mode 100644 index 29f46ee..0000000 --- a/Models/Reminder/ReminderRecordInput.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReminderRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public string Date { get; set; } = DateTime.Now.AddDays(1).ToShortDateString(); - public int Mileage { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public bool IsRecurring { get; set; } = false; - public bool UseCustomThresholds { get; set; } = false; - public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig(); - public int CustomMileageInterval { get; set; } = 0; - public int CustomMonthInterval { get; set; } = 0; - public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months; - public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles; - public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear; - public ReminderMetric Metric { get; set; } = ReminderMetric.Date; - public List Tags { get; set; } = new List(); - public ReminderRecord ToReminderRecord() - { - return new ReminderRecord - { - Id = Id, - VehicleId = VehicleId, - Date = DateTime.Parse(string.IsNullOrWhiteSpace(Date) ? DateTime.Now.AddDays(1).ToShortDateString() : Date), - Mileage = Mileage, - Description = Description, - Metric = Metric, - IsRecurring = IsRecurring, - UseCustomThresholds = UseCustomThresholds, - CustomThresholds = CustomThresholds, - ReminderMileageInterval = ReminderMileageInterval, - ReminderMonthInterval = ReminderMonthInterval, - CustomMileageInterval = CustomMileageInterval, - CustomMonthInterval = CustomMonthInterval, - CustomMonthIntervalUnit = CustomMonthIntervalUnit, - Notes = Notes, - Tags = Tags - }; - } - } -} diff --git a/Models/Reminder/ReminderRecordViewModel.cs b/Models/Reminder/ReminderRecordViewModel.cs deleted file mode 100644 index 050a38d..0000000 --- a/Models/Reminder/ReminderRecordViewModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReminderRecordViewModel - { - public int Id { get; set; } - public int VehicleId { get; set; } - public DateTime Date { get; set; } - public int Mileage { get; set; } - public int DueDays { get; set; } - public int DueMileage { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - /// - /// The metric the user selected to calculate the urgency of this reminder. - /// - public ReminderMetric UserMetric { get; set; } = ReminderMetric.Date; - /// - /// Reason why this reminder is urgent - /// - public ReminderMetric Metric { get; set; } = ReminderMetric.Date; - public ReminderUrgency Urgency { get; set; } = ReminderUrgency.NotUrgent; - /// - /// Recurring Reminders - /// - public bool IsRecurring { get; set; } = false; - public List Tags { get; set; } = new List(); - } -} diff --git a/Models/Report/CostDistanceTableForVehicle.cs b/Models/Report/CostDistanceTableForVehicle.cs deleted file mode 100644 index ffe3e31..0000000 --- a/Models/Report/CostDistanceTableForVehicle.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class CostDistanceTableForVehicle - { - public string DistanceUnit { get; set; } = "mi."; - public List CostData { get; set; } = new List(); - } -} diff --git a/Models/Report/CostForVehicleByMonth.cs b/Models/Report/CostForVehicleByMonth.cs deleted file mode 100644 index 67abdb2..0000000 --- a/Models/Report/CostForVehicleByMonth.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class CostForVehicleByMonth - { - public int Year { get; set; } - public int MonthId { get; set; } - public string MonthName { get; set; } - public decimal Cost { get; set; } - public int DistanceTraveled { get; set; } - public decimal CostPerDistanceTraveled { get { if (DistanceTraveled > 0) { return Cost / DistanceTraveled; } else { return 0M; } } } - } -} diff --git a/Models/Report/CostMakeUpForVehicle.cs b/Models/Report/CostMakeUpForVehicle.cs deleted file mode 100644 index 54cc994..0000000 --- a/Models/Report/CostMakeUpForVehicle.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class CostMakeUpForVehicle - { - public decimal ServiceRecordSum { get; set; } - public decimal GasRecordSum { get; set; } - public decimal TaxRecordSum { get; set; } - public decimal UpgradeRecordSum { get; set; } - } -} diff --git a/Models/Report/CostTableForVehicle.cs b/Models/Report/CostTableForVehicle.cs deleted file mode 100644 index 4f2b412..0000000 --- a/Models/Report/CostTableForVehicle.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class CostTableForVehicle - { - public string DistanceUnit { get; set; } = "Cost Per Mile"; - public int TotalDistance { get; set; } - public int NumberOfDays { get; set; } - public decimal ServiceRecordSum { get; set; } - public decimal GasRecordSum { get; set; } - public decimal TaxRecordSum { get; set; } - public decimal UpgradeRecordSum { get; set; } - public decimal ServiceRecordPerMile { get { return TotalDistance != default ? ServiceRecordSum / TotalDistance : 0; } } - public decimal GasRecordPerMile { get { return TotalDistance != default ? GasRecordSum / TotalDistance : 0; } } - public decimal UpgradeRecordPerMile { get { return TotalDistance != default ? UpgradeRecordSum / TotalDistance : 0; } } - public decimal TaxRecordPerMile { get { return TotalDistance != default ? TaxRecordSum / TotalDistance : 0; } } - public decimal ServiceRecordPerDay { get { return NumberOfDays != default ? ServiceRecordSum / NumberOfDays : 0; } } - public decimal GasRecordPerDay { get { return NumberOfDays != default ? GasRecordSum / NumberOfDays : 0; } } - public decimal UpgradeRecordPerDay { get { return NumberOfDays != default ? UpgradeRecordSum / NumberOfDays : 0; } } - public decimal TaxRecordPerDay { get { return NumberOfDays != default ? TaxRecordSum / NumberOfDays : 0; } } - public decimal TotalPerDay { get { return ServiceRecordPerDay + UpgradeRecordPerDay + GasRecordPerDay + TaxRecordPerDay; } } - public decimal TotalPerMile { get { return ServiceRecordPerMile + UpgradeRecordPerMile + GasRecordPerMile + TaxRecordPerMile; } } - public decimal TotalCost { get { return ServiceRecordSum + UpgradeRecordSum + GasRecordSum + TaxRecordSum; } } - } -} diff --git a/Models/Report/GenericReportModel.cs b/Models/Report/GenericReportModel.cs deleted file mode 100644 index 15521b9..0000000 --- a/Models/Report/GenericReportModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MotoVaultPro.Models -{ - /// - /// Generic Model used for vehicle history report. - /// - public class GenericReportModel - { - public ImportMode DataType { get; set; } - public DateTime Date { get; set; } - public int Odometer { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public decimal Cost { get; set; } - public List Files { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - } -} diff --git a/Models/Report/MPGForVehicleByMonth.cs b/Models/Report/MPGForVehicleByMonth.cs deleted file mode 100644 index 066e740..0000000 --- a/Models/Report/MPGForVehicleByMonth.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class MPGForVehicleByMonth - { - public List CostData { get; set; } = new List(); - public List SortedCostData { get; set; } = new List(); - public string Unit { get; set; } - } -} diff --git a/Models/Report/ReminderMakeUpForVehicle.cs b/Models/Report/ReminderMakeUpForVehicle.cs deleted file mode 100644 index 7264082..0000000 --- a/Models/Report/ReminderMakeUpForVehicle.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReminderMakeUpForVehicle - { - public int NotUrgentCount { get; set; } - public int UrgentCount { get; set; } - public int VeryUrgentCount { get; set; } - public int PastDueCount { get; set; } - } -} diff --git a/Models/Report/ReportHeader.cs b/Models/Report/ReportHeader.cs deleted file mode 100644 index e4c98c8..0000000 --- a/Models/Report/ReportHeader.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReportHeader - { - public int MaxOdometer { get; set; } - public int DistanceTraveled { get; set; } - public decimal TotalCost { get; set; } - public string AverageMPG { get; set; } - } -} diff --git a/Models/Report/ReportParameter.cs b/Models/Report/ReportParameter.cs deleted file mode 100644 index 96cdcd6..0000000 --- a/Models/Report/ReportParameter.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReportParameter - { - public List VisibleColumns { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public TagFilter TagFilter { get; set; } = TagFilter.Exclude; - public List Tags { get; set; } = new List(); - public bool FilterByDateRange { get; set; } = false; - public string StartDate { get; set; } = ""; - public string EndDate { get; set; } = ""; - public bool PrintIndividualRecords { get; set; } = false; - } -} diff --git a/Models/Report/ReportViewModel.cs b/Models/Report/ReportViewModel.cs deleted file mode 100644 index 82c075f..0000000 --- a/Models/Report/ReportViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ReportViewModel - { - public ReportHeader ReportHeaderForVehicle { get; set; } = new ReportHeader(); - public List CostForVehicleByMonth { get; set; } = new List(); - public MPGForVehicleByMonth FuelMileageForVehicleByMonth { get; set; } = new MPGForVehicleByMonth(); - public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle(); - public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle(); - public List Years { get; set; } = new List(); - public List Collaborators { get; set; } = new List(); - public bool CustomWidgetsConfigured { get; set; } = false; - public List AvailableMetrics { get; set; } = new List(); - } -} diff --git a/Models/Report/VehicleHistoryViewModel.cs b/Models/Report/VehicleHistoryViewModel.cs deleted file mode 100644 index 097e6ed..0000000 --- a/Models/Report/VehicleHistoryViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class VehicleHistoryViewModel - { - public Vehicle VehicleData { get; set; } - public List VehicleHistory { get; set; } - public ReportParameter ReportParameters { get; set; } - public string Odometer { get; set; } - public string MPG { get; set; } - public decimal TotalCost { get; set; } - public decimal TotalGasCost { get; set; } - public string DaysOwned { get; set; } - public string DistanceTraveled { get; set; } - public decimal TotalCostPerMile { get; set; } - public decimal TotalGasCostPerMile { get; set; } - public string DistanceUnit { get; set; } - public decimal TotalDepreciation { get; set; } - public decimal DepreciationPerDay { get; set; } - public decimal DepreciationPerMile { get; set; } - public string StartDate { get; set; } - public string EndDate { get; set; } - } -} diff --git a/Models/SearchResult.cs b/Models/SearchResult.cs deleted file mode 100644 index a53483d..0000000 --- a/Models/SearchResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SearchResult - { - public int Id { get; set; } - public ImportMode RecordType { get; set; } - public string Description { get; set; } - } -} diff --git a/Models/ServerConfig.cs b/Models/ServerConfig.cs deleted file mode 100644 index 28fd81a..0000000 --- a/Models/ServerConfig.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MotoVaultPro.Models -{ - public class ServerConfig - { - [JsonPropertyName("POSTGRES_CONNECTION")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? PostgresConnection { get; set; } - - [JsonPropertyName("LUBELOGGER_ALLOWED_FILE_EXTENSIONS")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? AllowedFileExtensions { get; set; } - - [JsonPropertyName("LUBELOGGER_LOGO_URL")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? CustomLogoURL { get; set; } - - [JsonPropertyName("LUBELOGGER_LOGO_SMALL_URL")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? CustomSmallLogoURL { get; set; } - - [JsonPropertyName("LUBELOGGER_MOTD")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? MessageOfTheDay { get; set; } - - [JsonPropertyName("LUBELOGGER_WEBHOOK")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? WebHookURL { get; set; } - - [JsonPropertyName("LUBELOGGER_DOMAIN")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? ServerURL { get; set; } - - [JsonPropertyName("LUBELOGGER_CUSTOM_WIDGETS")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? CustomWidgetsEnabled { get; set; } - - [JsonPropertyName("LUBELOGGER_INVARIANT_API")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? InvariantAPIEnabled { get; set; } - - [JsonPropertyName("MailConfig")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public MailConfig? SMTPConfig { get; set; } - - [JsonPropertyName("OpenIDConfig")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public OpenIDConfig? OIDCConfig { get; set; } - - [JsonPropertyName("ReminderUrgencyConfig")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public ReminderUrgencyConfig? ReminderUrgencyConfig { get; set; } - - [JsonPropertyName("LUBELOGGER_OPEN_REGISTRATION")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? OpenRegistration { get; set; } - - [JsonPropertyName("DisableRegistration")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? DisableRegistration { get; set; } - - [JsonPropertyName("DefaultReminderEmail")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? DefaultReminderEmail { get; set; } = string.Empty; - - [JsonPropertyName("EnableRootUserOIDC")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? EnableRootUserOIDC { get; set; } - } -} \ No newline at end of file diff --git a/Models/ServiceRecord/ServiceRecord.cs b/Models/ServiceRecord/ServiceRecord.cs deleted file mode 100644 index 5afdbbe..0000000 --- a/Models/ServiceRecord/ServiceRecord.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ServiceRecord: GenericRecord - { - } -} diff --git a/Models/ServiceRecord/ServiceRecordInput.cs b/Models/ServiceRecord/ServiceRecordInput.cs deleted file mode 100644 index 71abb9a..0000000 --- a/Models/ServiceRecord/ServiceRecordInput.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ServiceRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public List ReminderRecordId { get; set; } = new List(); - public string Date { get; set; } = DateTime.Now.ToShortDateString(); - public int Mileage { get; set; } - public string Description { get; set; } - public decimal Cost { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Supplies { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - public List DeletedRequisitionHistory { get; set; } = new List(); - public bool CopySuppliesAttachment { get; set; } = false; - public ServiceRecord ToServiceRecord() { return new ServiceRecord { - Id = Id, - VehicleId = VehicleId, - Date = DateTime.Parse(Date), - Cost = Cost, - Mileage = Mileage, - Description = Description, - Notes = Notes, - Files = Files, - Tags = Tags, - ExtraFields = ExtraFields, - RequisitionHistory = RequisitionHistory - }; - } - } -} diff --git a/Models/Settings/ServerSettingsViewModel.cs b/Models/Settings/ServerSettingsViewModel.cs deleted file mode 100644 index 1bcf3fe..0000000 --- a/Models/Settings/ServerSettingsViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ServerSettingsViewModel - { - public string LocaleInfo { get; set; } - public string PostgresConnection { get; set; } - public string AllowedFileExtensions { get; set; } - public string CustomLogoURL { get; set; } - public string CustomSmallLogoURL { get; set; } - public string MessageOfTheDay { get; set; } - public string WebHookURL { get; set; } - public string Domain { get; set; } - public bool CustomWidgetsEnabled { get; set; } - public bool InvariantAPIEnabled { get; set; } - public MailConfig SMTPConfig { get; set; } = new MailConfig(); - public OpenIDConfig OIDCConfig { get; set; } = new OpenIDConfig(); - public bool OpenRegistration { get; set; } - public bool DisableRegistration { get; set; } - public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig(); - public string DefaultReminderEmail { get; set; } = string.Empty; - public bool EnableRootUserOIDC { get; set; } - public bool EnableAuth { get; set; } - } -} diff --git a/Models/Settings/SettingsViewModel.cs b/Models/Settings/SettingsViewModel.cs deleted file mode 100644 index c200214..0000000 --- a/Models/Settings/SettingsViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SettingsViewModel - { - public UserConfig UserConfig { get; set; } - public List UILanguages { get; set; } - } -} diff --git a/Models/Shared/ExtraField.cs b/Models/Shared/ExtraField.cs deleted file mode 100644 index 8aec514..0000000 --- a/Models/Shared/ExtraField.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class ExtraField - { - public string Name { get; set; } - public string Value { get; set; } - public bool IsRequired { get; set; } - public ExtraFieldType FieldType { get; set; } = ExtraFieldType.Text; - } -} diff --git a/Models/Shared/GenericRecord.cs b/Models/Shared/GenericRecord.cs deleted file mode 100644 index 93b1091..0000000 --- a/Models/Shared/GenericRecord.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class GenericRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - public DateTime Date { get; set; } - public int Mileage { get; set; } - public string Description { get; set; } - public decimal Cost { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Tags { get; set;} = new List(); - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - } -} diff --git a/Models/Shared/ImportModel.cs b/Models/Shared/ImportModel.cs deleted file mode 100644 index 488b13e..0000000 --- a/Models/Shared/ImportModel.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MotoVaultPro.Models -{ - /// - /// Import model used for importing records via CSV. - /// - public class ImportModel - { - public string Date { get; set; } - public string Day { get; set; } - public string Month { get; set; } - public string Year { get; set; } - public string DateCreated { get; set; } - public string DateModified { get; set; } - public string Type { get; set; } - public string Priority { get; set; } - public string Progress { get; set; } - public string InitialOdometer { get; set; } - public string Odometer { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public string FuelConsumed { get; set; } - public string Cost { get; set; } - public string Price { get; set; } - public string PartialFuelUp { get; set; } - public string IsFillToFull { get; set; } - public string MissedFuelUp { get; set; } - public string PartNumber { get; set; } - public string PartSupplier { get; set; } - public string PartQuantity { get; set; } - public string Tags { get; set; } - public Dictionary ExtraFields {get;set;} - } - - public class SupplyRecordExportModel - { - public string Date { get; set; } - public string PartNumber { get; set; } - public string PartSupplier { get; set; } - public string PartQuantity { get; set; } - public string Description { get; set; } - public string Cost { get; set; } - public string Notes { get; set; } - public string Tags { get; set; } - public List ExtraFields { get; set; } = new List(); - } - public class GenericRecordExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string Date { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string Odometer { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - [JsonConverter(typeof(FromDecimalOptional))] - public string Cost { get; set; } - public string Tags { get; set; } - public List ExtraFields { get; set; } = new List(); - public List Files { get; set; } = new List(); - } - public class OdometerRecordExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string Date { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string InitialOdometer { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string Odometer { get; set; } - public string Notes { get; set; } - public string Tags { get; set; } - public List ExtraFields { get; set; } = new List(); - public List Files { get; set; } = new List(); - } - public class TaxRecordExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string Date { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - [JsonConverter(typeof(FromDecimalOptional))] - public string Cost { get; set; } - public string Tags { get; set; } - public List ExtraFields { get; set; } = new List(); - public List Files { get; set; } = new List(); - } - public class GasRecordExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string Date { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string Odometer { get; set; } - [JsonConverter(typeof(FromDecimalOptional))] - public string FuelConsumed { get; set; } - [JsonConverter(typeof(FromDecimalOptional))] - public string Cost { get; set; } - [JsonConverter(typeof(FromDecimalOptional))] - public string FuelEconomy { get; set; } - [JsonConverter(typeof(FromBoolOptional))] - public string IsFillToFull { get; set; } - [JsonConverter(typeof(FromBoolOptional))] - public string MissedFuelUp { get; set; } - public string Notes { get; set; } - public string Tags { get; set; } - public List ExtraFields { get; set; } = new List(); - public List Files { get; set; } = new List(); - } - public class ReminderExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - public string Description { get; set; } - public string Urgency { get; set; } - public string Metric { get; set; } - public string Notes { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string DueDate { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string DueOdometer { get; set; } - public string Tags { get; set; } - } - /// - /// Only used for the API GET Method - /// - public class ReminderAPIExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - public string Description { get; set; } - public string Urgency { get; set; } - public string Metric { get; set; } - public string UserMetric { get; set; } - public string Notes { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string DueDate { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string DueOdometer { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string DueDays { get; set; } - [JsonConverter(typeof(FromIntOptional))] - public string DueDistance { get; set; } - public string Tags { get; set; } - } - public class PlanRecordExportModel - { - [JsonConverter(typeof(FromIntOptional))] - public string Id { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string DateCreated { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string DateModified { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public string Type { get; set; } - public string Priority { get; set; } - public string Progress { get; set; } - [JsonConverter(typeof(FromDecimalOptional))] - public string Cost { get; set; } - public List ExtraFields { get; set; } = new List(); - public List Files { get; set; } = new List(); - } - public class UserExportModel - { - public string Username { get; set; } - public string EmailAddress { get; set; } - [JsonConverter(typeof(FromBoolOptional))] - public string IsAdmin { get; set; } - [JsonConverter(typeof(FromBoolOptional))] - public string IsRoot { get; set; } - } -} diff --git a/Models/Shared/RecordExtraField.cs b/Models/Shared/RecordExtraField.cs deleted file mode 100644 index 1ef085c..0000000 --- a/Models/Shared/RecordExtraField.cs +++ /dev/null @@ -1,14 +0,0 @@ -using LiteDB; - -namespace MotoVaultPro.Models -{ - public class RecordExtraField - { - /// - /// Corresponds to int value of ImportMode enum - /// - [BsonId(false)] - public int Id { get; set; } - public List ExtraFields { get; set; } = new List(); - } -} diff --git a/Models/Shared/StickerViewModel.cs b/Models/Shared/StickerViewModel.cs deleted file mode 100644 index 97542d0..0000000 --- a/Models/Shared/StickerViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class StickerViewModel - { - public ImportMode RecordType { get; set; } - public Vehicle VehicleData { get; set; } = new Vehicle(); - public List ReminderRecords { get; set; } = new List(); - public List GenericRecords { get; set; } = new List(); - public List SupplyRecords { get; set; } = new List(); - } -} diff --git a/Models/Shared/UploadedFiles.cs b/Models/Shared/UploadedFiles.cs deleted file mode 100644 index e75ffde..0000000 --- a/Models/Shared/UploadedFiles.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UploadedFiles - { - public string Name { get; set; } - public string Location { get; set; } - public bool IsPending { get; set; } - } -} diff --git a/Models/Shared/VehicleRecords.cs b/Models/Shared/VehicleRecords.cs deleted file mode 100644 index 12d26fb..0000000 --- a/Models/Shared/VehicleRecords.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class VehicleRecords - { - public List ServiceRecords { get; set; } = new List(); - public List UpgradeRecords { get; set; } = new List(); - public List GasRecords { get; set; } = new List(); - public List TaxRecords { get; set; } = new List(); - public List OdometerRecords { get; set; } = new List(); - } -} diff --git a/Models/Shared/WebHookPayload.cs b/Models/Shared/WebHookPayload.cs deleted file mode 100644 index d2ebad9..0000000 --- a/Models/Shared/WebHookPayload.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MotoVaultPro.Models -{ - /// - /// WebHookPayload Object - /// - public class WebHookPayloadBase - { - public string Type { get; set; } = ""; - public string Timestamp - { - get { return DateTime.UtcNow.ToString("O"); } - } - public Dictionary Data { get; set; } = new Dictionary(); - /// - /// Legacy attributes below - /// - public string VehicleId { get; set; } = ""; - public string Username { get; set; } = ""; - public string Action { get; set; } = ""; - } - public class DiscordWebHook - { - public string Username { get { return "MotoVaultPro"; } } - [JsonPropertyName("avatar_url")] - public string AvatarUrl { get { return "https://github.com/ericgullickson/motovaultpro_logo_small.png"; } } - public string Content { get; set; } = ""; - public static DiscordWebHook FromWebHookPayload(WebHookPayload webHookPayload) - { - return new DiscordWebHook - { - Content = webHookPayload.Action, - }; - } - } - public class WebHookPayload: WebHookPayloadBase - { - private static string GetFriendlyActionType(string actionType) - { - var actionTypeParts = actionType.Split('.'); - if (actionTypeParts.Length == 2) - { - var recordType = actionTypeParts[0]; - var recordAction = actionTypeParts[1]; - switch (recordAction) - { - case "add": - recordAction = "Added"; - break; - case "update": - recordAction = "Updated"; - break; - case "delete": - recordAction = "Deleted"; - break; - } - if (recordType.ToLower().Contains("record")) - { - var cleanedRecordType = recordType.ToLower().Replace("record", ""); - cleanedRecordType = $"{char.ToUpper(cleanedRecordType[0])}{cleanedRecordType.Substring(1)} Record"; - recordType = cleanedRecordType; - } else - { - recordType = $"{char.ToUpper(recordType[0])}{recordType.Substring(1)}"; - } - return $"{recordAction} {recordType}"; - } else if (actionTypeParts.Length == 3) - { - var recordType = actionTypeParts[0]; - var recordAction = actionTypeParts[1]; - var thirdPart = actionTypeParts[2]; - switch (recordAction) - { - case "add": - recordAction = "Added"; - break; - case "update": - recordAction = "Updated"; - break; - case "delete": - recordAction = "Deleted"; - break; - } - if (recordType.ToLower().Contains("record")) - { - var cleanedRecordType = recordType.ToLower().Replace("record", ""); - cleanedRecordType = $"{char.ToUpper(cleanedRecordType[0])}{cleanedRecordType.Substring(1)} Record"; - recordType = cleanedRecordType; - } - else - { - recordType = $"{char.ToUpper(recordType[0])}{recordType.Substring(1)}"; - } - if (thirdPart == "api") - { - return $"{recordAction} {recordType} via API"; - } else - { - return $"{recordAction} {recordType}"; - } - } - return actionType; - } - public static WebHookPayload FromGenericRecord(GenericRecord genericRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("description", genericRecord.Description); - payloadDictionary.Add("odometer", genericRecord.Mileage.ToString()); - payloadDictionary.Add("vehicleId", genericRecord.VehicleId.ToString()); - payloadDictionary.Add("cost", genericRecord.Cost.ToString("F2")); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = genericRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {genericRecord.Description}" - }; - } - public static WebHookPayload FromGasRecord(GasRecord gasRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("odometer", gasRecord.Mileage.ToString()); - payloadDictionary.Add("fuelconsumed", gasRecord.Gallons.ToString()); - payloadDictionary.Add("vehicleId", gasRecord.VehicleId.ToString()); - payloadDictionary.Add("cost", gasRecord.Cost.ToString("F2")); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = gasRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Odometer: {gasRecord.Mileage}" - }; - } - public static WebHookPayload FromOdometerRecord(OdometerRecord odometerRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("initialodometer", odometerRecord.InitialMileage.ToString()); - payloadDictionary.Add("odometer", odometerRecord.Mileage.ToString()); - payloadDictionary.Add("vehicleId", odometerRecord.VehicleId.ToString()); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = odometerRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Odometer: {odometerRecord.Mileage}" - }; - } - public static WebHookPayload FromTaxRecord(TaxRecord taxRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("description", taxRecord.Description); - payloadDictionary.Add("vehicleId", taxRecord.VehicleId.ToString()); - payloadDictionary.Add("cost", taxRecord.Cost.ToString("F2")); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = taxRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {taxRecord.Description}" - }; - } - public static WebHookPayload FromPlanRecord(PlanRecord planRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("description", planRecord.Description); - payloadDictionary.Add("vehicleId", planRecord.VehicleId.ToString()); - payloadDictionary.Add("cost", planRecord.Cost.ToString("F2")); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = planRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {planRecord.Description}" - }; - } - public static WebHookPayload FromReminderRecord(ReminderRecord reminderRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("description", reminderRecord.Description); - payloadDictionary.Add("vehicleId", reminderRecord.VehicleId.ToString()); - payloadDictionary.Add("metric", reminderRecord.Metric.ToString()); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = reminderRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {reminderRecord.Description}" - }; - } - public static WebHookPayload FromSupplyRecord(SupplyRecord supplyRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("description", supplyRecord.Description); - payloadDictionary.Add("vehicleId", supplyRecord.VehicleId.ToString()); - payloadDictionary.Add("cost", supplyRecord.Cost.ToString("F2")); - payloadDictionary.Add("quantity", supplyRecord.Quantity.ToString("F2")); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = supplyRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {supplyRecord.Description}" - }; - } - public static WebHookPayload FromNoteRecord(Note noteRecord, string actionType, string userName) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - payloadDictionary.Add("description", noteRecord.Description); - payloadDictionary.Add("vehicleId", noteRecord.VehicleId.ToString()); - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = noteRecord.VehicleId.ToString(), - Username = userName, - Action = $"{userName} {GetFriendlyActionType(actionType)} Description: {noteRecord.Description}" - }; - } - public static WebHookPayload Generic(string payload, string actionType, string userName, string vehicleId) - { - Dictionary payloadDictionary = new Dictionary(); - payloadDictionary.Add("user", userName); - if (!string.IsNullOrWhiteSpace(payload)) - { - payloadDictionary.Add("description", payload); - } - return new WebHookPayload - { - Type = actionType, - Data = payloadDictionary, - VehicleId = string.IsNullOrWhiteSpace(vehicleId) ? "N/A" : vehicleId, - Username = userName, - Action = string.IsNullOrWhiteSpace(payload) ? $"{userName} {GetFriendlyActionType(actionType)}" : $"{userName} {payload}" - }; - } - } -} diff --git a/Models/Sponsors.cs b/Models/Sponsors.cs deleted file mode 100644 index b202bbd..0000000 --- a/Models/Sponsors.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class Sponsors - { - public List LifeTime { get; set; } = new List(); - public List Bronze { get; set; } = new List(); - public List Silver { get; set; } = new List(); - public List Gold { get; set; } = new List(); - } -} diff --git a/Models/Supply/SupplyAvailability.cs b/Models/Supply/SupplyAvailability.cs deleted file mode 100644 index 509ead6..0000000 --- a/Models/Supply/SupplyAvailability.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyAvailability - { - public bool Missing { get; set; } - public string Description { get; set; } = string.Empty; - public decimal Required { get; set; } - public decimal InStock { get; set; } - public bool Insufficient { get { return Required > InStock; } } - } -} diff --git a/Models/Supply/SupplyRecord.cs b/Models/Supply/SupplyRecord.cs deleted file mode 100644 index ce0eaa3..0000000 --- a/Models/Supply/SupplyRecord.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - /// - /// When the part or supplies were purchased. - /// - public DateTime Date { get; set; } - /// - /// Part number can be alphanumeric. - /// - public string PartNumber { get; set; } - /// - /// Where the part/supplies were purchased from. - /// - public string PartSupplier { get; set; } - /// - /// Amount purchased, can be partial quantities such as fluids. - /// - public decimal Quantity { get; set; } - /// - /// Description of the part/supplies purchased. - /// - public string Description { get; set; } - /// - /// How much it costs - /// - public decimal Cost { get; set; } - /// - /// Additional notes. - /// - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - } -} diff --git a/Models/Supply/SupplyRecordInput.cs b/Models/Supply/SupplyRecordInput.cs deleted file mode 100644 index f72a42b..0000000 --- a/Models/Supply/SupplyRecordInput.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public string Date { get; set; } = DateTime.Now.ToShortDateString(); - public string PartNumber { get; set; } - public string PartSupplier { get; set; } - public decimal Quantity { get; set; } - public string Description { get; set; } - public decimal Cost { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - public SupplyRecord ToSupplyRecord() { return new SupplyRecord { - Id = Id, - VehicleId = VehicleId, - Date = DateTime.Parse(Date), - Cost = Cost, - PartNumber = PartNumber, - PartSupplier = PartSupplier, - Quantity = Quantity, - Description = Description, - Notes = Notes, - Files = Files, - Tags = Tags, - ExtraFields = ExtraFields, - RequisitionHistory = RequisitionHistory - }; } - } -} diff --git a/Models/Supply/SupplyRequisitionHistory.cs b/Models/Supply/SupplyRequisitionHistory.cs deleted file mode 100644 index 6cbd59d..0000000 --- a/Models/Supply/SupplyRequisitionHistory.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyRequisitionHistory - { - public string CostInputId { get; set; } - public List RequisitionHistory { get; set; } = new List(); - } -} diff --git a/Models/Supply/SupplyStore.cs b/Models/Supply/SupplyStore.cs deleted file mode 100644 index e0b461c..0000000 --- a/Models/Supply/SupplyStore.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyStore - { - public string Tab { get; set; } - public bool AdditionalSupplies { get; set; } - } -} diff --git a/Models/Supply/SupplyUsage.cs b/Models/Supply/SupplyUsage.cs deleted file mode 100644 index 9f73cee..0000000 --- a/Models/Supply/SupplyUsage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyUsage { - public int SupplyId { get; set; } - public decimal Quantity { get; set; } - } -} diff --git a/Models/Supply/SupplyUsageHistory.cs b/Models/Supply/SupplyUsageHistory.cs deleted file mode 100644 index 60171c0..0000000 --- a/Models/Supply/SupplyUsageHistory.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyUsageHistory { - public int Id { get; set; } - public DateTime Date { get; set; } - public string PartNumber { get; set; } - public string Description { get; set; } - public decimal Quantity { get; set; } - public decimal Cost { get; set; } - } -} diff --git a/Models/Supply/SupplyUsageViewModel.cs b/Models/Supply/SupplyUsageViewModel.cs deleted file mode 100644 index e920840..0000000 --- a/Models/Supply/SupplyUsageViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class SupplyUsageViewModel - { - public List Supplies { get; set; } = new List(); - public List Usage { get; set; } = new List(); - } -} diff --git a/Models/TaxRecord/TaxRecord.cs b/Models/TaxRecord/TaxRecord.cs deleted file mode 100644 index cd3d9ba..0000000 --- a/Models/TaxRecord/TaxRecord.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class TaxRecord - { - public int Id { get; set; } - public int VehicleId { get; set; } - public DateTime Date { get; set; } - public string Description { get; set; } - public decimal Cost { get; set; } - public string Notes { get; set; } - public bool IsRecurring { get; set; } = false; - public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.OneYear; - public int CustomMonthInterval { get; set; } = 0; - public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months; - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - } -} diff --git a/Models/TaxRecord/TaxRecordInput.cs b/Models/TaxRecord/TaxRecordInput.cs deleted file mode 100644 index 5fcefb7..0000000 --- a/Models/TaxRecord/TaxRecordInput.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class TaxRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public List ReminderRecordId { get; set; } = new List(); - public string Date { get; set; } = DateTime.Now.ToShortDateString(); - public string Description { get; set; } - public decimal Cost { get; set; } - public string Notes { get; set; } - public bool IsRecurring { get; set; } = false; - public ReminderMonthInterval RecurringInterval { get; set; } = ReminderMonthInterval.ThreeMonths; - public int CustomMonthInterval { get; set; } = 0; - public ReminderIntervalUnit CustomMonthIntervalUnit { get; set; } = ReminderIntervalUnit.Months; - public List Files { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public TaxRecord ToTaxRecord() { return new TaxRecord { - Id = Id, - VehicleId = VehicleId, - Date = DateTime.Parse(Date), - Cost = Cost, - Description = Description, - Notes = Notes, - IsRecurring = IsRecurring, - RecurringInterval = RecurringInterval, - CustomMonthInterval = CustomMonthInterval, - CustomMonthIntervalUnit = CustomMonthIntervalUnit, - Files = Files, - Tags = Tags, - ExtraFields = ExtraFields - }; } - } -} diff --git a/Models/Translations.cs b/Models/Translations.cs deleted file mode 100644 index 7025b97..0000000 --- a/Models/Translations.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class Translations - { - public List Africa { get; set; } = new List(); - public List Asia { get; set; } = new List(); - public List Europe { get; set; } = new List(); - public List NorthAmerica { get; set; } = new List(); - public List SouthAmerica { get; set; } = new List(); - public List Oceania { get; set; } = new List(); - } -} diff --git a/Models/UpgradeRecord/UpgradeRecord.cs b/Models/UpgradeRecord/UpgradeRecord.cs deleted file mode 100644 index 6eac340..0000000 --- a/Models/UpgradeRecord/UpgradeRecord.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UpgradeRecord: GenericRecord - { - } -} diff --git a/Models/UpgradeRecord/UpgradeReportInput.cs b/Models/UpgradeRecord/UpgradeReportInput.cs deleted file mode 100644 index 23d9b54..0000000 --- a/Models/UpgradeRecord/UpgradeReportInput.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UpgradeRecordInput - { - public int Id { get; set; } - public int VehicleId { get; set; } - public List ReminderRecordId { get; set; } = new List(); - public string Date { get; set; } = DateTime.Now.ToShortDateString(); - public int Mileage { get; set; } - public string Description { get; set; } - public decimal Cost { get; set; } - public string Notes { get; set; } - public List Files { get; set; } = new List(); - public List Supplies { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public List ExtraFields { get; set; } = new List(); - public List RequisitionHistory { get; set; } = new List(); - public List DeletedRequisitionHistory { get; set; } = new List(); - public bool CopySuppliesAttachment { get; set; } = false; - public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { - Id = Id, - VehicleId = VehicleId, - Date = DateTime.Parse(Date), - Cost = Cost, - Mileage = Mileage, - Description = Description, - Notes = Notes, - Files = Files, - Tags = Tags, - ExtraFields = ExtraFields, - RequisitionHistory = RequisitionHistory - }; - } - } -} diff --git a/Models/User/UserAccess.cs b/Models/User/UserAccess.cs deleted file mode 100644 index 02bfa9c..0000000 --- a/Models/User/UserAccess.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UserVehicle - { - public int UserId { get; set; } - public int VehicleId { get; set; } - } - public class UserAccess - { - public UserVehicle Id { get; set; } - } -} diff --git a/Models/User/UserCollaborator.cs b/Models/User/UserCollaborator.cs deleted file mode 100644 index 46fa65f..0000000 --- a/Models/User/UserCollaborator.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UserCollaborator - { - public string UserName { get; set; } - public UserVehicle UserVehicle { get; set; } - } -} diff --git a/Models/User/UserColumnPreference.cs b/Models/User/UserColumnPreference.cs deleted file mode 100644 index a8c337d..0000000 --- a/Models/User/UserColumnPreference.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UserColumnPreference - { - public ImportMode Tab { get; set; } - public List VisibleColumns { get; set; } = new List(); - public List ColumnOrder { get; set; } = new List(); - } -} \ No newline at end of file diff --git a/Models/User/UserConfigData.cs b/Models/User/UserConfigData.cs deleted file mode 100644 index ce6ee96..0000000 --- a/Models/User/UserConfigData.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UserConfigData - { - /// - /// User ID - /// - public int Id { get; set; } - public UserConfig UserConfig { get; set; } - } -} diff --git a/Models/User/UserData.cs b/Models/User/UserData.cs deleted file mode 100644 index 77f718b..0000000 --- a/Models/User/UserData.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UserData - { - public int Id { get; set; } - public string UserName { get; set; } - public string EmailAddress { get; set; } - public string Password { get; set; } - public bool IsAdmin { get; set; } - public bool IsRootUser { get; set; } = false; - } -} \ No newline at end of file diff --git a/Models/UserConfig.cs b/Models/UserConfig.cs deleted file mode 100644 index 2ceb4dc..0000000 --- a/Models/UserConfig.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class UserConfig - { - public bool UseDarkMode { get; set; } - public bool UseSystemColorMode { get; set; } - public bool EnableCsvImports { get; set; } - public bool UseMPG { get; set; } - public bool UseDescending { get; set; } - public bool EnableAuth { get; set; } - public bool HideZero { get; set; } - public bool UseUKMPG {get;set;} - public bool UseThreeDecimalGasCost { get; set; } - public bool UseThreeDecimalGasConsumption { get; set; } - public bool UseMarkDownOnSavedNotes { get; set; } - public bool EnableAutoReminderRefresh { get; set; } - public bool EnableAutoOdometerInsert { get; set; } - public bool EnableShopSupplies { get; set; } - public bool EnableExtraFieldColumns { get; set; } - public bool HideSoldVehicles { get; set; } - public bool AutomaticDecimalFormat { get; set; } - public string PreferredGasUnit { get; set; } = string.Empty; - public string PreferredGasMileageUnit { get; set; } = string.Empty; - public bool UseUnitForFuelCost { get; set; } - public bool UseSimpleFuelEntry { get; set; } - public bool ShowCalendar { get; set; } - public bool ShowVehicleThumbnail { get; set; } - public List UserColumnPreferences { get; set; } = new List(); - public string UserNameHash { get; set; } - public string UserPasswordHash { get; set;} - public string UserLanguage { get; set; } = "en_US"; - public List VisibleTabs { get; set; } = new List() { - ImportMode.Dashboard, - ImportMode.ServiceRecord, - ImportMode.GasRecord, - ImportMode.UpgradeRecord, - ImportMode.TaxRecord, - ImportMode.ReminderRecord, - ImportMode.NoteRecord - }; - public ImportMode DefaultTab { get; set; } = ImportMode.Dashboard; - public List TabOrder { get; set; } = new List() { - ImportMode.Dashboard, - ImportMode.PlanRecord, - ImportMode.OdometerRecord, - ImportMode.ServiceRecord, - ImportMode.UpgradeRecord, - ImportMode.GasRecord, - ImportMode.SupplyRecord, - ImportMode.TaxRecord, - ImportMode.NoteRecord, - ImportMode.ReminderRecord - }; - } -} \ No newline at end of file diff --git a/Models/Vehicle.cs b/Models/Vehicle.cs deleted file mode 100644 index a647d4b..0000000 --- a/Models/Vehicle.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MotoVaultPro.Models -{ - public class Vehicle - { - public int Id { get; set; } - public string ImageLocation { get; set; } = "/defaults/noimage.png"; - public int Year { get; set; } - public string Make { get; set; } - public string Model { get; set; } - public string LicensePlate { get; set; } - public string VinNumber { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string PurchaseDate { get; set; } - [JsonConverter(typeof(FromDateOptional))] - public string SoldDate { get; set; } - public decimal PurchasePrice { get; set; } - public decimal SoldPrice { get; set; } - public bool IsElectric { get; set; } = false; - public bool IsDiesel { get; set; } = false; - public bool UseHours { get; set; } = false; - public bool OdometerOptional { get; set; } = false; - public List ExtraFields { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public bool HasOdometerAdjustment { get; set; } = false; - /// - /// Primarily used for vehicles with odometer units different from user's settings. - /// - [JsonConverter(typeof(FromDecimalOptional))] - public string OdometerMultiplier { get; set; } = "1"; - /// - /// Primarily used for vehicles where the odometer does not reflect actual mileage. - /// - [JsonConverter(typeof(FromIntOptional))] - public string OdometerDifference { get; set; } = "0"; - public List DashboardMetrics { get; set; } = new List(); - /// - /// Determines what is displayed in place of the license plate. - /// - public string VehicleIdentifier { get; set; } = "LicensePlate"; - } -} diff --git a/Models/VehicleViewModel.cs b/Models/VehicleViewModel.cs deleted file mode 100644 index 13b2a39..0000000 --- a/Models/VehicleViewModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MotoVaultPro.Models -{ - public class VehicleViewModel - { - public int Id { get; set; } - public string ImageLocation { get; set; } = "/defaults/noimage.png"; - public int Year { get; set; } - public string Make { get; set; } - public string Model { get; set; } - public string LicensePlate { get; set; } - public string VinNumber { get; set; } - public string SoldDate { get; set; } - public bool IsElectric { get; set; } = false; - public bool IsDiesel { get; set; } = false; - public bool UseHours { get; set; } = false; - public bool OdometerOptional { get; set; } = false; - public List ExtraFields { get; set; } = new List(); - public List Tags { get; set; } = new List(); - public string VehicleIdentifier { get; set; } = "LicensePlate"; - //Dashboard Metric Attributes - public List DashboardMetrics { get; set; } = new List(); - public int LastReportedMileage { get; set; } - public bool HasReminders { get; set; } = false; - public decimal CostPerMile { get; set; } - public decimal TotalCost { get; set; } - public string DistanceUnit { get; set; } - } -} diff --git a/MotoVaultPro.csproj b/MotoVaultPro.csproj deleted file mode 100644 index 3aae8aa..0000000 --- a/MotoVaultPro.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - diff --git a/MotoVaultPro.sln b/MotoVaultPro.sln deleted file mode 100644 index 693e6d1..0000000 --- a/MotoVaultPro.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34330.188 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MotoVaultPro", "MotoVaultPro.csproj", "{0DB85611-6555-4127-A66E-DADD25A16526}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0DB85611-6555-4127-A66E-DADD25A16526}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DB85611-6555-4127-A66E-DADD25A16526}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DB85611-6555-4127-A66E-DADD25A16526}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DB85611-6555-4127-A66E-DADD25A16526}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E1B446D5-E91D-47E5-819B-1902378770D7} - EndGlobalSection -EndGlobal diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 2fd5422..0000000 --- a/Program.cs +++ /dev/null @@ -1,175 +0,0 @@ -using MotoVaultPro.External.Implementations; -using MotoVaultPro.External.Interfaces; -using MotoVaultPro.Helper; -using MotoVaultPro.Logic; -using MotoVaultPro.Middleware; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.FileProviders; - -var builder = WebApplication.CreateBuilder(args); - -//Additional JsonFile -builder.Configuration.AddJsonFile(StaticHelper.UserConfigPath, optional: true, reloadOnChange: true); -builder.Configuration.AddJsonFile(StaticHelper.ServerConfigPath, optional: true, reloadOnChange: true); - -//Print Messages -StaticHelper.InitMessage(builder.Configuration); -//Check Migration -StaticHelper.CheckMigration(builder.Environment.WebRootPath, builder.Environment.ContentRootPath); - -// Add services to the container. -builder.Services.AddControllersWithViews(); - -//LiteDB is always injected even if user uses Postgres. -builder.Services.AddSingleton(); - -//data access method -if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])){ - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} -else -{ - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} - -//configure helpers -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -//configure logic -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -//Configure Auth -builder.Services.AddDataProtection(); -builder.Services.AddHttpContextAccessor(); -builder.Services.AddAuthentication("AuthN").AddScheme("AuthN", opts => { }); -builder.Services.AddAuthorization(options => -{ - options.DefaultPolicy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes("AuthN").RequireAuthenticatedUser().Build(); -}); -//Configure max file upload size -builder.Services.Configure(options => -{ - options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB -}); -builder.Services.Configure(options => -{ - options.ValueLengthLimit = int.MaxValue; - options.MultipartBodyLengthLimit = int.MaxValue; // if don't set default value is: 128 MB - options.MultipartHeadersLengthLimit = int.MaxValue; -}); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -app.UseExceptionHandler("/Home/Error"); - -app.UseStaticFiles(); -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "data", "images")), - RequestPath = "/images", - OnPrepareResponse = ctx => - { - if (ctx.Context.Request.Path.StartsWithSegments("/images")) - { - ctx.Context.Response.Headers.Append("Cache-Control", "no-store"); - if (!ctx.Context.User.Identity.IsAuthenticated) - { - ctx.Context.Response.Redirect("/Login"); - } - } - } -}); -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "data", "documents")), - RequestPath = "/documents", - OnPrepareResponse = ctx => - { - if (ctx.Context.Request.Path.StartsWithSegments("/documents")) - { - ctx.Context.Response.Headers.Append("Cache-Control", "no-store"); - if (!ctx.Context.User.Identity.IsAuthenticated) - { - ctx.Context.Response.Redirect("/Login"); - } - } - } -}); -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "data", "translations")), - RequestPath = "/translations" -}); -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "data", "temp")), - RequestPath = "/temp", - OnPrepareResponse = ctx => - { - if (ctx.Context.Request.Path.StartsWithSegments("/temp")) - { - ctx.Context.Response.Headers.Append("Cache-Control", "no-store"); - if (!ctx.Context.User.Identity.IsAuthenticated) - { - ctx.Context.Response.Redirect("/Login"); - } - } - } -}); - -app.UseRouting(); - -app.UseAuthorization(); - -app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - -app.Run(); diff --git a/README.md b/README.md deleted file mode 100644 index 8a65d33..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## Why -Because manually tracking vehicle maintenace can be challenging. The purpose is to maket his as easy as possible while allowing manual entry as well. - -## Showcase -[Promotional Brochure](https://motovaultpro.com/brochure.pdf) - -[Screenshots](/docs/screenshots.md) - -## Dependencies -- [Bootstrap](https://github.com/twbs/bootstrap) -- [LiteDB](https://github.com/mbdavid/litedb) -- [Npgsql](https://github.com/npgsql/npgsql) -- [Bootstrap-DatePicker](https://github.com/uxsolutions/bootstrap-datepicker) -- [SweetAlert2](https://github.com/sweetalert2/sweetalert2) -- [CsvHelper](https://github.com/JoshClose/CsvHelper) -- [Chart.js](https://github.com/chartjs/Chart.js) -- [Drawdown](https://github.com/adamvleggett/drawdown) -- [MailKit](https://github.com/jstedfast/MailKit) -- [Masonry](https://github.com/desandro/masonry) - -## License -MIT - -## Support -Support this project coming soon \ No newline at end of file diff --git a/Views/API/Index.cshtml b/Views/API/Index.cshtml deleted file mode 100644 index fd7a0f8..0000000 --- a/Views/API/Index.cshtml +++ /dev/null @@ -1,842 +0,0 @@ -@using MotoVaultPro.Helper -@{ - ViewData["Title"] = "API"; -} -@inject IConfigHelper config -@section Nav { -
-
-
-
- -
- API - If authentication is enabled, use the credentials of the user for Basic Auth(RFC2617) -
-
-
-
-} -
-
-
-
Method
-
-
-
Endpoint
-
-
-
Description
-
-
-
Parameters
-
-
-
-
- GET -
-
- /api/whoami -
-
- Returns information for current user -
-
- No Params -
-
-
-
- GET -
-
- /api/version -
-
- Returns current version of MotoVaultPro and checks for updates -
-
- CheckForUpdate(bool) - checks for update(optional) -
-
-
-
- GET -
-
- /api/vehicles -
-
- Returns a list of vehicles -
-
- No Params -
-
-
-
- GET -
-
- /api/vehicle/info -
-
- Returns details for list of vehicles or specific vehicle -
-
- VehicleId - Id of Vehicle(optional) -
-
-
-
- GET -
-
- /api/vehicle/adjustedodometer -
-
- Returns odometer reading with adjustments applied -
-
- vehicleId - Id of Vehicle -
- odometer - Unadjusted odometer -
-
-
-
- GET -
-
- /api/vehicle/odometerrecords -
-
- Returns a list of odometer records for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- GET -
-
- /api/vehicle/odometerrecords/latest -
-
- Returns last reported odometer for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- POST -
-
- /api/vehicle/odometerrecords/add -
-
- Adds Odometer Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- date - Date to be entered
- initialOdometer - Initial Odometer reading(optional)
- odometer - Odometer reading
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/odometerrecords/update -
-
- Updates Odometer Record -
-
- Body(form-data): {
- Id - Id of Odometer Record
- date - Date to be entered
- initialOdometer - Initial Odometer reading
- odometer - Odometer reading
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/odometerrecords/delete -
-
- Deletes Odometer Record -
-
- Id - Id of Odometer Record -
-
-
-
- GET -
-
- /api/vehicle/planrecords -
-
- Returns a list of plan records for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- POST -
-
- /api/vehicle/planrecords/add -
-
- Adds Plan Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- description - Description
- cost - Cost
- priority - Low/Normal/Critical
- progress - Backlog/InProgress/Testing
- notes - notes(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/planrecords/update -
-
- Updates Plan Record -
-
- Body(form-data): {
- Id - Id of Plan Record
- description - Description
- cost - Cost
- priority - Low/Normal/Critical
- progress - Backlog/InProgress/Testing
- notes - notes(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/planrecords/delete -
-
- Deletes Plan Record -
-
- Id - Id of Plan Record -
-
-
-
- GET -
-
- /api/vehicle/servicerecords -
-
- Returns a list of service records for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- POST -
-
- /api/vehicle/servicerecords/add -
-
- Adds Service Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- date - Date to be entered
- odometer - Odometer reading
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/servicerecords/update -
-
- Updates Service Record -
-
- Body(form-data): {
- Id - Id of Service Record
- date - Date to be entered
- odometer - Odometer reading
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/servicerecords/delete -
-
- Deletes Service Record -
-
- Id - Id of Service Record -
-
-
-
- GET -
-
-
-
-
-
- vehicleId - Id of Vehicle -
-
-
-
- POST -
-
-
-
-
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- date - Date to be entered
- odometer - Odometer reading
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
-
-
-
-
- Body(form-data): {
- date - Date to be entered
- odometer - Odometer reading
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
-
-
-
-
-
-
-
-
- GET -
-
- /api/vehicle/upgraderecords -
-
- Returns a list of upgrade records for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- POST -
-
- /api/vehicle/upgraderecords/add -
-
- Adds Upgrade Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- date - Date to be entered
- odometer - Odometer reading
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/upgraderecords/update -
-
- Updates Upgrade Record -
-
- Body(form-data): {
- Id - Id of Upgrade Record
- date - Date to be entered
- odometer - Odometer reading
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/upgraderecords/delete -
-
- Deletes Upgrade Record -
-
- Id - Id of Upgrade Record -
-
-
-
- GET -
-
- /api/vehicle/taxrecords -
-
- Returns a list of tax records for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- GET -
-
- /api/vehicle/taxrecords/check -
-
- Updates Outdated Recurring Tax Records -
-
- No Params -
-
-
-
- POST -
-
- /api/vehicle/taxrecords/add -
-
- Adds Tax Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- date - Date to be entered
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/taxrecords/update -
-
- Updates Tax Record -
-
- Body(form-data): {
- Id - Id of Tax Record
- date - Date to be entered
- description - Description
- cost - Cost
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/taxrecords/delete -
-
- Deletes Tax Record -
-
- Id - Id of Tax Record -
-
-
-
- GET -
-
- /api/vehicle/gasrecords -
-
- Returns a list of gas records for the vehicle -
-
- vehicleId - Id of Vehicle -
- useMPG(bool) - Use Imperial Units and Calculation -
- useUKMPG(bool) - Use UK Imperial Calculation -
-
-
-
- POST -
-
- /api/vehicle/gasrecords/add -
-
- Adds Gas Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- date - Date to be entered
- odometer - Odometer reading
- fuelConsumed - Fuel Consumed
- cost - Cost
- isFillToFull(bool) - Filled To Full
- missedFuelUp(bool) - Missed Fuel Up
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/gasrecords/update -
-
- Updates Gas Record -
-
- Body(form-data): {
- Id - Id of Gas Record
- date - Date to be entered
- odometer - Odometer reading
- fuelConsumed - Fuel Consumed
- cost - Cost
- isFillToFull(bool) - Filled To Full
- missedFuelUp(bool) - Missed Fuel Up
- notes - notes(optional)
- tags - tags separated by space(optional)
- extrafields - extrafields(optional)
- files - attachments(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/gasrecords/delete -
-
- Deletes Gas Record -
-
- Id - Id of Gas Record -
-
-
-
- GET -
-
- /api/vehicle/reminders -
-
- Returns a list of reminders for the vehicle -
-
- vehicleId - Id of Vehicle -
-
-
-
- POST -
-
- /api/vehicle/reminders/add -
-
- Adds Reminder Record to the vehicle -
-
- vehicleId - Id of Vehicle -
- Body(form-data): {
- description - Description
- dueDate - Due Date
- dueOdometer - Due Odometer reading
- metric - Date/Odometer/Both
- notes - notes(optional)
- tags - tags separated by space(optional)
- } -
-
-
-
- PUT -
-
- /api/vehicle/reminders/update -
-
- Updates Reminder Record -
-
- Body(form-data): {
- Id - Id of Reminder Record
- description - Description
- dueDate - Due Date
- dueOdometer - Due Odometer reading
- metric - Date/Odometer/Both
- notes - notes(optional)
- tags - tags separated by space(optional)
- } -
-
-
-
- DELETE -
-
- /api/vehicle/reminders/delete -
-
- Deletes Reminder Record -
-
- Id - Id of Reminder Record -
-
-
-
- GET -
-
- /api/calendar -
-
- Returns reminder calendar in ICS format -
-
- No Params -
-
-
-
- POST -
-
- /api/documents/upload -
-
- Upload Documents -
-
- Body(form-data): {
- documents[] - Files to Upload
- } -
-
-@if (User.IsInRole(nameof(UserData.IsRootUser))) -{ -
-
- GET -
-
- /api/vehicle/reminders/send -
-
- Send reminder emails out to collaborators based on specified urgency. -
-
- (must be root user)
- urgencies[]=[NotUrgent,Urgent,VeryUrgent,PastDue](optional) -
-
-
-
- GET -
-
- /api/makebackup -
-
- Creates a snapshot/backup of the DB at the time and returns a URL to download it. -
-
- No Params(must be root user) -
-
-
-
- GET -
-
- /api/cleanup -
-
- Clears out temp files. Deep clean will also delete unlinked thumbnails and documents. Returns number of deleted files. -
-
- (must be root user)
- deepClean(bool) - Perform deep clean(optional) -
-
-} -
- \ No newline at end of file diff --git a/Views/Admin/Index.cshtml b/Views/Admin/Index.cshtml deleted file mode 100644 index 1aebfac..0000000 --- a/Views/Admin/Index.cshtml +++ /dev/null @@ -1,189 +0,0 @@ -@using MotoVaultPro.Helper -@{ - ViewData["Title"] = "Admin Panel"; -} -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - bool emailServerIsSetup = true; - var mailConfig = config.GetMailConfig(); - var userLanguage = config.GetServerLanguage(); - if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - emailServerIsSetup = false; - } -} -@section Nav { -
-
-
-
- -
- @translator.Translate(userLanguage, "Admin Panel") -
-
-
-
-} -@model AdminViewModel -
-
-
-
-
- @translator.Translate(userLanguage, "Users") -
-
- -
-
-
- - - - - - - - - - - @await Html.PartialAsync("_Users", Model.Users) - -
@translator.Translate(userLanguage, "Username")@translator.Translate(userLanguage, "Email")@translator.Translate(userLanguage, "Is Admin")@translator.Translate(userLanguage, "Delete")
-
-
- -
- \ No newline at end of file diff --git a/Views/Admin/_Tokens.cshtml b/Views/Admin/_Tokens.cshtml deleted file mode 100644 index 844cf34..0000000 --- a/Views/Admin/_Tokens.cshtml +++ /dev/null @@ -1,12 +0,0 @@ -@using MotoVaultPro.Helper -@model List -@foreach (Token token in Model) -{ - - @token.Body - @StaticHelper.TruncateStrings(token.EmailAddress) - - - - -} \ No newline at end of file diff --git a/Views/Admin/_Users.cshtml b/Views/Admin/_Users.cshtml deleted file mode 100644 index c6d5860..0000000 --- a/Views/Admin/_Users.cshtml +++ /dev/null @@ -1,11 +0,0 @@ -@using MotoVaultPro.Helper -@model List -@foreach (UserData userData in Model) -{ - - @StaticHelper.TruncateStrings(userData.UserName) - @StaticHelper.TruncateStrings(userData.EmailAddress) - - - -} \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml deleted file mode 100644 index 0219051..0000000 --- a/Views/Home/Index.cshtml +++ /dev/null @@ -1,172 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model string -@{ - ViewData["Title"] = "Garage"; -} -@section Scripts { - - - - -} -@section Nav { -
-
-
-
- - -
- -
- -
-
-
-
-
-} -
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - -
-
- \ No newline at end of file diff --git a/Views/Home/Kiosk.cshtml b/Views/Home/Kiosk.cshtml deleted file mode 100644 index cefc5e7..0000000 --- a/Views/Home/Kiosk.cshtml +++ /dev/null @@ -1,157 +0,0 @@ -@{ - ViewData["Title"] = "Kiosk"; -} -@model KioskViewModel -@section Scripts { - - -} -
-
-
-
-
-
-
- \ No newline at end of file diff --git a/Views/Home/Setup.cshtml b/Views/Home/Setup.cshtml deleted file mode 100644 index 8f69b90..0000000 --- a/Views/Home/Setup.cshtml +++ /dev/null @@ -1,276 +0,0 @@ -@using MotoVaultPro.Helper -@{ - ViewData["Title"] = "Server Settings"; -} -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - bool emailServerIsSetup = true; - var mailConfig = config.GetMailConfig(); - var userLanguage = config.GetServerLanguage(); - if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer)) - { - emailServerIsSetup = false; - } -} -@section Scripts { - -} -@section Nav { -
-
-
-
- -
- @translator.Translate(userLanguage, "Server Settings Configurator") -
-
-
-
-} -@model ServerSettingsViewModel -
-
-
-
@translator.Translate(userLanguage, "Server Settings Configurator")
-
@translator.Translate(userLanguage, "By proceeding, you acknowledge that you are solely responsible for all consequences from utilizing the Server Settings Configurator")
- -
-
- - - - - - -
\ No newline at end of file diff --git a/Views/Home/_AccountModal.cshtml b/Views/Home/_AccountModal.cshtml deleted file mode 100644 index 56b7ec0..0000000 --- a/Views/Home/_AccountModal.cshtml +++ /dev/null @@ -1,40 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model UserData -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - \ No newline at end of file diff --git a/Views/Home/_Calendar.cshtml b/Views/Home/_Calendar.cshtml deleted file mode 100644 index fec038b..0000000 --- a/Views/Home/_Calendar.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@using MotoVaultPro.Helper -@model List - -
-
-
-
-
-
- - \ No newline at end of file diff --git a/Views/Home/_ExtraFields.cshtml b/Views/Home/_ExtraFields.cshtml deleted file mode 100644 index c4c6f0e..0000000 --- a/Views/Home/_ExtraFields.cshtml +++ /dev/null @@ -1,142 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model RecordExtraField - - - - - \ No newline at end of file diff --git a/Views/Home/_GarageDisplay.cshtml b/Views/Home/_GarageDisplay.cshtml deleted file mode 100644 index 17a33eb..0000000 --- a/Views/Home/_GarageDisplay.cshtml +++ /dev/null @@ -1,89 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model List -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); -} -@if (recordTags.Any()) -{ -
-
- @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
-} -
- @foreach (VehicleViewModel vehicle in Model) - { - @if (!(userConfig.HideSoldVehicles && !string.IsNullOrWhiteSpace(vehicle.SoldDate))) - { -
-
- - @if (!string.IsNullOrWhiteSpace(vehicle.SoldDate)) - { -

@translator.Translate(userLanguage, "SOLD")

- } else if (vehicle.DashboardMetrics.Any()) - { -
- @if (vehicle.DashboardMetrics.Contains(DashboardMetric.Default) && vehicle.LastReportedMileage != default) - { - -
-
- @vehicle.LastReportedMileage.ToString("N0") -
- @if (vehicle.HasReminders) - { -
- -
- } -
- } - @if (vehicle.DashboardMetrics.Contains(DashboardMetric.CostPerMile) && vehicle.CostPerMile != default) - { -
-
- @($"{vehicle.CostPerMile.ToString("C2")}/{vehicle.DistanceUnit}") -
-
- } - @if (vehicle.DashboardMetrics.Contains(DashboardMetric.TotalCost) && vehicle.TotalCost != default) - { -
-
- @($"{vehicle.TotalCost.ToString("C2")}") -
-
- } -
- } -
-
@($"{vehicle.Year}")
-
@($"{vehicle.Make}")
-
@($"{vehicle.Model}")
-

@StaticHelper.GetVehicleIdentifier(vehicle)

-
-
-
- } - } -
-
- -
-
-
\ No newline at end of file diff --git a/Views/Home/_Kiosk.cshtml b/Views/Home/_Kiosk.cshtml deleted file mode 100644 index d792583..0000000 --- a/Views/Home/_Kiosk.cshtml +++ /dev/null @@ -1,124 +0,0 @@ -@using MotoVaultPro.Helper -@model List -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@if (Model.Any()) -{ -
- @foreach (VehicleInfo vehicle in Model) - { -
-
-
-
@($"{vehicle.VehicleData.Year} {vehicle.VehicleData.Make} {vehicle.VehicleData.Model} ({StaticHelper.GetVehicleIdentifier(vehicle.VehicleData)})")
-
-
-

@vehicle.ServiceRecordCount

-

@translator.Translate(userLanguage, "Service")

-

@vehicle.ServiceRecordCost.ToString("C0")

-
-
-

@translator.Translate(userLanguage, "Repairs")

-
-
-

@vehicle.UpgradeRecordCount

-

@translator.Translate(userLanguage, "Upgrades")

-

@vehicle.UpgradeRecordCost.ToString("C0")

-
-
-

@vehicle.GasRecordCount

-

@translator.Translate(userLanguage, "Fuel")

-

@vehicle.GasRecordCost.ToString("C0")

-
-
-
- @if (vehicle.PastDueReminderCount + vehicle.VeryUrgentReminderCount + vehicle.UrgentReminderCount + vehicle.NotUrgentReminderCount > 0) - { -
-
-
@translator.Translate(userLanguage, "Reminders")
-
-
-

@vehicle.PastDueReminderCount

-

@translator.Translate(userLanguage, "Past Due")

-
-
-

@vehicle.VeryUrgentReminderCount

-

@translator.Translate(userLanguage, "Very Urgent")

-
-
-

@vehicle.UrgentReminderCount

-

@translator.Translate(userLanguage, "Urgent")

-
-
-

@vehicle.NotUrgentReminderCount

-

@translator.Translate(userLanguage, "Not Urgent")

-
-
-
- } - @if (vehicle.NextReminder != null) - { -
-
-
@translator.Translate(userLanguage, "Upcoming Reminder")
-
-
-

@vehicle.NextReminder.Description

-

@translator.Translate(userLanguage, StaticHelper.GetTitleCaseReminderUrgency(vehicle.NextReminder.Urgency))

-
- @if (vehicle.NextReminder.Metric == "Date" || vehicle.NextReminder.Metric == "Both") - { -
@vehicle.NextReminder.DueDate
- } - @if (vehicle.NextReminder.Metric == "Odometer" || vehicle.NextReminder.Metric == "Both") - { -
@vehicle.NextReminder.DueOdometer
- } -
-
-
-
- } - @if (vehicle.PlanRecordBackLogCount + vehicle.PlanRecordInProgressCount + vehicle.PlanRecordTestingCount + vehicle.PlanRecordBackLogCount > 0) - { -
-
-
@translator.Translate(userLanguage, "Plans")
-
-
-

@vehicle.PlanRecordBackLogCount

-

@translator.Translate(userLanguage, "Planned")

-
-
-

@vehicle.PlanRecordInProgressCount

-

@translator.Translate(userLanguage, "Doing")

-
-
-

@vehicle.PlanRecordTestingCount

-

@translator.Translate(userLanguage, "Testing")

-
-
-

@vehicle.PlanRecordDoneCount

-

@translator.Translate(userLanguage, "Done")

-
-
-
- } -
-
- } -
-} -else -{ -
-
- @translator.Translate(userLanguage, "No records available to display") -
-
-} diff --git a/Views/Home/_KioskPlan.cshtml b/Views/Home/_KioskPlan.cshtml deleted file mode 100644 index c6bed03..0000000 --- a/Views/Home/_KioskPlan.cshtml +++ /dev/null @@ -1,85 +0,0 @@ -@using MotoVaultPro.Helper -@model List -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@if (Model.Any()) -{ -
- @foreach (PlanRecord plan in Model) - { -
-
-
-
@plan.Description
-
-
-

@plan.Notes

-

@translator.Translate(userLanguage, StaticHelper.GetPlanRecordProgress(plan.Progress))

-
-
- @if (plan.ImportMode == ImportMode.ServiceRecord) - { - @translator.Translate(userLanguage, "Service") - } - else if (plan.ImportMode == ImportMode.UpgradeRecord) - { - @translator.Translate(userLanguage, "Repairs") - } - { - @translator.Translate(userLanguage, "Upgrades") - } -
-
-
-
-
- @if (plan.RequisitionHistory.Any()) - { -
    -
  • -
    -
    - @translator.Translate(userLanguage, "Part Number") -
    -
    - @translator.Translate(userLanguage, "Description") -
    -
    - @translator.Translate(userLanguage, "Quantity") -
    -
    -
  • - @foreach (SupplyUsageHistory supply in plan.RequisitionHistory) - { -
  • -
    -
    - @supply.PartNumber -
    -
    - @supply.Description -
    -
    - @supply.Quantity -
    -
    -
  • - } -
- } -
-
- } -
-} else -{ -
-
- @translator.Translate(userLanguage, "No records available to display") -
-
-} \ No newline at end of file diff --git a/Views/Home/_KioskReminder.cshtml b/Views/Home/_KioskReminder.cshtml deleted file mode 100644 index 1c6c7fb..0000000 --- a/Views/Home/_KioskReminder.cshtml +++ /dev/null @@ -1,47 +0,0 @@ -@using MotoVaultPro.Helper -@model List -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@if (Model.Any()) -{ -
- @foreach (ReminderRecordViewModel reminder in Model) - { -
-
-
-
@reminder.Description
-
-
-

@reminder.Notes

-

@translator.Translate(userLanguage, StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency))

-
- @if (reminder.Metric == ReminderMetric.Date || reminder.Metric == ReminderMetric.Both) - { -
@reminder.Date.ToShortDateString()
- } - @if (reminder.Metric == ReminderMetric.Odometer || reminder.Metric == ReminderMetric.Both) - { -
@reminder.Mileage
- } -
-
-
-
-
-
- } -
-} -else -{ -
-
- @translator.Translate(userLanguage, "No records available to display") -
-
-} \ No newline at end of file diff --git a/Views/Home/_ReminderRecordCalendarModal.cshtml b/Views/Home/_ReminderRecordCalendarModal.cshtml deleted file mode 100644 index aa86e75..0000000 --- a/Views/Home/_ReminderRecordCalendarModal.cshtml +++ /dev/null @@ -1,39 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model ReminderRecordViewModel -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - \ No newline at end of file diff --git a/Views/Home/_RootAccountModal.cshtml b/Views/Home/_RootAccountModal.cshtml deleted file mode 100644 index 152a93f..0000000 --- a/Views/Home/_RootAccountModal.cshtml +++ /dev/null @@ -1,31 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model UserData -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - \ No newline at end of file diff --git a/Views/Home/_Settings.cshtml b/Views/Home/_Settings.cshtml deleted file mode 100644 index 102f80b..0000000 --- a/Views/Home/_Settings.cshtml +++ /dev/null @@ -1,421 +0,0 @@ -@using MotoVaultPro.Helper -@model SettingsViewModel -@inject ITranslationHelper translator -@{ - var userLanguage = Model.UserConfig.UserLanguage; -} - -
-
-
@translator.Translate(userLanguage, "Settings")
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
-
- - -
-
- -
-
-
-
- - -
-
- - -
-
- - -
-
- @if (User.IsInRole(nameof(UserData.IsRootUser))) - { -
-
- - -
- } -
-
-
-
-
-
- @translator.Translate(userLanguage, "Visible Tabs") -
-
- -
-
-
-
-
    -
  • - - -
  • -
  • - - -
  • -
  • -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
-
-
-
    -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
-
-
-
-
- @translator.Translate(userLanguage, "Default Tab") - -
-
- @translator.Translate(userLanguage, "Language") - @if (User.IsInRole(nameof(UserData.IsRootUser))) - { -
- -
- -
-
- } else - { - - } -
-
- @if (User.IsInRole(nameof(UserData.IsRootUser))) - { -
-
- @translator.Translate(userLanguage, "Backups") -
-
- -
-
- - -
-
-
-
- @translator.Translate(userLanguage, "Manage Languages") -
-
- -
- - - -
- -
-
- -
-
-
-
-
-
-
-
- @translator.Translate(userLanguage, "Server-wide Settings") -
-
-
-
- -
- -
-
-
- } -
-
-
-
-
@translator.Translate(userLanguage, "About")
-
-
-
-
- -
-
- @($"{translator.Translate(userLanguage, "Version")} {StaticHelper.VersionNumber}") -
-

- Developed in Madison, Wisconsin by FB Technologies LLC. -

-

- If you enjoyed using this app, please consider posting and building the community.
-

-

- This was forked from https://github.com/hargata/lubelog/
-

-
-
-
-
Open Source Dependencies
-
-

- MotoVaultPro utilizes open-source dependencies to serve you the best possible user experience, those dependencies are: -

-
    -
  • Bootstrap
  • -
  • Bootstrap-DatePicker
  • -
  • LiteDB
  • -
  • Npgsql
  • -
  • SweetAlert2
  • -
  • CsvHelper
  • -
  • Chart.js
  • -
  • Drawdown
  • -
  • MailKit
  • -
  • Masonry
  • -
-
-
-
- - - - - - \ No newline at end of file diff --git a/Views/Home/_Sponsors.cshtml b/Views/Home/_Sponsors.cshtml deleted file mode 100644 index ddab2b2..0000000 --- a/Views/Home/_Sponsors.cshtml +++ /dev/null @@ -1,70 +0,0 @@ -@using MotoVaultPro.Helper -@model Sponsors -@inject ITranslationHelper translator -@inject IConfigHelper config -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -
-
@translator.Translate(userLanguage, "Sponsors")
-
-
- - @if (Model.LifeTime.Any()) - { -
-
-
Lifetime
-
-
-

- @string.Join(", ", Model.LifeTime) -

-
-
- } - @if (Model.Gold.Any()) - { -
-
-
Gold
-
-
-

- @string.Join(", ", Model.Gold) -

-
-
- } - @if (Model.Silver.Any()) - { -
-
-
Silver
-
-
-

- @string.Join(", ", Model.Silver) -

-
-
- } - @if (Model.Bronze.Any()) - { -
-
-
Bronze
-
-
-

- @string.Join(", ", Model.Bronze) -

-
-
- } - diff --git a/Views/Home/_TranslationEditor.cshtml b/Views/Home/_TranslationEditor.cshtml deleted file mode 100644 index 12fb86d..0000000 --- a/Views/Home/_TranslationEditor.cshtml +++ /dev/null @@ -1,48 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@inject IConfiguration serverConfig; -@model Dictionary -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - bool showDelete = bool.Parse(serverConfig["MOTOVAULTPRO_TRANSLATOR"] ?? "false"); -} - - - \ No newline at end of file diff --git a/Views/Home/_Translations.cshtml b/Views/Home/_Translations.cshtml deleted file mode 100644 index 155209f..0000000 --- a/Views/Home/_Translations.cshtml +++ /dev/null @@ -1,76 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model Translations -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - \ No newline at end of file diff --git a/Views/Home/_VehicleExtraFields.cshtml b/Views/Home/_VehicleExtraFields.cshtml deleted file mode 100644 index d27fe87..0000000 --- a/Views/Home/_VehicleExtraFields.cshtml +++ /dev/null @@ -1,10 +0,0 @@ -@model List -@if (Model.Any()) -{ -
    - @foreach (ExtraField field in Model) - { -
  • @field.Name : @field.Value
  • - } -
-} \ No newline at end of file diff --git a/Views/Home/_VehicleSelector.cshtml b/Views/Home/_VehicleSelector.cshtml deleted file mode 100644 index 5098c4d..0000000 --- a/Views/Home/_VehicleSelector.cshtml +++ /dev/null @@ -1,26 +0,0 @@ -@inject IConfigHelper config -@inject ITranslationHelper translator -@using MotoVaultPro.Helper -@model List - -@{ - var userLanguage = config.GetUserConfig(User).UserLanguage; -} - -@if (Model.Any()) -{ -
-
    - @foreach (Vehicle vehicle in Model) - { -
  • - - -
  • - } -
-
-} else -{ -
@translator.Translate(userLanguage, "No Vehicles Available")
-} \ No newline at end of file diff --git a/Views/Home/_WidgetEditor.cshtml b/Views/Home/_WidgetEditor.cshtml deleted file mode 100644 index 2a2e281..0000000 --- a/Views/Home/_WidgetEditor.cshtml +++ /dev/null @@ -1,28 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model string -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - \ No newline at end of file diff --git a/Views/Login/ForgotPassword.cshtml b/Views/Login/ForgotPassword.cshtml deleted file mode 100644 index f4fb17a..0000000 --- a/Views/Login/ForgotPassword.cshtml +++ /dev/null @@ -1,32 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userLanguage = config.GetServerLanguage(); -} -@{ - ViewData["Title"] = "Forgot Password"; -} -@section Scripts { - -} -
-
-
- -
- - -
-
- -
- - -
-
-
\ No newline at end of file diff --git a/Views/Login/Index.cshtml b/Views/Login/Index.cshtml deleted file mode 100644 index 7108f75..0000000 --- a/Views/Login/Index.cshtml +++ /dev/null @@ -1,67 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model string -@{ - var userLanguage = config.GetServerLanguage(); - var registrationDisabled = config.GetServerDisabledRegistration(); - var openIdConfigName = config.GetOpenIDConfig().Name; -} -@{ - ViewData["Title"] = "Login"; -} -@section Scripts { - -} -
-
-
- -
- - -
-
- -
- -
- -
-
-
-
- - -
-
- -
- @if (!string.IsNullOrWhiteSpace(openIdConfigName)) - { -
- -
- } -
-
- @config.GetMOTD() -
-
- - @if (!registrationDisabled) - { - - } -
-
-
- \ No newline at end of file diff --git a/Views/Login/OpenIDRegistration.cshtml b/Views/Login/OpenIDRegistration.cshtml deleted file mode 100644 index f0653ae..0000000 --- a/Views/Login/OpenIDRegistration.cshtml +++ /dev/null @@ -1,72 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userLanguage = config.GetServerLanguage(); - var openRegistrationEnabled = config.GetServerOpenRegistration(); -} -@model string -@{ - ViewData["Title"] = "Register"; -} -@section Scripts { - -} -
-
-
- -
- - @if (openRegistrationEnabled) - { -
- -
- -
-
- } - else - { - - } -
-
- - -
-
- -
- -
-
-
- \ No newline at end of file diff --git a/Views/Login/Registration.cshtml b/Views/Login/Registration.cshtml deleted file mode 100644 index 1e112ef..0000000 --- a/Views/Login/Registration.cshtml +++ /dev/null @@ -1,57 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userLanguage = config.GetServerLanguage(); - var openRegistrationEnabled = config.GetServerOpenRegistration(); -} -@model LoginModel -@{ - ViewData["Title"] = "Register"; -} -@section Scripts { - -} -
-
-
- -
- - @if (openRegistrationEnabled) { -
- -
- -
-
- } else { - - } -
-
- - -
-
- - -
-
- -
- -
- -
-
-
-
- -
- -
-
-
\ No newline at end of file diff --git a/Views/Login/RemoteAuthDebug.cshtml b/Views/Login/RemoteAuthDebug.cshtml deleted file mode 100644 index f695ce4..0000000 --- a/Views/Login/RemoteAuthDebug.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@model List -@{ - ViewData["Title"] = "Remote Auth Debug"; -} -
-
- @foreach (OperationResponse result in Model) - { -
-
- -
-
- } -
-
- - \ No newline at end of file diff --git a/Views/Login/ResetPassword.cshtml b/Views/Login/ResetPassword.cshtml deleted file mode 100644 index e89cc33..0000000 --- a/Views/Login/ResetPassword.cshtml +++ /dev/null @@ -1,43 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userLanguage = config.GetServerLanguage(); -} -@model LoginModel -@{ - ViewData["Title"] = "Reset Password"; -} -@section Scripts { - -} -
-
-
- -
- - -
-
- - -
-
- -
- -
- -
-
-
-
- -
- -
-
-
\ No newline at end of file diff --git a/Views/Migration/Index.cshtml b/Views/Migration/Index.cshtml deleted file mode 100644 index 749b00b..0000000 --- a/Views/Migration/Index.cshtml +++ /dev/null @@ -1,92 +0,0 @@ -@{ - ViewData["Title"] = "Database Migration"; -} -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userLanguage = config.GetServerLanguage(); -} -@using MotoVaultPro.Helper -@section Nav { -
-
-
-
- -
- @translator.Translate(userLanguage, "Database Migration") -
-
-
-
-} -@model AdminViewModel -
-
-
-
    -
  • @translator.Translate(userLanguage, "Instructions")
  • -
  • @translator.Translate(userLanguage, "Use this tool to migrate data between LiteDB and Postgres")
  • -
  • @translator.Translate(userLanguage, "Note that it is recommended that the Postgres DB is empty when importing from LiteDB to prevent primary key errors.")
  • -
-
-
-
-
-
-
- - -
-
- -
-
-
-
- \ No newline at end of file diff --git a/Views/Shared/401.cshtml b/Views/Shared/401.cshtml deleted file mode 100644 index 8b70feb..0000000 --- a/Views/Shared/401.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -
-
- -
-
-

Access Denied

-
-
\ No newline at end of file diff --git a/Views/Shared/Error.cshtml b/Views/Shared/Error.cshtml deleted file mode 100644 index a1e0478..0000000 --- a/Views/Shared/Error.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@model ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml deleted file mode 100644 index 93fd487..0000000 --- a/Views/Shared/_Layout.cshtml +++ /dev/null @@ -1,190 +0,0 @@ -@using MotoVaultPro.Helper -@using System.Globalization - -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var useDarkMode = userConfig.UseDarkMode; - var useSystemColorMode = userConfig.UseSystemColorMode; - var enableCsvImports = userConfig.EnableCsvImports; - var useMPG = userConfig.UseMPG; - var useMarkDown = userConfig.UseMarkDownOnSavedNotes; - var useThreeDecimals = userConfig.UseThreeDecimalGasCost; - var automaticDecimalFormat = userConfig.AutomaticDecimalFormat; - var shortDatePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; - var firstDayOfWeek = (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek; - var numberFormat = CultureInfo.CurrentCulture.NumberFormat; - var userLanguage = userConfig.UserLanguage; - shortDatePattern = shortDatePattern.ToLower(); - if (!shortDatePattern.Contains("dd")) - { - shortDatePattern = shortDatePattern.Replace("d", "dd"); - } - if (!shortDatePattern.Contains("mm")) - { - shortDatePattern = shortDatePattern.Replace("m", "mm"); - } -} - - - - - - - - - - @ViewData["Title"] - MotoVaultPro - - - - - - - - - - - - - - - - - - - - - - - @await RenderSectionAsync("Scripts", required: false) - - - @await RenderSectionAsync("Nav", required: false) -
-
- @RenderBody() -
-
- @await RenderSectionAsync("Footer", required: false) - - -@if (useSystemColorMode) -{ - -} \ No newline at end of file diff --git a/Views/Shared/_Layout.cshtml.css b/Views/Shared/_Layout.cshtml.css deleted file mode 100644 index c187c02..0000000 --- a/Views/Shared/_Layout.cshtml.css +++ /dev/null @@ -1,48 +0,0 @@ -/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification -for details on configuring this project to bundle and minify static web assets. */ - -a.navbar-brand { - white-space: normal; - text-align: center; - word-break: break-all; -} - -a { - color: #0077cc; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.border-top { - border-top: 1px solid #e5e5e5; -} -.border-bottom { - border-bottom: 1px solid #e5e5e5; -} - -.box-shadow { - box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); -} - -button.accept-policy { - font-size: 1rem; - line-height: inherit; -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - white-space: nowrap; - line-height: 60px; -} diff --git a/Views/Shared/_ValidationScriptsPartial.cshtml b/Views/Shared/_ValidationScriptsPartial.cshtml deleted file mode 100644 index 5a16d80..0000000 --- a/Views/Shared/_ValidationScriptsPartial.cshtml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/Views/Vehicle/Gas/_Gas.cshtml b/Views/Vehicle/Gas/_Gas.cshtml deleted file mode 100644 index 3dd8c91..0000000 --- a/Views/Vehicle/Gas/_Gas.cshtml +++ /dev/null @@ -1,292 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject IGasHelper gasHelper -@inject ITranslationHelper translator -@model GasRecordViewModelContainer -@{ - var userConfig = config.GetUserConfig(User); - var enableCsvImports = userConfig.EnableCsvImports; - var useMPG = userConfig.UseMPG; - var useUKMPG = userConfig.UseUKMPG; - var hideZero = userConfig.HideZero; - var useThreeDecimals = userConfig.UseThreeDecimalGasCost; - var useThreeDecimalsConsumption = userConfig.UseThreeDecimalGasConsumption; - var gasCostFormat = useThreeDecimals ? "C3" : "C2"; - var gasConsumptionFormat = useThreeDecimalsConsumption ? "F3" : "F2"; - var userLanguage = userConfig.UserLanguage; - var useKwh = Model.UseKwh; - var useHours = Model.UseHours; - var recordTags = Model.GasRecords.SelectMany(x => x.Tags).Distinct(); - string preferredFuelEconomyUnit = userConfig.PreferredGasMileageUnit; - string preferredGasUnit = userConfig.PreferredGasUnit; - string consumptionUnit; - string fuelEconomyUnit; - string distanceUnit = useHours ? "h" : (useMPG ? "mi." : "km"); - if (useKwh) - { - consumptionUnit = "kWh"; - fuelEconomyUnit = useMPG ? $"{distanceUnit}/kWh" : $"kWh/100{distanceUnit}"; - } - else if (useMPG && useUKMPG) - { - consumptionUnit = "imp gal"; - fuelEconomyUnit = useHours ? "h/g" : "mpg"; - } - else if (useUKMPG) - { - fuelEconomyUnit = useHours ? "l/100h" : "l/100mi."; - consumptionUnit = "l"; - distanceUnit = useHours ? "h" : "mi."; - } - else - { - consumptionUnit = useMPG ? "US gal" : "l"; - fuelEconomyUnit = useHours ? (useMPG ? "h/g" : "l/100h") : (useMPG ? "mpg" : "l/100km"); - } - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.GasRecords.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.GasRecord); -} -
-
-
- @($"{translator.Translate(userLanguage, "# of Gas Records")}: {Model.GasRecords.Count()}") - @if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any()) - { - @($"{translator.Translate(userLanguage, "Average Fuel Economy")}: {gasHelper.GetAverageGasMileage(Model.GasRecords, useMPG)}") - if (useMPG) - { - @($"{translator.Translate(userLanguage, "Min Fuel Economy")}: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}") - @($"{translator.Translate(userLanguage, "Max Fuel Economy")}: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}") - } - else - { - @($"{translator.Translate(userLanguage, "Min Fuel Economy")}: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}") - @($"{translator.Translate(userLanguage, "Max Fuel Economy")}: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}") - } - @($"{translator.Translate(userLanguage, "Total Distance")}: {Model.GasRecords.Sum(x => x.DeltaMileage).ToString() ?? "0"} {distanceUnit}") - } - @($"{translator.Translate(userLanguage, "Total Fuel Consumed")}: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}") - @($"{translator.Translate(userLanguage, "Total Cost")}: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
- -
-
- - - - - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (GasRecordViewModel gasRecord in Model.GasRecords) - { - - - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Date Refueled")@($"{translator.Translate(userLanguage, "Odometer")}({distanceUnit})")@($"Δ({distanceUnit})")@($"{translator.Translate(userLanguage, "Consumption")}({consumptionUnit})")@($"{@translator.Translate(userLanguage, "Fuel Economy")}({fuelEconomyUnit})")@translator.Translate(userLanguage, "Cost")@translator.Translate(userLanguage, "Unit Cost")
@gasRecord.Date@(gasRecord.Mileage == default ? "---" : gasRecord.Mileage.ToString())@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)@gasRecord.Gallons.ToString(gasConsumptionFormat)@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))
- @StaticHelper.ReportNote -
-
-
- - - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} - \ No newline at end of file diff --git a/Views/Vehicle/Gas/_GasModal.cshtml b/Views/Vehicle/Gas/_GasModal.cshtml deleted file mode 100644 index d45e6a8..0000000 --- a/Views/Vehicle/Gas/_GasModal.cshtml +++ /dev/null @@ -1,171 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model GasRecordInputContainer -@{ - var userConfig = config.GetUserConfig(User); - var useMPG = userConfig.UseMPG; - var useUKMPG = userConfig.UseUKMPG; - var userLanguage = userConfig.UserLanguage; - var useThreeDecimals = userConfig.UseThreeDecimalGasCost; - var useThreeDecimalsConsumption = userConfig.UseThreeDecimalGasConsumption; - var useKwh = Model.UseKwh; - var useHours = Model.UseHours; - var isNew = Model.GasRecord.Id == 0; - var useUnitFuelCost = userConfig.UseUnitForFuelCost; - var useSimpleFuelEntry = userConfig.UseSimpleFuelEntry; - string consumptionUnit; - string distanceUnit; - if (useKwh) - { - consumptionUnit = "kWh"; - } else if (useUKMPG) - { - consumptionUnit = @translator.Translate(userLanguage, "liters"); - } - else - { - consumptionUnit = useMPG ? @translator.Translate(userLanguage, "gallons") : @translator.Translate(userLanguage, "liters"); - } - if (useHours) - { - distanceUnit = @translator.Translate(userLanguage, "hours"); - } - else if (useUKMPG) - { - distanceUnit = @translator.Translate(userLanguage, "miles"); - } - else - { - distanceUnit = useMPG ? @translator.Translate(userLanguage, "miles") : @translator.Translate(userLanguage, "kilometers"); - } -} - - - - \ No newline at end of file diff --git a/Views/Vehicle/Gas/_GasRecordsModal.cshtml b/Views/Vehicle/Gas/_GasRecordsModal.cshtml deleted file mode 100644 index a415343..0000000 --- a/Views/Vehicle/Gas/_GasRecordsModal.cshtml +++ /dev/null @@ -1,53 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model GasRecordEditModel -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var useThreeDecimals = userConfig.UseThreeDecimalGasCost; - var useThreeDecimalsConsumption = userConfig.UseThreeDecimalGasConsumption; -} - - - - \ No newline at end of file diff --git a/Views/Vehicle/Index.cshtml b/Views/Vehicle/Index.cshtml deleted file mode 100644 index f359f78..0000000 --- a/Views/Vehicle/Index.cshtml +++ /dev/null @@ -1,213 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model Vehicle -@{ - ViewData["Title"] = $"{Model.Year} {Model.Make} {Model.Model} ({StaticHelper.GetVehicleIdentifier(Model)})"; -} -@section Scripts { - - - - - - - - - - - - - -} -@section Nav{ -
-
-
-
- @if(userConfig.ShowVehicleThumbnail) { - - } else { - - } -
- - @($"{Model.Year} {Model.Make} {Model.Model}")@($"(#{StaticHelper.GetVehicleIdentifier(Model)})") -
- -
-
-
-
-
-} -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - -
-
- \ No newline at end of file diff --git a/Views/Vehicle/Note/_NoteModal.cshtml b/Views/Vehicle/Note/_NoteModal.cshtml deleted file mode 100644 index 3a01503..0000000 --- a/Views/Vehicle/Note/_NoteModal.cshtml +++ /dev/null @@ -1,71 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model Note -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - - diff --git a/Views/Vehicle/Note/_Notes.cshtml b/Views/Vehicle/Note/_Notes.cshtml deleted file mode 100644 index de33932..0000000 --- a/Views/Vehicle/Note/_Notes.cshtml +++ /dev/null @@ -1,178 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model List -@{ - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var enableCsvImports = userConfig.EnableCsvImports; - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.NoteRecord); -} -
-
-
- @($"{translator.Translate(userLanguage,"# of Notes")}: {Model.Count()}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (Note note in Model) - { - - @if (note.Pinned) - { - - } else - { - - } - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Note")
@note.Description@note.Description@StaticHelper.TruncateStrings(note.NoteText, 100)
- @StaticHelper.ReportNote -
-
-
- - - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/Odometer/_OdometerRecordModal.cshtml b/Views/Vehicle/Odometer/_OdometerRecordModal.cshtml deleted file mode 100644 index e6b909a..0000000 --- a/Views/Vehicle/Odometer/_OdometerRecordModal.cshtml +++ /dev/null @@ -1,121 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model OdometerRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - - - - \ No newline at end of file diff --git a/Views/Vehicle/Odometer/_OdometerRecords.cshtml b/Views/Vehicle/Odometer/_OdometerRecords.cshtml deleted file mode 100644 index 7a2dfec..0000000 --- a/Views/Vehicle/Odometer/_OdometerRecords.cshtml +++ /dev/null @@ -1,202 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var enableCsvImports = userConfig.EnableCsvImports; - var hideZero = userConfig.HideZero; - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var userLanguage = userConfig.UserLanguage; - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x=>x.Tab == ImportMode.OdometerRecord); -} -@model List -
-
-
- @($"{translator.Translate(userLanguage, "# of Odometer Records")}: {Model.Count()}") - @($"{translator.Translate(userLanguage, "Total Distance")}: {Model.Sum(x => x.DistanceTraveled)}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (OdometerRecord odometerRecord in Model) - { - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Date")@translator.Translate(userLanguage, "Initial Odometer")@translator.Translate(userLanguage, "Odometer")@translator.Translate(userLanguage, "Distance")@translator.Translate(userLanguage, "Notes")
@odometerRecord.Date.ToShortDateString()@odometerRecord.InitialMileage@odometerRecord.Mileage@(odometerRecord.DistanceTraveled == default ? "---" : odometerRecord.DistanceTraveled)@StaticHelper.TruncateStrings(odometerRecord.Notes, 75)
- @StaticHelper.ReportNote -
-
-
- - - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/Odometer/_OdometerRecordsModal.cshtml b/Views/Vehicle/Odometer/_OdometerRecordsModal.cshtml deleted file mode 100644 index 7031a2c..0000000 --- a/Views/Vehicle/Odometer/_OdometerRecordsModal.cshtml +++ /dev/null @@ -1,49 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model OdometerRecordEditModel -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - - \ No newline at end of file diff --git a/Views/Vehicle/Plan/_PlanOrderSupplies.cshtml b/Views/Vehicle/Plan/_PlanOrderSupplies.cshtml deleted file mode 100644 index 75520d8..0000000 --- a/Views/Vehicle/Plan/_PlanOrderSupplies.cshtml +++ /dev/null @@ -1,46 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model List -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - \ No newline at end of file diff --git a/Views/Vehicle/Plan/_PlanRecordItem.cshtml b/Views/Vehicle/Plan/_PlanRecordItem.cshtml deleted file mode 100644 index 71e3ff9..0000000 --- a/Views/Vehicle/Plan/_PlanRecordItem.cshtml +++ /dev/null @@ -1,54 +0,0 @@ -@model PlanRecord -
-
-
-
- @if (Model.Progress == PlanProgress.Done) - { - @Model.Description - } else - { - @Model.Description - } -
-
- @Model.Cost.ToString("C2") -
-
-
- @if (Model.ReminderRecordId != default) - { -
- -
- } -
- @if (Model.ImportMode == ImportMode.ServiceRecord) - { - - } - else if (Model.ImportMode == ImportMode.UpgradeRecord) - { - - } - { - - } -
-
- @if (Model.Priority == PlanPriority.Critical) - { - - } - else if (Model.Priority == PlanPriority.Normal) - { - - } - else if (Model.Priority == PlanPriority.Low) - { - - } -
-
-
-
\ No newline at end of file diff --git a/Views/Vehicle/Plan/_PlanRecordModal.cshtml b/Views/Vehicle/Plan/_PlanRecordModal.cshtml deleted file mode 100644 index 5f079e4..0000000 --- a/Views/Vehicle/Plan/_PlanRecordModal.cshtml +++ /dev/null @@ -1,102 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model PlanRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - -@await Html.PartialAsync("Supply/_SupplyRequisitionHistory", new SupplyRequisitionHistory { RequisitionHistory = Model.RequisitionHistory, CostInputId = "planRecordCost" }) - \ No newline at end of file diff --git a/Views/Vehicle/Plan/_PlanRecordTemplateEditModal.cshtml b/Views/Vehicle/Plan/_PlanRecordTemplateEditModal.cshtml deleted file mode 100644 index dce95bf..0000000 --- a/Views/Vehicle/Plan/_PlanRecordTemplateEditModal.cshtml +++ /dev/null @@ -1,89 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model PlanRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - -@await Html.PartialAsync("Supply/_SupplyRequisitionHistory", new SupplyRequisitionHistory { RequisitionHistory = Model.RequisitionHistory, CostInputId = "planRecordCost" }) - \ No newline at end of file diff --git a/Views/Vehicle/Plan/_PlanRecordTemplateModal.cshtml b/Views/Vehicle/Plan/_PlanRecordTemplateModal.cshtml deleted file mode 100644 index d1c6a5e..0000000 --- a/Views/Vehicle/Plan/_PlanRecordTemplateModal.cshtml +++ /dev/null @@ -1,90 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model List - - - \ No newline at end of file diff --git a/Views/Vehicle/Plan/_PlanRecords.cshtml b/Views/Vehicle/Plan/_PlanRecords.cshtml deleted file mode 100644 index 5e968a2..0000000 --- a/Views/Vehicle/Plan/_PlanRecords.cshtml +++ /dev/null @@ -1,134 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var enableCsvImports = userConfig.EnableCsvImports; - var hideZero = userConfig.HideZero; - var userLanguage = userConfig.UserLanguage; - var backLogItems = Model.Where(x => x.Progress == PlanProgress.Backlog).OrderBy(x=>x.Priority); - var inProgressItems = Model.Where(x => x.Progress == PlanProgress.InProgress).OrderBy(x => x.Priority); - var testingItems = Model.Where(x => x.Progress == PlanProgress.Testing).OrderBy(x => x.Priority); - var doneItems = Model.Where(x => x.Progress == PlanProgress.Done).OrderBy(x => x.Priority); -} -@model List -
-
-
- @($"{translator.Translate(userLanguage,"# of Plan Records")}: {Model.Count()}") -
-
- @if (enableCsvImports) - { - - } - else - { - - } -
-
-
-
-
-
-
- -
-
-
-
-
-
- @translator.Translate(userLanguage,"Planned") -
-
- @foreach (PlanRecord planRecord in backLogItems) - { - @await Html.PartialAsync("Plan/_PlanRecordItem", planRecord) - } -
-
-
-
- @translator.Translate(userLanguage,"Doing") -
-
- @foreach (PlanRecord planRecord in inProgressItems) - { - @await Html.PartialAsync("Plan/_PlanRecordItem", planRecord) - } -
-
-
-
- @translator.Translate(userLanguage,"Testing") -
-
- @foreach (PlanRecord planRecord in testingItems) - { - @await Html.PartialAsync("Plan/_PlanRecordItem", planRecord) - } -
-
-
-
- @translator.Translate(userLanguage,"Done") -
-
- @foreach (PlanRecord planRecord in doneItems) - { - @await Html.PartialAsync("Plan/_PlanRecordItem", planRecord) - } -
-
-
-
- - - - - - - - - \ No newline at end of file diff --git a/Views/Vehicle/Reminder/_ReminderRecordModal.cshtml b/Views/Vehicle/Reminder/_ReminderRecordModal.cshtml deleted file mode 100644 index b1ac694..0000000 --- a/Views/Vehicle/Reminder/_ReminderRecordModal.cshtml +++ /dev/null @@ -1,143 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model ReminderRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - - \ No newline at end of file diff --git a/Views/Vehicle/Reminder/_ReminderRecords.cshtml b/Views/Vehicle/Reminder/_ReminderRecords.cshtml deleted file mode 100644 index 4b05a2b..0000000 --- a/Views/Vehicle/Reminder/_ReminderRecords.cshtml +++ /dev/null @@ -1,246 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model List -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var hasRefresh = Model.Where(x => (x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue) && x.IsRecurring).Any(); - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var enableCsvImports = userConfig.EnableCsvImports; - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.ReminderRecord); -} -
-
-
- @($"{translator.Translate(userLanguage, "# of Reminders")}: {Model.Count()}") - @($"{translator.Translate(userLanguage, "Past Due")}: {Model.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()}") - @($"{translator.Translate(userLanguage, "Very Urgent")}: {Model.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count()}") - @($"{translator.Translate(userLanguage, "Urgent")}: {Model.Where(x => x.Urgency == ReminderUrgency.Urgent).Count()}") - @($"{translator.Translate(userLanguage, "Not Urgent")}: {Model.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count()}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - - - - - - @if (hasRefresh) - { - - } - - - - - @foreach (ReminderRecordViewModel reminderRecord in Model) - { - - - - - - - - - - @if (hasRefresh) - { - - } - - - } - - - - - - -
@translator.Translate(userLanguage, "Urgency")@translator.Translate(userLanguage, "Metric")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Notes")@translator.Translate(userLanguage, "Done")@translator.Translate(userLanguage, "Delete")
- @if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent) - { - @translator.Translate(userLanguage, "Very Urgent") - } - else if (reminderRecord.Urgency == ReminderUrgency.Urgent) - { - @translator.Translate(userLanguage, "Urgent") - } - else if (reminderRecord.Urgency == ReminderUrgency.PastDue) - { - @translator.Translate(userLanguage, "Past Due") - } - else - { - @translator.Translate(userLanguage, "Not Urgent") - } - - @if (reminderRecord.Metric == ReminderMetric.Date) - { - @reminderRecord.Date.ToShortDateString() - } - else if (reminderRecord.Metric == ReminderMetric.Odometer) - { - @reminderRecord.Mileage - } - else - { - @reminderRecord.Metric - } - @reminderRecord.Description@StaticHelper.TruncateStrings(reminderRecord.Notes) - @if((reminderRecord.Urgency == ReminderUrgency.VeryUrgent || reminderRecord.Urgency == ReminderUrgency.PastDue) && reminderRecord.IsRecurring) - { - - } - - -
- @StaticHelper.ReportNote -
-
-
- - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/Service/_ServiceRecordModal.cshtml b/Views/Vehicle/Service/_ServiceRecordModal.cshtml deleted file mode 100644 index 1f7f20e..0000000 --- a/Views/Vehicle/Service/_ServiceRecordModal.cshtml +++ /dev/null @@ -1,115 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model ServiceRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - -@await Html.PartialAsync("Supply/_SupplyRequisitionHistory", new SupplyRequisitionHistory { RequisitionHistory = Model.RequisitionHistory, CostInputId = "serviceRecordCost" }) - \ No newline at end of file diff --git a/Views/Vehicle/Service/_ServiceRecords.cshtml b/Views/Vehicle/Service/_ServiceRecords.cshtml deleted file mode 100644 index 0e56410..0000000 --- a/Views/Vehicle/Service/_ServiceRecords.cshtml +++ /dev/null @@ -1,204 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var enableCsvImports = userConfig.EnableCsvImports; - var hideZero = userConfig.HideZero; - var userLanguage = userConfig.UserLanguage; - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.ServiceRecord); -} -@model List -
-
-
- @($"{translator.Translate(userLanguage, "# of Service Records")}: {Model.Count()}") - @($"{translator.Translate(userLanguage, "Total")}: {Model.Sum(x => x.Cost).ToString("C")}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (ServiceRecord serviceRecord in Model) - { - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Date")@translator.Translate(userLanguage, "Odometer")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Cost")@translator.Translate(userLanguage, "Notes")
@serviceRecord.Date.ToShortDateString()@(serviceRecord.Mileage == default ? "---" : serviceRecord.Mileage.ToString())@serviceRecord.Description@(StaticHelper.HideZeroCost(serviceRecord.Cost, hideZero))@StaticHelper.TruncateStrings(serviceRecord.Notes)
- @StaticHelper.ReportNote -
-
-
- - - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/Supply/_SupplyRecordModal.cshtml b/Views/Vehicle/Supply/_SupplyRecordModal.cshtml deleted file mode 100644 index 1c2a586..0000000 --- a/Views/Vehicle/Supply/_SupplyRecordModal.cshtml +++ /dev/null @@ -1,92 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model SupplyRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - -@await Html.PartialAsync("Supply/_SupplyRequisitionHistory", new SupplyRequisitionHistory { RequisitionHistory = Model.RequisitionHistory, CostInputId = "" }) - \ No newline at end of file diff --git a/Views/Vehicle/Supply/_SupplyRecords.cshtml b/Views/Vehicle/Supply/_SupplyRecords.cshtml deleted file mode 100644 index b92b589..0000000 --- a/Views/Vehicle/Supply/_SupplyRecords.cshtml +++ /dev/null @@ -1,213 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var enableCsvImports = userConfig.EnableCsvImports; - var hideZero = userConfig.HideZero; - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.SupplyRecord); -} -@model List -
-
-
- @($"{translator.Translate(userLanguage, "# of Supply Records")}: {Model.Count()}") - @($"{translator.Translate(userLanguage, "Total")}: {Model.Sum(x => x.Cost).ToString("C")}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (SupplyRecord supplyRecord in Model) - { - - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Date")@translator.Translate(userLanguage, "Part #")@translator.Translate(userLanguage, "Supplier")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Quantity")@translator.Translate(userLanguage, "Cost")@translator.Translate(userLanguage, "Notes")
@supplyRecord.Date.ToShortDateString()@supplyRecord.PartNumber@supplyRecord.PartSupplier@supplyRecord.Description@supplyRecord.Quantity@(StaticHelper.HideZeroCost(supplyRecord.Cost, hideZero))@StaticHelper.TruncateStrings(supplyRecord.Notes)
- @StaticHelper.ReportNote -
-
-
- - - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/Supply/_SupplyRequisitionHistory.cshtml b/Views/Vehicle/Supply/_SupplyRequisitionHistory.cshtml deleted file mode 100644 index 0449f05..0000000 --- a/Views/Vehicle/Supply/_SupplyRequisitionHistory.cshtml +++ /dev/null @@ -1,106 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model SupplyRequisitionHistory -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var showDelete = Model.RequisitionHistory.All(x => x.Id != default); - var showPartNumber = Model.RequisitionHistory.Any(x => !string.IsNullOrWhiteSpace(x.PartNumber)); -} - -
- - -
\ No newline at end of file diff --git a/Views/Vehicle/Supply/_SupplyStore.cshtml b/Views/Vehicle/Supply/_SupplyStore.cshtml deleted file mode 100644 index 7a812ea..0000000 --- a/Views/Vehicle/Supply/_SupplyStore.cshtml +++ /dev/null @@ -1,127 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model SupplyStore - - \ No newline at end of file diff --git a/Views/Vehicle/Supply/_SupplyUsage.cshtml b/Views/Vehicle/Supply/_SupplyUsage.cshtml deleted file mode 100644 index 014eccc..0000000 --- a/Views/Vehicle/Supply/_SupplyUsage.cshtml +++ /dev/null @@ -1,147 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var recordTags = Model.Supplies.SelectMany(x => x.Tags).Distinct(); -} -@model SupplyUsageViewModel - - - - \ No newline at end of file diff --git a/Views/Vehicle/Tax/_TaxRecordModal.cshtml b/Views/Vehicle/Tax/_TaxRecordModal.cshtml deleted file mode 100644 index 691218b..0000000 --- a/Views/Vehicle/Tax/_TaxRecordModal.cshtml +++ /dev/null @@ -1,105 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model TaxRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - - \ No newline at end of file diff --git a/Views/Vehicle/Tax/_TaxRecords.cshtml b/Views/Vehicle/Tax/_TaxRecords.cshtml deleted file mode 100644 index 39f800f..0000000 --- a/Views/Vehicle/Tax/_TaxRecords.cshtml +++ /dev/null @@ -1,189 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var enableCsvImports = userConfig.EnableCsvImports; - var hideZero = userConfig.HideZero; - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.TaxRecord); -} -@model List -
-
-
- @($"{translator.Translate(userLanguage,"# of Tax Records")}: {Model.Count()}") - @($"{translator.Translate(userLanguage,"Total")}: {Model.Sum(x => x.Cost).ToString("C")}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (TaxRecord taxRecord in Model) - { - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Date")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Cost")@translator.Translate(userLanguage, "Notes")
@taxRecord.Date.ToShortDateString()@taxRecord.Description@(StaticHelper.HideZeroCost(taxRecord.Cost, hideZero))@StaticHelper.TruncateStrings(taxRecord.Notes)
- @StaticHelper.ReportNote -
-
-
- - - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/Upgrade/_UpgradeRecordModal.cshtml b/Views/Vehicle/Upgrade/_UpgradeRecordModal.cshtml deleted file mode 100644 index e5a1ab8..0000000 --- a/Views/Vehicle/Upgrade/_UpgradeRecordModal.cshtml +++ /dev/null @@ -1,115 +0,0 @@ -@inject IConfigHelper config -@inject ITranslationHelper translator -@using MotoVaultPro.Helper -@model UpgradeRecordInput -@{ - var isNew = Model.Id == 0; - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - -@await Html.PartialAsync("Supply/_SupplyRequisitionHistory", new SupplyRequisitionHistory { RequisitionHistory = Model.RequisitionHistory, CostInputId = "upgradeRecordCost" }) - \ No newline at end of file diff --git a/Views/Vehicle/Upgrade/_UpgradeRecords.cshtml b/Views/Vehicle/Upgrade/_UpgradeRecords.cshtml deleted file mode 100644 index 85bff62..0000000 --- a/Views/Vehicle/Upgrade/_UpgradeRecords.cshtml +++ /dev/null @@ -1,205 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var enableCsvImports = userConfig.EnableCsvImports; - var hideZero = userConfig.HideZero; - var recordTags = Model.SelectMany(x => x.Tags).Distinct(); - var extraFields = new List(); - if (userConfig.EnableExtraFieldColumns) - { - extraFields = Model.SelectMany(x => x.ExtraFields).Select(y => y.Name).Distinct().ToList(); - } - var userColumnPreferences = userConfig.UserColumnPreferences.Where(x => x.Tab == ImportMode.UpgradeRecord); -} -@model List -
-
-
- @($"{translator.Translate(userLanguage,"# of Upgrade Records")}: {Model.Count()}") - @($"{translator.Translate(userLanguage,"Total")}: {Model.Sum(x => x.Cost).ToString("C")}") - @foreach (string recordTag in recordTags) - { - @recordTag - } - - @foreach (string recordTag in recordTags) - { - - } - -
-
- @if (enableCsvImports) - { -
- - - -
- } - else - { - - } -
-
-
-
-
-
-
- -
-
- - - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - - - @foreach (UpgradeRecord upgradeRecord in Model) - { - - - - - - - - @foreach (string extraFieldColumn in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Date")@translator.Translate(userLanguage, "Odometer")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Cost")@translator.Translate(userLanguage, "Notes")
@upgradeRecord.Date.ToShortDateString()@(upgradeRecord.Mileage == default ? "---" : upgradeRecord.Mileage.ToString())@upgradeRecord.Description@(StaticHelper.HideZeroCost(upgradeRecord.Cost, hideZero))@StaticHelper.TruncateStrings(upgradeRecord.Notes)
- @StaticHelper.ReportNote -
-
-
- - - - -@if (userColumnPreferences.Any()) -{ - @await Html.PartialAsync("_UserColumnPreferences", userColumnPreferences) -} \ No newline at end of file diff --git a/Views/Vehicle/_AttachmentColumn.cshtml b/Views/Vehicle/_AttachmentColumn.cshtml deleted file mode 100644 index aff8b6c..0000000 --- a/Views/Vehicle/_AttachmentColumn.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@model List -@if (Model.Any()){ - - - - @Model.Count() - - -} \ No newline at end of file diff --git a/Views/Vehicle/_BulkDataImporter.cshtml b/Views/Vehicle/_BulkDataImporter.cshtml deleted file mode 100644 index 4680037..0000000 --- a/Views/Vehicle/_BulkDataImporter.cshtml +++ /dev/null @@ -1,71 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model ImportMode - - - - \ No newline at end of file diff --git a/Views/Vehicle/_Collaborators.cshtml b/Views/Vehicle/_Collaborators.cshtml deleted file mode 100644 index 53a2acc..0000000 --- a/Views/Vehicle/_Collaborators.cshtml +++ /dev/null @@ -1,81 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model List -
-
- @translator.Translate(userLanguage, "Collaborators") -
-
- -
-
-
- - - - - - - - - @foreach (UserCollaborator user in Model) - { - - - - - } - -
@translator.Translate(userLanguage, "Username")@translator.Translate(userLanguage, "Delete")
@user.UserName - @if(User.Identity.Name != user.UserName) - { - - } -
-
- \ No newline at end of file diff --git a/Views/Vehicle/_CostDistanceTableReport.cshtml b/Views/Vehicle/_CostDistanceTableReport.cshtml deleted file mode 100644 index 5ffc617..0000000 --- a/Views/Vehicle/_CostDistanceTableReport.cshtml +++ /dev/null @@ -1,96 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model CostDistanceTableForVehicle -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var hideZero = userConfig.HideZero; - var years = Model.CostData.Select(x => x.Year).OrderByDescending(x => x).Distinct(); - var months = Model.CostData.OrderBy(x => x.MonthId).Select(x => x.MonthName).Distinct(); -} -@if (Model.CostData.Any()) -{ -
- - -
-} -else -{ -
-

@translator.Translate(userLanguage, "No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.")

-
-} diff --git a/Views/Vehicle/_CostMakeUpReport.cshtml b/Views/Vehicle/_CostMakeUpReport.cshtml deleted file mode 100644 index dbbd989..0000000 --- a/Views/Vehicle/_CostMakeUpReport.cshtml +++ /dev/null @@ -1,88 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model CostMakeUpForVehicle -{ - - -} -else -{ -
-

@translator.Translate(userLanguage, "No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.")

-
-} diff --git a/Views/Vehicle/_CostTableReport.cshtml b/Views/Vehicle/_CostTableReport.cshtml deleted file mode 100644 index 6f9b354..0000000 --- a/Views/Vehicle/_CostTableReport.cshtml +++ /dev/null @@ -1,76 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var hideZero = userConfig.HideZero; -} -@model CostTableForVehicle -{ -
- - -
-} -else -{ -
-

@translator.Translate(userLanguage, "No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.")

-
-} diff --git a/Views/Vehicle/_ExtraField.cshtml b/Views/Vehicle/_ExtraField.cshtml deleted file mode 100644 index f87e0c1..0000000 --- a/Views/Vehicle/_ExtraField.cshtml +++ /dev/null @@ -1,42 +0,0 @@ -@model List -@if (Model.Any()){ - @foreach (ExtraField field in Model) - { - var elementId = Guid.NewGuid(); -
- - @switch(field.FieldType){ - case (ExtraFieldType.Text): - - break; - case (ExtraFieldType.Number): - - break; - case (ExtraFieldType.Decimal): - - break; - case (ExtraFieldType.Date): -
- - -
- - break; - case (ExtraFieldType.Time): - - break; - case (ExtraFieldType.Location): -
- -
- -
-
- break; - default: - - break; - } -
- } -} \ No newline at end of file diff --git a/Views/Vehicle/_ExtraFieldMultiple.cshtml b/Views/Vehicle/_ExtraFieldMultiple.cshtml deleted file mode 100644 index 821fcbe..0000000 --- a/Views/Vehicle/_ExtraFieldMultiple.cshtml +++ /dev/null @@ -1,49 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model List -@if (Model.Any()){ - @foreach (ExtraField field in Model) - { - var elementId = Guid.NewGuid(); -
- - @switch(field.FieldType){ - case (ExtraFieldType.Text): - - break; - case (ExtraFieldType.Number): - - break; - case (ExtraFieldType.Decimal): - - break; - case (ExtraFieldType.Date): -
- - -
- - break; - case (ExtraFieldType.Time): - - break; - case (ExtraFieldType.Location): -
- -
- -
-
- break; - default: - - break; - } -
- } -} \ No newline at end of file diff --git a/Views/Vehicle/_FileUploader.cshtml b/Views/Vehicle/_FileUploader.cshtml deleted file mode 100644 index f460692..0000000 --- a/Views/Vehicle/_FileUploader.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var uploaderId = Guid.NewGuid(); -} -@model bool - - -
- -
@translator.Translate(userLanguage, "Max File Size: 28.6MB")
-
- \ No newline at end of file diff --git a/Views/Vehicle/_FilesToUpload.cshtml b/Views/Vehicle/_FilesToUpload.cshtml deleted file mode 100644 index ce0bb84..0000000 --- a/Views/Vehicle/_FilesToUpload.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model List - - \ No newline at end of file diff --git a/Views/Vehicle/_GasCostByMonthReport.cshtml b/Views/Vehicle/_GasCostByMonthReport.cshtml deleted file mode 100644 index 1969d93..0000000 --- a/Views/Vehicle/_GasCostByMonthReport.cshtml +++ /dev/null @@ -1,103 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model List -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var barGraphColors = StaticHelper.GetBarChartColors(); - var sortedByMPG = Model.OrderBy(x => x.Cost).ToList(); -} -@if (Model.Any(x=>x.Cost > 0) || Model.Any(x=>x.DistanceTraveled > 0)) -{ - - -} else -{ -
-

@translator.Translate(userLanguage,"No data found, insert/select some data to see visualizations here.")

-
-} \ No newline at end of file diff --git a/Views/Vehicle/_GenericRecordModal.cshtml b/Views/Vehicle/_GenericRecordModal.cshtml deleted file mode 100644 index 355d723..0000000 --- a/Views/Vehicle/_GenericRecordModal.cshtml +++ /dev/null @@ -1,56 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model GenericRecordEditModel -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} - - - - \ No newline at end of file diff --git a/Views/Vehicle/_GlobalSearchResult.cshtml b/Views/Vehicle/_GlobalSearchResult.cshtml deleted file mode 100644 index 9bff194..0000000 --- a/Views/Vehicle/_GlobalSearchResult.cshtml +++ /dev/null @@ -1,34 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model List -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -
- @if (Model.Any()) - { - @foreach (SearchResult result in Model) - { -
-
- -
-
- @result.Description -
-
- } - } else - { -
-
- -
-
- @translator.Translate(userLanguage, "No Data Found") -
-
- } -
diff --git a/Views/Vehicle/_MPGByMonthReport.cshtml b/Views/Vehicle/_MPGByMonthReport.cshtml deleted file mode 100644 index f7451be..0000000 --- a/Views/Vehicle/_MPGByMonthReport.cshtml +++ /dev/null @@ -1,101 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model MPGForVehicleByMonth -@{ - var barGraphColors = StaticHelper.GetBarChartColors(); - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@if (Model.CostData.Any(x => x.Cost > 0)) -{ - var chartMax = Math.Ceiling(Model.CostData.Max(x => x.Cost)); - var graphGrace = Decimal.ToInt32(Model.CostData.Max(x => x.Cost) - Model.CostData.Min(x => x.Cost)); - var chartMin = Math.Floor(Model.CostData.Min(x => x.Cost) - graphGrace); - if (graphGrace < 0 || chartMin < 0) - { - graphGrace = 0; - chartMin = 0; - } - var stepSize = StaticHelper.CalculateNiceStepSize(chartMin, chartMax, 8); - var remMin = stepSize > 0 ? chartMin % stepSize : 0; - var cleanedMin = chartMin - remMin; - if (remMin >= (stepSize / 2)) - { - cleanedMin += stepSize; - } - var remMax = stepSize > 0 ? chartMax % stepSize : 0; - var cleanedMax = chartMax - remMax; - if (remMax >= (stepSize / 2)) - { - cleanedMax += stepSize; - } - - -} -else -{ -
-

@translator.Translate(userLanguage,"No data found, insert/select some data to see visualizations here.")

-
-} \ No newline at end of file diff --git a/Views/Vehicle/_RecurringReminderSelector.cshtml b/Views/Vehicle/_RecurringReminderSelector.cshtml deleted file mode 100644 index 870b053..0000000 --- a/Views/Vehicle/_RecurringReminderSelector.cshtml +++ /dev/null @@ -1,69 +0,0 @@ -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@using MotoVaultPro.Helper -@model List -@if (Model.Count() > 1) -{ -
- - -
-} - - \ No newline at end of file diff --git a/Views/Vehicle/_ReminderMakeUpReport.cshtml b/Views/Vehicle/_ReminderMakeUpReport.cshtml deleted file mode 100644 index 162db9d..0000000 --- a/Views/Vehicle/_ReminderMakeUpReport.cshtml +++ /dev/null @@ -1,62 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model ReminderMakeUpForVehicle -@if (Model.UrgentCount + Model.VeryUrgentCount + Model.NotUrgentCount + Model.PastDueCount > 0) -{ - - -} -else -{ -
-

@translator.Translate(userLanguage,"No data found, create reminders to see visualizations here.")

-
-} diff --git a/Views/Vehicle/_Report.cshtml b/Views/Vehicle/_Report.cshtml deleted file mode 100644 index 66042b3..0000000 --- a/Views/Vehicle/_Report.cshtml +++ /dev/null @@ -1,167 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model ReportViewModel - -
-
- @await Html.PartialAsync("_ReportHeader", Model.ReportHeaderForVehicle) -
-
-
-
-
-
- -
-
-
-
- @await Html.PartialAsync("_CostMakeUpReport", Model.CostMakeUpForVehicle) -
-
-
-
-
-
-
- -
-
-
-
-
- @await Html.PartialAsync("_GasCostByMonthReport", Model.CostForVehicleByMonth) -
-
-
-
-
-
- -
-
-
-
- @await Html.PartialAsync("_ReminderMakeUpReport", Model.ReminderMakeUpForVehicle) -
-
-
-
-
-
-
- @await Html.PartialAsync("_Collaborators", Model.Collaborators) -
-
-
- @await Html.PartialAsync("_MPGByMonthReport", Model.FuelMileageForVehicleByMonth) -
-
-
-
- -
-
- -
-
- -
- @if (Model.CustomWidgetsConfigured) - { -
- -
- } -
-
-
- -@if (Model.CustomWidgetsConfigured) -{ - -} - - \ No newline at end of file diff --git a/Views/Vehicle/_ReportHeader.cshtml b/Views/Vehicle/_ReportHeader.cshtml deleted file mode 100644 index 0ca2d13..0000000 --- a/Views/Vehicle/_ReportHeader.cshtml +++ /dev/null @@ -1,24 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model ReportHeader -
- @Model.MaxOdometer.ToString("N0")
- @translator.Translate(userLanguage, "Last Reported Odometer Reading") -
-
- @Model.DistanceTraveled.ToString("N0")
- @translator.Translate(userLanguage, "Distance Traveled") -
-
- @StaticHelper.HideZeroCost(Model.TotalCost.ToString("C2"), true)
- @translator.Translate(userLanguage, "Total Cost") -
-
- @Model.AverageMPG
- @translator.Translate(userLanguage, "Average Fuel Economy") -
\ No newline at end of file diff --git a/Views/Vehicle/_ReportParameters.cshtml b/Views/Vehicle/_ReportParameters.cshtml deleted file mode 100644 index fc6f226..0000000 --- a/Views/Vehicle/_ReportParameters.cshtml +++ /dev/null @@ -1,63 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model ReportParameter -

@translator.Translate(userLanguage, "Select Columns")

-
-
    - @foreach (string column in Model.VisibleColumns) - { -
  • - - -
  • - } - @foreach (string extraField in Model.ExtraFields) - { -
  • - - -
  • - } -
-
-
-
    -
  • - - -
  • -
-
-
-
    -
  • - @translator.Translate(userLanguage, "Advanced Filters") -
  • -
-
-

@translator.Translate(userLanguage, "Filter by Tags")

-
- - -
-

@translator.Translate(userLanguage, "Filter by Date Range")

-
-
- - -
-
- @translator.Translate(userLanguage, "From") - - @translator.Translate(userLanguage, "To") - -
-
\ No newline at end of file diff --git a/Views/Vehicle/_ReportWidgets.cshtml b/Views/Vehicle/_ReportWidgets.cshtml deleted file mode 100644 index 63f4797..0000000 --- a/Views/Vehicle/_ReportWidgets.cshtml +++ /dev/null @@ -1,2 +0,0 @@ -@model string -@Html.Raw(Model) \ No newline at end of file diff --git a/Views/Vehicle/_Stickers.cshtml b/Views/Vehicle/_Stickers.cshtml deleted file mode 100644 index 3810dcb..0000000 --- a/Views/Vehicle/_Stickers.cshtml +++ /dev/null @@ -1,276 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model StickerViewModel -@{ - var userConfig = config.GetUserConfig(User); - var hideZero = userConfig.HideZero; - var userLanguage = userConfig.UserLanguage; -} -@if( Model.ReminderRecords.Any()){ - @foreach(ReminderRecord reminder in Model.ReminderRecords){ -
-
- -
-
-
-

@($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}")

-
-
-
-
-

@($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}")

-
-
-
-
-

@($"{reminder.Description}")

-
-
- @if (reminder.Metric == ReminderMetric.Odometer || reminder.Metric == ReminderMetric.Both) - { -
-
-

@($"{translator.Translate(userLanguage, "Odometer")}")

-
-
-
-
-

@($"{reminder.Mileage}")

-
-
- } - @if (reminder.Metric == ReminderMetric.Date || reminder.Metric == ReminderMetric.Both) - { -
-
-

@($"{translator.Translate(userLanguage, "Date")}")

-
-
-
-
-

@($"{reminder.Date.ToShortDateString()}")

-
-
- } - @if (reminder.Metric == ReminderMetric.Both) - { -
-
-

@($"{translator.Translate(userLanguage, "Whichever comes first")}")

-
-
- } -
- } -} else if (Model.GenericRecords.Any()){ - @foreach(GenericRecord genericRecord in Model.GenericRecords){ -
-
- -
-
-
-
-
    -
  • - @($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}") -
  • -
  • - @($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}") -
  • - @foreach (ExtraField extraField in Model.VehicleData.ExtraFields) - { - if (!string.IsNullOrWhiteSpace(extraField.Value)) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } - } -
-
-
-
    - @if(!string.IsNullOrWhiteSpace(genericRecord.Description)){ -
  • - @($"{translator.Translate(userLanguage, "Description")}: {genericRecord.Description}") -
  • - } - @switch(Model.RecordType){ - case ImportMode.ServiceRecord: - case ImportMode.UpgradeRecord: - case ImportMode.GasRecord: -
  • - @($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Mileage}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}") -
  • - break; - case ImportMode.TaxRecord: - case ImportMode.PlanRecord: -
  • - @($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}") -
  • - break; - case ImportMode.OdometerRecord: -
  • - @($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Mileage}") -
  • - break; - } - @foreach(ExtraField extraField in genericRecord.ExtraFields){ -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } -
-
-
-
- @if(genericRecord.RequisitionHistory.Any()){ -
-
- - - - - - - - - - - @foreach (SupplyUsageHistory usageHistory in genericRecord.RequisitionHistory) - { - - - - - - - } - -
@translator.Translate(userLanguage, "Part Number")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Quantity")@translator.Translate(userLanguage, "Cost")
@usageHistory.PartNumber@usageHistory.Description@usageHistory.Quantity.ToString("F")@usageHistory.Cost.ToString("C2")
-
-
-
- } -
-
-
- @(genericRecord.Notes) -
-
-
-
- } - -} else if (Model.SupplyRecords.Any()){ - @foreach (SupplyRecord supplyRecord in Model.SupplyRecords) - { -
-
- -
-
-
- @if(Model.VehicleData.Id != default){ -
-
    -
  • - @($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}") -
  • -
  • - @($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}") -
  • - @foreach (ExtraField extraField in Model.VehicleData.ExtraFields) - { - if (!string.IsNullOrWhiteSpace(extraField.Value)) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } - } -
-
-
-
    -
  • - @($"{translator.Translate(userLanguage, "Description")}: {supplyRecord.Description}") -
  • - @if(!string.IsNullOrWhiteSpace(supplyRecord.PartNumber)){ -
  • - @($"{translator.Translate(userLanguage, "Part Number")}: {supplyRecord.PartNumber}") -
  • - } -
  • - @($"{translator.Translate(userLanguage, "Supplier/Vendor")}: {supplyRecord.PartSupplier}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Cost")}: {supplyRecord.Cost.ToString("C")}") -
  • - @foreach (ExtraField extraField in supplyRecord.ExtraFields) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } -
-
- } else { -
-
    -
  • - @($"{translator.Translate(userLanguage, "Description")}: {supplyRecord.Description}") -
  • - @if (!string.IsNullOrWhiteSpace(supplyRecord.PartNumber)) - { -
  • - @($"{translator.Translate(userLanguage, "Part Number")}: {supplyRecord.PartNumber}") -
  • - } -
  • - @($"{translator.Translate(userLanguage, "Supplier/Vendor")}: {supplyRecord.PartSupplier}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Cost")}: {supplyRecord.Cost.ToString("C")}") -
  • -
-
-
-
    - @foreach (ExtraField extraField in supplyRecord.ExtraFields) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } -
-
- } -
-
-
-
-
- @(supplyRecord.Notes) -
-
-
-
- } - -} diff --git a/Views/Vehicle/_UploadedFiles.cshtml b/Views/Vehicle/_UploadedFiles.cshtml deleted file mode 100644 index beb33c5..0000000 --- a/Views/Vehicle/_UploadedFiles.cshtml +++ /dev/null @@ -1,35 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; -} -@model List -@if (Model.Any()) -{ - - -} - \ No newline at end of file diff --git a/Views/Vehicle/_UserColumnPreferences.cshtml b/Views/Vehicle/_UserColumnPreferences.cshtml deleted file mode 100644 index 0fb59fd..0000000 --- a/Views/Vehicle/_UserColumnPreferences.cshtml +++ /dev/null @@ -1,13 +0,0 @@ -@model IEnumerable - \ No newline at end of file diff --git a/Views/Vehicle/_VehicleHistory.cshtml b/Views/Vehicle/_VehicleHistory.cshtml deleted file mode 100644 index eb0f0ef..0000000 --- a/Views/Vehicle/_VehicleHistory.cshtml +++ /dev/null @@ -1,286 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model VehicleHistoryViewModel -@{ - var userConfig = config.GetUserConfig(User); - var hideZero = userConfig.HideZero; - var userLanguage = userConfig.UserLanguage; - var extraFields = Model.ReportParameters.ExtraFields; -} -
-
-
- -
- - @translator.Translate(userLanguage, "Vehicle Maintenance Report") - - @if (!string.IsNullOrWhiteSpace(Model.StartDate) && !string.IsNullOrWhiteSpace(Model.EndDate)) - { -
- - @($"{@translator.Translate(userLanguage, "From")} {Model.StartDate} {@translator.Translate(userLanguage, "To")} {Model.EndDate}") - - } -
-
-
-
-
-
-
    -
  • - @($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}") -
  • - @if (!string.IsNullOrWhiteSpace(Model.VehicleData.LicensePlate)) - { -
  • - @Model.VehicleData.LicensePlate -
  • - } - @foreach(ExtraField extraField in Model.VehicleData.ExtraFields) - { - if (!string.IsNullOrWhiteSpace(extraField.Value)) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } - } -
  • -
    -
    - @if (Model.VehicleData.IsElectric) - { - @translator.Translate(userLanguage, "Electric") - } - else if (Model.VehicleData.IsDiesel) - { - @translator.Translate(userLanguage, "Diesel") - } - else - { - @translator.Translate(userLanguage, "Gasoline") - } -
    - @if (!string.IsNullOrWhiteSpace(Model.DaysOwned)) - { -
    - @($"{Model.DaysOwned} {translator.Translate(userLanguage, "Days")}") -
    - } - @if (Model.DistanceTraveled != default) - { -
    - @($"{Model.DistanceTraveled} {Model.DistanceUnit}") -
    - } -
    -
  • -
-
-
-
    -
  • @($"{translator.Translate(userLanguage, "Last Reported Odometer Reading")}: {Model.Odometer}")
  • -
  • @($"{translator.Translate(userLanguage, "Average Fuel Economy")}: {Model.MPG}")
  • -
  • @($"{translator.Translate(userLanguage, "Total Spent(excl. fuel)")}: {Model.TotalCost.ToString("C")} ({Model.TotalCostPerMile.ToString("C")}/{Model.DistanceUnit})")
  • -
  • @($"{translator.Translate(userLanguage, "Total Spent on Fuel")}: {Model.TotalGasCost.ToString("C")} ({Model.TotalGasCostPerMile.ToString("C")}/{Model.DistanceUnit})")
  • -
-
-
-
- @if (Model.TotalDepreciation != default) - { -
-
- @(Model.TotalDepreciation > 0 ? translator.Translate(userLanguage, "Depreciation") : translator.Translate(userLanguage, "Appreciation")) -
-
- @Math.Abs(Model.TotalDepreciation).ToString("C") -
- @if (Model.DepreciationPerDay != default) - { -
- @($"{Model.DepreciationPerDay.ToString("C")}/{translator.Translate(userLanguage, "day")}") -
- } - @if (Model.DepreciationPerMile != default) - { -
- @($"{Model.DepreciationPerMile.ToString("C")}/{Model.DistanceUnit}") -
- } -
-
- } -
-
- - - - - - - - - - @foreach(string extraField in extraFields) - { - - } - - - - @foreach (GenericReportModel reportData in Model.VehicleHistory) - { - - - - - - - - @foreach(string extraField in extraFields) - { - - } - - } - - - - - - -
@translator.Translate(userLanguage, "Type")@translator.Translate(userLanguage, "Date")@translator.Translate(userLanguage, "Odometer")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Cost")@translator.Translate(userLanguage, "Notes")@extraField
- @if (reportData.DataType == ImportMode.ServiceRecord) - { - @translator.Translate(userLanguage, "Service") - } - { - @translator.Translate(userLanguage, "Repair") - } - else if (reportData.DataType == ImportMode.UpgradeRecord) - { - @translator.Translate(userLanguage, "Upgrade") - } - else if (reportData.DataType == ImportMode.TaxRecord) - { - @translator.Translate(userLanguage, "Tax") - } - @reportData.Date.ToShortDateString()@(reportData.Odometer == default ? "---" : reportData.Odometer.ToString("N0"))@reportData.Description@(StaticHelper.HideZeroCost(reportData.Cost, hideZero))@StaticHelper.TruncateStrings(reportData.Notes, 100)@(reportData.ExtraFields.Where(x => x.Name == extraField)?.FirstOrDefault()?.Value ?? "")
- @StaticHelper.ReportNote -
-
-
-
-@if (Model.ReportParameters.PrintIndividualRecords){ - @foreach (GenericReportModel genericRecord in Model.VehicleHistory) - { -
-
- -
-
-
-
-
    -
  • - @($"{Model.VehicleData.Year} {Model.VehicleData.Make} {Model.VehicleData.Model}") -
  • -
  • - @($"{StaticHelper.GetVehicleIdentifier(Model.VehicleData)}") -
  • - @foreach (ExtraField extraField in Model.VehicleData.ExtraFields) - { - if (!string.IsNullOrWhiteSpace(extraField.Value)) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } - } -
-
-
-
    - @if (!string.IsNullOrWhiteSpace(genericRecord.Description)) - { -
  • - @($"{translator.Translate(userLanguage, "Description")}: {genericRecord.Description}") -
  • - } - @switch (genericRecord.DataType) - { - case ImportMode.ServiceRecord: - case ImportMode.UpgradeRecord: -
  • - @($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Odometer")}: {genericRecord.Odometer}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}") -
  • - break; - case ImportMode.TaxRecord: -
  • - @($"{translator.Translate(userLanguage, "Date")}: {genericRecord.Date.ToShortDateString()}") -
  • -
  • - @($"{translator.Translate(userLanguage, "Cost")}: {genericRecord.Cost.ToString("C")}") -
  • - break; - } - @foreach (ExtraField extraField in genericRecord.ExtraFields) - { -
  • - @($"{extraField.Name}: {extraField.Value}") -
  • - } -
-
-
-
- @if (genericRecord.RequisitionHistory.Any()) - { -
-
- - - - - - - - - - - @foreach (SupplyUsageHistory usageHistory in genericRecord.RequisitionHistory) - { - - - - - - - } - -
@translator.Translate(userLanguage, "Part Number")@translator.Translate(userLanguage, "Description")@translator.Translate(userLanguage, "Quantity")@translator.Translate(userLanguage, "Cost")
@usageHistory.PartNumber@usageHistory.Description@usageHistory.Quantity.ToString("F")@usageHistory.Cost.ToString("C2")
-
-
-
- } -
-
-
- @(genericRecord.Notes) -
-
-
-
- } - -} \ No newline at end of file diff --git a/Views/Vehicle/_VehicleModal.cshtml b/Views/Vehicle/_VehicleModal.cshtml deleted file mode 100644 index b2d5b1e..0000000 --- a/Views/Vehicle/_VehicleModal.cshtml +++ /dev/null @@ -1,165 +0,0 @@ -@using MotoVaultPro.Helper -@inject IConfigHelper config -@inject ITranslationHelper translator -@model Vehicle -@{ - var userConfig = config.GetUserConfig(User); - var userLanguage = userConfig.UserLanguage; - var isNew = Model.Id == 0; - if (Model.ImageLocation == "/defaults/noimage.png") - { - Model.ImageLocation = ""; - } -} - - - - \ No newline at end of file diff --git a/Views/_ViewImports.cshtml b/Views/_ViewImports.cshtml deleted file mode 100644 index 73decb7..0000000 --- a/Views/_ViewImports.cshtml +++ /dev/null @@ -1,4 +0,0 @@ -@using MotoVaultPro -@using MotoVaultPro.Models -@using Microsoft.Extensions.Options -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Views/_ViewStart.cshtml b/Views/_ViewStart.cshtml deleted file mode 100644 index a5f1004..0000000 --- a/Views/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "_Layout"; -} diff --git a/appsettings.Development.json b/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/appsettings.json b/appsettings.json deleted file mode 100644 index 0b66d54..0000000 --- a/appsettings.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "UseDarkMode": true, - "UseSystemColorMode": false, - "EnableCsvImports": true, - "UseMPG": true, - "UseDescending": false, - "EnableAuth": false, - "DisableRegistration": false, - "EnableRootUserOIDC": false, - "HideZero": false, - "AutomaticDecimalFormat": false, - "EnableAutoReminderRefresh": false, - "EnableAutoOdometerInsert": false, - "EnableShopSupplies": false, - "ShowCalendar": true, - "ShowVehicleThumbnail": true, - "EnableExtraFieldColumns": false, - "UseUKMPG": false, - "UseThreeDecimalGasCost": true, - "UseThreeDecimalGasConsumption": true, - "UseMarkDownOnSavedNotes": false, - "HideSoldVehicles": false, - "PreferredGasMileageUnit": "", - "UserColumnPreferences": [], - "UseUnitForFuelCost": false, - "PreferredGasUnit": "", - "UserLanguage": "en_US", - "VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ], - "TabOrder": [ 8, 9, 10, 0, 1, 4, 2, 7, 3, 6, 5 ], - "DefaultTab": 8, - "UserNameHash": "", - "UserPasswordHash": "", - "DefaultReminderEmail": "" -} diff --git a/docker-compose.postgresql.yml b/docker-compose.postgresql.yml deleted file mode 100644 index e9cef0f..0000000 --- a/docker-compose.postgresql.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -version: "3.4" - -services: - app: - image: ghcr.io/ericgullickson/motovaultpro:latest - build: . - restart: unless-stopped - # volumes used to keep data persistent - volumes: - - data:/App/data - - keys:/root/.aspnet/DataProtection-Keys - # expose port and/or use serving via traefik - ports: - - 8080:8080 - env_file: - - .env - - postgres: - image: postgres:14 - restart: unless-stopped - environment: - POSTGRES_USER: "motovaultpro" - POSTGRES_PASSWORD: "motovaultpass" - POSTGRES_DB: "motovaultpro" - volumes: - - ./init.sql:/docker-entrypoint-initdb.d/init.sql - - postgres:/var/lib/postgresql/data - - /etc/localtime:/etc/localtime:ro - -volumes: - data: - keys: - postgres: diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml deleted file mode 100644 index 28729a9..0000000 --- a/docker-compose.traefik.yml +++ /dev/null @@ -1,42 +0,0 @@ ---- -version: "3.4" - -services: - app: - image: ghcr.io/ericgullickson/motovaultpro:latest - build: . - restart: unless-stopped - # volumes used to keep data persistent - volumes: - - data:/App/data - - keys:/root/.aspnet/DataProtection-Keys - # expose port and/or use serving via traefik - ports: - - 8080:8080 - env_file: - - .env - # traefik configurations, including networks can be commented out if not needed - networks: - - traefik-ingress - labels: - ## Traefik General - # We set 'enable by default' to false, so this tells Traefik we want it to connect here - traefik.enable: true - # define network for traefik<>app communication - traefik.docker.network: traefik-ingress - ## HTTP Routers - traefik.http.routers.whoami.entrypoints: https - traefik.http.routers.whoami.rule: Host(`motovaultpro.mydomain.tld`) - ## Middlewares - #traefik.http.routers.whoami.middlewares: authentik@docker - # none - ## HTTP Services - traefik.http.services.whoami.loadbalancer.server.port: 5000 - -volumes: - data: - keys: - -networks: - traefik-ingress: - external: true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ac948a6..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -version: "3.4" - -services: - app: - image: ghcr.io/ericgullickson/motovaultpro:latest - build: . - restart: unless-stopped - # volumes used to keep data persistent - volumes: - - data:/App/data - - keys:/root/.aspnet/DataProtection-Keys - # expose port and/or use serving via traefik - ports: - - 8080:8080 - env_file: - - .env - -volumes: - data: - keys: diff --git a/docs/K8S-OVERVIEW.md b/docs/K8S-OVERVIEW.md deleted file mode 100644 index d43840c..0000000 --- a/docs/K8S-OVERVIEW.md +++ /dev/null @@ -1,308 +0,0 @@ -# Kubernetes Modernization Plan for MotoVaultPro - -## Executive Summary - -This document provides an overview of the comprehensive plan to modernize MotoVaultPro from a traditional self-hosted application to a cloud-native, highly available system running on Kubernetes. The modernization focuses on transforming the current monolithic ASP.NET Core application into a resilient, scalable platform capable of handling enterprise-level workloads while maintaining the existing feature set and user experience. - -### Key Objectives -- **High Availability**: Eliminate single points of failure through distributed architecture -- **Scalability**: Enable horizontal scaling to handle increased user loads -- **Resilience**: Implement fault tolerance and automatic recovery mechanisms -- **Cloud-Native**: Adopt Kubernetes-native patterns and best practices -- **Operational Excellence**: Improve monitoring, logging, and maintenance capabilities - -### Strategic Benefits -- **Reduced Downtime**: Multi-replica deployments with automatic failover -- **Improved Performance**: Distributed caching and optimized data access patterns -- **Enhanced Security**: Pod-level isolation and secret management -- **Cost Optimization**: Efficient resource utilization through auto-scaling -- **Future-Ready**: Foundation for microservices and advanced cloud features - -## Current Architecture Analysis - -### Existing System Overview -MotoVaultPro is currently deployed as a monolithic ASP.NET Core 8.0 application with the following characteristics: - -#### Application Architecture -- **Monolithic Design**: Single deployable unit containing all functionality -- **MVC Pattern**: Traditional Model-View-Controller architecture -- **Dual Database Support**: LiteDB (embedded) and PostgreSQL (external) -- **File Storage**: Local filesystem for document attachments -- **Session Management**: In-memory or cookie-based sessions -- **Configuration**: File-based configuration with environment variables - -#### Identified Limitations for Kubernetes -1. **State Dependencies**: LiteDB and local file storage prevent stateless operation -2. **Configuration Management**: File-based configuration not suitable for container orchestration -3. **Health Monitoring**: Lacks Kubernetes-compatible health check endpoints -4. **Logging**: Basic logging not optimized for centralized log aggregation -5. **Resource Management**: No resource constraints or auto-scaling capabilities -6. **Secret Management**: Sensitive configuration stored in plain text files - -## Target Architecture - -### Cloud-Native Design Principles -The modernized architecture will embrace the following cloud-native principles: - -#### Stateless Application Design -- **External State Storage**: All state moved to external, highly available services -- **Horizontal Scalability**: Multiple application replicas with load balancing -- **Configuration as Code**: All configuration externalized to ConfigMaps and Secrets -- **Ephemeral Containers**: Pods can be created, destroyed, and recreated without data loss - -#### Distributed Data Architecture -- **PostgreSQL Cluster**: Primary/replica configuration with automatic failover -- **MinIO High Availability**: Distributed object storage for file attachments -- **Redis Cluster**: Distributed caching and session storage -- **Backup Strategy**: Automated backups with point-in-time recovery - -#### Observability and Operations -- **Structured Logging**: JSON logging with correlation IDs for distributed tracing -- **Metrics Collection**: Prometheus-compatible metrics for monitoring -- **Health Checks**: Kubernetes-native readiness and liveness probes -- **Distributed Tracing**: OpenTelemetry integration for request flow analysis - -### High-Level Architecture Diagram -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Kubernetes Cluster │ -├─────────────────────────────────────────────────────────────────┤ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ MotoVault │ │ MotoVault │ │ MotoVault │ │ -│ │ Pod (1) │ │ Pod (2) │ │ Pod (3) │ │ -│ │ │ │ │ │ │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ │ │ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ Load Balancer Service │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ │ │ │ -├───────────┼─────────────────────┼─────────────────────┼──────────┤ -│ ┌────────▼──────┐ ┌─────────▼──────┐ ┌─────────▼──────┐ │ -│ │ PostgreSQL │ │ Redis Cluster │ │ MinIO Cluster │ │ -│ │ Primary │ │ (3 nodes) │ │ (4+ nodes) │ │ -│ │ + 2 Replicas │ │ │ │ Erasure Coded │ │ -│ └───────────────┘ └────────────────┘ └────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Implementation Phases Overview - -The modernization is structured in four distinct phases, each building upon the previous phase to ensure a smooth and risk-managed transition: - -### [Phase 1: Core Kubernetes Readiness](K8S-PHASE-1.md) (Weeks 1-4) - -**Objective**: Make the application compatible with Kubernetes deployment patterns. - -**Key Deliverables**: -- Configuration externalization to ConfigMaps and Secrets -- Removal of LiteDB dependencies -- PostgreSQL connection pooling optimization -- Kubernetes health check endpoints -- Structured logging implementation - -**Success Criteria**: -- Application starts using only environment variables -- Health checks return appropriate status codes -- Database migrations work seamlessly -- Structured JSON logging operational - -### [Phase 2: High Availability Infrastructure](K8S-PHASE-2.md) (Weeks 5-8) - -**Objective**: Deploy highly available supporting infrastructure. - -**Key Deliverables**: -- MinIO distributed object storage cluster -- File storage abstraction layer -- PostgreSQL HA cluster with automated failover -- Redis cluster for distributed sessions and caching -- Comprehensive monitoring setup - -**Success Criteria**: -- MinIO cluster operational with erasure coding -- PostgreSQL cluster with automatic failover -- Redis cluster providing distributed sessions -- All file operations using object storage -- Infrastructure monitoring and alerting active - -### [Phase 3: Production Deployment](K8S-PHASE-3.md) (Weeks 9-12) - -**Objective**: Deploy to production with security, monitoring, and backup strategies. - -**Key Deliverables**: -- Production Kubernetes manifests with HPA -- Secure ingress with automated TLS certificates -- Comprehensive application and infrastructure monitoring -- Automated backup and disaster recovery procedures -- Migration tools and procedures - -**Success Criteria**: -- Production deployment with 99.9% availability target -- Secure external access with TLS -- Monitoring dashboards and alerting operational -- Backup and recovery procedures validated -- Migration dry runs successful - -### [Phase 4: Advanced Features and Optimization](K8S-PHASE-4.md) (Weeks 13-16) - -**Objective**: Implement advanced features and optimize for scale and performance. - -**Key Deliverables**: -- Multi-layer caching (Memory, Redis, CDN) -- Advanced performance optimizations -- Enhanced security features and compliance -- Production migration execution -- Operational excellence and automation - -**Success Criteria**: -- Multi-layer caching reducing database load by 70% -- 95th percentile response time under 500ms -- Zero-downtime production migration completed -- Advanced security policies implemented -- Team trained on new operational procedures - -## Migration Strategy - -### Pre-Migration Assessment -1. **Data Inventory**: Catalog all existing data, configurations, and file attachments -2. **Dependency Mapping**: Identify all external dependencies and integrations -3. **Performance Baseline**: Establish current performance metrics for comparison -4. **User Impact Assessment**: Analyze potential downtime and user experience changes - -### Migration Execution Plan - -#### Blue-Green Deployment Strategy -- Parallel environment setup to minimize risk -- Gradual traffic migration with automated rollback -- Comprehensive validation at each step -- Minimal downtime through DNS cutover - -#### Data Migration Approach -- Initial bulk data migration during low-usage periods -- Incremental synchronization during cutover -- Automated validation and integrity checks -- Point-in-time recovery capabilities - -## Risk Assessment and Mitigation - -### High Impact Risks - -**Data Loss or Corruption** -- **Probability**: Low | **Impact**: Critical -- **Mitigation**: Multiple backup strategies, parallel systems, automated validation - -**Extended Downtime During Migration** -- **Probability**: Medium | **Impact**: High -- **Mitigation**: Blue-green deployment, comprehensive rollback procedures - -**Performance Degradation** -- **Probability**: Medium | **Impact**: Medium -- **Mitigation**: Load testing, performance monitoring, auto-scaling - -### Mitigation Strategies -- Comprehensive testing at each phase -- Automated rollback procedures -- Parallel running systems during transition -- 24/7 monitoring during critical periods - -## Success Metrics - -### Technical Success Criteria -- **Availability**: 99.9% uptime (≤ 8.76 hours downtime/year) -- **Performance**: 95th percentile response time < 500ms -- **Scalability**: Handle 10x current user load -- **Recovery**: RTO < 1 hour, RPO < 15 minutes - -### Operational Success Criteria -- **Deployment Frequency**: Weekly deployments with zero downtime -- **Mean Time to Recovery**: < 30 minutes for critical issues -- **Change Failure Rate**: < 5% of deployments require rollback -- **Monitoring Coverage**: 100% of critical services monitored - -### Business Success Criteria -- **User Satisfaction**: No degradation in user experience -- **Cost Efficiency**: Infrastructure costs within 20% of current spending -- **Maintenance Overhead**: 50% reduction in operational maintenance time -- **Future Readiness**: Foundation for advanced features and scaling - -## Implementation Timeline - -### 16-Week Detailed Schedule - -**Weeks 1-4**: [Phase 1 - Core Kubernetes Readiness](K8S-PHASE-1.md) -- Application configuration externalization -- Database architecture modernization -- Health checks and logging implementation - -**Weeks 5-8**: [Phase 2 - High Availability Infrastructure](K8S-PHASE-2.md) -- MinIO and PostgreSQL HA deployment -- File storage abstraction -- Redis cluster implementation - -**Weeks 9-12**: [Phase 3 - Production Deployment](K8S-PHASE-3.md) -- Production Kubernetes deployment -- Security and monitoring implementation -- Backup and recovery procedures - -**Weeks 13-16**: [Phase 4 - Advanced Features](K8S-PHASE-4.md) -- Performance optimization -- Security enhancements -- Production migration execution - -## Team Requirements - -### Skills and Training -- **Kubernetes Administration**: Container orchestration and cluster management -- **Cloud-Native Development**: Microservices patterns and distributed systems -- **Monitoring and Observability**: Prometheus, Grafana, and logging systems -- **Security**: Container security, network policies, and secret management - -### Operational Procedures -- **Deployment Automation**: CI/CD pipelines and GitOps workflows -- **Incident Response**: Monitoring, alerting, and escalation procedures -- **Backup and Recovery**: Automated backup validation and recovery testing -- **Performance Management**: Capacity planning and scaling procedures - -## Getting Started - -### Prerequisites -- Kubernetes cluster (development/staging/production) -- Container registry for Docker images -- Persistent storage classes -- Network policies and ingress controller -- Monitoring infrastructure (Prometheus/Grafana) - -### Phase 1 Quick Start -1. Review [Phase 1 implementation guide](K8S-PHASE-1.md) -2. Set up development Kubernetes environment -3. Create ConfigMap and Secret templates -4. Begin application configuration externalization -5. Remove LiteDB dependencies - -### Next Steps -After completing Phase 1, proceed with: -- [Phase 2: High Availability Infrastructure](K8S-PHASE-2.md) -- [Phase 3: Production Deployment](K8S-PHASE-3.md) -- [Phase 4: Advanced Features and Optimization](K8S-PHASE-4.md) - -## Support and Documentation - -### Additional Resources -- **Architecture Documentation**: See [docs/architecture.md](docs/architecture.md) -- **Development Guidelines**: Follow existing code conventions and patterns -- **Testing Strategy**: Comprehensive testing at each phase -- **Security Guidelines**: Container and Kubernetes security best practices - -### Team Contacts -- **Project Lead**: Kubernetes modernization coordination -- **DevOps Team**: Infrastructure and deployment automation -- **Security Team**: Security policies and compliance validation -- **QA Team**: Testing and validation procedures - ---- - -**Document Version**: 1.0 -**Last Updated**: January 2025 -**Status**: Implementation Ready - -This comprehensive modernization plan provides a structured approach to transforming MotoVaultPro into a cloud-native, highly available application running on Kubernetes. Each phase builds upon the previous one, ensuring minimal risk while delivering maximum benefits for future growth and reliability. \ No newline at end of file diff --git a/docs/K8S-PHASE-1-DETAILED.md b/docs/K8S-PHASE-1-DETAILED.md deleted file mode 100644 index 6025cf2..0000000 --- a/docs/K8S-PHASE-1-DETAILED.md +++ /dev/null @@ -1,3416 +0,0 @@ -# Phase 1: Core Kubernetes Readiness - Detailed Implementation Plan - -## Executive Summary - -This document provides a comprehensive, step-by-step implementation plan for Phase 1 that ensures minimal risk through incremental changes, thorough testing, and debugging at each step. Each change is isolated, tested, and verified before proceeding to the next step. - -## Improved Implementation Strategy - -### Key Principles -1. **One Change at a Time**: Each step focuses on a single, well-defined change -2. **Non-Destructive First**: Start with safest changes that don't affect data or core functionality -3. **Comprehensive Testing**: Automated and manual validation at each step -4. **Rollback Ready**: Every change includes a rollback procedure -5. **Debugging First**: Extensive debugging and diagnostic capabilities before making changes -6. **Continuous Validation**: Performance and functionality validation throughout - -### Risk Mitigation Improvements -- **Comprehensive Logging**: Extensive structured logging for troubleshooting -- **Feature Flags**: Enable/disable new functionality without code changes -- **Automated Testing**: Comprehensive test suite validation at each step -- **Performance Monitoring**: Baseline and continuous performance validation -- **User Experience**: Functional testing ensures no user-facing regressions - -## Step-by-Step Implementation Plan - -### Step 1: Structured Logging Implementation -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Implement structured JSON logging to improve observability during subsequent changes. - -#### Implementation - -```csharp -// 1.1: Add logging configuration (Program.cs) -builder.Services.AddLogging(loggingBuilder => -{ - loggingBuilder.ClearProviders(); - - if (builder.Environment.IsDevelopment()) - { - loggingBuilder.AddConsole(); - } - else - { - loggingBuilder.AddJsonConsole(options => - { - options.IncludeScopes = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; - options.JsonWriterOptions = new JsonWriterOptions { Indented = false }; - }); - } -}); - -// 1.2: Add correlation ID service -public class CorrelationIdService -{ - public string CorrelationId { get; } = Guid.NewGuid().ToString(); -} - -// 1.3: Add correlation ID middleware -public class CorrelationIdMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public async Task InvokeAsync(HttpContext context) - { - var correlationId = context.Request.Headers["X-Correlation-ID"] - .FirstOrDefault() ?? Guid.NewGuid().ToString(); - - context.Items["CorrelationId"] = correlationId; - context.Response.Headers.Add("X-Correlation-ID", correlationId); - - using var scope = _logger.BeginScope(new Dictionary - { - ["CorrelationId"] = correlationId, - ["RequestPath"] = context.Request.Path, - ["RequestMethod"] = context.Request.Method - }); - - await _next(context); - } -} -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public async Task StructuredLogging_ProducesValidJson() -{ - // Arrange - var logOutput = new StringWriter(); - var logger = CreateTestLogger(logOutput); - - // Act - logger.LogInformation("Test message", new { TestProperty = "TestValue" }); - - // Assert - var logEntry = JsonSerializer.Deserialize(logOutput.ToString()); - Assert.IsNotNull(logEntry.Timestamp); - Assert.AreEqual("Information", logEntry.Level); - Assert.Contains("Test message", logEntry.Message); -} - -[Test] -public async Task CorrelationId_PreservedAcrossRequests() -{ - // Test that correlation ID flows through request pipeline - var client = _factory.CreateClient(); - var correlationId = Guid.NewGuid().ToString(); - - client.DefaultRequestHeaders.Add("X-Correlation-ID", correlationId); - var response = await client.GetAsync("/health"); - - Assert.AreEqual(correlationId, response.Headers.GetValues("X-Correlation-ID").First()); -} -``` - -**Manual Validation**: -1. Start application and verify JSON log format in console -2. Make HTTP requests and verify correlation IDs in logs -3. Check log aggregation works with external tools -4. Verify existing functionality unchanged - -**Success Criteria**: -- [ ] All logs output in structured JSON format -- [ ] Correlation IDs generated and preserved -- [ ] No existing functionality affected -- [ ] Performance impact < 5ms per request - -**Rollback Procedure**: -```bash -# Revert logging configuration -git checkout HEAD~1 -- Program.cs -# Remove middleware registration -# Restart application -``` - ---- - -### Step 2: Health Check Infrastructure -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Implement comprehensive health check endpoints for Kubernetes readiness and liveness probes. - -#### Implementation - -```csharp -// 2.1: Add health check services -public class DatabaseHealthCheck : IHealthCheck -{ - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - - public async Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default) - { - try - { - var connectionString = _configuration.GetConnectionString("DefaultConnection"); - - if (connectionString?.Contains("LiteDB") == true) - { - return await CheckLiteDBHealthAsync(connectionString); - } - else if (!string.IsNullOrEmpty(connectionString)) - { - return await CheckPostgreSQLHealthAsync(connectionString, cancellationToken); - } - - return HealthCheckResult.Unhealthy("No database configuration found"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Database health check failed"); - return HealthCheckResult.Unhealthy("Database health check failed", ex); - } - } - - private async Task CheckPostgreSQLHealthAsync( - string connectionString, - CancellationToken cancellationToken) - { - using var connection = new NpgsqlConnection(connectionString); - await connection.OpenAsync(cancellationToken); - - using var command = new NpgsqlCommand("SELECT 1", connection); - var result = await command.ExecuteScalarAsync(cancellationToken); - - return HealthCheckResult.Healthy($"PostgreSQL connection successful. Result: {result}"); - } - - private async Task CheckLiteDBHealthAsync(string connectionString) - { - try - { - using var db = new LiteDatabase(connectionString); - var collections = db.GetCollectionNames().ToList(); - return HealthCheckResult.Healthy($"LiteDB connection successful. Collections: {collections.Count}"); - } - catch (Exception ex) - { - return HealthCheckResult.Unhealthy("LiteDB connection failed", ex); - } - } -} - -// 2.2: Add application health check -public class ApplicationHealthCheck : IHealthCheck -{ - private readonly IServiceProvider _serviceProvider; - - public async Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default) - { - try - { - // Verify essential services are available - var vehicleLogic = _serviceProvider.GetService(); - var userLogic = _serviceProvider.GetService(); - - if (vehicleLogic == null || userLogic == null) - { - return HealthCheckResult.Unhealthy("Essential services not available"); - } - - return HealthCheckResult.Healthy("Application services available"); - } - catch (Exception ex) - { - return HealthCheckResult.Unhealthy("Application health check failed", ex); - } - } -} - -// 2.3: Configure health checks in Program.cs -builder.Services.AddHealthChecks() - .AddCheck("database", tags: new[] { "ready", "db" }) - .AddCheck("application", tags: new[] { "ready", "app" }); - -// 2.4: Add health check endpoints -app.MapHealthChecks("/health/live", new HealthCheckOptions -{ - Predicate = _ => false, // Only checks if the app is running - ResponseWriter = async (context, report) => - { - context.Response.ContentType = "application/json"; - var response = new - { - status = "Healthy", - timestamp = DateTime.UtcNow, - uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime - }; - await context.Response.WriteAsync(JsonSerializer.Serialize(response)); - } -}); - -app.MapHealthChecks("/health/ready", new HealthCheckOptions -{ - Predicate = check => check.Tags.Contains("ready"), - ResponseWriter = async (context, report) => - { - context.Response.ContentType = "application/json"; - var response = new - { - status = report.Status.ToString(), - timestamp = DateTime.UtcNow, - checks = report.Entries.Select(x => new - { - name = x.Key, - status = x.Value.Status.ToString(), - description = x.Value.Description, - duration = x.Value.Duration.TotalMilliseconds - }) - }; - await context.Response.WriteAsync(JsonSerializer.Serialize(response)); - } -}); -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public async Task HealthCheck_Live_ReturnsHealthy() -{ - var client = _factory.CreateClient(); - var response = await client.GetAsync("/health/live"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - - var content = await response.Content.ReadAsStringAsync(); - var healthResponse = JsonSerializer.Deserialize(content); - - Assert.AreEqual("Healthy", healthResponse.Status); - Assert.IsTrue(healthResponse.Uptime > TimeSpan.Zero); -} - -[Test] -public async Task HealthCheck_Ready_ValidatesAllServices() -{ - var client = _factory.CreateClient(); - var response = await client.GetAsync("/health/ready"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - - var content = await response.Content.ReadAsStringAsync(); - var healthResponse = JsonSerializer.Deserialize(content); - - Assert.Contains(healthResponse.Checks, c => c.Name == "database"); - Assert.Contains(healthResponse.Checks, c => c.Name == "application"); -} - -[Test] -public async Task HealthCheck_DatabaseFailure_ReturnsUnhealthy() -{ - // Test with invalid connection string - var factory = CreateTestFactory(invalidConnectionString: true); - var client = factory.CreateClient(); - - var response = await client.GetAsync("/health/ready"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, response.StatusCode); -} -``` - -**Manual Validation**: -1. Verify `/health/live` returns 200 OK with JSON response -2. Verify `/health/ready` returns detailed health information -3. Test with database disconnected - should return 503 -4. Verify health checks work with both LiteDB and PostgreSQL -5. Test health check performance (< 100ms response time) - -**Success Criteria**: -- [ ] Health endpoints return appropriate HTTP status codes -- [ ] JSON responses contain required health information -- [ ] Database connectivity properly validated -- [ ] Health checks complete within 100ms -- [ ] Unhealthy conditions properly detected - ---- - -### Step 3: Configuration Framework Enhancement -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Enhance configuration framework to support both file-based and environment variable configuration. - -#### Implementation - -```csharp -// 3.1: Create configuration validation service -public class ConfigurationValidationService -{ - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - - public ValidationResult ValidateConfiguration() - { - var result = new ValidationResult(); - - // Database configuration - ValidateDatabaseConfiguration(result); - - // Application configuration - ValidateApplicationConfiguration(result); - - // External service configuration - ValidateExternalServiceConfiguration(result); - - return result; - } - - private void ValidateDatabaseConfiguration(ValidationResult result) - { - var postgresConnection = _configuration.GetConnectionString("DefaultConnection"); - var liteDbPath = _configuration["LiteDB:DatabasePath"]; - - if (string.IsNullOrEmpty(postgresConnection) && string.IsNullOrEmpty(liteDbPath)) - { - result.AddError("Database", "No database configuration found"); - } - - if (!string.IsNullOrEmpty(postgresConnection)) - { - try - { - var builder = new NpgsqlConnectionStringBuilder(postgresConnection); - if (string.IsNullOrEmpty(builder.Database)) - { - result.AddWarning("Database", "PostgreSQL database name not specified"); - } - } - catch (Exception ex) - { - result.AddError("Database", $"Invalid PostgreSQL connection string: {ex.Message}"); - } - } - } - - private void ValidateApplicationConfiguration(ValidationResult result) - { - var appName = _configuration["App:Name"]; - if (string.IsNullOrEmpty(appName)) - { - result.AddWarning("Application", "Application name not configured"); - } - - var logLevel = _configuration["Logging:LogLevel:Default"]; - if (!IsValidLogLevel(logLevel)) - { - result.AddWarning("Logging", $"Invalid log level: {logLevel}"); - } - } - - private void ValidateExternalServiceConfiguration(ValidationResult result) - { - // Validate email configuration if enabled - var emailEnabled = _configuration.GetValue("Email:Enabled"); - if (emailEnabled) - { - var smtpServer = _configuration["Email:SmtpServer"]; - if (string.IsNullOrEmpty(smtpServer)) - { - result.AddError("Email", "SMTP server required when email is enabled"); - } - } - - // Validate OIDC configuration if enabled - var oidcEnabled = _configuration.GetValue("Authentication:OpenIDConnect:Enabled"); - if (oidcEnabled) - { - var authority = _configuration["Authentication:OpenIDConnect:Authority"]; - var clientId = _configuration["Authentication:OpenIDConnect:ClientId"]; - - if (string.IsNullOrEmpty(authority) || string.IsNullOrEmpty(clientId)) - { - result.AddError("OIDC", "Authority and ClientId required for OpenID Connect"); - } - } - } -} - -// 3.2: Create startup configuration validation -public class StartupConfigurationValidator : IHostedService -{ - private readonly ConfigurationValidationService _validator; - private readonly ILogger _logger; - private readonly IHostApplicationLifetime _lifetime; - - public async Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Validating application configuration..."); - - var result = _validator.ValidateConfiguration(); - - foreach (var warning in result.Warnings) - { - _logger.LogWarning("Configuration warning: {Category}: {Message}", - warning.Category, warning.Message); - } - - if (result.HasErrors) - { - foreach (var error in result.Errors) - { - _logger.LogError("Configuration error: {Category}: {Message}", - error.Category, error.Message); - } - - _logger.LogCritical("Application startup failed due to configuration errors"); - _lifetime.StopApplication(); - return; - } - - _logger.LogInformation("Configuration validation completed successfully"); - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} - -// 3.3: Enhanced configuration builder -public static class ConfigurationBuilderExtensions -{ - public static IConfigurationBuilder AddMotoVaultConfiguration( - this IConfigurationBuilder builder, - IWebHostEnvironment environment) - { - // Base configuration files - builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); - builder.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", - optional: true, reloadOnChange: true); - - // Environment variables with prefix - builder.AddEnvironmentVariables("MOTOVAULT_"); - - // Standard environment variables for compatibility - builder.AddEnvironmentVariables(); - - return builder; - } -} - -// 3.4: Register services -builder.Services.AddSingleton(); -builder.Services.AddHostedService(); -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public void ConfigurationValidation_ValidConfiguration_ReturnsSuccess() -{ - var configuration = CreateTestConfiguration(new Dictionary - { - ["ConnectionStrings:DefaultConnection"] = "Host=localhost;Database=test;Username=test;Password=test", - ["App:Name"] = "MotoVaultPro", - ["Logging:LogLevel:Default"] = "Information" - }); - - var validator = new ConfigurationValidationService(configuration, Mock.Of>()); - var result = validator.ValidateConfiguration(); - - Assert.IsFalse(result.HasErrors); - Assert.AreEqual(0, result.Errors.Count); -} - -[Test] -public void ConfigurationValidation_MissingDatabase_ReturnsError() -{ - var configuration = CreateTestConfiguration(new Dictionary - { - ["App:Name"] = "MotoVaultPro" - }); - - var validator = new ConfigurationValidationService(configuration, Mock.Of>()); - var result = validator.ValidateConfiguration(); - - Assert.IsTrue(result.HasErrors); - Assert.Contains(result.Errors, e => e.Category == "Database"); -} - -[Test] -public async Task StartupValidator_InvalidConfiguration_StopsApplication() -{ - var mockLifetime = new Mock(); - var validator = CreateStartupValidator(invalidConfig: true, mockLifetime.Object); - - await validator.StartAsync(CancellationToken.None); - - mockLifetime.Verify(x => x.StopApplication(), Times.Once); -} -``` - -**Manual Validation**: -1. Start application with valid configuration - should start normally -2. Start with missing database configuration - should fail with clear error -3. Start with invalid PostgreSQL connection string - should fail -4. Test environment variable override of JSON configuration -5. Verify configuration warnings are logged but don't stop startup - -**Success Criteria**: -- [ ] Configuration validation runs at startup -- [ ] Invalid configuration prevents application startup -- [ ] Clear error messages for configuration issues -- [ ] Environment variables properly override JSON settings -- [ ] Existing functionality unchanged - ---- - -### Step 4: Configuration Externalization -**Duration**: 3-4 days -**Risk Level**: Medium -**Rollback Complexity**: Moderate - -#### Objective -Externalize configuration to support Kubernetes ConfigMaps and Secrets while maintaining compatibility. - -#### Implementation - -```csharp -// 4.1: Create Kubernetes configuration extensions -public static class KubernetesConfigurationExtensions -{ - public static IConfigurationBuilder AddKubernetesConfiguration( - this IConfigurationBuilder builder) - { - // Check if running in Kubernetes - var kubernetesServiceHost = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST"); - if (!string.IsNullOrEmpty(kubernetesServiceHost)) - { - builder.AddKubernetesSecrets(); - builder.AddKubernetesConfigMaps(); - } - - return builder; - } - - private static IConfigurationBuilder AddKubernetesSecrets(this IConfigurationBuilder builder) - { - var secretsPath = "/var/secrets"; - if (Directory.Exists(secretsPath)) - { - foreach (var secretFile in Directory.GetFiles(secretsPath)) - { - var key = Path.GetFileName(secretFile); - var value = File.ReadAllText(secretFile); - builder.AddInMemoryCollection(new[] { new KeyValuePair(key, value) }); - } - } - return builder; - } - - private static IConfigurationBuilder AddKubernetesConfigMaps(this IConfigurationBuilder builder) - { - var configPath = "/var/config"; - if (Directory.Exists(configPath)) - { - foreach (var configFile in Directory.GetFiles(configPath)) - { - var key = Path.GetFileName(configFile); - var value = File.ReadAllText(configFile); - builder.AddInMemoryCollection(new[] { new KeyValuePair(key, value) }); - } - } - return builder; - } -} - -// 4.2: Create configuration mapping service -public class ConfigurationMappingService -{ - private readonly IConfiguration _configuration; - - public DatabaseConfiguration GetDatabaseConfiguration() - { - return new DatabaseConfiguration - { - PostgreSQLConnectionString = GetConnectionString("POSTGRES_CONNECTION", "ConnectionStrings:DefaultConnection"), - LiteDBPath = GetConfigValue("LITEDB_PATH", "LiteDB:DatabasePath"), - CommandTimeout = GetConfigValue("DB_COMMAND_TIMEOUT", "Database:CommandTimeout", 30), - MaxPoolSize = GetConfigValue("DB_MAX_POOL_SIZE", "Database:MaxPoolSize", 100), - MinPoolSize = GetConfigValue("DB_MIN_POOL_SIZE", "Database:MinPoolSize", 10) - }; - } - - public ApplicationConfiguration GetApplicationConfiguration() - { - return new ApplicationConfiguration - { - Name = GetConfigValue("APP_NAME", "App:Name", "MotoVaultPro"), - Environment = GetConfigValue("ASPNETCORE_ENVIRONMENT", "App:Environment", "Production"), - LogLevel = GetConfigValue("LOG_LEVEL", "Logging:LogLevel:Default", "Information"), - EnableFeatures = GetConfigValue("ENABLE_FEATURES", "App:EnableFeatures", "").Split(','), - CacheExpiryMinutes = GetConfigValue("CACHE_EXPIRY_MINUTES", "App:CacheExpiryMinutes", 30) - }; - } - - public EmailConfiguration GetEmailConfiguration() - { - return new EmailConfiguration - { - Enabled = GetConfigValue("EMAIL_ENABLED", "Email:Enabled", false), - SmtpServer = GetConfigValue("EMAIL_SMTP_SERVER", "Email:SmtpServer"), - SmtpPort = GetConfigValue("EMAIL_SMTP_PORT", "Email:SmtpPort", 587), - Username = GetConfigValue("EMAIL_USERNAME", "Email:Username"), - Password = GetConfigValue("EMAIL_PASSWORD", "Email:Password"), - FromAddress = GetConfigValue("EMAIL_FROM_ADDRESS", "Email:FromAddress"), - EnableSsl = GetConfigValue("EMAIL_ENABLE_SSL", "Email:EnableSsl", true) - }; - } - - private string GetConnectionString(string envKey, string configKey) - { - return _configuration[envKey] ?? _configuration.GetConnectionString(configKey); - } - - private string GetConfigValue(string envKey, string configKey, string defaultValue = null) - { - return _configuration[envKey] ?? _configuration[configKey] ?? defaultValue; - } - - private T GetConfigValue(string envKey, string configKey, T defaultValue = default) - { - var value = _configuration[envKey] ?? _configuration[configKey]; - if (string.IsNullOrEmpty(value)) - return defaultValue; - - return (T)Convert.ChangeType(value, typeof(T)); - } -} - -// 4.3: Create configuration models -public class DatabaseConfiguration -{ - public string PostgreSQLConnectionString { get; set; } - public string LiteDBPath { get; set; } - public int CommandTimeout { get; set; } - public int MaxPoolSize { get; set; } - public int MinPoolSize { get; set; } -} - -public class ApplicationConfiguration -{ - public string Name { get; set; } - public string Environment { get; set; } - public string LogLevel { get; set; } - public string[] EnableFeatures { get; set; } - public int CacheExpiryMinutes { get; set; } -} - -public class EmailConfiguration -{ - public bool Enabled { get; set; } - public string SmtpServer { get; set; } - public int SmtpPort { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string FromAddress { get; set; } - public bool EnableSsl { get; set; } -} - -// 4.4: Update Program.cs configuration -var builder = WebApplication.CreateBuilder(args); - -// Enhanced configuration setup -builder.Configuration - .AddMotoVaultConfiguration(builder.Environment) - .AddKubernetesConfiguration(); - -// Register configuration services -builder.Services.AddSingleton(); -builder.Services.Configure(config => - config = builder.Services.GetRequiredService().GetDatabaseConfiguration()); -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public void ConfigurationMapping_EnvironmentVariableOverride_TakesPrecedence() -{ - Environment.SetEnvironmentVariable("APP_NAME", "TestApp"); - var configuration = CreateTestConfiguration(new Dictionary - { - ["App:Name"] = "ConfigApp" - }); - - var mapper = new ConfigurationMappingService(configuration); - var appConfig = mapper.GetApplicationConfiguration(); - - Assert.AreEqual("TestApp", appConfig.Name); - - Environment.SetEnvironmentVariable("APP_NAME", null); // Cleanup -} - -[Test] -public void KubernetesConfiguration_SecretsPath_LoadsSecrets() -{ - // Create temporary secrets directory - var secretsPath = Path.Combine(Path.GetTempPath(), "secrets"); - Directory.CreateDirectory(secretsPath); - File.WriteAllText(Path.Combine(secretsPath, "POSTGRES_CONNECTION"), "test-connection-string"); - - try - { - Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", "localhost"); - var builder = new ConfigurationBuilder(); - builder.AddKubernetesConfiguration(); - var configuration = builder.Build(); - - Assert.AreEqual("test-connection-string", configuration["POSTGRES_CONNECTION"]); - } - finally - { - Directory.Delete(secretsPath, true); - Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", null); - } -} - -[Test] -public async Task Application_StartupWithExternalizedConfig_Succeeds() -{ - var factory = new WebApplicationFactory() - .WithWebHostBuilder(builder => - { - builder.UseEnvironment("Testing"); - builder.ConfigureAppConfiguration((context, config) => - { - config.AddInMemoryCollection(new[] - { - new KeyValuePair("POSTGRES_CONNECTION", "Host=localhost;Database=test;Username=test;Password=test"), - new KeyValuePair("APP_NAME", "TestApp") - }); - }); - }); - - var client = factory.CreateClient(); - var response = await client.GetAsync("/health/ready"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); -} -``` - -**Kubernetes Manifests for Testing**: -```yaml -# test-configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: motovault-config-test -data: - APP_NAME: "MotoVaultPro" - LOG_LEVEL: "Information" - CACHE_EXPIRY_MINUTES: "30" - ENABLE_FEATURES: "OpenIDConnect,EmailNotifications" - ---- -# test-secret.yaml -apiVersion: v1 -kind: Secret -metadata: - name: motovault-secrets-test -type: Opaque -data: - POSTGRES_CONNECTION: - EMAIL_PASSWORD: - JWT_SECRET: - ---- -# test-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: motovault-config-test -spec: - replicas: 1 - selector: - matchLabels: - app: motovault-test - template: - spec: - containers: - - name: motovault - image: motovault:test - envFrom: - - configMapRef: - name: motovault-config-test - - secretRef: - name: motovault-secrets-test - volumeMounts: - - name: config-volume - mountPath: /var/config - - name: secrets-volume - mountPath: /var/secrets - volumes: - - name: config-volume - configMap: - name: motovault-config-test - - name: secrets-volume - secret: - secretName: motovault-secrets-test -``` - -**Manual Validation**: -1. Test with environment variables only - application should start -2. Test with JSON configuration only - application should start -3. Test with Kubernetes ConfigMap/Secret simulation - application should start -4. Verify environment variables override JSON configuration -5. Test configuration validation with externalized config -6. Deploy to test Kubernetes environment and verify functionality - -**Success Criteria**: -- [ ] Application starts with environment variables only -- [ ] Kubernetes ConfigMap/Secret integration works -- [ ] Environment variables override JSON configuration -- [ ] Configuration validation works with externalized config -- [ ] All existing functionality preserved -- [ ] No hardcoded configuration remains in code - ---- - -### Step 5: PostgreSQL Connection Optimization -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Optimize PostgreSQL connections for high availability and performance without affecting LiteDB functionality. - -#### Implementation - -```csharp -// 5.1: Enhanced PostgreSQL configuration -public class PostgreSQLConnectionService -{ - private readonly DatabaseConfiguration _config; - private readonly ILogger _logger; - private readonly IHostEnvironment _environment; - - public NpgsqlConnectionStringBuilder CreateOptimizedConnectionString() - { - var builder = new NpgsqlConnectionStringBuilder(_config.PostgreSQLConnectionString); - - // Connection pooling optimization - builder.MaxPoolSize = _config.MaxPoolSize; - builder.MinPoolSize = _config.MinPoolSize; - builder.ConnectionLifetime = 300; // 5 minutes - builder.ConnectionIdleLifetime = 300; // 5 minutes - builder.ConnectionPruningInterval = 10; // 10 seconds - - // Performance optimization - builder.CommandTimeout = _config.CommandTimeout; - builder.NoResetOnClose = true; - builder.Enlist = false; // Disable distributed transactions for performance - - // Reliability settings - builder.KeepAlive = 30; // 30 seconds - builder.TcpKeepAliveTime = 30; - builder.TcpKeepAliveInterval = 5; - - // Application name for monitoring - builder.ApplicationName = $"{_config.ApplicationName}-{_environment.EnvironmentName}"; - - _logger.LogInformation("PostgreSQL connection configured: Pool({MinPoolSize}-{MaxPoolSize}), Timeout({CommandTimeout}s)", - builder.MinPoolSize, builder.MaxPoolSize, builder.CommandTimeout); - - return builder; - } - - public async Task TestConnectionAsync(CancellationToken cancellationToken = default) - { - try - { - var connectionString = CreateOptimizedConnectionString().ConnectionString; - using var connection = new NpgsqlConnection(connectionString); - - await connection.OpenAsync(cancellationToken); - - using var command = new NpgsqlCommand("SELECT version()", connection); - var version = await command.ExecuteScalarAsync(cancellationToken); - - _logger.LogInformation("PostgreSQL connection test successful. Version: {Version}", version); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "PostgreSQL connection test failed"); - return false; - } - } -} - -// 5.2: Enhanced database context configuration -public static class DatabaseServiceExtensions -{ - public static IServiceCollection AddOptimizedDatabase( - this IServiceCollection services, - DatabaseConfiguration config) - { - if (!string.IsNullOrEmpty(config.PostgreSQLConnectionString)) - { - services.AddOptimizedPostgreSQL(config); - } - else if (!string.IsNullOrEmpty(config.LiteDBPath)) - { - services.AddLiteDB(config); - } - else - { - throw new InvalidOperationException("No database configuration provided"); - } - - return services; - } - - private static IServiceCollection AddOptimizedPostgreSQL( - this IServiceCollection services, - DatabaseConfiguration config) - { - services.AddSingleton(); - - services.AddDbContextFactory((serviceProvider, options) => - { - var connectionService = serviceProvider.GetRequiredService(); - var connectionString = connectionService.CreateOptimizedConnectionString().ConnectionString; - - options.UseNpgsql(connectionString, npgsqlOptions => - { - npgsqlOptions.EnableRetryOnFailure( - maxRetryCount: 3, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorCodesToAdd: null); - - npgsqlOptions.CommandTimeout(config.CommandTimeout); - npgsqlOptions.MigrationsAssembly(typeof(MotoVaultContext).Assembly.FullName); - }); - - // Performance optimizations - options.EnableSensitiveDataLogging(false); - options.EnableServiceProviderCaching(); - options.EnableDetailedErrors(false); - - }, ServiceLifetime.Singleton); - - // Register data access implementations - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - return services; - } - - private static IServiceCollection AddLiteDB( - this IServiceCollection services, - DatabaseConfiguration config) - { - // Keep existing LiteDB configuration unchanged - services.AddSingleton(provider => - { - var connectionString = $"Filename={config.LiteDBPath};Connection=shared"; - return new LiteDatabase(connectionString); - }); - - // Register LiteDB data access implementations - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - return services; - } -} - -// 5.3: Connection monitoring service -public class DatabaseConnectionMonitoringService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly Counter _connectionAttempts; - private readonly Counter _connectionFailures; - private readonly Gauge _activeConnections; - - public DatabaseConnectionMonitoringService(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - - _connectionAttempts = Metrics.CreateCounter( - "motovault_db_connection_attempts_total", - "Total database connection attempts"); - - _connectionFailures = Metrics.CreateCounter( - "motovault_db_connection_failures_total", - "Total database connection failures"); - - _activeConnections = Metrics.CreateGauge( - "motovault_db_active_connections", - "Number of active database connections"); - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - await MonitorConnections(); - await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); - } - } - - private async Task MonitorConnections() - { - try - { - using var scope = _serviceProvider.CreateScope(); - var connectionService = scope.ServiceProvider.GetService(); - - if (connectionService != null) - { - _connectionAttempts.Inc(); - - var isHealthy = await connectionService.TestConnectionAsync(); - if (!isHealthy) - { - _connectionFailures.Inc(); - _logger.LogWarning("Database connection health check failed"); - } - - // Monitor connection pool if available - await MonitorConnectionPool(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error monitoring database connections"); - _connectionFailures.Inc(); - } - } - - private async Task MonitorConnectionPool() - { - // This would require access to Npgsql connection pool metrics - // For now, we'll implement a basic check - try - { - using var scope = _serviceProvider.CreateScope(); - var contextFactory = scope.ServiceProvider.GetService>(); - - if (contextFactory != null) - { - using var context = contextFactory.CreateDbContext(); - var connectionState = context.Database.GetDbConnection().State; - - _logger.LogDebug("Database connection state: {State}", connectionState); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to monitor connection pool"); - } - } -} - -// 5.4: Register services in Program.cs -var databaseConfig = builder.Services.GetRequiredService() - .GetDatabaseConfiguration(); - -builder.Services.AddOptimizedDatabase(databaseConfig); -builder.Services.AddHostedService(); -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public void PostgreSQLConnectionService_CreatesOptimizedConnectionString() -{ - var config = new DatabaseConfiguration - { - PostgreSQLConnectionString = "Host=localhost;Database=test;Username=test;Password=test", - MaxPoolSize = 50, - MinPoolSize = 5, - CommandTimeout = 30 - }; - - var service = new PostgreSQLConnectionService(config, Mock.Of>(), Mock.Of()); - var builder = service.CreateOptimizedConnectionString(); - - Assert.AreEqual(50, builder.MaxPoolSize); - Assert.AreEqual(5, builder.MinPoolSize); - Assert.AreEqual(30, builder.CommandTimeout); - Assert.AreEqual(300, builder.ConnectionLifetime); -} - -[Test] -public async Task PostgreSQLConnectionService_TestConnection_ValidConnection_ReturnsTrue() -{ - var config = CreateValidDatabaseConfiguration(); - var service = new PostgreSQLConnectionService(config, Mock.Of>(), Mock.Of()); - - var result = await service.TestConnectionAsync(); - - Assert.IsTrue(result); -} - -[Test] -public async Task DatabaseServiceExtensions_PostgreSQLConfiguration_RegistersCorrectServices() -{ - var services = new ServiceCollection(); - var config = new DatabaseConfiguration - { - PostgreSQLConnectionString = "Host=localhost;Database=test;Username=test;Password=test" - }; - - services.AddOptimizedDatabase(config); - - var serviceProvider = services.BuildServiceProvider(); - - Assert.IsNotNull(serviceProvider.GetService>()); - Assert.IsNotNull(serviceProvider.GetService()); - Assert.IsInstanceOf(serviceProvider.GetService()); -} - -[Test] -public async Task DatabaseServiceExtensions_LiteDBConfiguration_RegistersCorrectServices() -{ - var services = new ServiceCollection(); - var config = new DatabaseConfiguration - { - LiteDBPath = ":memory:" - }; - - services.AddOptimizedDatabase(config); - - var serviceProvider = services.BuildServiceProvider(); - - Assert.IsNotNull(serviceProvider.GetService()); - Assert.IsInstanceOf(serviceProvider.GetService()); -} -``` - -**Performance Tests**: -```csharp -[Test] -public async Task PostgreSQLConnection_ConcurrentConnections_HandlesLoad() -{ - var config = CreateValidDatabaseConfiguration(); - var service = new PostgreSQLConnectionService(config, Mock.Of>(), Mock.Of()); - - var tasks = Enumerable.Range(0, 20).Select(async i => - { - var stopwatch = Stopwatch.StartNew(); - var result = await service.TestConnectionAsync(); - stopwatch.Stop(); - - return new { Success = result, Duration = stopwatch.ElapsedMilliseconds }; - }); - - var results = await Task.WhenAll(tasks); - - Assert.IsTrue(results.All(r => r.Success)); - Assert.IsTrue(results.All(r => r.Duration < 1000)); // All connections under 1 second -} - -[Test] -public async Task DatabaseContext_ConcurrentQueries_OptimalPerformance() -{ - using var factory = CreateDbContextFactory(); - - var tasks = Enumerable.Range(0, 10).Select(async i => - { - using var context = factory.CreateDbContext(); - var stopwatch = Stopwatch.StartNew(); - - var count = await context.Vehicles.CountAsync(); - - stopwatch.Stop(); - return stopwatch.ElapsedMilliseconds; - }); - - var durations = await Task.WhenAll(tasks); - - Assert.IsTrue(durations.All(d => d < 500)); // All queries under 500ms - Assert.IsTrue(durations.Average() < 200); // Average under 200ms -} -``` - -**Manual Validation**: -1. Test PostgreSQL connection with optimized settings -2. Verify connection pooling behavior under load -3. Test connection recovery after database restart -4. Verify LiteDB functionality remains unchanged -5. Monitor connection metrics during testing -6. Test with both PostgreSQL and LiteDB configurations - -**Success Criteria**: -- [ ] PostgreSQL connections use optimized settings -- [ ] Connection pooling configured correctly -- [ ] Connection monitoring provides metrics -- [ ] LiteDB functionality unchanged -- [ ] Performance improvement measurable -- [ ] Connection recovery works after database restart - ---- - -### Step 6: Database Provider Selection and Debugging Infrastructure -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Implement a clean database provider selection mechanism with comprehensive debugging and diagnostic capabilities. - -#### Implementation - -```csharp -// 6.1: Database provider selector -public enum DatabaseProvider -{ - LiteDB, - PostgreSQL -} - -public class DatabaseProviderService -{ - private readonly DatabaseConfiguration _config; - private readonly ILogger _logger; - - public DatabaseProvider GetActiveProvider() - { - var hasPostgreSQL = !string.IsNullOrEmpty(_config.PostgreSQLConnectionString); - var hasLiteDB = !string.IsNullOrEmpty(_config.LiteDBPath); - - if (hasPostgreSQL) - { - _logger.LogInformation("PostgreSQL database mode enabled. Connection: {ConnectionInfo}", - GetConnectionInfo(_config.PostgreSQLConnectionString)); - return DatabaseProvider.PostgreSQL; - } - - if (hasLiteDB) - { - _logger.LogInformation("LiteDB database mode enabled. Path: {LiteDBPath}", _config.LiteDBPath); - return DatabaseProvider.LiteDB; - } - - throw new InvalidOperationException("No database provider configured"); - } - - private string GetConnectionInfo(string connectionString) - { - try - { - var builder = new NpgsqlConnectionStringBuilder(connectionString); - return $"Host={builder.Host}, Database={builder.Database}, Port={builder.Port}"; - } - catch - { - return "Invalid connection string"; - } - } -} - -// 6.2: Database diagnostics service -public class DatabaseDiagnosticsService -{ - private readonly ILogger _logger; - private readonly DatabaseConfiguration _config; - private readonly DatabaseProviderService _providerService; - - public async Task PerformDiagnosticsAsync() - { - var result = new DatabaseDiagnosticResult(); - var provider = _providerService.GetActiveProvider(); - - _logger.LogInformation("Starting database diagnostics for provider: {Provider}", provider); - - switch (provider) - { - case DatabaseProvider.PostgreSQL: - await DiagnosePostgreSQLAsync(result); - break; - case DatabaseProvider.LiteDB: - await DiagnoseLiteDBAsync(result); - break; - } - - _logger.LogInformation("Database diagnostics completed. Status: {Status}, Issues: {IssueCount}", - result.OverallStatus, result.Issues.Count); - - return result; - } - - private async Task DiagnosePostgreSQLAsync(DatabaseDiagnosticResult result) - { - result.Provider = "PostgreSQL"; - - // Test connection string parsing - try - { - var builder = new NpgsqlConnectionStringBuilder(_config.PostgreSQLConnectionString); - result.ConnectionDetails = new Dictionary - { - ["Host"] = builder.Host, - ["Port"] = builder.Port, - ["Database"] = builder.Database, - ["Username"] = builder.Username, - ["MaxPoolSize"] = builder.MaxPoolSize, - ["MinPoolSize"] = builder.MinPoolSize, - ["CommandTimeout"] = builder.CommandTimeout - }; - _logger.LogDebug("PostgreSQL connection string parsed successfully"); - } - catch (Exception ex) - { - result.Issues.Add($"Invalid PostgreSQL connection string: {ex.Message}"); - _logger.LogError(ex, "Failed to parse PostgreSQL connection string"); - result.OverallStatus = "Failed"; - return; - } - - // Test connectivity - try - { - using var connection = new NpgsqlConnection(_config.PostgreSQLConnectionString); - var stopwatch = Stopwatch.StartNew(); - await connection.OpenAsync(); - stopwatch.Stop(); - - result.ConnectionTime = stopwatch.ElapsedMilliseconds; - _logger.LogDebug("PostgreSQL connection established in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); - - // Test basic query - using var command = new NpgsqlCommand("SELECT version(), current_database(), current_user", connection); - using var reader = await command.ExecuteReaderAsync(); - - if (await reader.ReadAsync()) - { - result.ServerInfo = new Dictionary - { - ["Version"] = reader.GetString(0), - ["Database"] = reader.GetString(1), - ["User"] = reader.GetString(2) - }; - } - - result.OverallStatus = "Healthy"; - _logger.LogInformation("PostgreSQL diagnostics successful. Version: {Version}", - result.ServerInfo?["Version"]); - } - catch (Exception ex) - { - result.Issues.Add($"PostgreSQL connection failed: {ex.Message}"); - result.OverallStatus = "Failed"; - _logger.LogError(ex, "PostgreSQL connection failed during diagnostics"); - } - } - - private async Task DiagnoseLiteDBAsync(DatabaseDiagnosticResult result) - { - result.Provider = "LiteDB"; - - try - { - var dbPath = _config.LiteDBPath; - var directory = Path.GetDirectoryName(dbPath); - - result.ConnectionDetails = new Dictionary - { - ["DatabasePath"] = dbPath, - ["Directory"] = directory, - ["DirectoryExists"] = Directory.Exists(directory), - ["FileExists"] = File.Exists(dbPath) - }; - - // Test directory access - if (!Directory.Exists(directory)) - { - _logger.LogWarning("LiteDB directory does not exist: {Directory}", directory); - Directory.CreateDirectory(directory); - _logger.LogInformation("Created LiteDB directory: {Directory}", directory); - } - - // Test LiteDB access - var stopwatch = Stopwatch.StartNew(); - using var db = new LiteDatabase($"Filename={dbPath};Connection=shared"); - var collections = db.GetCollectionNames().ToList(); - stopwatch.Stop(); - - result.ConnectionTime = stopwatch.ElapsedMilliseconds; - result.ServerInfo = new Dictionary - { - ["Collections"] = collections, - ["CollectionCount"] = collections.Count, - ["FileSize"] = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0 - }; - - result.OverallStatus = "Healthy"; - _logger.LogInformation("LiteDB diagnostics successful. Collections: {CollectionCount}, Size: {FileSize} bytes", - collections.Count, result.ServerInfo["FileSize"]); - } - catch (Exception ex) - { - result.Issues.Add($"LiteDB access failed: {ex.Message}"); - result.OverallStatus = "Failed"; - _logger.LogError(ex, "LiteDB access failed during diagnostics"); - } - } -} - -// 6.3: Database diagnostic result model -public class DatabaseDiagnosticResult -{ - public string Provider { get; set; } - public string OverallStatus { get; set; } = "Unknown"; - public long ConnectionTime { get; set; } - public Dictionary ConnectionDetails { get; set; } = new(); - public Dictionary ServerInfo { get; set; } = new(); - public List Issues { get; set; } = new(); - public List Recommendations { get; set; } = new(); -} - -// 6.4: Database startup diagnostics service -public class DatabaseStartupDiagnosticsService : IHostedService -{ - private readonly DatabaseDiagnosticsService _diagnostics; - private readonly ILogger _logger; - - public DatabaseStartupDiagnosticsService( - DatabaseDiagnosticsService diagnostics, - ILogger logger) - { - _diagnostics = diagnostics; - _logger = logger; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - _logger.LogInformation("Running database diagnostics at startup"); - var result = await _diagnostics.PerformDiagnosticsAsync(); - - _logger.LogInformation("Database diagnostics completed. Provider: {Provider}, Status: {Status}, ConnectionTime: {ConnectionTime}ms", - result.Provider, result.OverallStatus, result.ConnectionTime); - - if (result.Issues.Any()) - { - foreach (var issue in result.Issues) - { - _logger.LogWarning("Database diagnostic issue: {Issue}", issue); - } - } - - foreach (var detail in result.ConnectionDetails) - { - _logger.LogDebug("Database connection detail - {Key}: {Value}", detail.Key, detail.Value); - } - - foreach (var info in result.ServerInfo) - { - _logger.LogInformation("Database server info - {Key}: {Value}", info.Key, info.Value); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Database startup diagnostics failed"); - } - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} - -// 6.5: Enhanced database service registration -public static class DatabaseServiceExtensions -{ - public static IServiceCollection AddDatabaseWithDiagnostics( - this IServiceCollection services, - DatabaseConfiguration config) - { - services.AddSingleton(); - services.AddSingleton(); - - var providerService = new DatabaseProviderService(config, null); - var provider = providerService.GetActiveProvider(); - - switch (provider) - { - case DatabaseProvider.PostgreSQL: - services.AddOptimizedPostgreSQL(config); - break; - - case DatabaseProvider.LiteDB: - services.AddLiteDB(config); - break; - } - - return services; - } -} -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public void DatabaseProviderService_PostgreSQLConfigured_ReturnsPostgreSQL() -{ - var config = new DatabaseConfiguration - { - PostgreSQLConnectionString = "Host=localhost;Database=test;Username=test;Password=test" - }; - - var service = new DatabaseProviderService(config, Mock.Of>()); - var provider = service.GetActiveProvider(); - - Assert.AreEqual(DatabaseProvider.PostgreSQL, provider); -} - -[Test] -public void DatabaseProviderService_LiteDBConfigured_ReturnsLiteDB() -{ - var config = new DatabaseConfiguration - { - LiteDBPath = "/tmp/test.db" - }; - - var service = new DatabaseProviderService(config, Mock.Of>()); - var provider = service.GetActiveProvider(); - - Assert.AreEqual(DatabaseProvider.LiteDB, provider); -} - -[Test] -public void DatabaseProviderService_NoConfiguration_ThrowsException() -{ - var config = new DatabaseConfiguration(); - - var service = new DatabaseProviderService(config, Mock.Of>()); - - Assert.Throws(() => service.GetActiveProvider()); -} - -[Test] -public async Task DatabaseDiagnosticsService_PostgreSQL_ValidConnection_ReturnsHealthy() -{ - var config = new DatabaseConfiguration - { - PostgreSQLConnectionString = GetTestPostgreSQLConnectionString() - }; - - var providerService = new DatabaseProviderService(config, Mock.Of>()); - var diagnostics = new DatabaseDiagnosticsService( - Mock.Of>(), - config, - providerService); - - var result = await diagnostics.PerformDiagnosticsAsync(); - - Assert.AreEqual("Healthy", result.OverallStatus); - Assert.AreEqual("PostgreSQL", result.Provider); - Assert.IsTrue(result.ConnectionTime > 0); - Assert.IsTrue(result.ServerInfo.ContainsKey("Version")); -} - -[Test] -public async Task DatabaseDiagnosticsService_LiteDB_ValidPath_ReturnsHealthy() -{ - var tempPath = Path.GetTempFileName(); - var config = new DatabaseConfiguration - { - LiteDBPath = tempPath - }; - - try - { - var providerService = new DatabaseProviderService(config, Mock.Of>()); - var diagnostics = new DatabaseDiagnosticsService( - Mock.Of>(), - config, - providerService); - - var result = await diagnostics.PerformDiagnosticsAsync(); - - Assert.AreEqual("Healthy", result.OverallStatus); - Assert.AreEqual("LiteDB", result.Provider); - Assert.IsTrue(result.ConnectionTime >= 0); - } - finally - { - File.Delete(tempPath); - } -} - -[Test] -public async Task DatabaseStartupDiagnosticsService_RunsAtStartup_LogsResults() -{ - var mockLogger = new Mock>(); - var mockDiagnostics = new Mock(); - - var diagnosticResult = new DatabaseDiagnosticResult - { - Provider = "PostgreSQL", - OverallStatus = "Healthy", - ConnectionTime = 50 - }; - - mockDiagnostics.Setup(x => x.PerformDiagnosticsAsync()) - .ReturnsAsync(diagnosticResult); - - var service = new DatabaseStartupDiagnosticsService(mockDiagnostics.Object, mockLogger.Object); - await service.StartAsync(CancellationToken.None); - - // Verify that diagnostic information was logged - mockLogger.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Database diagnostics completed")), - It.IsAny(), - It.IsAny>()), - Times.Once); -} -``` - -**Manual Validation**: -1. Test database provider selection with PostgreSQL configuration -2. Test database provider selection with LiteDB configuration -3. Review startup logs for diagnostic information -4. Test with invalid PostgreSQL connection string and verify error logging -5. Test with invalid LiteDB path and verify error logging -6. Verify logging output provides comprehensive debugging information - -**Success Criteria**: -- [ ] Database provider correctly selected based on configuration -- [ ] Comprehensive diagnostic information logged at startup -- [ ] Error conditions properly detected and logged -- [ ] Logging provides sufficient detail for debugging -- [ ] Connection details and server info logged appropriately -- [ ] No impact on existing functionality - ---- - -### Step 7: Database Migration Preparation and Tooling -**Duration**: 3-4 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Create comprehensive database migration tools and validation utilities for future PostgreSQL transition. - -#### Implementation - -```csharp -// 7.1: Database migration service -public class DatabaseMigrationService -{ - private readonly ILogger _logger; - private readonly DatabaseConfiguration _config; - - public async Task PrepareMigrationAsync() - { - var result = new MigrationPreparationResult(); - - _logger.LogInformation("Starting migration preparation analysis"); - - // Analyze current data structure - await AnalyzeDataStructureAsync(result); - - // Validate migration prerequisites - await ValidateMigrationPrerequisitesAsync(result); - - // Generate migration plan - await GenerateMigrationPlanAsync(result); - - _logger.LogInformation("Migration preparation completed. Status: {Status}", result.Status); - return result; - } - - private async Task AnalyzeDataStructureAsync(MigrationPreparationResult result) - { - try - { - if (!string.IsNullOrEmpty(_config.LiteDBPath) && File.Exists(_config.LiteDBPath)) - { - using var db = new LiteDatabase(_config.LiteDBPath); - var collections = db.GetCollectionNames().ToList(); - - result.DataAnalysis = new Dictionary(); - - foreach (var collectionName in collections) - { - var collection = db.GetCollection(collectionName); - var count = collection.Count(); - - result.DataAnalysis[collectionName] = new - { - RecordCount = count, - EstimatedSize = count * 1024 // Rough estimate - }; - - _logger.LogDebug("Collection {Collection}: {Count} records", collectionName, count); - } - - result.TotalRecords = result.DataAnalysis.Values - .Cast() - .Sum(x => (int)x.RecordCount); - - _logger.LogInformation("Data analysis completed. Total records: {TotalRecords}", result.TotalRecords); - } - else - { - result.DataAnalysis = new Dictionary(); - result.TotalRecords = 0; - _logger.LogInformation("No LiteDB database found for analysis"); - } - } - catch (Exception ex) - { - result.Issues.Add($"Data analysis failed: {ex.Message}"); - _logger.LogError(ex, "Failed to analyze data structure"); - } - } - - private async Task ValidateMigrationPrerequisitesAsync(MigrationPreparationResult result) - { - _logger.LogDebug("Validating migration prerequisites"); - - // Check PostgreSQL availability if configured - if (!string.IsNullOrEmpty(_config.PostgreSQLConnectionString)) - { - try - { - using var connection = new NpgsqlConnection(_config.PostgreSQLConnectionString); - await connection.OpenAsync(); - - // Check if database is empty or has expected schema - using var command = new NpgsqlCommand( - "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'", - connection); - var tableCount = (long)await command.ExecuteScalarAsync(); - - result.Prerequisites["PostgreSQLConnectivity"] = true; - result.Prerequisites["PostgreSQLTableCount"] = tableCount; - - if (tableCount > 0) - { - result.Recommendations.Add("PostgreSQL database contains existing tables. Consider backup before migration."); - } - - _logger.LogDebug("PostgreSQL validation successful. Table count: {TableCount}", tableCount); - } - catch (Exception ex) - { - result.Prerequisites["PostgreSQLConnectivity"] = false; - result.Issues.Add($"PostgreSQL validation failed: {ex.Message}"); - _logger.LogWarning(ex, "PostgreSQL validation failed"); - } - } - - // Check disk space - try - { - var currentPath = Environment.CurrentDirectory; - var drive = new DriveInfo(Path.GetPathRoot(currentPath)); - var freeSpaceGB = drive.AvailableFreeSpace / (1024 * 1024 * 1024); - - result.Prerequisites["DiskSpaceGB"] = freeSpaceGB; - - if (freeSpaceGB < 1) - { - result.Issues.Add("Insufficient disk space for migration (< 1GB available)"); - } - else if (freeSpaceGB < 5) - { - result.Recommendations.Add("Limited disk space available. Monitor during migration."); - } - - _logger.LogDebug("Disk space check: {FreeSpaceGB}GB available", freeSpaceGB); - } - catch (Exception ex) - { - result.Issues.Add($"Disk space check failed: {ex.Message}"); - _logger.LogWarning(ex, "Failed to check disk space"); - } - } - - private async Task GenerateMigrationPlanAsync(MigrationPreparationResult result) - { - _logger.LogDebug("Generating migration plan"); - - var plan = new List(); - - if (result.TotalRecords > 0) - { - plan.Add("1. Create PostgreSQL database schema"); - plan.Add("2. Export data from LiteDB"); - plan.Add("3. Transform data for PostgreSQL compatibility"); - plan.Add("4. Import data to PostgreSQL"); - plan.Add("5. Validate data integrity"); - plan.Add("6. Update configuration to use PostgreSQL"); - plan.Add("7. Test application functionality"); - plan.Add("8. Archive LiteDB data"); - - // Estimate migration time based on record count - var estimatedMinutes = Math.Max(5, result.TotalRecords / 1000); // Rough estimate - result.EstimatedMigrationTime = TimeSpan.FromMinutes(estimatedMinutes); - - plan.Add($"Estimated migration time: {result.EstimatedMigrationTime.TotalMinutes:F0} minutes"); - } - else - { - plan.Add("1. Create PostgreSQL database schema"); - plan.Add("2. Update configuration to use PostgreSQL"); - plan.Add("3. Test application functionality"); - result.EstimatedMigrationTime = TimeSpan.FromMinutes(5); - } - - result.MigrationPlan = plan; - _logger.LogInformation("Migration plan generated with {StepCount} steps", plan.Count); - } -} - -// 7.2: Migration result models -public class MigrationPreparationResult -{ - public string Status { get; set; } = "Success"; - public Dictionary DataAnalysis { get; set; } = new(); - public Dictionary Prerequisites { get; set; } = new(); - public List Issues { get; set; } = new(); - public List Recommendations { get; set; } = new(); - public List MigrationPlan { get; set; } = new(); - public int TotalRecords { get; set; } - public TimeSpan EstimatedMigrationTime { get; set; } -} - -// 7.3: Data validation service -public class DataValidationService -{ - private readonly ILogger _logger; - - public async Task ValidateDataIntegrityAsync(DatabaseProvider provider) - { - var result = new DataValidationResult { Provider = provider.ToString() }; - - _logger.LogInformation("Starting data integrity validation for {Provider}", provider); - - switch (provider) - { - case DatabaseProvider.LiteDB: - await ValidateLiteDBIntegrityAsync(result); - break; - case DatabaseProvider.PostgreSQL: - await ValidatePostgreSQLIntegrityAsync(result); - break; - } - - _logger.LogInformation("Data validation completed for {Provider}. Status: {Status}, Issues: {IssueCount}", - provider, result.Status, result.Issues.Count); - - return result; - } - - private async Task ValidateLiteDBIntegrityAsync(DataValidationResult result) - { - try - { - // Implement LiteDB-specific validation logic - result.ValidationChecks["LiteDBAccessible"] = true; - result.ValidationChecks["CollectionsAccessible"] = true; - result.Status = "Healthy"; - } - catch (Exception ex) - { - result.Issues.Add($"LiteDB validation failed: {ex.Message}"); - result.Status = "Failed"; - _logger.LogError(ex, "LiteDB validation failed"); - } - } - - private async Task ValidatePostgreSQLIntegrityAsync(DataValidationResult result) - { - try - { - // Implement PostgreSQL-specific validation logic - result.ValidationChecks["PostgreSQLAccessible"] = true; - result.ValidationChecks["TablesAccessible"] = true; - result.Status = "Healthy"; - } - catch (Exception ex) - { - result.Issues.Add($"PostgreSQL validation failed: {ex.Message}"); - result.Status = "Failed"; - _logger.LogError(ex, "PostgreSQL validation failed"); - } - } -} - -public class DataValidationResult -{ - public string Provider { get; set; } - public string Status { get; set; } = "Unknown"; - public Dictionary ValidationChecks { get; set; } = new(); - public List Issues { get; set; } = new(); - public Dictionary Statistics { get; set; } = new(); -} - -// 7.4: Migration analysis startup service -public class MigrationAnalysisService : IHostedService -{ - private readonly DatabaseMigrationService _migrationService; - private readonly DataValidationService _validationService; - private readonly DatabaseProviderService _providerService; - private readonly FeatureFlagService _featureFlags; - private readonly ILogger _logger; - - public MigrationAnalysisService( - DatabaseMigrationService migrationService, - DataValidationService validationService, - DatabaseProviderService providerService, - FeatureFlagService featureFlags, - ILogger logger) - { - _migrationService = migrationService; - _validationService = validationService; - _providerService = providerService; - _featureFlags = featureFlags; - _logger = logger; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - if (!_featureFlags.IsEnabled("MigrationTools", true)) - { - _logger.LogDebug("Migration tools feature is disabled, skipping migration analysis"); - return; - } - - try - { - var currentProvider = _providerService.GetActiveProvider(); - _logger.LogInformation("Running migration analysis for current provider: {Provider}", currentProvider); - - var migrationResult = await _migrationService.PrepareMigrationAsync(); - _logger.LogInformation("Migration analysis completed. Status: {Status}, Total Records: {TotalRecords}, Estimated Time: {EstimatedTime}", - migrationResult.Status, migrationResult.TotalRecords, migrationResult.EstimatedMigrationTime); - - if (migrationResult.Issues.Any()) - { - foreach (var issue in migrationResult.Issues) - { - _logger.LogWarning("Migration analysis issue: {Issue}", issue); - } - } - - if (migrationResult.Recommendations.Any()) - { - foreach (var recommendation in migrationResult.Recommendations) - { - _logger.LogInformation("Migration recommendation: {Recommendation}", recommendation); - } - } - - foreach (var step in migrationResult.MigrationPlan) - { - _logger.LogDebug("Migration plan step: {Step}", step); - } - - var validationResult = await _validationService.ValidateDataIntegrityAsync(currentProvider); - _logger.LogInformation("Data validation completed for {Provider}. Status: {Status}", - validationResult.Provider, validationResult.Status); - - if (validationResult.Issues.Any()) - { - foreach (var issue in validationResult.Issues) - { - _logger.LogWarning("Data validation issue: {Issue}", issue); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Migration analysis failed during startup"); - } - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public async Task DatabaseMigrationService_PrepareMigration_GeneratesValidPlan() -{ - var config = new DatabaseConfiguration - { - LiteDBPath = CreateTestLiteDBWithData(), - PostgreSQLConnectionString = GetTestPostgreSQLConnectionString() - }; - - var migrationService = new DatabaseMigrationService( - Mock.Of>(), - config); - - var result = await migrationService.PrepareMigrationAsync(); - - Assert.AreEqual("Success", result.Status); - Assert.IsTrue(result.MigrationPlan.Count > 0); - Assert.IsTrue(result.TotalRecords >= 0); - Assert.IsTrue(result.EstimatedMigrationTime > TimeSpan.Zero); -} - -[Test] -public async Task DataValidationService_LiteDB_ReturnsValidationResult() -{ - var validationService = new DataValidationService(Mock.Of>()); - - var result = await validationService.ValidateDataIntegrityAsync(DatabaseProvider.LiteDB); - - Assert.AreEqual("LiteDB", result.Provider); - Assert.IsNotNull(result.Status); - Assert.IsNotNull(result.ValidationChecks); -} - -[Test] -public async Task MigrationAnalysisService_RunsAtStartup_LogsAnalysis() -{ - var mockLogger = new Mock>(); - var mockMigrationService = new Mock(); - var mockValidationService = new Mock(); - var mockProviderService = new Mock(); - var mockFeatureFlags = new Mock(); - - mockFeatureFlags.Setup(x => x.IsEnabled("MigrationTools", true)).Returns(true); - mockProviderService.Setup(x => x.GetActiveProvider()).Returns(DatabaseProvider.LiteDB); - - var migrationResult = new MigrationPreparationResult { Status = "Success", TotalRecords = 100 }; - mockMigrationService.Setup(x => x.PrepareMigrationAsync()).ReturnsAsync(migrationResult); - - var validationResult = new DataValidationResult { Provider = "LiteDB", Status = "Healthy" }; - mockValidationService.Setup(x => x.ValidateDataIntegrityAsync(It.IsAny())) - .ReturnsAsync(validationResult); - - var service = new MigrationAnalysisService( - mockMigrationService.Object, - mockValidationService.Object, - mockProviderService.Object, - mockFeatureFlags.Object, - mockLogger.Object); - - await service.StartAsync(CancellationToken.None); - - // Verify migration analysis was logged - mockLogger.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Migration analysis completed")), - It.IsAny(), - It.IsAny>()), - Times.Once); -} -``` - -**Manual Validation**: -1. Review startup logs for migration analysis information -2. Test migration preparation with existing LiteDB data -3. Test migration preparation with empty database -4. Verify PostgreSQL connectivity validation in logs -5. Test with invalid PostgreSQL configuration and check error logs -6. Verify migration plan generation logic through log output - -**Success Criteria**: -- [ ] Migration preparation analysis works correctly -- [ ] Data structure analysis provides accurate information and logs details -- [ ] Migration plan generated with realistic time estimates -- [ ] Prerequisites validation identifies potential issues and logs them -- [ ] Comprehensive migration information logged at startup -- [ ] No impact on existing application functionality - ---- - -### Step 8: Performance Monitoring and Benchmarking -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Implement comprehensive performance monitoring and benchmarking to establish baselines and detect regressions. - -#### Implementation - -```csharp -// 8.1: Performance monitoring service -public class PerformanceMonitoringService -{ - private readonly ILogger _logger; - private readonly Counter _requestCounter; - private readonly Histogram _requestDuration; - private readonly Histogram _databaseOperationDuration; - private readonly Gauge _activeConnections; - - public PerformanceMonitoringService(ILogger logger) - { - _logger = logger; - - _requestCounter = Metrics.CreateCounter( - "motovault_http_requests_total", - "Total HTTP requests", - new[] { "method", "endpoint", "status_code" }); - - _requestDuration = Metrics.CreateHistogram( - "motovault_http_request_duration_seconds", - "HTTP request duration in seconds", - new[] { "method", "endpoint" }); - - _databaseOperationDuration = Metrics.CreateHistogram( - "motovault_database_operation_duration_seconds", - "Database operation duration in seconds", - new[] { "operation", "provider" }); - - _activeConnections = Metrics.CreateGauge( - "motovault_active_connections", - "Number of active connections"); - } - - public void RecordHttpRequest(string method, string endpoint, int statusCode, double durationSeconds) - { - _requestCounter.WithLabels(method, endpoint, statusCode.ToString()).Inc(); - _requestDuration.WithLabels(method, endpoint).Observe(durationSeconds); - } - - public void RecordDatabaseOperation(string operation, string provider, double durationSeconds) - { - _databaseOperationDuration.WithLabels(operation, provider).Observe(durationSeconds); - } - - public void SetActiveConnections(double count) - { - _activeConnections.Set(count); - } -} - -// 8.2: Performance monitoring middleware -public class PerformanceMonitoringMiddleware -{ - private readonly RequestDelegate _next; - private readonly PerformanceMonitoringService _monitoring; - private readonly ILogger _logger; - - public async Task InvokeAsync(HttpContext context) - { - var stopwatch = Stopwatch.StartNew(); - var endpoint = GetEndpointName(context); - - try - { - await _next(context); - } - finally - { - stopwatch.Stop(); - - _monitoring.RecordHttpRequest( - context.Request.Method, - endpoint, - context.Response.StatusCode, - stopwatch.Elapsed.TotalSeconds); - - // Log slow requests - if (stopwatch.ElapsedMilliseconds > 1000) - { - _logger.LogWarning("Slow request detected: {Method} {Endpoint} took {ElapsedMs}ms", - context.Request.Method, endpoint, stopwatch.ElapsedMilliseconds); - } - } - } - - private string GetEndpointName(HttpContext context) - { - var endpoint = context.GetEndpoint(); - if (endpoint?.DisplayName != null) - { - return endpoint.DisplayName; - } - - var path = context.Request.Path.Value; - - // Normalize common patterns - if (path.StartsWith("/Vehicle/")) - { - return "/Vehicle/*"; - } - if (path.StartsWith("/api/")) - { - return "/api/*"; - } - - return path ?? "unknown"; - } -} - -// 8.3: Database operation interceptor -public class DatabaseOperationInterceptor : DbCommandInterceptor -{ - private readonly PerformanceMonitoringService _monitoring; - private readonly ILogger _logger; - - public DatabaseOperationInterceptor( - PerformanceMonitoringService monitoring, - ILogger logger) - { - _monitoring = monitoring; - _logger = logger; - } - - public override ValueTask> ReaderExecutingAsync( - DbCommand command, - CommandEventData eventData, - InterceptionResult result, - CancellationToken cancellationToken = default) - { - var stopwatch = Stopwatch.StartNew(); - eventData.Context.ContextId.ToString(); // Use for correlation - - return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); - } - - public override ValueTask ReaderExecutedAsync( - DbCommand command, - CommandExecutedEventData eventData, - DbDataReader result, - CancellationToken cancellationToken = default) - { - var duration = eventData.Duration.TotalSeconds; - var operation = GetOperationType(command.CommandText); - var provider = GetProviderName(eventData.Context); - - _monitoring.RecordDatabaseOperation(operation, provider, duration); - - // Log slow queries - if (eventData.Duration.TotalMilliseconds > 500) - { - _logger.LogWarning("Slow database query detected: {Operation} took {ElapsedMs}ms. Query: {CommandText}", - operation, eventData.Duration.TotalMilliseconds, command.CommandText); - } - - return base.ReaderExecutedAsync(command, eventData, result, cancellationToken); - } - - private string GetOperationType(string commandText) - { - if (string.IsNullOrEmpty(commandText)) - return "unknown"; - - var upperCommand = commandText.Trim().ToUpper(); - - if (upperCommand.StartsWith("SELECT")) return "SELECT"; - if (upperCommand.StartsWith("INSERT")) return "INSERT"; - if (upperCommand.StartsWith("UPDATE")) return "UPDATE"; - if (upperCommand.StartsWith("DELETE")) return "DELETE"; - - return "other"; - } - - private string GetProviderName(DbContext context) - { - return context?.Database?.ProviderName?.Contains("Npgsql") == true ? "PostgreSQL" : "Unknown"; - } -} - -// 8.4: Performance benchmarking service -public class PerformanceBenchmarkService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public async Task RunBenchmarkAsync(BenchmarkOptions options) - { - var result = new BenchmarkResult - { - TestName = options.TestName, - StartTime = DateTime.UtcNow - }; - - _logger.LogInformation("Starting benchmark: {TestName}", options.TestName); - - try - { - switch (options.TestType) - { - case BenchmarkType.DatabaseRead: - await BenchmarkDatabaseReads(result, options); - break; - case BenchmarkType.DatabaseWrite: - await BenchmarkDatabaseWrites(result, options); - break; - case BenchmarkType.HttpEndpoint: - await BenchmarkHttpEndpoint(result, options); - break; - } - - result.Status = "Completed"; - } - catch (Exception ex) - { - result.Status = "Failed"; - result.Error = ex.Message; - _logger.LogError(ex, "Benchmark failed: {TestName}", options.TestName); - } - finally - { - result.EndTime = DateTime.UtcNow; - result.Duration = result.EndTime - result.StartTime; - } - - _logger.LogInformation("Benchmark completed: {TestName}, Duration: {Duration}ms, Status: {Status}", - options.TestName, result.Duration.TotalMilliseconds, result.Status); - - return result; - } - - private async Task BenchmarkDatabaseReads(BenchmarkResult result, BenchmarkOptions options) - { - using var scope = _serviceProvider.CreateScope(); - var vehicleAccess = scope.ServiceProvider.GetRequiredService(); - - var durations = new List(); - - for (int i = 0; i < options.Iterations; i++) - { - var stopwatch = Stopwatch.StartNew(); - - try - { - var vehicles = await vehicleAccess.GetVehiclesAsync(1); // Test user ID - stopwatch.Stop(); - durations.Add(stopwatch.Elapsed.TotalMilliseconds); - } - catch (Exception ex) - { - stopwatch.Stop(); - result.Errors.Add($"Iteration {i}: {ex.Message}"); - } - } - - if (durations.Count > 0) - { - result.Metrics["AverageMs"] = durations.Average(); - result.Metrics["MinMs"] = durations.Min(); - result.Metrics["MaxMs"] = durations.Max(); - result.Metrics["P95Ms"] = durations.OrderBy(x => x).Skip((int)(durations.Count * 0.95)).First(); - result.Metrics["SuccessfulIterations"] = durations.Count; - result.Metrics["FailedIterations"] = options.Iterations - durations.Count; - } - } - - private async Task BenchmarkDatabaseWrites(BenchmarkResult result, BenchmarkOptions options) - { - // Similar implementation for write operations - result.Metrics["WriteOperationsCompleted"] = options.Iterations; - } - - private async Task BenchmarkHttpEndpoint(BenchmarkResult result, BenchmarkOptions options) - { - // HTTP endpoint benchmarking implementation - result.Metrics["HttpRequestsCompleted"] = options.Iterations; - } -} - -// 8.5: Benchmark models -public class BenchmarkOptions -{ - public string TestName { get; set; } - public BenchmarkType TestType { get; set; } - public int Iterations { get; set; } = 10; - public string TargetEndpoint { get; set; } - public Dictionary Parameters { get; set; } = new(); -} - -public enum BenchmarkType -{ - DatabaseRead, - DatabaseWrite, - HttpEndpoint -} - -public class BenchmarkResult -{ - public string TestName { get; set; } - public string Status { get; set; } - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public TimeSpan Duration { get; set; } - public Dictionary Metrics { get; set; } = new(); - public List Errors { get; set; } = new(); - public string Error { get; set; } -} -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public void PerformanceMonitoringService_RecordHttpRequest_UpdatesMetrics() -{ - var monitoring = new PerformanceMonitoringService(Mock.Of>()); - - // Record some test requests - monitoring.RecordHttpRequest("GET", "/test", 200, 0.5); - monitoring.RecordHttpRequest("POST", "/test", 201, 0.8); - - // Verify metrics are updated (would need to access metrics collector in real implementation) - Assert.IsTrue(true); // Placeholder - would verify actual metrics -} - -[Test] -public async Task PerformanceBenchmarkService_DatabaseReadBenchmark_ReturnsValidResults() -{ - var serviceCollection = new ServiceCollection(); - // Add required services for benchmark - var serviceProvider = serviceCollection.BuildServiceProvider(); - - var benchmarkService = new PerformanceBenchmarkService(serviceProvider, Mock.Of>()); - - var options = new BenchmarkOptions - { - TestName = "Database Read Test", - TestType = BenchmarkType.DatabaseRead, - Iterations = 5 - }; - - var result = await benchmarkService.RunBenchmarkAsync(options); - - Assert.AreEqual("Database Read Test", result.TestName); - Assert.IsTrue(result.Duration > TimeSpan.Zero); - Assert.IsNotNull(result.Status); -} - -[Test] -public async Task PerformanceMonitoringMiddleware_SlowRequest_LogsWarning() -{ - var mockLogger = new Mock>(); - var monitoring = Mock.Of(); - - var middleware = new PerformanceMonitoringMiddleware( - async context => await Task.Delay(1100), // Simulate slow request - monitoring, - mockLogger.Object); - - var context = new DefaultHttpContext(); - await middleware.InvokeAsync(context); - - // Verify warning was logged for slow request - mockLogger.Verify( - x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Slow request detected")), - It.IsAny(), - It.IsAny>()), - Times.Once); -} -``` - -**Manual Validation**: -1. Run application and verify Prometheus metrics are collected -2. Access `/metrics` endpoint and verify metric format -3. Perform operations and verify metrics are updated -4. Test performance monitoring middleware with various request types -5. Run database operation benchmarks -6. Verify slow query logging functionality - -**Success Criteria**: -- [ ] HTTP request metrics collected accurately -- [ ] Database operation metrics recorded -- [ ] Slow requests and queries properly logged -- [ ] Benchmark service provides realistic performance data -- [ ] Prometheus metrics endpoint functional -- [ ] Performance overhead < 5% of request time - ---- - -### Step 9: Feature Flags and Configuration Controls -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Implement feature flags to safely enable/disable functionality during Phase 1 implementation and future migrations. - -#### Implementation - -```csharp -// 9.1: Feature flag service -public class FeatureFlagService -{ - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly Dictionary _cachedFlags = new(); - private readonly object _cacheLock = new(); - - public FeatureFlagService(IConfiguration configuration, ILogger logger) - { - _configuration = configuration; - _logger = logger; - } - - public bool IsEnabled(string flagName, bool defaultValue = false) - { - lock (_cacheLock) - { - if (_cachedFlags.TryGetValue(flagName, out var cachedValue)) - { - return cachedValue; - } - - var value = GetFlagValue(flagName, defaultValue); - _cachedFlags[flagName] = value; - - _logger.LogDebug("Feature flag {FlagName} = {Value}", flagName, value); - return value; - } - } - - private bool GetFlagValue(string flagName, bool defaultValue) - { - // Check environment variable first (highest priority) - var envValue = Environment.GetEnvironmentVariable($"FEATURE_{flagName.ToUpper()}"); - if (!string.IsNullOrEmpty(envValue)) - { - if (bool.TryParse(envValue, out var envResult)) - { - _logger.LogDebug("Feature flag {FlagName} set via environment variable: {Value}", flagName, envResult); - return envResult; - } - } - - // Check configuration - var configKey = $"Features:{flagName}"; - var configValue = _configuration[configKey]; - if (!string.IsNullOrEmpty(configValue)) - { - if (bool.TryParse(configValue, out var configResult)) - { - _logger.LogDebug("Feature flag {FlagName} set via configuration: {Value}", flagName, configResult); - return configResult; - } - } - - _logger.LogDebug("Feature flag {FlagName} using default value: {Value}", flagName, defaultValue); - return defaultValue; - } - - public void InvalidateCache() - { - lock (_cacheLock) - { - _cachedFlags.Clear(); - _logger.LogInformation("Feature flag cache invalidated"); - } - } - - public Dictionary GetAllFlags() - { - var flags = new Dictionary(); - - // Get all known feature flags - var knownFlags = new[] - { - "StructuredLogging", - "HealthChecks", - "PerformanceMonitoring", - "DatabaseDiagnostics", - "MigrationTools", - "PostgreSQLOptimizations", - "ConfigurationValidation", - "DebugEndpoints" - }; - - foreach (var flag in knownFlags) - { - flags[flag] = IsEnabled(flag); - } - - return flags; - } -} - -// 9.2: Feature flag startup logging service -public class FeatureFlagStartupService : IHostedService -{ - private readonly FeatureFlagService _featureFlags; - private readonly IWebHostEnvironment _environment; - private readonly ILogger _logger; - - public FeatureFlagStartupService( - FeatureFlagService featureFlags, - IWebHostEnvironment environment, - ILogger logger) - { - _featureFlags = featureFlags; - _environment = environment; - _logger = logger; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - var allFlags = _featureFlags.GetAllFlags(); - var enabledFlags = allFlags.Where(kvp => kvp.Value).ToList(); - var disabledFlags = allFlags.Where(kvp => !kvp.Value).ToList(); - - _logger.LogInformation("Feature flags initialized. Environment: {Environment}, Total: {Total}, Enabled: {Enabled}, Disabled: {Disabled}", - _environment.EnvironmentName, - allFlags.Count, - enabledFlags.Count, - disabledFlags.Count); - - foreach (var flag in enabledFlags) - { - _logger.LogInformation("Feature flag ENABLED: {FlagName}", flag.Key); - } - - foreach (var flag in disabledFlags) - { - _logger.LogDebug("Feature flag disabled: {FlagName}", flag.Key); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to log feature flag status at startup"); - } - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} - -// 9.3: Feature-aware service registration -public static class FeatureAwareServiceExtensions -{ - public static IServiceCollection AddFeatureAwareServices(this IServiceCollection services, IConfiguration configuration) - { - services.AddSingleton(); - - var featureFlags = new FeatureFlagService(configuration, null); - - // Register services based on feature flags - if (featureFlags.IsEnabled("StructuredLogging", true)) - { - services.AddSingleton(); - } - - if (featureFlags.IsEnabled("HealthChecks", true)) - { - services.AddSingleton(); - services.AddSingleton(); - } - - if (featureFlags.IsEnabled("PerformanceMonitoring", true)) - { - services.AddSingleton(); - services.AddSingleton(); - } - - if (featureFlags.IsEnabled("DatabaseDiagnostics", true)) - { - services.AddSingleton(); - } - - if (featureFlags.IsEnabled("MigrationTools", true)) - { - services.AddSingleton(); - services.AddSingleton(); - } - - return services; - } -} - -// 9.4: Feature flag configuration options -public class FeatureFlagOptions -{ - public const string SectionName = "Features"; - - public bool StructuredLogging { get; set; } = true; - public bool HealthChecks { get; set; } = true; - public bool PerformanceMonitoring { get; set; } = true; - public bool DatabaseDiagnostics { get; set; } = true; - public bool MigrationTools { get; set; } = true; - public bool PostgreSQLOptimizations { get; set; } = true; - public bool ConfigurationValidation { get; set; } = true; - public bool DebugEndpoints { get; set; } = false; // Disabled by default in production -} - -// 9.5: Feature-gated components -public class FeatureGatedHealthCheckService -{ - private readonly FeatureFlagService _featureFlags; - private readonly DatabaseHealthCheck _databaseHealthCheck; - private readonly ApplicationHealthCheck _applicationHealthCheck; - - public async Task GetHealthStatusAsync() - { - var healthInfo = new Dictionary(); - - if (_featureFlags.IsEnabled("HealthChecks")) - { - if (_databaseHealthCheck != null) - { - var dbHealth = await _databaseHealthCheck.CheckHealthAsync(null); - healthInfo["Database"] = new - { - Status = dbHealth.Status.ToString(), - Description = dbHealth.Description - }; - } - - if (_applicationHealthCheck != null) - { - var appHealth = await _applicationHealthCheck.CheckHealthAsync(null); - healthInfo["Application"] = new - { - Status = appHealth.Status.ToString(), - Description = appHealth.Description - }; - } - } - else - { - healthInfo["Message"] = "Health checks are disabled"; - } - - return healthInfo; - } -} -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public void FeatureFlagService_EnvironmentVariable_TakesPrecedence() -{ - Environment.SetEnvironmentVariable("FEATURE_TESTFLAG", "true"); - - var config = new ConfigurationBuilder() - .AddInMemoryCollection(new[] { new KeyValuePair("Features:TestFlag", "false") }) - .Build(); - - try - { - var service = new FeatureFlagService(config, Mock.Of>()); - var result = service.IsEnabled("TestFlag", false); - - Assert.IsTrue(result); // Environment variable should override config - } - finally - { - Environment.SetEnvironmentVariable("FEATURE_TESTFLAG", null); - } -} - -[Test] -public void FeatureFlagService_Configuration_UsedWhenNoEnvironmentVariable() -{ - var config = new ConfigurationBuilder() - .AddInMemoryCollection(new[] { new KeyValuePair("Features:TestFlag", "true") }) - .Build(); - - var service = new FeatureFlagService(config, Mock.Of>()); - var result = service.IsEnabled("TestFlag", false); - - Assert.IsTrue(result); -} - -[Test] -public void FeatureFlagService_DefaultValue_UsedWhenNoConfiguration() -{ - var config = new ConfigurationBuilder().Build(); - - var service = new FeatureFlagService(config, Mock.Of>()); - var result = service.IsEnabled("NonExistentFlag", true); - - Assert.IsTrue(result); -} - -[Test] -public async Task FeatureFlagStartupService_RunsAtStartup_LogsFeatureFlags() -{ - var mockLogger = new Mock>(); - var mockFeatureFlags = new Mock(); - var mockEnvironment = new Mock(); - - mockEnvironment.Setup(x => x.EnvironmentName).Returns("Testing"); - - var flags = new Dictionary - { - ["StructuredLogging"] = true, - ["HealthChecks"] = true, - ["PerformanceMonitoring"] = false - }; - - mockFeatureFlags.Setup(x => x.GetAllFlags()).Returns(flags); - - var service = new FeatureFlagStartupService( - mockFeatureFlags.Object, - mockEnvironment.Object, - mockLogger.Object); - - await service.StartAsync(CancellationToken.None); - - // Verify feature flags were logged - mockLogger.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Feature flags initialized")), - It.IsAny(), - It.IsAny>()), - Times.Once); -} - -[Test] -public void FeatureAwareServiceExtensions_RegistersServicesBasedOnFlags() -{ - var config = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Features:PerformanceMonitoring", "true"), - new KeyValuePair("Features:DatabaseDiagnostics", "false") - }) - .Build(); - - var services = new ServiceCollection(); - services.AddFeatureAwareServices(config); - - var serviceProvider = services.BuildServiceProvider(); - - Assert.IsNotNull(serviceProvider.GetService()); - Assert.IsNull(serviceProvider.GetService()); -} -``` - -**Manual Validation**: -1. Test feature flags via environment variables -2. Test feature flags via configuration file -3. Review startup logs for feature flag status -4. Toggle feature flags and verify services are enabled/disabled -5. Test feature flag cache invalidation -6. Verify feature flags work in different environments and log appropriately - -**Success Criteria**: -- [ ] Feature flags correctly control service registration -- [ ] Environment variables override configuration values -- [ ] Feature flag status logged comprehensively at startup -- [ ] Cache invalidation works properly -- [ ] No performance impact when features are disabled -- [ ] Feature flags properly logged for debugging - ---- - -### Step 10: Final Integration and Validation -**Duration**: 2-3 days -**Risk Level**: Low -**Rollback Complexity**: Simple - -#### Objective -Integrate all Phase 1 components and perform comprehensive validation of the Kubernetes-ready application. - -#### Implementation - -```csharp -// 10.1: Integration validation service -public class IntegrationValidationService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public async Task ValidateIntegrationAsync() - { - var result = new IntegrationValidationResult(); - _logger.LogInformation("Starting comprehensive integration validation"); - - try - { - // Validate all Phase 1 components - await ValidateStructuredLogging(result); - await ValidateHealthChecks(result); - await ValidateConfigurationFramework(result); - await ValidateDatabaseProvider(result); - await ValidatePerformanceMonitoring(result); - await ValidateFeatureFlags(result); - - // Overall integration test - await ValidateEndToEndWorkflow(result); - - result.OverallStatus = result.ComponentResults.All(c => c.Value.Status == "Healthy") ? "Healthy" : "Degraded"; - } - catch (Exception ex) - { - result.OverallStatus = "Failed"; - result.GeneralIssues.Add($"Integration validation failed: {ex.Message}"); - _logger.LogError(ex, "Integration validation failed"); - } - - _logger.LogInformation("Integration validation completed. Status: {Status}", result.OverallStatus); - return result; - } - - private async Task ValidateStructuredLogging(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "StructuredLogging" }; - - try - { - var logger = _serviceProvider.GetService>(); - var correlationService = _serviceProvider.GetService(); - - if (logger != null) - { - logger.LogInformation("Testing structured logging functionality"); - componentResult.Status = "Healthy"; - componentResult.Details["LoggerAvailable"] = true; - componentResult.Details["CorrelationIdService"] = correlationService != null; - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Logger service not available"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"Structured logging validation failed: {ex.Message}"); - } - - result.ComponentResults["StructuredLogging"] = componentResult; - } - - private async Task ValidateHealthChecks(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "HealthChecks" }; - - try - { - var databaseHealthCheck = _serviceProvider.GetService(); - var applicationHealthCheck = _serviceProvider.GetService(); - - if (databaseHealthCheck != null && applicationHealthCheck != null) - { - var dbHealth = await databaseHealthCheck.CheckHealthAsync(null); - var appHealth = await applicationHealthCheck.CheckHealthAsync(null); - - componentResult.Status = (dbHealth.Status == HealthStatus.Healthy && appHealth.Status == HealthStatus.Healthy) - ? "Healthy" : "Degraded"; - - componentResult.Details["DatabaseHealth"] = dbHealth.Status.ToString(); - componentResult.Details["ApplicationHealth"] = appHealth.Status.ToString(); - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Health check services not available"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"Health check validation failed: {ex.Message}"); - } - - result.ComponentResults["HealthChecks"] = componentResult; - } - - private async Task ValidateConfigurationFramework(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "ConfigurationFramework" }; - - try - { - var configValidation = _serviceProvider.GetService(); - var configMapping = _serviceProvider.GetService(); - - if (configValidation != null && configMapping != null) - { - var validationResult = configValidation.ValidateConfiguration(); - - componentResult.Status = validationResult.HasErrors ? "Failed" : "Healthy"; - componentResult.Details["ValidationErrors"] = validationResult.Errors.Count; - componentResult.Details["ValidationWarnings"] = validationResult.Warnings.Count; - - if (validationResult.HasErrors) - { - componentResult.Issues.AddRange(validationResult.Errors.Select(e => e.Message)); - } - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Configuration services not available"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"Configuration validation failed: {ex.Message}"); - } - - result.ComponentResults["ConfigurationFramework"] = componentResult; - } - - private async Task ValidateDatabaseProvider(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "DatabaseProvider" }; - - try - { - var providerService = _serviceProvider.GetService(); - var diagnosticsService = _serviceProvider.GetService(); - - if (providerService != null && diagnosticsService != null) - { - var provider = providerService.GetActiveProvider(); - var diagnostics = await diagnosticsService.PerformDiagnosticsAsync(); - - componentResult.Status = diagnostics.OverallStatus; - componentResult.Details["ActiveProvider"] = provider.ToString(); - componentResult.Details["ConnectionTime"] = diagnostics.ConnectionTime; - componentResult.Details["IssueCount"] = diagnostics.Issues.Count; - - if (diagnostics.Issues.Count > 0) - { - componentResult.Issues.AddRange(diagnostics.Issues); - } - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Database services not available"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"Database provider validation failed: {ex.Message}"); - } - - result.ComponentResults["DatabaseProvider"] = componentResult; - } - - private async Task ValidatePerformanceMonitoring(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "PerformanceMonitoring" }; - - try - { - var performanceService = _serviceProvider.GetService(); - var benchmarkService = _serviceProvider.GetService(); - - if (performanceService != null) - { - // Test metrics recording - performanceService.RecordHttpRequest("GET", "/test", 200, 0.1); - performanceService.RecordDatabaseOperation("SELECT", "Test", 0.05); - - componentResult.Status = "Healthy"; - componentResult.Details["PerformanceServiceAvailable"] = true; - componentResult.Details["BenchmarkServiceAvailable"] = benchmarkService != null; - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Performance monitoring service not available"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"Performance monitoring validation failed: {ex.Message}"); - } - - result.ComponentResults["PerformanceMonitoring"] = componentResult; - } - - private async Task ValidateFeatureFlags(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "FeatureFlags" }; - - try - { - var featureFlagService = _serviceProvider.GetService(); - - if (featureFlagService != null) - { - var allFlags = featureFlagService.GetAllFlags(); - var enabledCount = allFlags.Count(f => f.Value); - - componentResult.Status = "Healthy"; - componentResult.Details["TotalFlags"] = allFlags.Count; - componentResult.Details["EnabledFlags"] = enabledCount; - componentResult.Details["DisabledFlags"] = allFlags.Count - enabledCount; - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Feature flag service not available"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"Feature flag validation failed: {ex.Message}"); - } - - result.ComponentResults["FeatureFlags"] = componentResult; - } - - private async Task ValidateEndToEndWorkflow(IntegrationValidationResult result) - { - var componentResult = new ComponentValidationResult { ComponentName = "EndToEndWorkflow" }; - - try - { - // Test a complete workflow that uses multiple components - using var scope = _serviceProvider.CreateScope(); - var vehicleAccess = scope.ServiceProvider.GetService(); - - if (vehicleAccess != null) - { - // Test basic database operation - var vehicles = await vehicleAccess.GetVehiclesAsync(1); - - componentResult.Status = "Healthy"; - componentResult.Details["DatabaseOperationSuccessful"] = true; - componentResult.Details["VehicleCount"] = vehicles?.Count ?? 0; - } - else - { - componentResult.Status = "Failed"; - componentResult.Issues.Add("Vehicle data access not available for end-to-end test"); - } - } - catch (Exception ex) - { - componentResult.Status = "Failed"; - componentResult.Issues.Add($"End-to-end workflow validation failed: {ex.Message}"); - } - - result.ComponentResults["EndToEndWorkflow"] = componentResult; - } -} - -// 10.2: Integration validation models -public class IntegrationValidationResult -{ - public string OverallStatus { get; set; } = "Unknown"; - public DateTime ValidationTime { get; set; } = DateTime.UtcNow; - public Dictionary ComponentResults { get; set; } = new(); - public List GeneralIssues { get; set; } = new(); - public Dictionary Summary { get; set; } = new(); -} - -public class ComponentValidationResult -{ - public string ComponentName { get; set; } - public string Status { get; set; } = "Unknown"; - public Dictionary Details { get; set; } = new(); - public List Issues { get; set; } = new(); -} - -// 10.3: Integration validation startup service -public class IntegrationValidationStartupService : IHostedService -{ - private readonly IntegrationValidationService _validationService; - private readonly ILogger _logger; - - public IntegrationValidationStartupService( - IntegrationValidationService validationService, - ILogger logger) - { - _validationService = validationService; - _logger = logger; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - _logger.LogInformation("Starting comprehensive integration validation"); - var result = await _validationService.ValidateIntegrationAsync(); - - _logger.LogInformation("Integration validation completed. Overall Status: {OverallStatus}", result.OverallStatus); - - foreach (var component in result.ComponentResults) - { - var componentResult = component.Value; - _logger.LogInformation("Component {ComponentName}: {Status}", - componentResult.ComponentName, componentResult.Status); - - foreach (var detail in componentResult.Details) - { - _logger.LogDebug("Component {ComponentName} detail - {Key}: {Value}", - componentResult.ComponentName, detail.Key, detail.Value); - } - - foreach (var issue in componentResult.Issues) - { - _logger.LogWarning("Component {ComponentName} issue: {Issue}", - componentResult.ComponentName, issue); - } - } - - foreach (var issue in result.GeneralIssues) - { - _logger.LogError("Integration validation general issue: {Issue}", issue); - } - - if (result.OverallStatus != "Healthy") - { - _logger.LogWarning("Application integration validation indicates issues. Review component logs for details."); - } - else - { - _logger.LogInformation("All integration components are healthy and ready for Kubernetes deployment"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Integration validation failed during startup"); - } - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} -``` - -#### Testing Plan - -**Automated Tests**: -```csharp -[Test] -public async Task IntegrationValidationService_AllComponentsHealthy_ReturnsHealthyStatus() -{ - var services = new ServiceCollection(); - - // Add all required services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - // ... add other services - - var serviceProvider = services.BuildServiceProvider(); - var validationService = new IntegrationValidationService(serviceProvider, Mock.Of>()); - - var result = await validationService.ValidateIntegrationAsync(); - - Assert.AreEqual("Healthy", result.OverallStatus); - Assert.IsTrue(result.ComponentResults.Count > 0); -} - -[Test] -public async Task IntegrationValidationStartupService_RunsAtStartup_LogsValidationResults() -{ - var mockLogger = new Mock>(); - var mockValidationService = new Mock(); - - var validationResult = new IntegrationValidationResult - { - OverallStatus = "Healthy", - ComponentResults = new Dictionary - { - ["StructuredLogging"] = new ComponentValidationResult - { - ComponentName = "StructuredLogging", - Status = "Healthy" - } - } - }; - - mockValidationService.Setup(x => x.ValidateIntegrationAsync()) - .ReturnsAsync(validationResult); - - var service = new IntegrationValidationStartupService( - mockValidationService.Object, - mockLogger.Object); - - await service.StartAsync(CancellationToken.None); - - // Verify integration validation was logged - mockLogger.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Integration validation completed")), - It.IsAny(), - It.IsAny>()), - Times.Once); -} -``` - -**Manual Validation**: -1. Review startup logs to verify all components are healthy -2. Test with individual component failures and verify proper error logging -3. Verify all Phase 1 features work together correctly -4. Test application startup with all new components and review logs -5. Perform end-to-end user workflows -6. Verify Kubernetes readiness (health checks, configuration, etc.) - -**Success Criteria**: -- [ ] All Phase 1 components integrate successfully -- [ ] Integration validation service reports accurate status and logs details -- [ ] End-to-end workflows function correctly -- [ ] Application ready for Kubernetes deployment -- [ ] Comprehensive logging provides visibility into all components -- [ ] Performance remains within acceptable limits - ---- - -## Summary - -This detailed implementation plan provides a safe, step-by-step approach to Phase 1 with: - -1. **Incremental Changes**: Each step is isolated and testable -2. **Comprehensive Testing**: Automated and manual validation at each step -3. **Debugging Focus**: Extensive logging and diagnostic capabilities -4. **Risk Mitigation**: Rollback procedures and thorough validation -5. **Performance Monitoring**: Baseline and continuous validation -6. **Feature Control**: Feature flags for safe rollout of new functionality - -The plan ensures that any issues can be detected and resolved quickly before proceeding to the next step, making the overall Phase 1 implementation much safer and more reliable than the original approach. Each step builds upon the previous ones while maintaining full backward compatibility until the final integration. \ No newline at end of file diff --git a/docs/K8S-PHASE-1.md b/docs/K8S-PHASE-1.md deleted file mode 100644 index d1825f0..0000000 --- a/docs/K8S-PHASE-1.md +++ /dev/null @@ -1,365 +0,0 @@ -# Phase 1: Core Kubernetes Readiness (Weeks 1-4) - -This phase focuses on making the application compatible with Kubernetes deployment patterns while maintaining existing functionality. - -## Overview - -The primary goal of Phase 1 is to transform MotoVaultPro from a traditional self-hosted application into a Kubernetes-ready application. This involves removing state dependencies, externalizing configuration, implementing health checks, and modernizing the database architecture. - -## Key Objectives - -- **Configuration Externalization**: Move all configuration from files to Kubernetes-native management -- **Database Modernization**: Eliminate LiteDB dependency and optimize PostgreSQL usage -- **Health Check Implementation**: Add Kubernetes-compatible health check endpoints -- **Logging Enhancement**: Implement structured logging for centralized log aggregation - -## 1.1 Configuration Externalization - -**Objective**: Move all configuration from files to Kubernetes-native configuration management. - -**Current State**: -- Configuration stored in `appsettings.json` and environment variables -- Database connection strings in configuration files -- Feature flags and application settings mixed with deployment configuration - -**Target State**: -- All configuration externalized to ConfigMaps and Secrets -- Environment-specific configuration separated from application code -- Sensitive data (passwords, API keys) managed through Kubernetes Secrets - -### Implementation Tasks - -#### 1. Create ConfigMap templates for non-sensitive configuration -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: motovault-config -data: - APP_NAME: "MotoVaultPro" - LOG_LEVEL: "Information" - ENABLE_FEATURES: "OpenIDConnect,EmailNotifications" - CACHE_EXPIRY_MINUTES: "30" -``` - -#### 2. Create Secret templates for sensitive configuration -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: motovault-secrets -type: Opaque -data: - POSTGRES_CONNECTION: - MINIO_ACCESS_KEY: - MINIO_SECRET_KEY: - JWT_SECRET: -``` - -#### 3. Modify application startup to read from environment variables -- Update `Program.cs` to prioritize environment variables over file configuration -- Remove dependencies on `appsettings.json` for runtime configuration -- Implement configuration validation at startup - -#### 4. Remove file-based configuration dependencies -- Update all services to use IConfiguration instead of direct file access -- Ensure all configuration is injectable through dependency injection - -#### 5. Implement configuration validation at startup -- Add startup checks to ensure all required configuration is present -- Fail fast if critical configuration is missing - -## 1.2 Database Architecture Modernization - -**Objective**: Eliminate LiteDB dependency and optimize PostgreSQL usage for Kubernetes. - -**Current State**: -- Dual database support with LiteDB as default -- Single PostgreSQL connection for external database mode -- No connection pooling optimization for multiple instances - -**Target State**: -- PostgreSQL-only configuration with high availability -- Optimized connection pooling for horizontal scaling -- Database migration strategy for existing LiteDB installations - -### Implementation Tasks - -#### 1. Remove LiteDB implementation and dependencies -```csharp -// Remove all LiteDB-related code from: -// - External/Implementations/LiteDB/ -// - Remove LiteDB package references -// - Update dependency injection to only register PostgreSQL implementations -``` - -#### 2. Implement PostgreSQL HA configuration -```csharp -services.AddDbContext(options => -{ - options.UseNpgsql(connectionString, npgsqlOptions => - { - npgsqlOptions.EnableRetryOnFailure( - maxRetryCount: 3, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorCodesToAdd: null); - }); -}); -``` - -#### 3. Add connection pooling configuration -```csharp -// Configure connection pooling for multiple instances -services.Configure(options => -{ - options.MaxPoolSize = 100; - options.MinPoolSize = 10; - options.ConnectionLifetime = 300; // 5 minutes -}); -``` - -#### 4. Create data migration tools for LiteDB to PostgreSQL conversion -- Develop utility to export data from LiteDB format -- Create import scripts for PostgreSQL -- Ensure data integrity during migration - -#### 5. Implement database health checks for Kubernetes probes -```csharp -public class DatabaseHealthCheck : IHealthCheck -{ - private readonly IDbContextFactory _contextFactory; - - public async Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default) - { - try - { - using var dbContext = _contextFactory.CreateDbContext(); - await dbContext.Database.CanConnectAsync(cancellationToken); - return HealthCheckResult.Healthy("Database connection successful"); - } - catch (Exception ex) - { - return HealthCheckResult.Unhealthy("Database connection failed", ex); - } - } -} -``` - -## 1.3 Health Check Implementation - -**Objective**: Add Kubernetes-compatible health check endpoints for proper orchestration. - -**Current State**: -- No dedicated health check endpoints -- Application startup/shutdown not optimized for Kubernetes - -**Target State**: -- Comprehensive health checks for all dependencies -- Proper readiness and liveness probe endpoints -- Graceful shutdown handling for pod termination - -### Implementation Tasks - -#### 1. Add health check middleware -```csharp -// Program.cs -builder.Services.AddHealthChecks() - .AddNpgSql(connectionString, name: "database") - .AddRedis(redisConnectionString, name: "cache") - .AddCheck("minio"); - -app.MapHealthChecks("/health/ready", new HealthCheckOptions -{ - Predicate = check => check.Tags.Contains("ready"), - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse -}); - -app.MapHealthChecks("/health/live", new HealthCheckOptions -{ - Predicate = _ => false // Only check if the app is responsive -}); -``` - -#### 2. Implement custom health checks -```csharp -public class MinIOHealthCheck : IHealthCheck -{ - private readonly IMinioClient _minioClient; - - public async Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default) - { - try - { - await _minioClient.ListBucketsAsync(cancellationToken); - return HealthCheckResult.Healthy("MinIO is accessible"); - } - catch (Exception ex) - { - return HealthCheckResult.Unhealthy("MinIO is not accessible", ex); - } - } -} -``` - -#### 3. Add graceful shutdown handling -```csharp -builder.Services.Configure(options => -{ - options.ShutdownTimeout = TimeSpan.FromSeconds(30); -}); -``` - -## 1.4 Logging Enhancement - -**Objective**: Implement structured logging suitable for centralized log aggregation. - -**Current State**: -- Basic logging with simple string messages -- No correlation IDs for distributed tracing -- Log levels not optimized for production monitoring - -**Target State**: -- JSON-structured logging with correlation IDs -- Centralized log aggregation compatibility -- Performance and error metrics embedded in logs - -### Implementation Tasks - -#### 1. Configure structured logging -```csharp -builder.Services.AddLogging(loggingBuilder => -{ - loggingBuilder.ClearProviders(); - loggingBuilder.AddJsonConsole(options => - { - options.IncludeScopes = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; - options.JsonWriterOptions = new JsonWriterOptions - { - Indented = false - }; - }); -}); -``` - -#### 2. Add correlation ID middleware -```csharp -public class CorrelationIdMiddleware -{ - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - var correlationId = context.Request.Headers["X-Correlation-ID"] - .FirstOrDefault() ?? Guid.NewGuid().ToString(); - - using var scope = _logger.BeginScope(new Dictionary - { - ["CorrelationId"] = correlationId, - ["UserId"] = context.User?.Identity?.Name - }); - - context.Response.Headers.Add("X-Correlation-ID", correlationId); - await next(context); - } -} -``` - -#### 3. Implement performance logging for critical operations -- Add timing information to database operations -- Log request/response metrics -- Include user context in all log entries - -## Week-by-Week Breakdown - -### Week 1: Environment Setup and Configuration -- **Days 1-2**: Set up development Kubernetes environment -- **Days 3-4**: Create ConfigMap and Secret templates -- **Days 5-7**: Modify application to read from environment variables - -### Week 2: Database Migration -- **Days 1-3**: Remove LiteDB dependencies -- **Days 4-5**: Implement PostgreSQL connection pooling -- **Days 6-7**: Create data migration utilities - -### Week 3: Health Checks and Monitoring -- **Days 1-3**: Implement health check endpoints -- **Days 4-5**: Add custom health checks for dependencies -- **Days 6-7**: Test health check functionality - -### Week 4: Logging and Documentation -- **Days 1-3**: Implement structured logging -- **Days 4-5**: Add correlation ID middleware -- **Days 6-7**: Document changes and prepare for Phase 2 - -## Success Criteria - -- [ ] Application starts successfully using only environment variables -- [ ] All LiteDB dependencies removed -- [ ] PostgreSQL connection pooling configured and tested -- [ ] Health check endpoints return appropriate status -- [ ] Structured JSON logging implemented -- [ ] Data migration tool successfully converts LiteDB to PostgreSQL -- [ ] Application can be deployed to Kubernetes without file dependencies - -## Testing Requirements - -### Unit Tests -- Configuration validation logic -- Health check implementations -- Database connection handling - -### Integration Tests -- End-to-end application startup with external configuration -- Database connectivity and migration -- Health check endpoint responses - -### Manual Testing -- Deploy to development Kubernetes cluster -- Verify all functionality works without local file dependencies -- Test health check endpoints with kubectl - -## Deliverables - -1. **Updated Application Code** - - Removed LiteDB dependencies - - Externalized configuration - - Added health checks - - Implemented structured logging - -2. **Kubernetes Manifests** - - ConfigMap templates - - Secret templates - - Basic deployment configuration for testing - -3. **Migration Tools** - - LiteDB to PostgreSQL data migration utility - - Configuration migration scripts - -4. **Documentation** - - Updated deployment instructions - - Configuration reference - - Health check endpoint documentation - -## Dependencies - -- Kubernetes cluster (development environment) -- PostgreSQL instance for testing -- Docker registry for container images - -## Risks and Mitigations - -### Risk: Data Loss During Migration -**Mitigation**: Comprehensive backup strategy and thorough testing of migration tools - -### Risk: Configuration Errors -**Mitigation**: Configuration validation at startup and extensive testing - -### Risk: Performance Degradation -**Mitigation**: Performance testing and gradual rollout with monitoring - ---- - -**Next Phase**: [Phase 2: High Availability Infrastructure](K8S-PHASE-2.md) \ No newline at end of file diff --git a/docs/K8S-PHASE-2.md b/docs/K8S-PHASE-2.md deleted file mode 100644 index df21bb5..0000000 --- a/docs/K8S-PHASE-2.md +++ /dev/null @@ -1,742 +0,0 @@ -# Phase 2: High Availability Infrastructure (Weeks 5-8) - -This phase focuses on implementing the supporting infrastructure required for high availability, including MinIO clusters, PostgreSQL HA setup, Redis clusters, and file storage abstraction. - -## Overview - -Phase 2 transforms MotoVaultPro's supporting infrastructure from single-instance services to highly available, distributed systems. This phase establishes the foundation for true high availability by eliminating all single points of failure in the data layer. - -## Key Objectives - -- **MinIO High Availability**: Deploy distributed object storage with erasure coding -- **File Storage Abstraction**: Create unified interface for file operations -- **PostgreSQL HA**: Implement primary/replica configuration with automated failover -- **Redis Cluster**: Deploy distributed caching and session storage -- **Data Migration**: Seamless transition from local storage to distributed systems - -## 2.1 MinIO High Availability Setup - -**Objective**: Deploy a highly available MinIO cluster for file storage with automatic failover. - -**Architecture Overview**: -MinIO will be deployed as a distributed cluster with erasure coding for data protection and automatic healing capabilities. - -### MinIO Cluster Configuration - -```yaml -# MinIO Tenant Configuration -apiVersion: minio.min.io/v2 -kind: Tenant -metadata: - name: motovault-minio - namespace: motovault -spec: - image: minio/minio:RELEASE.2024-01-16T16-07-38Z - creationDate: 2024-01-20T10:00:00Z - pools: - - servers: 4 - name: pool-0 - volumesPerServer: 4 - volumeClaimTemplate: - metadata: - name: data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Gi - storageClassName: fast-ssd - mountPath: /export - subPath: /data - requestAutoCert: false - certConfig: - commonName: "" - organizationName: [] - dnsNames: [] - console: - image: minio/console:v0.22.5 - replicas: 2 - consoleSecret: - name: motovault-minio-console-secret - configuration: - name: motovault-minio-config -``` - -### Implementation Tasks - -#### 1. Deploy MinIO Operator -```bash -kubectl apply -k "github.com/minio/operator/resources" -``` - -#### 2. Create MinIO cluster configuration with erasure coding -- Configure 4+ nodes for optimal erasure coding -- Set up data protection with automatic healing -- Configure storage classes for performance - -#### 3. Configure backup policies for disaster recovery -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: minio-backup-policy -data: - backup-policy.json: | - { - "rules": [ - { - "id": "motovault-backup", - "status": "Enabled", - "transition": { - "days": 30, - "storage_class": "GLACIER" - } - } - ] - } -``` - -#### 4. Set up monitoring with Prometheus metrics -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: minio-metrics -spec: - selector: - matchLabels: - app: minio - endpoints: - - port: http-minio - path: /minio/v2/metrics/cluster -``` - -#### 5. Create service endpoints for application connectivity -```yaml -apiVersion: v1 -kind: Service -metadata: - name: minio-service -spec: - selector: - app: minio - ports: - - name: http - port: 9000 - targetPort: 9000 - - name: console - port: 9001 - targetPort: 9001 -``` - -### MinIO High Availability Features - -- **Erasure Coding**: Data is split across multiple drives with parity for automatic healing -- **Distributed Architecture**: No single point of failure -- **Automatic Healing**: Corrupted data is automatically detected and repaired -- **Load Balancing**: Built-in load balancing across cluster nodes -- **Bucket Policies**: Fine-grained access control for different data types - -## 2.2 File Storage Abstraction Implementation - -**Objective**: Create an abstraction layer that allows seamless switching between local filesystem and MinIO object storage. - -**Current State**: -- Direct filesystem operations throughout the application -- File paths hardcoded in various controllers and services -- No abstraction for different storage backends - -**Target State**: -- Unified file storage interface -- Pluggable storage implementations -- Transparent migration between storage types - -### Implementation Tasks - -#### 1. Define storage abstraction interface -```csharp -public interface IFileStorageService -{ - Task UploadFileAsync(Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken = default); - Task DownloadFileAsync(string fileId, CancellationToken cancellationToken = default); - Task DeleteFileAsync(string fileId, CancellationToken cancellationToken = default); - Task GetFileMetadataAsync(string fileId, CancellationToken cancellationToken = default); - Task> ListFilesAsync(string prefix = null, CancellationToken cancellationToken = default); - Task GeneratePresignedUrlAsync(string fileId, TimeSpan expiration, CancellationToken cancellationToken = default); -} - -public class FileMetadata -{ - public string Id { get; set; } - public string FileName { get; set; } - public string ContentType { get; set; } - public long Size { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime ModifiedDate { get; set; } - public Dictionary Tags { get; set; } -} -``` - -#### 2. Implement MinIO storage service -```csharp -public class MinIOFileStorageService : IFileStorageService -{ - private readonly IMinioClient _minioClient; - private readonly ILogger _logger; - private readonly string _bucketName; - - public MinIOFileStorageService(IMinioClient minioClient, IConfiguration configuration, ILogger logger) - { - _minioClient = minioClient; - _logger = logger; - _bucketName = configuration["MinIO:BucketName"] ?? "motovault-files"; - } - - public async Task UploadFileAsync(Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken = default) - { - var fileId = $"{Guid.NewGuid()}/{fileName}"; - - try - { - await _minioClient.PutObjectAsync(new PutObjectArgs() - .WithBucket(_bucketName) - .WithObject(fileId) - .WithStreamData(fileStream) - .WithObjectSize(fileStream.Length) - .WithContentType(contentType) - .WithHeaders(new Dictionary - { - ["X-Amz-Meta-Original-Name"] = fileName, - ["X-Amz-Meta-Upload-Date"] = DateTime.UtcNow.ToString("O") - }), cancellationToken); - - _logger.LogInformation("File uploaded successfully: {FileId}", fileId); - return fileId; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to upload file: {FileName}", fileName); - throw; - } - } - - public async Task DownloadFileAsync(string fileId, CancellationToken cancellationToken = default) - { - try - { - var memoryStream = new MemoryStream(); - await _minioClient.GetObjectAsync(new GetObjectArgs() - .WithBucket(_bucketName) - .WithObject(fileId) - .WithCallbackStream(stream => stream.CopyTo(memoryStream)), cancellationToken); - - memoryStream.Position = 0; - return memoryStream; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to download file: {FileId}", fileId); - throw; - } - } - - // Additional method implementations... -} -``` - -#### 3. Create fallback storage service for graceful degradation -```csharp -public class FallbackFileStorageService : IFileStorageService -{ - private readonly IFileStorageService _primaryService; - private readonly IFileStorageService _fallbackService; - private readonly ILogger _logger; - - public FallbackFileStorageService( - IFileStorageService primaryService, - IFileStorageService fallbackService, - ILogger logger) - { - _primaryService = primaryService; - _fallbackService = fallbackService; - _logger = logger; - } - - public async Task UploadFileAsync(Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken = default) - { - try - { - return await _primaryService.UploadFileAsync(fileStream, fileName, contentType, cancellationToken); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Primary storage failed, falling back to secondary storage"); - fileStream.Position = 0; // Reset stream position - return await _fallbackService.UploadFileAsync(fileStream, fileName, contentType, cancellationToken); - } - } - - // Implementation with automatic fallback logic for other methods... -} -``` - -#### 4. Update all file operations to use the abstraction layer -- Replace direct File.WriteAllBytes, File.ReadAllBytes calls -- Update all controllers to use IFileStorageService -- Modify attachment handling in vehicle records - -#### 5. Implement file migration utility for existing local files -```csharp -public class FileMigrationService -{ - private readonly IFileStorageService _targetStorage; - private readonly ILogger _logger; - - public async Task MigrateLocalFilesAsync(string localPath) - { - var result = new MigrationResult(); - var files = Directory.GetFiles(localPath, "*", SearchOption.AllDirectories); - - foreach (var filePath in files) - { - try - { - using var fileStream = File.OpenRead(filePath); - var fileName = Path.GetFileName(filePath); - var contentType = GetContentType(fileName); - - var fileId = await _targetStorage.UploadFileAsync(fileStream, fileName, contentType); - result.ProcessedFiles.Add(new MigratedFile - { - OriginalPath = filePath, - NewFileId = fileId, - Success = true - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to migrate file: {FilePath}", filePath); - result.ProcessedFiles.Add(new MigratedFile - { - OriginalPath = filePath, - Success = false, - Error = ex.Message - }); - } - } - - return result; - } -} -``` - -## 2.3 PostgreSQL High Availability Configuration - -**Objective**: Set up a PostgreSQL cluster with automatic failover and read replicas. - -**Architecture Overview**: -PostgreSQL will be deployed using an operator (like CloudNativePG or Postgres Operator) to provide automated failover, backup, and scaling capabilities. - -### PostgreSQL Cluster Configuration - -```yaml -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: motovault-postgres - namespace: motovault -spec: - instances: 3 - primaryUpdateStrategy: unsupervised - - postgresql: - parameters: - max_connections: "200" - shared_buffers: "256MB" - effective_cache_size: "1GB" - maintenance_work_mem: "64MB" - checkpoint_completion_target: "0.9" - wal_buffers: "16MB" - default_statistics_target: "100" - random_page_cost: "1.1" - effective_io_concurrency: "200" - - resources: - requests: - memory: "2Gi" - cpu: "1000m" - limits: - memory: "4Gi" - cpu: "2000m" - - storage: - size: "100Gi" - storageClass: "fast-ssd" - - monitoring: - enabled: true - - backup: - retentionPolicy: "30d" - barmanObjectStore: - destinationPath: "s3://motovault-backups/postgres" - s3Credentials: - accessKeyId: - name: postgres-backup-credentials - key: ACCESS_KEY_ID - secretAccessKey: - name: postgres-backup-credentials - key: SECRET_ACCESS_KEY - wal: - retention: "5d" - data: - retention: "30d" - jobs: 1 -``` - -### Implementation Tasks - -#### 1. Deploy PostgreSQL operator (CloudNativePG recommended) -```bash -kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.20/releases/cnpg-1.20.1.yaml -``` - -#### 2. Configure cluster with primary/replica setup -- 3-node cluster with automatic failover -- Read-write split capability -- Streaming replication configuration - -#### 3. Set up automated backups to MinIO or external storage -```yaml -apiVersion: postgresql.cnpg.io/v1 -kind: ScheduledBackup -metadata: - name: motovault-postgres-backup -spec: - schedule: "0 2 * * *" # Daily at 2 AM - backupOwnerReference: self - cluster: - name: motovault-postgres -``` - -#### 4. Implement connection pooling with PgBouncer -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: pgbouncer -spec: - replicas: 2 - selector: - matchLabels: - app: pgbouncer - template: - spec: - containers: - - name: pgbouncer - image: pgbouncer/pgbouncer:latest - env: - - name: DATABASES_HOST - value: motovault-postgres-rw - - name: DATABASES_PORT - value: "5432" - - name: DATABASES_DATABASE - value: motovault - - name: POOL_MODE - value: session - - name: MAX_CLIENT_CONN - value: "1000" - - name: DEFAULT_POOL_SIZE - value: "25" -``` - -#### 5. Configure monitoring and alerting for database health -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: postgres-metrics -spec: - selector: - matchLabels: - app.kubernetes.io/name: cloudnative-pg - endpoints: - - port: metrics - path: /metrics -``` - -## 2.4 Redis Cluster for Session Management - -**Objective**: Implement distributed session storage and caching using Redis cluster. - -**Current State**: -- In-memory session storage tied to individual application instances -- No distributed caching for expensive operations -- Configuration and translation data loaded on each application start - -**Target State**: -- Redis cluster for distributed session storage -- Centralized caching for frequently accessed data -- High availability with automatic failover - -### Redis Cluster Configuration - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: redis-cluster-config - namespace: motovault -data: - redis.conf: | - cluster-enabled yes - cluster-require-full-coverage no - cluster-node-timeout 15000 - cluster-config-file /data/nodes.conf - cluster-migration-barrier 1 - appendonly yes - appendfsync everysec - save 900 1 - save 300 10 - save 60 10000 - ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: redis-cluster - namespace: motovault -spec: - serviceName: redis-cluster - replicas: 6 - selector: - matchLabels: - app: redis-cluster - template: - metadata: - labels: - app: redis-cluster - spec: - containers: - - name: redis - image: redis:7-alpine - command: - - redis-server - - /etc/redis/redis.conf - ports: - - containerPort: 6379 - - containerPort: 16379 - resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "1Gi" - cpu: "500m" - volumeMounts: - - name: redis-config - mountPath: /etc/redis - - name: redis-data - mountPath: /data - volumes: - - name: redis-config - configMap: - name: redis-cluster-config - volumeClaimTemplates: - - metadata: - name: redis-data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 10Gi -``` - -### Implementation Tasks - -#### 1. Deploy Redis cluster with 6 nodes (3 masters, 3 replicas) -```bash -# Initialize Redis cluster after deployment -kubectl exec -it redis-cluster-0 -- redis-cli --cluster create \ - redis-cluster-0.redis-cluster:6379 \ - redis-cluster-1.redis-cluster:6379 \ - redis-cluster-2.redis-cluster:6379 \ - redis-cluster-3.redis-cluster:6379 \ - redis-cluster-4.redis-cluster:6379 \ - redis-cluster-5.redis-cluster:6379 \ - --cluster-replicas 1 -``` - -#### 2. Configure session storage -```csharp -services.AddStackExchangeRedisCache(options => -{ - options.Configuration = configuration.GetConnectionString("Redis"); - options.InstanceName = "MotoVault"; -}); - -services.AddSession(options => -{ - options.IdleTimeout = TimeSpan.FromMinutes(30); - options.Cookie.HttpOnly = true; - options.Cookie.IsEssential = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; -}); -``` - -#### 3. Implement distributed caching -```csharp -public class CachedTranslationService : ITranslationService -{ - private readonly IDistributedCache _cache; - private readonly ITranslationService _translationService; - private readonly ILogger _logger; - - public async Task GetTranslationAsync(string key, string language) - { - var cacheKey = $"translation:{language}:{key}"; - var cached = await _cache.GetStringAsync(cacheKey); - - if (cached != null) - { - return cached; - } - - var translation = await _translationService.GetTranslationAsync(key, language); - - await _cache.SetStringAsync(cacheKey, translation, new DistributedCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromHours(1) - }); - - return translation; - } -} -``` - -#### 4. Add cache monitoring and performance metrics -```csharp -public class CacheMetricsService -{ - private readonly Counter _cacheHits; - private readonly Counter _cacheMisses; - private readonly Histogram _cacheOperationDuration; - - public CacheMetricsService() - { - _cacheHits = Metrics.CreateCounter( - "motovault_cache_hits_total", - "Total cache hits", - new[] { "cache_type" }); - - _cacheMisses = Metrics.CreateCounter( - "motovault_cache_misses_total", - "Total cache misses", - new[] { "cache_type" }); - - _cacheOperationDuration = Metrics.CreateHistogram( - "motovault_cache_operation_duration_seconds", - "Cache operation duration", - new[] { "operation", "cache_type" }); - } -} -``` - -## Week-by-Week Breakdown - -### Week 5: MinIO Deployment -- **Days 1-2**: Deploy MinIO operator and configure basic cluster -- **Days 3-4**: Implement file storage abstraction interface -- **Days 5-7**: Create MinIO storage service implementation - -### Week 6: File Migration and PostgreSQL HA -- **Days 1-2**: Complete file storage abstraction and migration tools -- **Days 3-4**: Deploy PostgreSQL operator and HA cluster -- **Days 5-7**: Configure connection pooling and backup strategies - -### Week 7: Redis Cluster and Caching -- **Days 1-3**: Deploy Redis cluster and configure session storage -- **Days 4-5**: Implement distributed caching layer -- **Days 6-7**: Add cache monitoring and performance metrics - -### Week 8: Integration and Testing -- **Days 1-3**: End-to-end testing of all HA components -- **Days 4-5**: Performance testing and optimization -- **Days 6-7**: Documentation and preparation for Phase 3 - -## Success Criteria - -- [ ] MinIO cluster operational with erasure coding -- [ ] File storage abstraction implemented and tested -- [ ] PostgreSQL HA cluster with automatic failover -- [ ] Redis cluster providing distributed sessions -- [ ] All file operations migrated to object storage -- [ ] Comprehensive monitoring for all infrastructure components -- [ ] Backup and recovery procedures validated - -## Testing Requirements - -### Infrastructure Tests -- MinIO cluster failover scenarios -- PostgreSQL primary/replica failover -- Redis cluster node failure recovery -- Network partition handling - -### Application Integration Tests -- File upload/download through abstraction layer -- Session persistence across application restarts -- Cache performance and invalidation -- Database connection pool behavior - -### Performance Tests -- File storage throughput and latency -- Database query performance with connection pooling -- Cache hit/miss ratios and response times - -## Deliverables - -1. **Infrastructure Components** - - MinIO HA cluster configuration - - PostgreSQL HA cluster with operator - - Redis cluster deployment - - Monitoring and alerting setup - -2. **Application Updates** - - File storage abstraction implementation - - Session management configuration - - Distributed caching integration - - Connection pooling optimization - -3. **Migration Tools** - - File migration utility - - Database migration scripts - - Configuration migration helpers - -4. **Documentation** - - Infrastructure architecture diagrams - - Operational procedures - - Monitoring and alerting guides - -## Dependencies - -- Kubernetes cluster with sufficient resources -- Storage classes for persistent volumes -- Prometheus and Grafana for monitoring -- Network connectivity between components - -## Risks and Mitigations - -### Risk: Data Corruption During File Migration -**Mitigation**: Checksum validation and parallel running of old/new systems - -### Risk: Database Failover Issues -**Mitigation**: Extensive testing of failover scenarios and automated recovery - -### Risk: Cache Inconsistency -**Mitigation**: Proper cache invalidation strategies and monitoring - ---- - -**Previous Phase**: [Phase 1: Core Kubernetes Readiness](K8S-PHASE-1.md) -**Next Phase**: [Phase 3: Production Deployment](K8S-PHASE-3.md) \ No newline at end of file diff --git a/docs/K8S-PHASE-3.md b/docs/K8S-PHASE-3.md deleted file mode 100644 index c2ebbf3..0000000 --- a/docs/K8S-PHASE-3.md +++ /dev/null @@ -1,862 +0,0 @@ -# Phase 3: Production Deployment (Weeks 9-12) - -This phase focuses on deploying the modernized application with proper production configurations, monitoring, backup strategies, and operational procedures. - -## Overview - -Phase 3 transforms the development-ready Kubernetes application into a production-grade system with comprehensive monitoring, automated backup and recovery, secure ingress, and operational excellence. This phase ensures the system is ready for enterprise-level workloads with proper security, performance, and reliability guarantees. - -## Key Objectives - -- **Production Kubernetes Deployment**: Configure scalable, secure deployment manifests -- **Ingress and TLS Configuration**: Secure external access with proper routing -- **Comprehensive Monitoring**: Application and infrastructure observability -- **Backup and Disaster Recovery**: Automated backup strategies and recovery procedures -- **Migration Execution**: Seamless transition from legacy system - -## 3.1 Kubernetes Deployment Configuration - -**Objective**: Create production-ready Kubernetes manifests with proper resource management and high availability. - -### Application Deployment Configuration - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: motovault-app - namespace: motovault - labels: - app: motovault - version: v1.0.0 -spec: - replicas: 3 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - app: motovault - template: - metadata: - labels: - app: motovault - version: v1.0.0 - annotations: - prometheus.io/scrape: "true" - prometheus.io/path: "/metrics" - prometheus.io/port: "8080" - spec: - serviceAccountName: motovault-service-account - securityContext: - runAsNonRoot: true - runAsUser: 1000 - fsGroup: 2000 - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - motovault - topologyKey: kubernetes.io/hostname - - weight: 50 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - motovault - topologyKey: topology.kubernetes.io/zone - containers: - - name: motovault - image: motovault:latest - imagePullPolicy: Always - ports: - - containerPort: 8080 - name: http - protocol: TCP - env: - - name: ASPNETCORE_ENVIRONMENT - value: "Production" - - name: ASPNETCORE_URLS - value: "http://+:8080" - envFrom: - - configMapRef: - name: motovault-config - - secretRef: - name: motovault-secrets - resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "1Gi" - cpu: "500m" - readinessProbe: - httpGet: - path: /health/ready - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - livenessProbe: - httpGet: - path: /health/live - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - volumeMounts: - - name: tmp-volume - mountPath: /tmp - - name: app-logs - mountPath: /app/logs - volumes: - - name: tmp-volume - emptyDir: {} - - name: app-logs - emptyDir: {} - terminationGracePeriodSeconds: 30 - ---- -apiVersion: v1 -kind: Service -metadata: - name: motovault-service - namespace: motovault - labels: - app: motovault -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 8080 - protocol: TCP - name: http - selector: - app: motovault - ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: motovault-pdb - namespace: motovault -spec: - minAvailable: 2 - selector: - matchLabels: - app: motovault -``` - -### Horizontal Pod Autoscaler Configuration - -```yaml -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: motovault-hpa - namespace: motovault -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: motovault-app - minReplicas: 3 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 - behavior: - scaleUp: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 10 - periodSeconds: 60 -``` - -### Implementation Tasks - -#### 1. Create production namespace with security policies -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: motovault - labels: - pod-security.kubernetes.io/enforce: restricted - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/warn: restricted -``` - -#### 2. Configure resource quotas and limits -```yaml -apiVersion: v1 -kind: ResourceQuota -metadata: - name: motovault-quota - namespace: motovault -spec: - hard: - requests.cpu: "4" - requests.memory: 8Gi - limits.cpu: "8" - limits.memory: 16Gi - persistentvolumeclaims: "10" - pods: "20" -``` - -#### 3. Set up service accounts and RBAC -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: motovault-service-account - namespace: motovault ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: motovault-role - namespace: motovault -rules: -- apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get", "list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: motovault-rolebinding - namespace: motovault -subjects: -- kind: ServiceAccount - name: motovault-service-account - namespace: motovault -roleRef: - kind: Role - name: motovault-role - apiGroup: rbac.authorization.k8s.io -``` - -#### 4. Configure pod anti-affinity for high availability -- Spread pods across nodes and availability zones -- Ensure no single point of failure -- Optimize for both performance and availability - -#### 5. Implement rolling update strategy with zero downtime -- Configure progressive rollout with health checks -- Automatic rollback on failure -- Canary deployment capabilities - -## 3.2 Ingress and TLS Configuration - -**Objective**: Configure secure external access with proper TLS termination and routing. - -### Ingress Configuration - -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: motovault-ingress - namespace: motovault - annotations: - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/proxy-body-size: "50m" - nginx.ingress.kubernetes.io/proxy-read-timeout: "300" - nginx.ingress.kubernetes.io/proxy-send-timeout: "300" - cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/rate-limit: "100" - nginx.ingress.kubernetes.io/rate-limit-window: "1m" -spec: - ingressClassName: nginx - tls: - - hosts: - - motovault.example.com - secretName: motovault-tls - rules: - - host: motovault.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: motovault-service - port: - number: 80 -``` - -### TLS Certificate Management - -```yaml -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: letsencrypt-prod -spec: - acme: - server: https://acme-v02.api.letsencrypt.org/directory - email: admin@motovault.example.com - privateKeySecretRef: - name: letsencrypt-prod - solvers: - - http01: - ingress: - class: nginx -``` - -### Implementation Tasks - -#### 1. Deploy cert-manager for automated TLS -```bash -kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml -``` - -#### 2. Configure Let's Encrypt for SSL certificates -- Automated certificate provisioning and renewal -- DNS-01 or HTTP-01 challenge configuration -- Certificate monitoring and alerting - -#### 3. Set up WAF and DDoS protection -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: motovault-ingress-policy - namespace: motovault -spec: - podSelector: - matchLabels: - app: motovault - policyTypes: - - Ingress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: nginx-ingress - ports: - - protocol: TCP - port: 8080 -``` - -#### 4. Configure rate limiting and security headers -- Request rate limiting per IP -- Security headers (HSTS, CSP, etc.) -- Request size limitations - -#### 5. Set up health check endpoints for load balancer -- Configure ingress health checks -- Implement graceful degradation -- Monitor certificate expiration - -## 3.3 Monitoring and Observability Setup - -**Objective**: Implement comprehensive monitoring, logging, and alerting for production operations. - -### Prometheus ServiceMonitor Configuration - -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: motovault-metrics - namespace: motovault - labels: - app: motovault -spec: - selector: - matchLabels: - app: motovault - endpoints: - - port: http - path: /metrics - interval: 30s - scrapeTimeout: 10s -``` - -### Application Metrics Implementation - -```csharp -public class MetricsService -{ - private readonly Counter _httpRequestsTotal; - private readonly Histogram _httpRequestDuration; - private readonly Gauge _activeConnections; - private readonly Counter _databaseOperationsTotal; - private readonly Histogram _databaseOperationDuration; - - public MetricsService() - { - _httpRequestsTotal = Metrics.CreateCounter( - "motovault_http_requests_total", - "Total number of HTTP requests", - new[] { "method", "endpoint", "status_code" }); - - _httpRequestDuration = Metrics.CreateHistogram( - "motovault_http_request_duration_seconds", - "Duration of HTTP requests in seconds", - new[] { "method", "endpoint" }); - - _activeConnections = Metrics.CreateGauge( - "motovault_active_connections", - "Number of active database connections"); - - _databaseOperationsTotal = Metrics.CreateCounter( - "motovault_database_operations_total", - "Total number of database operations", - new[] { "operation", "table", "status" }); - - _databaseOperationDuration = Metrics.CreateHistogram( - "motovault_database_operation_duration_seconds", - "Duration of database operations in seconds", - new[] { "operation", "table" }); - } - - public void RecordHttpRequest(string method, string endpoint, int statusCode, double duration) - { - _httpRequestsTotal.WithLabels(method, endpoint, statusCode.ToString()).Inc(); - _httpRequestDuration.WithLabels(method, endpoint).Observe(duration); - } - - public void RecordDatabaseOperation(string operation, string table, bool success, double duration) - { - var status = success ? "success" : "error"; - _databaseOperationsTotal.WithLabels(operation, table, status).Inc(); - _databaseOperationDuration.WithLabels(operation, table).Observe(duration); - } -} -``` - -### Grafana Dashboard Configuration - -```json -{ - "dashboard": { - "title": "MotoVaultPro Application Dashboard", - "panels": [ - { - "title": "HTTP Request Rate", - "type": "graph", - "targets": [ - { - "expr": "rate(motovault_http_requests_total[5m])", - "legendFormat": "{{method}} {{endpoint}}" - } - ] - }, - { - "title": "Response Time Percentiles", - "type": "graph", - "targets": [ - { - "expr": "histogram_quantile(0.50, rate(motovault_http_request_duration_seconds_bucket[5m]))", - "legendFormat": "50th percentile" - }, - { - "expr": "histogram_quantile(0.95, rate(motovault_http_request_duration_seconds_bucket[5m]))", - "legendFormat": "95th percentile" - } - ] - }, - { - "title": "Database Connection Pool", - "type": "singlestat", - "targets": [ - { - "expr": "motovault_active_connections", - "legendFormat": "Active Connections" - } - ] - }, - { - "title": "Error Rate", - "type": "graph", - "targets": [ - { - "expr": "rate(motovault_http_requests_total{status_code=~\"5..\"}[5m])", - "legendFormat": "5xx errors" - } - ] - } - ] - } -} -``` - -### Alert Manager Configuration - -```yaml -groups: -- name: motovault.rules - rules: - - alert: HighErrorRate - expr: rate(motovault_http_requests_total{status_code=~"5.."}[5m]) > 0.1 - for: 2m - labels: - severity: critical - annotations: - summary: "High error rate detected" - description: "Error rate is {{ $value }}% for the last 5 minutes" - - - alert: HighResponseTime - expr: histogram_quantile(0.95, rate(motovault_http_request_duration_seconds_bucket[5m])) > 2 - for: 5m - labels: - severity: warning - annotations: - summary: "High response time detected" - description: "95th percentile response time is {{ $value }}s" - - - alert: DatabaseConnectionPoolExhaustion - expr: motovault_active_connections > 80 - for: 2m - labels: - severity: warning - annotations: - summary: "Database connection pool nearly exhausted" - description: "Active connections: {{ $value }}/100" - - - alert: PodCrashLooping - expr: rate(kube_pod_container_status_restarts_total{namespace="motovault"}[15m]) > 0 - for: 5m - labels: - severity: critical - annotations: - summary: "Pod is crash looping" - description: "Pod {{ $labels.pod }} is restarting frequently" -``` - -### Implementation Tasks - -#### 1. Deploy Prometheus and Grafana stack -```bash -kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/bundle.yaml -``` - -#### 2. Configure application metrics endpoints -- Add Prometheus metrics middleware -- Implement custom business metrics -- Configure metric collection intervals - -#### 3. Set up centralized logging with structured logs -```csharp -builder.Services.AddLogging(loggingBuilder => -{ - loggingBuilder.AddJsonConsole(options => - { - options.JsonWriterOptions = new JsonWriterOptions { Indented = false }; - options.IncludeScopes = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; - }); -}); -``` - -#### 4. Create operational dashboards and alerts -- Application performance dashboards -- Infrastructure monitoring dashboards -- Business metrics and KPIs -- Alert routing and escalation - -#### 5. Implement distributed tracing -```csharp -services.AddOpenTelemetry() - .WithTracing(builder => - { - builder - .AddAspNetCoreInstrumentation() - .AddNpgsql() - .AddRedisInstrumentation() - .AddJaegerExporter(); - }); -``` - -## 3.4 Backup and Disaster Recovery - -**Objective**: Implement comprehensive backup strategies and disaster recovery procedures. - -### Velero Backup Configuration - -```yaml -apiVersion: velero.io/v1 -kind: Schedule -metadata: - name: motovault-daily-backup - namespace: velero -spec: - schedule: "0 2 * * *" # Daily at 2 AM - template: - includedNamespaces: - - motovault - includedResources: - - "*" - storageLocation: default - ttl: 720h0m0s # 30 days - snapshotVolumes: true - ---- -apiVersion: velero.io/v1 -kind: Schedule -metadata: - name: motovault-weekly-backup - namespace: velero -spec: - schedule: "0 3 * * 0" # Weekly on Sunday at 3 AM - template: - includedNamespaces: - - motovault - includedResources: - - "*" - storageLocation: default - ttl: 2160h0m0s # 90 days - snapshotVolumes: true -``` - -### Database Backup Strategy - -```bash -#!/bin/bash -# Automated database backup script - -BACKUP_DATE=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="motovault_backup_${BACKUP_DATE}.sql" -S3_BUCKET="motovault-backups" - -# Create database backup -kubectl exec -n motovault motovault-postgres-1 -- \ - pg_dump -U postgres motovault > "${BACKUP_FILE}" - -# Compress backup -gzip "${BACKUP_FILE}" - -# Upload to S3/MinIO -aws s3 cp "${BACKUP_FILE}.gz" "s3://${S3_BUCKET}/database/" - -# Clean up local file -rm "${BACKUP_FILE}.gz" - -# Retain only last 30 days of backups -aws s3api list-objects-v2 \ - --bucket "${S3_BUCKET}" \ - --prefix "database/" \ - --query 'Contents[?LastModified<=`'$(date -d "30 days ago" --iso-8601)'`].[Key]' \ - --output text | \ - xargs -I {} aws s3 rm "s3://${S3_BUCKET}/{}" -``` - -### Disaster Recovery Procedures - -```bash -#!/bin/bash -# Full system recovery script - -BACKUP_DATE=$1 -if [ -z "$BACKUP_DATE" ]; then - echo "Usage: $0 " - echo "Example: $0 20240120_020000" - exit 1 -fi - -# Stop application -echo "Scaling down application..." -kubectl scale deployment motovault-app --replicas=0 -n motovault - -# Restore database -echo "Restoring database from backup..." -aws s3 cp "s3://motovault-backups/database/database_backup_${BACKUP_DATE}.sql.gz" . -gunzip "database_backup_${BACKUP_DATE}.sql.gz" -kubectl exec -i motovault-postgres-1 -n motovault -- \ - psql -U postgres -d motovault < "database_backup_${BACKUP_DATE}.sql" - -# Restore MinIO data -echo "Restoring MinIO data..." -aws s3 sync "s3://motovault-backups/minio/${BACKUP_DATE}/" /tmp/minio_restore/ -mc mirror /tmp/minio_restore/ motovault-minio/motovault-files/ - -# Restart application -echo "Scaling up application..." -kubectl scale deployment motovault-app --replicas=3 -n motovault - -# Verify health -echo "Waiting for application to be ready..." -kubectl wait --for=condition=ready pod -l app=motovault -n motovault --timeout=300s - -echo "Recovery completed successfully" -``` - -### Implementation Tasks - -#### 1. Deploy Velero for Kubernetes backup -```bash -velero install \ - --provider aws \ - --plugins velero/velero-plugin-for-aws:v1.7.0 \ - --bucket motovault-backups \ - --backup-location-config region=us-west-2 \ - --snapshot-location-config region=us-west-2 -``` - -#### 2. Configure automated database backups -- Point-in-time recovery setup -- Incremental backup strategies -- Cross-region backup replication - -#### 3. Implement MinIO backup synchronization -- Automated file backup to external storage -- Metadata backup and restoration -- Verification of backup integrity - -#### 4. Create disaster recovery runbooks -- Step-by-step recovery procedures -- RTO/RPO definitions and testing -- Contact information and escalation procedures - -#### 5. Set up backup monitoring and alerting -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - name: backup-alerts -spec: - groups: - - name: backup.rules - rules: - - alert: BackupFailed - expr: velero_backup_failure_total > 0 - labels: - severity: critical - annotations: - summary: "Backup operation failed" - description: "Velero backup has failed" -``` - -## Week-by-Week Breakdown - -### Week 9: Production Kubernetes Configuration -- **Days 1-2**: Create production deployment manifests -- **Days 3-4**: Configure HPA, PDB, and resource quotas -- **Days 5-7**: Set up RBAC and security policies - -### Week 10: Ingress and TLS Setup -- **Days 1-2**: Deploy and configure ingress controller -- **Days 3-4**: Set up cert-manager and TLS certificates -- **Days 5-7**: Configure security policies and rate limiting - -### Week 11: Monitoring and Observability -- **Days 1-3**: Deploy Prometheus and Grafana stack -- **Days 4-5**: Configure application metrics and dashboards -- **Days 6-7**: Set up alerting and notification channels - -### Week 12: Backup and Migration Preparation -- **Days 1-3**: Deploy and configure backup solutions -- **Days 4-5**: Create migration scripts and procedures -- **Days 6-7**: Execute migration dry runs and validation - -## Success Criteria - -- [ ] Production Kubernetes deployment with 99.9% availability -- [ ] Secure ingress with automated TLS certificate management -- [ ] Comprehensive monitoring with alerting -- [ ] Automated backup and recovery procedures tested -- [ ] Migration procedures validated and documented -- [ ] Security policies and network controls implemented -- [ ] Performance baselines established and monitored - -## Testing Requirements - -### Production Readiness Tests -- Load testing under expected traffic patterns -- Failover testing for all components -- Security penetration testing -- Backup and recovery validation - -### Performance Tests -- Application response time under load -- Database performance with connection pooling -- Cache performance and hit ratios -- Network latency and throughput - -### Security Tests -- Container image vulnerability scanning -- Network policy validation -- Authentication and authorization testing -- TLS configuration verification - -## Deliverables - -1. **Production Deployment** - - Complete Kubernetes manifests - - Security configurations - - Monitoring and alerting setup - - Backup and recovery procedures - -2. **Documentation** - - Operational runbooks - - Security procedures - - Monitoring guides - - Disaster recovery plans - -3. **Migration Tools** - - Data migration scripts - - Validation tools - - Rollback procedures - -## Dependencies - -- Production Kubernetes cluster -- External storage for backups -- DNS management for ingress -- Certificate authority for TLS -- Monitoring infrastructure - -## Risks and Mitigations - -### Risk: Extended Downtime During Migration -**Mitigation**: Blue-green deployment strategy with comprehensive rollback plan - -### Risk: Data Integrity Issues -**Mitigation**: Extensive validation and parallel running during transition - -### Risk: Performance Degradation -**Mitigation**: Load testing and gradual traffic migration - ---- - -**Previous Phase**: [Phase 2: High Availability Infrastructure](K8S-PHASE-2.md) -**Next Phase**: [Phase 4: Advanced Features and Optimization](K8S-PHASE-4.md) \ No newline at end of file diff --git a/docs/K8S-PHASE-4.md b/docs/K8S-PHASE-4.md deleted file mode 100644 index 1b343d6..0000000 --- a/docs/K8S-PHASE-4.md +++ /dev/null @@ -1,885 +0,0 @@ -# Phase 4: Advanced Features and Optimization (Weeks 13-16) - -This phase focuses on advanced cloud-native features, performance optimization, security enhancements, and final production migration. - -## Overview - -Phase 4 elevates MotoVaultPro to a truly cloud-native application with enterprise-grade features including advanced caching strategies, performance optimization, enhanced security, and seamless production migration. This phase ensures the system is optimized for scale, security, and operational excellence. - -## Key Objectives - -- **Advanced Caching Strategies**: Multi-layer caching for optimal performance -- **Performance Optimization**: Database and application tuning for high load -- **Security Enhancements**: Advanced security features and compliance -- **Production Migration**: Final cutover and optimization -- **Operational Excellence**: Advanced monitoring and automation - -## 4.1 Advanced Caching Strategies - -**Objective**: Implement multi-layer caching for optimal performance and reduced database load. - -### Cache Architecture - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Browser │ │ CDN/Proxy │ │ Application │ -│ Cache │◄──►│ Cache │◄──►│ Memory Cache │ -│ (Static) │ │ (Static + │ │ (L1) │ -│ │ │ Dynamic) │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ - ┌─────────────────┐ - │ Redis Cache │ - │ (L2) │ - │ Distributed │ - └─────────────────┘ - │ - ┌─────────────────┐ - │ Database │ - │ (Source) │ - │ │ - └─────────────────┘ -``` - -### Multi-Level Cache Service Implementation - -```csharp -public class MultiLevelCacheService -{ - private readonly IMemoryCache _memoryCache; - private readonly IDistributedCache _distributedCache; - private readonly ILogger _logger; - - public async Task GetAsync(string key, Func> factory, TimeSpan? expiration = null) - { - // L1 Cache - Memory - if (_memoryCache.TryGetValue(key, out T cachedValue)) - { - _logger.LogDebug("Cache hit (L1): {Key}", key); - return cachedValue; - } - - // L2 Cache - Redis - var distributedValue = await _distributedCache.GetStringAsync(key); - if (distributedValue != null) - { - var deserializedValue = JsonSerializer.Deserialize(distributedValue); - _memoryCache.Set(key, deserializedValue, TimeSpan.FromMinutes(5)); // Short-lived L1 cache - _logger.LogDebug("Cache hit (L2): {Key}", key); - return deserializedValue; - } - - // Cache miss - fetch from source - _logger.LogDebug("Cache miss: {Key}", key); - var value = await factory(); - - // Store in both cache levels - var serializedValue = JsonSerializer.Serialize(value); - await _distributedCache.SetStringAsync(key, serializedValue, new DistributedCacheEntryOptions - { - SlidingExpiration = expiration ?? TimeSpan.FromHours(1) - }); - - _memoryCache.Set(key, value, TimeSpan.FromMinutes(5)); - - return value; - } -} -``` - -### Cache Invalidation Strategy - -```csharp -public class CacheInvalidationService -{ - private readonly IDistributedCache _distributedCache; - private readonly IMemoryCache _memoryCache; - private readonly ILogger _logger; - - public async Task InvalidatePatternAsync(string pattern) - { - // Implement cache invalidation using Redis key pattern matching - var keys = await GetKeysMatchingPatternAsync(pattern); - - var tasks = keys.Select(async key => - { - await _distributedCache.RemoveAsync(key); - _memoryCache.Remove(key); - _logger.LogDebug("Invalidated cache key: {Key}", key); - }); - - await Task.WhenAll(tasks); - } - - public async Task InvalidateVehicleDataAsync(int vehicleId) - { - var patterns = new[] - { - $"vehicle:{vehicleId}:*", - $"dashboard:{vehicleId}:*", - $"reports:{vehicleId}:*" - }; - - foreach (var pattern in patterns) - { - await InvalidatePatternAsync(pattern); - } - } -} -``` - -### Implementation Tasks - -#### 1. Implement intelligent cache warming -```csharp -public class CacheWarmupService : BackgroundService -{ - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - await WarmupFrequentlyAccessedData(); - await Task.Delay(TimeSpan.FromHours(1), stoppingToken); - } - } - - private async Task WarmupFrequentlyAccessedData() - { - // Pre-load dashboard data for active users - var activeUsers = await GetActiveUsersAsync(); - - var warmupTasks = activeUsers.Select(async user => - { - await _cacheService.GetAsync($"dashboard:{user.Id}", - () => _dashboardService.GetDashboardDataAsync(user.Id)); - }); - - await Task.WhenAll(warmupTasks); - } -} -``` - -#### 2. Configure CDN integration for static assets -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: motovault-cdn-ingress - annotations: - nginx.ingress.kubernetes.io/configuration-snippet: | - add_header Cache-Control "public, max-age=31536000, immutable"; - add_header X-Cache-Status $upstream_cache_status; -spec: - rules: - - host: cdn.motovault.example.com - http: - paths: - - path: /static - pathType: Prefix - backend: - service: - name: motovault-service - port: - number: 80 -``` - -#### 3. Implement cache monitoring and metrics -```csharp -public class CacheMetricsMiddleware -{ - private readonly Counter _cacheHits; - private readonly Counter _cacheMisses; - private readonly Histogram _cacheLatency; - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - var stopwatch = Stopwatch.StartNew(); - - // Track cache operations during request - context.Response.OnStarting(() => - { - var cacheStatus = context.Response.Headers["X-Cache-Status"].FirstOrDefault(); - - if (cacheStatus == "HIT") - _cacheHits.Inc(); - else if (cacheStatus == "MISS") - _cacheMisses.Inc(); - - _cacheLatency.Observe(stopwatch.Elapsed.TotalSeconds); - return Task.CompletedTask; - }); - - await next(context); - } -} -``` - -## 4.2 Performance Optimization - -**Objective**: Optimize application performance for high-load scenarios. - -### Database Query Optimization - -```csharp -public class OptimizedVehicleService -{ - private readonly IDbContextFactory _dbContextFactory; - private readonly IMemoryCache _cache; - - public async Task GetDashboardDataAsync(int userId, int vehicleId) - { - var cacheKey = $"dashboard:{userId}:{vehicleId}"; - - if (_cache.TryGetValue(cacheKey, out VehicleDashboardData cached)) - { - return cached; - } - - using var context = _dbContextFactory.CreateDbContext(); - - // Optimized single query with projections - var dashboardData = await context.Vehicles - .Where(v => v.Id == vehicleId && v.UserId == userId) - .Select(v => new VehicleDashboardData - { - Vehicle = v, - RecentServices = v.ServiceRecords - .OrderByDescending(s => s.Date) - .Take(5) - .ToList(), - UpcomingReminders = v.ReminderRecords - .Where(r => r.IsActive && r.DueDate > DateTime.Now) - .OrderBy(r => r.DueDate) - .Take(5) - .ToList(), - FuelEfficiency = v.GasRecords - .Where(g => g.Date >= DateTime.Now.AddMonths(-3)) - .Average(g => g.Efficiency), - TotalMileage = v.OdometerRecords - .OrderByDescending(o => o.Date) - .FirstOrDefault().Mileage ?? 0 - }) - .AsNoTracking() - .FirstOrDefaultAsync(); - - _cache.Set(cacheKey, dashboardData, TimeSpan.FromMinutes(15)); - return dashboardData; - } -} -``` - -### Connection Pool Optimization - -```csharp -services.AddDbContextFactory(options => -{ - options.UseNpgsql(connectionString, npgsqlOptions => - { - npgsqlOptions.EnableRetryOnFailure( - maxRetryCount: 3, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorCodesToAdd: null); - npgsqlOptions.CommandTimeout(30); - }); - - // Optimize for read-heavy workloads - options.EnableSensitiveDataLogging(false); - options.EnableServiceProviderCaching(); - options.EnableDetailedErrors(false); -}, ServiceLifetime.Singleton); - -// Configure connection pooling -services.Configure(builder => -{ - builder.MaxPoolSize = 100; - builder.MinPoolSize = 10; - builder.ConnectionLifetime = 300; - builder.ConnectionPruningInterval = 10; - builder.ConnectionIdleLifetime = 300; -}); -``` - -### Application Performance Optimization - -```csharp -public class PerformanceOptimizationService -{ - // Implement bulk operations for data modifications - public async Task BulkUpdateServiceRecordsAsync( - List records) - { - using var context = _dbContextFactory.CreateDbContext(); - - // Use EF Core bulk operations - context.AttachRange(records); - context.UpdateRange(records); - - var affectedRows = await context.SaveChangesAsync(); - - // Invalidate related cache entries - var vehicleIds = records.Select(r => r.VehicleId).Distinct(); - foreach (var vehicleId in vehicleIds) - { - await _cacheInvalidation.InvalidateVehicleDataAsync(vehicleId); - } - - return new BulkUpdateResult { AffectedRows = affectedRows }; - } - - // Implement read-through cache for expensive calculations - public async Task GetFuelEfficiencyReportAsync( - int vehicleId, - DateTime startDate, - DateTime endDate) - { - var cacheKey = $"fuel_report:{vehicleId}:{startDate:yyyyMM}:{endDate:yyyyMM}"; - - return await _multiLevelCache.GetAsync(cacheKey, async () => - { - using var context = _dbContextFactory.CreateDbContext(); - - var gasRecords = await context.GasRecords - .Where(g => g.VehicleId == vehicleId && - g.Date >= startDate && - g.Date <= endDate) - .AsNoTracking() - .ToListAsync(); - - return CalculateFuelEfficiencyReport(gasRecords); - }, TimeSpan.FromHours(6)); - } -} -``` - -### Implementation Tasks - -#### 1. Implement database indexing strategy -```sql --- Create optimized indexes for common queries -CREATE INDEX CONCURRENTLY idx_gasrecords_vehicle_date - ON gas_records(vehicle_id, date DESC); - -CREATE INDEX CONCURRENTLY idx_servicerecords_vehicle_date - ON service_records(vehicle_id, date DESC); - -CREATE INDEX CONCURRENTLY idx_reminderrecords_active_due - ON reminder_records(is_active, due_date) - WHERE is_active = true; - --- Partial indexes for better performance -CREATE INDEX CONCURRENTLY idx_vehicles_active_users - ON vehicles(user_id) - WHERE is_active = true; -``` - -#### 2. Configure response compression and bundling -```csharp -builder.Services.AddResponseCompression(options => -{ - options.Providers.Add(); - options.Providers.Add(); - options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( - new[] { "application/json", "text/css", "application/javascript" }); -}); - -builder.Services.Configure(options => -{ - options.Level = CompressionLevel.Optimal; -}); -``` - -#### 3. Implement request batching for API endpoints -```csharp -[HttpPost("batch")] -public async Task BatchOperations([FromBody] BatchRequest request) -{ - var results = new List(); - - // Execute operations in parallel where possible - var tasks = request.Operations.Select(async operation => - { - try - { - var result = await ExecuteOperationAsync(operation); - return new BatchResult { Success = true, Data = result }; - } - catch (Exception ex) - { - return new BatchResult { Success = false, Error = ex.Message }; - } - }); - - results.AddRange(await Task.WhenAll(tasks)); - return Ok(new { Results = results }); -} -``` - -## 4.3 Security Enhancements - -**Objective**: Implement advanced security features for production deployment. - -### Network Security Policies - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: motovault-network-policy - namespace: motovault -spec: - podSelector: - matchLabels: - app: motovault - policyTypes: - - Ingress - - Egress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: nginx-ingress - ports: - - protocol: TCP - port: 8080 - egress: - - to: - - namespaceSelector: - matchLabels: - name: motovault - ports: - - protocol: TCP - port: 5432 # PostgreSQL - - protocol: TCP - port: 6379 # Redis - - protocol: TCP - port: 9000 # MinIO - - to: [] # Allow external HTTPS for OIDC - ports: - - protocol: TCP - port: 443 - - protocol: TCP - port: 80 -``` - -### Pod Security Standards - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: motovault - labels: - pod-security.kubernetes.io/enforce: restricted - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/warn: restricted -``` - -### External Secrets Management - -```yaml -apiVersion: external-secrets.io/v1beta1 -kind: SecretStore -metadata: - name: vault-backend - namespace: motovault -spec: - provider: - vault: - server: "https://vault.example.com" - path: "secret" - version: "v2" - auth: - kubernetes: - mountPath: "kubernetes" - role: "motovault-role" - ---- -apiVersion: external-secrets.io/v1beta1 -kind: ExternalSecret -metadata: - name: motovault-secrets - namespace: motovault -spec: - refreshInterval: 1h - secretStoreRef: - name: vault-backend - kind: SecretStore - target: - name: motovault-secrets - creationPolicy: Owner - data: - - secretKey: POSTGRES_CONNECTION - remoteRef: - key: motovault/database - property: connection_string - - secretKey: JWT_SECRET - remoteRef: - key: motovault/auth - property: jwt_secret -``` - -### Application Security Enhancements - -```csharp -public class SecurityMiddleware -{ - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - // Add security headers - context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); - context.Response.Headers.Add("X-Frame-Options", "DENY"); - context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); - context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin"); - context.Response.Headers.Add("Permissions-Policy", "geolocation=(), microphone=(), camera=()"); - - // Content Security Policy - var csp = "default-src 'self'; " + - "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + - "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + - "img-src 'self' data: https:; " + - "connect-src 'self';"; - context.Response.Headers.Add("Content-Security-Policy", csp); - - await next(context); - } -} -``` - -### Implementation Tasks - -#### 1. Implement container image scanning -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: Workflow -metadata: - name: security-scan -spec: - entrypoint: scan-workflow - templates: - - name: scan-workflow - steps: - - - name: trivy-scan - template: trivy-container-scan - - - name: publish-results - template: publish-scan-results - - name: trivy-container-scan - container: - image: aquasec/trivy:latest - command: [trivy] - args: ["image", "--exit-code", "1", "--severity", "HIGH,CRITICAL", "motovault:latest"] -``` - -#### 2. Configure security monitoring and alerting -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - name: security-alerts -spec: - groups: - - name: security.rules - rules: - - alert: HighFailedLoginAttempts - expr: rate(motovault_failed_login_attempts_total[5m]) > 10 - labels: - severity: warning - annotations: - summary: "High number of failed login attempts" - description: "{{ $value }} failed login attempts per second" - - - alert: SuspiciousNetworkActivity - expr: rate(container_network_receive_bytes_total{namespace="motovault"}[5m]) > 1e8 - labels: - severity: critical - annotations: - summary: "Unusual network activity detected" -``` - -#### 3. Implement rate limiting and DDoS protection -```csharp -services.AddRateLimiter(options => -{ - options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - - options.AddFixedWindowLimiter("api", limiterOptions => - { - limiterOptions.PermitLimit = 100; - limiterOptions.Window = TimeSpan.FromMinutes(1); - limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; - limiterOptions.QueueLimit = 10; - }); - - options.AddSlidingWindowLimiter("login", limiterOptions => - { - limiterOptions.PermitLimit = 5; - limiterOptions.Window = TimeSpan.FromMinutes(5); - limiterOptions.SegmentsPerWindow = 5; - }); -}); -``` - -## 4.4 Production Migration Execution - -**Objective**: Execute seamless production migration with minimal downtime. - -### Blue-Green Deployment Strategy - -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: Rollout -metadata: - name: motovault-rollout - namespace: motovault -spec: - replicas: 5 - strategy: - blueGreen: - activeService: motovault-active - previewService: motovault-preview - autoPromotionEnabled: false - scaleDownDelaySeconds: 30 - prePromotionAnalysis: - templates: - - templateName: health-check - args: - - name: service-name - value: motovault-preview - postPromotionAnalysis: - templates: - - templateName: performance-check - args: - - name: service-name - value: motovault-active - selector: - matchLabels: - app: motovault - template: - metadata: - labels: - app: motovault - spec: - containers: - - name: motovault - image: motovault:latest - # ... container specification -``` - -### Migration Validation Scripts - -```bash -#!/bin/bash -# Production migration validation script - -echo "Starting production migration validation..." - -# Validate database connectivity -echo "Checking database connectivity..." -kubectl exec -n motovault deployment/motovault-app -- \ - curl -f http://localhost:8080/health/ready || exit 1 - -# Validate MinIO connectivity -echo "Checking MinIO connectivity..." -kubectl exec -n motovault deployment/motovault-app -- \ - curl -f http://minio-service:9000/minio/health/live || exit 1 - -# Validate Redis connectivity -echo "Checking Redis connectivity..." -kubectl exec -n motovault redis-cluster-0 -- \ - redis-cli ping || exit 1 - -# Test critical user journeys -echo "Testing critical user journeys..." -python3 migration_tests.py --endpoint https://motovault.example.com - -# Validate performance metrics -echo "Checking performance metrics..." -response_time=$(curl -s "http://prometheus:9090/api/v1/query?query=histogram_quantile(0.95,rate(motovault_http_request_duration_seconds_bucket[5m]))" | jq -r '.data.result[0].value[1]') -if (( $(echo "$response_time > 2.0" | bc -l) )); then - echo "Performance degradation detected: ${response_time}s" - exit 1 -fi - -echo "Migration validation completed successfully" -``` - -### Rollback Procedures - -```bash -#!/bin/bash -# Emergency rollback script - -echo "Initiating emergency rollback..." - -# Switch traffic back to previous version -kubectl patch rollout motovault-rollout -n motovault \ - --type='merge' -p='{"spec":{"strategy":{"blueGreen":{"activeService":"motovault-previous"}}}}' - -# Scale down new version -kubectl scale deployment motovault-app-new --replicas=0 -n motovault - -# Restore database from last known good backup -BACKUP_TIMESTAMP=$(date -d "1 hour ago" +"%Y%m%d_%H0000") -./restore_database.sh "$BACKUP_TIMESTAMP" - -# Validate rollback success -curl -f https://motovault.example.com/health/ready - -echo "Rollback completed" -``` - -### Implementation Tasks - -#### 1. Execute phased traffic migration -```yaml -apiVersion: networking.istio.io/v1beta1 -kind: VirtualService -metadata: - name: motovault-traffic-split -spec: - http: - - match: - - headers: - x-canary: - exact: "true" - route: - - destination: - host: motovault-service - subset: v2 - weight: 100 - - route: - - destination: - host: motovault-service - subset: v1 - weight: 90 - - destination: - host: motovault-service - subset: v2 - weight: 10 -``` - -#### 2. Implement automated rollback triggers -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: AnalysisTemplate -metadata: - name: automated-rollback -spec: - metrics: - - name: error-rate - provider: - prometheus: - address: http://prometheus:9090 - query: rate(motovault_http_requests_total{status_code=~"5.."}[2m]) - successCondition: result[0] < 0.05 - failureLimit: 3 - - name: response-time - provider: - prometheus: - address: http://prometheus:9090 - query: histogram_quantile(0.95, rate(motovault_http_request_duration_seconds_bucket[2m])) - successCondition: result[0] < 2.0 - failureLimit: 3 -``` - -#### 3. Configure comprehensive monitoring during migration -- Real-time error rate monitoring -- Performance metric tracking -- User experience validation -- Resource utilization monitoring - -## Week-by-Week Breakdown - -### Week 13: Advanced Caching and Performance -- **Days 1-2**: Implement multi-level caching architecture -- **Days 3-4**: Optimize database queries and connection pooling -- **Days 5-7**: Configure CDN and response optimization - -### Week 14: Security Enhancements -- **Days 1-2**: Implement advanced security policies -- **Days 3-4**: Configure external secrets management -- **Days 5-7**: Set up security monitoring and scanning - -### Week 15: Production Migration -- **Days 1-2**: Execute database migration and validation -- **Days 3-4**: Perform blue-green deployment cutover -- **Days 5-7**: Monitor performance and user experience - -### Week 16: Optimization and Documentation -- **Days 1-3**: Performance tuning based on production metrics -- **Days 4-5**: Complete operational documentation -- **Days 6-7**: Team training and knowledge transfer - -## Success Criteria - -- [ ] Multi-layer caching reducing database load by 70% -- [ ] 95th percentile response time under 500ms -- [ ] Zero-downtime production migration -- [ ] Advanced security policies implemented and validated -- [ ] Comprehensive monitoring and alerting operational -- [ ] Team trained on new operational procedures -- [ ] Performance optimization achieving 10x scalability - -## Testing Requirements - -### Performance Validation -- Load testing with 10x expected traffic -- Database performance under stress -- Cache efficiency and hit ratios -- End-to-end response time validation - -### Security Testing -- Penetration testing of all endpoints -- Container security scanning -- Network policy validation -- Authentication and authorization testing - -### Migration Testing -- Complete migration dry runs -- Rollback procedure validation -- Data integrity verification -- User acceptance testing - -## Deliverables - -1. **Optimized Application** - - Multi-layer caching implementation - - Performance-optimized queries - - Security-hardened deployment - - Production-ready configuration - -2. **Migration Artifacts** - - Migration scripts and procedures - - Rollback automation - - Validation tools - - Performance baselines - -3. **Documentation** - - Operational runbooks - - Performance tuning guides - - Security procedures - - Training materials - -## Final Success Metrics - -### Technical Achievements -- **Availability**: 99.9% uptime achieved -- **Performance**: 95th percentile response time < 500ms -- **Scalability**: 10x user load capacity demonstrated -- **Security**: Zero critical vulnerabilities - -### Operational Achievements -- **Deployment**: Zero-downtime deployments enabled -- **Recovery**: RTO < 30 minutes, RPO < 5 minutes -- **Monitoring**: 100% observability coverage -- **Automation**: 90% reduction in manual operations - -### Business Value -- **User Experience**: No degradation during migration -- **Cost Efficiency**: Infrastructure costs optimized -- **Future Readiness**: Foundation for advanced features -- **Operational Excellence**: Reduced maintenance overhead - ---- - -**Previous Phase**: [Phase 3: Production Deployment](K8S-PHASE-3.md) -**Project Overview**: [Kubernetes Modernization Overview](K8S-OVERVIEW.md) \ No newline at end of file diff --git a/docs/K8S-REFACTOR.md b/docs/K8S-REFACTOR.md deleted file mode 100644 index 207480a..0000000 --- a/docs/K8S-REFACTOR.md +++ /dev/null @@ -1,2009 +0,0 @@ -# Kubernetes Modernization Plan for MotoVaultPro - -## Executive Summary - -This document outlines a comprehensive plan to modernize MotoVaultPro from a traditional self-hosted application to a cloud-native, highly available system running on Kubernetes. The modernization focuses on transforming the current monolithic ASP.NET Core application into a resilient, scalable platform capable of handling enterprise-level workloads while maintaining the existing feature set and user experience. - -### Key Objectives -- **High Availability**: Eliminate single points of failure through distributed architecture -- **Scalability**: Enable horizontal scaling to handle increased user loads -- **Resilience**: Implement fault tolerance and automatic recovery mechanisms -- **Cloud-Native**: Adopt Kubernetes-native patterns and best practices -- **Operational Excellence**: Improve monitoring, logging, and maintenance capabilities - -### Strategic Benefits -- **Reduced Downtime**: Multi-replica deployments with automatic failover -- **Improved Performance**: Distributed caching and optimized data access patterns -- **Enhanced Security**: Pod-level isolation and secret management -- **Cost Optimization**: Efficient resource utilization through auto-scaling -- **Future-Ready**: Foundation for microservices and advanced cloud features - -## Current Architecture Analysis - -### Existing System Overview -MotoVaultPro is currently deployed as a monolithic ASP.NET Core 8.0 application with the following characteristics: - -#### Application Architecture -- **Monolithic Design**: Single deployable unit containing all functionality -- **MVC Pattern**: Traditional Model-View-Controller architecture -- **Dual Database Support**: LiteDB (embedded) and PostgreSQL (external) -- **File Storage**: Local filesystem for document attachments -- **Session Management**: In-memory or cookie-based sessions -- **Configuration**: File-based configuration with environment variables - -#### Current Deployment Model -- **Single Instance**: Typically deployed as a single container or VM -- **Stateful**: Relies on local storage for files and embedded database -- **Limited Scalability**: Cannot horizontally scale due to state dependencies -- **Single Point of Failure**: No redundancy or automatic recovery - -#### Identified Limitations for Kubernetes -1. **State Dependencies**: LiteDB and local file storage prevent stateless operation -2. **Configuration Management**: File-based configuration not suitable for container orchestration -3. **Health Monitoring**: Lacks Kubernetes-compatible health check endpoints -4. **Logging**: Basic logging not optimized for centralized log aggregation -5. **Resource Management**: No resource constraints or auto-scaling capabilities -6. **Secret Management**: Sensitive configuration stored in plain text files - -## Target Architecture - -### Cloud-Native Design Principles -The modernized architecture will embrace the following cloud-native principles: - -#### Stateless Application Design -- **External State Storage**: All state moved to external, highly available services -- **Horizontal Scalability**: Multiple application replicas with load balancing -- **Configuration as Code**: All configuration externalized to ConfigMaps and Secrets -- **Ephemeral Containers**: Pods can be created, destroyed, and recreated without data loss - -#### Distributed Data Architecture -- **PostgreSQL Cluster**: Primary/replica configuration with automatic failover -- **MinIO High Availability**: Distributed object storage for file attachments -- **Redis Cluster**: Distributed caching and session storage -- **Backup Strategy**: Automated backups with point-in-time recovery - -#### Observability and Operations -- **Structured Logging**: JSON logging with correlation IDs for distributed tracing -- **Metrics Collection**: Prometheus-compatible metrics for monitoring -- **Health Checks**: Kubernetes-native readiness and liveness probes -- **Distributed Tracing**: OpenTelemetry integration for request flow analysis - -### High-Level Architecture Diagram -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Kubernetes Cluster │ -├─────────────────────────────────────────────────────────────────┤ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ MotoVault │ │ MotoVault │ │ MotoVault │ │ -│ │ Pod (1) │ │ Pod (2) │ │ Pod (3) │ │ -│ │ │ │ │ │ │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ │ │ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ Load Balancer Service │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ │ │ │ -├───────────┼─────────────────────┼─────────────────────┼──────────┤ -│ ┌────────▼──────┐ ┌─────────▼──────┐ ┌─────────▼──────┐ │ -│ │ PostgreSQL │ │ Redis Cluster │ │ MinIO Cluster │ │ -│ │ Primary │ │ (3 nodes) │ │ (4+ nodes) │ │ -│ │ + 2 Replicas │ │ │ │ Erasure Coded │ │ -│ └───────────────┘ └────────────────┘ └────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Detailed Implementation Phases - -### Phase 1: Core Kubernetes Readiness (Weeks 1-4) - -This phase focuses on making the application compatible with Kubernetes deployment patterns while maintaining existing functionality. - -#### 1.1 Configuration Externalization - -**Objective**: Move all configuration from files to Kubernetes-native configuration management. - -**Current State**: -- Configuration stored in `appsettings.json` and environment variables -- Database connection strings in configuration files -- Feature flags and application settings mixed with deployment configuration - -**Target State**: -- All configuration externalized to ConfigMaps and Secrets -- Environment-specific configuration separated from application code -- Sensitive data (passwords, API keys) managed through Kubernetes Secrets - -**Implementation Tasks**: -1. **Create ConfigMap templates** for non-sensitive configuration - ```yaml - apiVersion: v1 - kind: ConfigMap - metadata: - name: motovault-config - data: - APP_NAME: "MotoVaultPro" - LOG_LEVEL: "Information" - ENABLE_FEATURES: "OpenIDConnect,EmailNotifications" - CACHE_EXPIRY_MINUTES: "30" - ``` - -2. **Create Secret templates** for sensitive configuration - ```yaml - apiVersion: v1 - kind: Secret - metadata: - name: motovault-secrets - type: Opaque - data: - POSTGRES_CONNECTION: - MINIO_ACCESS_KEY: - MINIO_SECRET_KEY: - JWT_SECRET: - ``` - -3. **Modify application startup** to read from environment variables -4. **Remove file-based configuration** dependencies -5. **Implement configuration validation** at startup - -#### 1.2 Database Architecture Modernization - -**Objective**: Eliminate LiteDB dependency and optimize PostgreSQL usage for Kubernetes. - -**Current State**: -- Dual database support with LiteDB as default -- Single PostgreSQL connection for external database mode -- No connection pooling optimization for multiple instances - -**Target State**: -- PostgreSQL-only configuration with high availability -- Optimized connection pooling for horizontal scaling -- Database migration strategy for existing LiteDB installations - -**Implementation Tasks**: -1. **Remove LiteDB implementation** and dependencies -2. **Implement PostgreSQL HA configuration**: - ```csharp - services.AddDbContext(options => - { - options.UseNpgsql(connectionString, npgsqlOptions => - { - npgsqlOptions.EnableRetryOnFailure( - maxRetryCount: 3, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorCodesToAdd: null); - }); - }); - ``` -3. **Add connection pooling configuration**: - ```csharp - // Configure connection pooling for multiple instances - services.Configure(options => - { - options.MaxPoolSize = 100; - options.MinPoolSize = 10; - options.ConnectionLifetime = 300; // 5 minutes - }); - ``` -4. **Create data migration tools** for LiteDB to PostgreSQL conversion -5. **Implement database health checks** for Kubernetes probes - -#### 1.3 Health Check Implementation - -**Objective**: Add Kubernetes-compatible health check endpoints for proper orchestration. - -**Current State**: -- No dedicated health check endpoints -- Application startup/shutdown not optimized for Kubernetes - -**Target State**: -- Comprehensive health checks for all dependencies -- Proper readiness and liveness probe endpoints -- Graceful shutdown handling for pod termination - -**Implementation Tasks**: -1. **Add health check middleware**: - ```csharp - // Program.cs - builder.Services.AddHealthChecks() - .AddNpgSql(connectionString, name: "database") - .AddRedis(redisConnectionString, name: "cache") - .AddCheck("minio"); - - app.MapHealthChecks("/health/ready", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("ready"), - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - - app.MapHealthChecks("/health/live", new HealthCheckOptions - { - Predicate = _ => false // Only check if the app is responsive - }); - ``` - -2. **Implement custom health checks**: - ```csharp - public class MinIOHealthCheck : IHealthCheck - { - private readonly IMinioClient _minioClient; - - public async Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default) - { - try - { - await _minioClient.ListBucketsAsync(cancellationToken); - return HealthCheckResult.Healthy("MinIO is accessible"); - } - catch (Exception ex) - { - return HealthCheckResult.Unhealthy("MinIO is not accessible", ex); - } - } - } - ``` - -3. **Add graceful shutdown handling**: - ```csharp - builder.Services.Configure(options => - { - options.ShutdownTimeout = TimeSpan.FromSeconds(30); - }); - ``` - -#### 1.4 Logging Enhancement - -**Objective**: Implement structured logging suitable for centralized log aggregation. - -**Current State**: -- Basic logging with simple string messages -- No correlation IDs for distributed tracing -- Log levels not optimized for production monitoring - -**Target State**: -- JSON-structured logging with correlation IDs -- Centralized log aggregation compatibility -- Performance and error metrics embedded in logs - -**Implementation Tasks**: -1. **Configure structured logging**: - ```csharp - builder.Services.AddLogging(loggingBuilder => - { - loggingBuilder.ClearProviders(); - loggingBuilder.AddJsonConsole(options => - { - options.IncludeScopes = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; - options.JsonWriterOptions = new JsonWriterOptions - { - Indented = false - }; - }); - }); - ``` - -2. **Add correlation ID middleware**: - ```csharp - public class CorrelationIdMiddleware - { - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - var correlationId = context.Request.Headers["X-Correlation-ID"] - .FirstOrDefault() ?? Guid.NewGuid().ToString(); - - using var scope = _logger.BeginScope(new Dictionary - { - ["CorrelationId"] = correlationId, - ["UserId"] = context.User?.Identity?.Name - }); - - context.Response.Headers.Add("X-Correlation-ID", correlationId); - await next(context); - } - } - ``` - -3. **Implement performance logging** for critical operations - -### Phase 2: High Availability Infrastructure (Weeks 5-8) - -This phase focuses on implementing the supporting infrastructure required for high availability. - -#### 2.1 MinIO High Availability Setup - -**Objective**: Deploy a highly available MinIO cluster for file storage with automatic failover. - -**Architecture Overview**: -MinIO will be deployed as a distributed cluster with erasure coding for data protection and automatic healing capabilities. - -**MinIO Cluster Configuration**: -```yaml -# MinIO Tenant Configuration -apiVersion: minio.min.io/v2 -kind: Tenant -metadata: - name: motovault-minio - namespace: motovault -spec: - image: minio/minio:RELEASE.2024-01-16T16-07-38Z - creationDate: 2024-01-20T10:00:00Z - pools: - - servers: 4 - name: pool-0 - volumesPerServer: 4 - volumeClaimTemplate: - metadata: - name: data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Gi - storageClassName: fast-ssd - mountPath: /export - subPath: /data - requestAutoCert: false - certConfig: - commonName: "" - organizationName: [] - dnsNames: [] - console: - image: minio/console:v0.22.5 - replicas: 2 - consoleSecret: - name: motovault-minio-console-secret - configuration: - name: motovault-minio-config - pools: - - servers: 4 - volumesPerServer: 4 - volumeClaimTemplate: - spec: - accessModes: [ "ReadWriteOnce" ] - resources: - requests: - storage: 100Gi -``` - -**Implementation Tasks**: -1. **Deploy MinIO Operator**: - ```bash - kubectl apply -k "github.com/minio/operator/resources" - ``` - -2. **Create MinIO cluster configuration** with erasure coding for data protection -3. **Configure backup policies** for disaster recovery -4. **Set up monitoring** with Prometheus metrics -5. **Create service endpoints** for application connectivity - -**MinIO High Availability Features**: -- **Erasure Coding**: Data is split across multiple drives with parity for automatic healing -- **Distributed Architecture**: No single point of failure -- **Automatic Healing**: Corrupted data is automatically detected and repaired -- **Load Balancing**: Built-in load balancing across cluster nodes -- **Bucket Policies**: Fine-grained access control for different data types - -#### 2.2 File Storage Abstraction Implementation - -**Objective**: Create an abstraction layer that allows seamless switching between local filesystem and MinIO object storage. - -**Current State**: -- Direct filesystem operations throughout the application -- File paths hardcoded in various controllers and services -- No abstraction for different storage backends - -**Target State**: -- Unified file storage interface -- Pluggable storage implementations -- Transparent migration between storage types - -**Implementation Tasks**: -1. **Define storage abstraction interface**: - ```csharp - public interface IFileStorageService - { - Task UploadFileAsync(Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken = default); - Task DownloadFileAsync(string fileId, CancellationToken cancellationToken = default); - Task DeleteFileAsync(string fileId, CancellationToken cancellationToken = default); - Task GetFileMetadataAsync(string fileId, CancellationToken cancellationToken = default); - Task> ListFilesAsync(string prefix = null, CancellationToken cancellationToken = default); - Task GeneratePresignedUrlAsync(string fileId, TimeSpan expiration, CancellationToken cancellationToken = default); - } - - public class FileMetadata - { - public string Id { get; set; } - public string FileName { get; set; } - public string ContentType { get; set; } - public long Size { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime ModifiedDate { get; set; } - public Dictionary Tags { get; set; } - } - ``` - -2. **Implement MinIO storage service**: - ```csharp - public class MinIOFileStorageService : IFileStorageService - { - private readonly IMinioClient _minioClient; - private readonly ILogger _logger; - private readonly string _bucketName; - - public MinIOFileStorageService(IMinioClient minioClient, IConfiguration configuration, ILogger logger) - { - _minioClient = minioClient; - _logger = logger; - _bucketName = configuration["MinIO:BucketName"] ?? "motovault-files"; - } - - public async Task UploadFileAsync(Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken = default) - { - var fileId = $"{Guid.NewGuid()}/{fileName}"; - - try - { - await _minioClient.PutObjectAsync(new PutObjectArgs() - .WithBucket(_bucketName) - .WithObject(fileId) - .WithStreamData(fileStream) - .WithObjectSize(fileStream.Length) - .WithContentType(contentType) - .WithHeaders(new Dictionary - { - ["X-Amz-Meta-Original-Name"] = fileName, - ["X-Amz-Meta-Upload-Date"] = DateTime.UtcNow.ToString("O") - }), cancellationToken); - - _logger.LogInformation("File uploaded successfully: {FileId}", fileId); - return fileId; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to upload file: {FileName}", fileName); - throw; - } - } - - // Additional method implementations... - } - ``` - -3. **Create fallback storage service** for graceful degradation: - ```csharp - public class FallbackFileStorageService : IFileStorageService - { - private readonly IFileStorageService _primaryService; - private readonly IFileStorageService _fallbackService; - private readonly ILogger _logger; - - // Implementation with automatic fallback logic - } - ``` - -4. **Update all file operations** to use the abstraction layer -5. **Implement file migration utility** for existing local files - -#### 2.3 PostgreSQL High Availability Configuration - -**Objective**: Set up a PostgreSQL cluster with automatic failover and read replicas. - -**Architecture Overview**: -PostgreSQL will be deployed using an operator (like CloudNativePG or Postgres Operator) to provide automated failover, backup, and scaling capabilities. - -**PostgreSQL Cluster Configuration**: -```yaml -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: motovault-postgres - namespace: motovault -spec: - instances: 3 - primaryUpdateStrategy: unsupervised - - postgresql: - parameters: - max_connections: "200" - shared_buffers: "256MB" - effective_cache_size: "1GB" - maintenance_work_mem: "64MB" - checkpoint_completion_target: "0.9" - wal_buffers: "16MB" - default_statistics_target: "100" - random_page_cost: "1.1" - effective_io_concurrency: "200" - - resources: - requests: - memory: "2Gi" - cpu: "1000m" - limits: - memory: "4Gi" - cpu: "2000m" - - storage: - size: "100Gi" - storageClass: "fast-ssd" - - monitoring: - enabled: true - - backup: - retentionPolicy: "30d" - barmanObjectStore: - destinationPath: "s3://motovault-backups/postgres" - s3Credentials: - accessKeyId: - name: postgres-backup-credentials - key: ACCESS_KEY_ID - secretAccessKey: - name: postgres-backup-credentials - key: SECRET_ACCESS_KEY - wal: - retention: "5d" - data: - retention: "30d" - jobs: 1 -``` - -**Implementation Tasks**: -1. **Deploy PostgreSQL operator** (CloudNativePG recommended) -2. **Configure cluster with primary/replica setup** -3. **Set up automated backups** to MinIO or external storage -4. **Implement connection pooling** with PgBouncer -5. **Configure monitoring** and alerting for database health - -#### 2.4 Redis Cluster for Session Management - -**Objective**: Implement distributed session storage and caching using Redis cluster. - -**Current State**: -- In-memory session storage tied to individual application instances -- No distributed caching for expensive operations -- Configuration and translation data loaded on each application start - -**Target State**: -- Redis cluster for distributed session storage -- Centralized caching for frequently accessed data -- High availability with automatic failover - -**Redis Cluster Configuration**: -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: redis-cluster-config - namespace: motovault -data: - redis.conf: | - cluster-enabled yes - cluster-require-full-coverage no - cluster-node-timeout 15000 - cluster-config-file /data/nodes.conf - cluster-migration-barrier 1 - appendonly yes - appendfsync everysec - save 900 1 - save 300 10 - save 60 10000 - ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: redis-cluster - namespace: motovault -spec: - serviceName: redis-cluster - replicas: 6 - selector: - matchLabels: - app: redis-cluster - template: - metadata: - labels: - app: redis-cluster - spec: - containers: - - name: redis - image: redis:7-alpine - command: - - redis-server - - /etc/redis/redis.conf - ports: - - containerPort: 6379 - - containerPort: 16379 - resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "1Gi" - cpu: "500m" - volumeMounts: - - name: redis-config - mountPath: /etc/redis - - name: redis-data - mountPath: /data - volumes: - - name: redis-config - configMap: - name: redis-cluster-config - volumeClaimTemplates: - - metadata: - name: redis-data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 10Gi -``` - -**Implementation Tasks**: -1. **Deploy Redis cluster** with 6 nodes (3 masters, 3 replicas) -2. **Configure session storage**: - ```csharp - services.AddStackExchangeRedisCache(options => - { - options.Configuration = configuration.GetConnectionString("Redis"); - options.InstanceName = "MotoVault"; - }); - - services.AddSession(options => - { - options.IdleTimeout = TimeSpan.FromMinutes(30); - options.Cookie.HttpOnly = true; - options.Cookie.IsEssential = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - }); - ``` - -3. **Implement distributed caching**: - ```csharp - public class CachedTranslationService : ITranslationService - { - private readonly IDistributedCache _cache; - private readonly ITranslationService _translationService; - private readonly ILogger _logger; - - public async Task GetTranslationAsync(string key, string language) - { - var cacheKey = $"translation:{language}:{key}"; - var cached = await _cache.GetStringAsync(cacheKey); - - if (cached != null) - { - return cached; - } - - var translation = await _translationService.GetTranslationAsync(key, language); - - await _cache.SetStringAsync(cacheKey, translation, new DistributedCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromHours(1) - }); - - return translation; - } - } - ``` - -4. **Add cache monitoring** and performance metrics - -### Phase 3: Production Deployment (Weeks 9-12) - -This phase focuses on deploying the modernized application with proper production configurations and operational procedures. - -#### 3.1 Kubernetes Deployment Configuration - -**Objective**: Create production-ready Kubernetes manifests with proper resource management and high availability. - -**Application Deployment Configuration**: -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: motovault-app - namespace: motovault - labels: - app: motovault - version: v1.0.0 -spec: - replicas: 3 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - app: motovault - template: - metadata: - labels: - app: motovault - version: v1.0.0 - annotations: - prometheus.io/scrape: "true" - prometheus.io/path: "/metrics" - prometheus.io/port: "8080" - spec: - serviceAccountName: motovault-service-account - securityContext: - runAsNonRoot: true - runAsUser: 1000 - fsGroup: 2000 - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - motovault - topologyKey: kubernetes.io/hostname - - weight: 50 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - motovault - topologyKey: topology.kubernetes.io/zone - containers: - - name: motovault - image: motovault:latest - imagePullPolicy: Always - ports: - - containerPort: 8080 - name: http - protocol: TCP - env: - - name: ASPNETCORE_ENVIRONMENT - value: "Production" - - name: ASPNETCORE_URLS - value: "http://+:8080" - envFrom: - - configMapRef: - name: motovault-config - - secretRef: - name: motovault-secrets - resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "1Gi" - cpu: "500m" - readinessProbe: - httpGet: - path: /health/ready - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - livenessProbe: - httpGet: - path: /health/live - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - volumeMounts: - - name: tmp-volume - mountPath: /tmp - - name: app-logs - mountPath: /app/logs - volumes: - - name: tmp-volume - emptyDir: {} - - name: app-logs - emptyDir: {} - terminationGracePeriodSeconds: 30 - ---- -apiVersion: v1 -kind: Service -metadata: - name: motovault-service - namespace: motovault - labels: - app: motovault -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 8080 - protocol: TCP - name: http - selector: - app: motovault - ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: motovault-pdb - namespace: motovault -spec: - minAvailable: 2 - selector: - matchLabels: - app: motovault -``` - -**Horizontal Pod Autoscaler Configuration**: -```yaml -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: motovault-hpa - namespace: motovault -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: motovault-app - minReplicas: 3 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 - behavior: - scaleUp: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 10 - periodSeconds: 60 -``` - -#### 3.2 Ingress and TLS Configuration - -**Objective**: Configure secure external access with proper TLS termination and routing. - -**Ingress Configuration**: -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: motovault-ingress - namespace: motovault - annotations: - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/proxy-body-size: "50m" - nginx.ingress.kubernetes.io/proxy-read-timeout: "300" - nginx.ingress.kubernetes.io/proxy-send-timeout: "300" - cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/rate-limit: "100" - nginx.ingress.kubernetes.io/rate-limit-window: "1m" -spec: - ingressClassName: nginx - tls: - - hosts: - - motovault.example.com - secretName: motovault-tls - rules: - - host: motovault.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: motovault-service - port: - number: 80 -``` - -#### 3.3 Monitoring and Observability Setup - -**Objective**: Implement comprehensive monitoring, logging, and alerting for production operations. - -**Prometheus ServiceMonitor Configuration**: -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: motovault-metrics - namespace: motovault - labels: - app: motovault -spec: - selector: - matchLabels: - app: motovault - endpoints: - - port: http - path: /metrics - interval: 30s - scrapeTimeout: 10s -``` - -**Application Metrics Implementation**: -```csharp -public class MetricsService -{ - private readonly Counter _httpRequestsTotal; - private readonly Histogram _httpRequestDuration; - private readonly Gauge _activeConnections; - private readonly Counter _databaseOperationsTotal; - private readonly Histogram _databaseOperationDuration; - - public MetricsService() - { - _httpRequestsTotal = Metrics.CreateCounter( - "motovault_http_requests_total", - "Total number of HTTP requests", - new[] { "method", "endpoint", "status_code" }); - - _httpRequestDuration = Metrics.CreateHistogram( - "motovault_http_request_duration_seconds", - "Duration of HTTP requests in seconds", - new[] { "method", "endpoint" }); - - _activeConnections = Metrics.CreateGauge( - "motovault_active_connections", - "Number of active database connections"); - - _databaseOperationsTotal = Metrics.CreateCounter( - "motovault_database_operations_total", - "Total number of database operations", - new[] { "operation", "table", "status" }); - - _databaseOperationDuration = Metrics.CreateHistogram( - "motovault_database_operation_duration_seconds", - "Duration of database operations in seconds", - new[] { "operation", "table" }); - } - - public void RecordHttpRequest(string method, string endpoint, int statusCode, double duration) - { - _httpRequestsTotal.WithLabels(method, endpoint, statusCode.ToString()).Inc(); - _httpRequestDuration.WithLabels(method, endpoint).Observe(duration); - } - - public void RecordDatabaseOperation(string operation, string table, bool success, double duration) - { - var status = success ? "success" : "error"; - _databaseOperationsTotal.WithLabels(operation, table, status).Inc(); - _databaseOperationDuration.WithLabels(operation, table).Observe(duration); - } -} -``` - -**Custom Grafana Dashboard Configuration**: -```json -{ - "dashboard": { - "title": "MotoVaultPro Application Dashboard", - "panels": [ - { - "title": "HTTP Request Rate", - "type": "graph", - "targets": [ - { - "expr": "rate(motovault_http_requests_total[5m])", - "legendFormat": "{{method}} {{endpoint}}" - } - ] - }, - { - "title": "Response Time Percentiles", - "type": "graph", - "targets": [ - { - "expr": "histogram_quantile(0.50, rate(motovault_http_request_duration_seconds_bucket[5m]))", - "legendFormat": "50th percentile" - }, - { - "expr": "histogram_quantile(0.95, rate(motovault_http_request_duration_seconds_bucket[5m]))", - "legendFormat": "95th percentile" - } - ] - }, - { - "title": "Database Connection Pool", - "type": "singlestat", - "targets": [ - { - "expr": "motovault_active_connections", - "legendFormat": "Active Connections" - } - ] - }, - { - "title": "Error Rate", - "type": "graph", - "targets": [ - { - "expr": "rate(motovault_http_requests_total{status_code=~\"5..\"}[5m])", - "legendFormat": "5xx errors" - } - ] - } - ] - } -} -``` - -#### 3.4 Backup and Disaster Recovery - -**Objective**: Implement comprehensive backup strategies and disaster recovery procedures. - -**Velero Backup Configuration**: -```yaml -apiVersion: velero.io/v1 -kind: Schedule -metadata: - name: motovault-daily-backup - namespace: velero -spec: - schedule: "0 2 * * *" # Daily at 2 AM - template: - includedNamespaces: - - motovault - includedResources: - - "*" - storageLocation: default - ttl: 720h0m0s # 30 days - snapshotVolumes: true - ---- -apiVersion: velero.io/v1 -kind: Schedule -metadata: - name: motovault-weekly-backup - namespace: velero -spec: - schedule: "0 3 * * 0" # Weekly on Sunday at 3 AM - template: - includedNamespaces: - - motovault - includedResources: - - "*" - storageLocation: default - ttl: 2160h0m0s # 90 days - snapshotVolumes: true -``` - -**Database Backup Strategy**: -```bash -#!/bin/bash -# Automated database backup script - -BACKUP_DATE=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="motovault_backup_${BACKUP_DATE}.sql" -S3_BUCKET="motovault-backups" - -# Create database backup -kubectl exec -n motovault motovault-postgres-1 -- \ - pg_dump -U postgres motovault > "${BACKUP_FILE}" - -# Compress backup -gzip "${BACKUP_FILE}" - -# Upload to S3/MinIO -aws s3 cp "${BACKUP_FILE}.gz" "s3://${S3_BUCKET}/database/" - -# Clean up local file -rm "${BACKUP_FILE}.gz" - -# Retain only last 30 days of backups -aws s3api list-objects-v2 \ - --bucket "${S3_BUCKET}" \ - --prefix "database/" \ - --query 'Contents[?LastModified<=`'$(date -d "30 days ago" --iso-8601)'`].[Key]' \ - --output text | \ - xargs -I {} aws s3 rm "s3://${S3_BUCKET}/{}" -``` - -### Phase 4: Advanced Features and Optimization (Weeks 13-16) - -This phase focuses on advanced cloud-native features and performance optimization. - -#### 4.1 Advanced Caching Strategies - -**Objective**: Implement multi-layer caching for optimal performance and reduced database load. - -**Cache Architecture**: -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Browser │ │ CDN/Proxy │ │ Application │ -│ Cache │◄──►│ Cache │◄──►│ Memory Cache │ -│ (Static) │ │ (Static + │ │ (L1) │ -│ │ │ Dynamic) │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ - ┌─────────────────┐ - │ Redis Cache │ - │ (L2) │ - │ Distributed │ - └─────────────────┘ - │ - ┌─────────────────┐ - │ Database │ - │ (Source) │ - │ │ - └─────────────────┘ -``` - -**Implementation Details**: -```csharp -public class MultiLevelCacheService -{ - private readonly IMemoryCache _memoryCache; - private readonly IDistributedCache _distributedCache; - private readonly ILogger _logger; - - public async Task GetAsync(string key, Func> factory, TimeSpan? expiration = null) - { - // L1 Cache - Memory - if (_memoryCache.TryGetValue(key, out T cachedValue)) - { - _logger.LogDebug("Cache hit (L1): {Key}", key); - return cachedValue; - } - - // L2 Cache - Redis - var distributedValue = await _distributedCache.GetStringAsync(key); - if (distributedValue != null) - { - var deserializedValue = JsonSerializer.Deserialize(distributedValue); - _memoryCache.Set(key, deserializedValue, TimeSpan.FromMinutes(5)); // Short-lived L1 cache - _logger.LogDebug("Cache hit (L2): {Key}", key); - return deserializedValue; - } - - // Cache miss - fetch from source - _logger.LogDebug("Cache miss: {Key}", key); - var value = await factory(); - - // Store in both cache levels - var serializedValue = JsonSerializer.Serialize(value); - await _distributedCache.SetStringAsync(key, serializedValue, new DistributedCacheEntryOptions - { - SlidingExpiration = expiration ?? TimeSpan.FromHours(1) - }); - - _memoryCache.Set(key, value, TimeSpan.FromMinutes(5)); - - return value; - } -} -``` - -#### 4.2 Performance Optimization - -**Objective**: Optimize application performance for high-load scenarios. - -**Database Query Optimization**: -```csharp -public class OptimizedVehicleService -{ - private readonly IDbContextFactory _dbContextFactory; - private readonly IMemoryCache _cache; - - public async Task GetDashboardDataAsync(int userId, int vehicleId) - { - var cacheKey = $"dashboard:{userId}:{vehicleId}"; - - if (_cache.TryGetValue(cacheKey, out VehicleDashboardData cached)) - { - return cached; - } - - using var context = _dbContextFactory.CreateDbContext(); - - // Optimized single query with projections - var dashboardData = await context.Vehicles - .Where(v => v.Id == vehicleId && v.UserId == userId) - .Select(v => new VehicleDashboardData - { - Vehicle = v, - RecentServices = v.ServiceRecords - .OrderByDescending(s => s.Date) - .Take(5) - .ToList(), - UpcomingReminders = v.ReminderRecords - .Where(r => r.IsActive && r.DueDate > DateTime.Now) - .OrderBy(r => r.DueDate) - .Take(5) - .ToList(), - FuelEfficiency = v.GasRecords - .Where(g => g.Date >= DateTime.Now.AddMonths(-3)) - .Average(g => g.Efficiency), - TotalMileage = v.OdometerRecords - .OrderByDescending(o => o.Date) - .FirstOrDefault().Mileage ?? 0 - }) - .AsNoTracking() - .FirstOrDefaultAsync(); - - _cache.Set(cacheKey, dashboardData, TimeSpan.FromMinutes(15)); - return dashboardData; - } -} -``` - -**Connection Pool Optimization**: -```csharp -services.AddDbContextFactory(options => -{ - options.UseNpgsql(connectionString, npgsqlOptions => - { - npgsqlOptions.EnableRetryOnFailure( - maxRetryCount: 3, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorCodesToAdd: null); - npgsqlOptions.CommandTimeout(30); - }); - - // Optimize for read-heavy workloads - options.EnableSensitiveDataLogging(false); - options.EnableServiceProviderCaching(); - options.EnableDetailedErrors(false); -}, ServiceLifetime.Singleton); - -// Configure connection pooling -services.Configure(builder => -{ - builder.MaxPoolSize = 100; - builder.MinPoolSize = 10; - builder.ConnectionLifetime = 300; - builder.ConnectionPruningInterval = 10; - builder.ConnectionIdleLifetime = 300; -}); -``` - -#### 4.3 Security Enhancements - -**Objective**: Implement advanced security features for production deployment. - -**Network Security Policies**: -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: motovault-network-policy - namespace: motovault -spec: - podSelector: - matchLabels: - app: motovault - policyTypes: - - Ingress - - Egress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: nginx-ingress - ports: - - protocol: TCP - port: 8080 - egress: - - to: - - namespaceSelector: - matchLabels: - name: motovault - ports: - - protocol: TCP - port: 5432 # PostgreSQL - - protocol: TCP - port: 6379 # Redis - - protocol: TCP - port: 9000 # MinIO - - to: [] # Allow external HTTPS for OIDC - ports: - - protocol: TCP - port: 443 - - protocol: TCP - port: 80 -``` - -**Pod Security Standards**: -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: motovault - labels: - pod-security.kubernetes.io/enforce: restricted - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/warn: restricted -``` - -**Secret Management with External Secrets Operator**: -```yaml -apiVersion: external-secrets.io/v1beta1 -kind: SecretStore -metadata: - name: vault-backend - namespace: motovault -spec: - provider: - vault: - server: "https://vault.example.com" - path: "secret" - version: "v2" - auth: - kubernetes: - mountPath: "kubernetes" - role: "motovault-role" - ---- -apiVersion: external-secrets.io/v1beta1 -kind: ExternalSecret -metadata: - name: motovault-secrets - namespace: motovault -spec: - refreshInterval: 1h - secretStoreRef: - name: vault-backend - kind: SecretStore - target: - name: motovault-secrets - creationPolicy: Owner - data: - - secretKey: POSTGRES_CONNECTION - remoteRef: - key: motovault/database - property: connection_string - - secretKey: JWT_SECRET - remoteRef: - key: motovault/auth - property: jwt_secret -``` - -## Migration Strategy - -### Pre-Migration Assessment - -**Current State Analysis**: -1. **Data Inventory**: Catalog all existing data, configurations, and file attachments -2. **Dependency Mapping**: Identify all external dependencies and integrations -3. **Performance Baseline**: Establish current performance metrics for comparison -4. **User Impact Assessment**: Analyze potential downtime and user experience changes - -**Migration Prerequisites**: -1. **Kubernetes Cluster Ready**: Properly configured cluster with required operators -2. **Infrastructure Deployed**: PostgreSQL, MinIO, and Redis clusters operational -3. **Backup Strategy**: Complete backup of current system and data -4. **Rollback Plan**: Detailed procedure for reverting to current system if needed - -### Migration Execution Plan - -#### Phase 1: Parallel Environment Setup (Week 1) -1. **Deploy target infrastructure** in parallel to existing system -2. **Configure monitoring and logging** for new environment -3. **Run initial data migration tests** with sample data -4. **Validate all health checks** and monitoring alerts - -#### Phase 2: Data Migration (Week 2) -1. **Initial data sync**: Migrate historical data during low-usage periods -2. **File migration**: Transfer all attachments to MinIO with validation -3. **Configuration migration**: Convert all settings to ConfigMaps/Secrets -4. **User data validation**: Verify data integrity and completeness - -#### Phase 3: Application Cutover (Week 3) -1. **Final data sync**: Synchronize any changes made during migration -2. **DNS cutover**: Redirect traffic to new Kubernetes deployment -3. **Monitor closely**: Watch for any issues or performance problems -4. **User acceptance testing**: Validate all functionality works correctly - -#### Phase 4: Optimization and Cleanup (Week 4) -1. **Performance tuning**: Optimize based on real-world usage patterns -2. **Clean up old infrastructure**: Decommission legacy deployment -3. **Update documentation**: Finalize operational procedures -4. **Training**: Train operations team on new procedures - -### Data Migration Tools - -**LiteDB to PostgreSQL Migration Utility**: -```csharp -public class DataMigrationService -{ - private readonly ILiteDatabase _liteDb; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public async Task MigrateAllDataAsync() - { - var result = new MigrationResult(); - - try - { - using var scope = _serviceProvider.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - // Migrate users first (dependencies) - result.UsersProcessed = await MigrateUsersAsync(context); - - // Migrate vehicles - result.VehiclesProcessed = await MigrateVehiclesAsync(context); - - // Migrate all record types - result.ServiceRecordsProcessed = await MigrateServiceRecordsAsync(context); - result.GasRecordsProcessed = await MigrateGasRecordsAsync(context); - result.FilesProcessed = await MigrateFilesAsync(); - - await context.SaveChangesAsync(); - result.Success = true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Migration failed"); - result.Success = false; - result.ErrorMessage = ex.Message; - } - - return result; - } - - private async Task MigrateFilesAsync() - { - var fileStorage = _serviceProvider.GetRequiredService(); - var filesProcessed = 0; - - var localFilesPath = "data/files"; - if (Directory.Exists(localFilesPath)) - { - var files = Directory.GetFiles(localFilesPath, "*", SearchOption.AllDirectories); - - foreach (var filePath in files) - { - using var fileStream = File.OpenRead(filePath); - var fileName = Path.GetFileName(filePath); - var contentType = GetContentType(fileName); - - await fileStorage.UploadFileAsync(fileStream, fileName, contentType); - filesProcessed++; - - _logger.LogInformation("Migrated file: {FileName}", fileName); - } - } - - return filesProcessed; - } -} -``` - -### Rollback Procedures - -**Emergency Rollback Plan**: -1. **Immediate Actions** (0-15 minutes): - - Redirect DNS back to original system - - Activate incident response team - - Begin root cause analysis - -2. **Data Consistency** (15-30 minutes): - - Verify data integrity in original system - - Sync any changes made during brief cutover period - - Validate all services are operational - -3. **Communication** (30-60 minutes): - - Notify stakeholders of rollback - - Provide status updates to users - - Document lessons learned - -4. **Post-Rollback Analysis** (1-24 hours): - - Complete root cause analysis - - Update migration plan based on findings - - Plan next migration attempt - -## Risk Assessment and Mitigation - -### Technical Risks - -#### High Impact Risks - -**1. Data Loss or Corruption** -- **Probability**: Low -- **Impact**: Critical -- **Mitigation**: - - Multiple backup strategies with point-in-time recovery - - Comprehensive data validation during migration - - Parallel running systems during cutover - - Automated data integrity checks - -**2. Extended Downtime During Migration** -- **Probability**: Medium -- **Impact**: High -- **Mitigation**: - - Phased migration approach with minimal downtime windows - - Blue-green deployment strategy - - Comprehensive rollback procedures - - 24/7 monitoring during cutover - -**3. Performance Degradation** -- **Probability**: Medium -- **Impact**: Medium -- **Mitigation**: - - Extensive load testing before migration - - Performance monitoring and alerting - - Auto-scaling capabilities - - Database query optimization - -#### Medium Impact Risks - -**4. Integration Failures** -- **Probability**: Medium -- **Impact**: Medium -- **Mitigation**: - - Thorough integration testing - - Circuit breaker patterns for external dependencies - - Graceful degradation for non-critical features - - Health check monitoring - -**5. Security Vulnerabilities** -- **Probability**: Low -- **Impact**: High -- **Mitigation**: - - Security scanning of all container images - - Network policies and Pod Security Standards - - Secret management best practices - - Regular security audits - -### Operational Risks - -**6. Team Knowledge Gaps** -- **Probability**: Medium -- **Impact**: Medium -- **Mitigation**: - - Comprehensive training program - - Detailed operational documentation - - On-call procedures and runbooks - - Knowledge transfer sessions - -**7. Infrastructure Capacity Issues** -- **Probability**: Low -- **Impact**: Medium -- **Mitigation**: - - Capacity planning and resource monitoring - - Auto-scaling policies - - Resource quotas and limits - - Infrastructure as Code for rapid scaling - -### Business Risks - -**8. User Adoption Challenges** -- **Probability**: Low -- **Impact**: Medium -- **Mitigation**: - - Transparent communication about changes - - User training and documentation - - Phased rollout to minimize impact - - User feedback collection and response - -## Testing Strategy - -### Test Environment Architecture - -**Multi-Environment Strategy**: -``` -Development → Staging → Pre-Production → Production - ↓ ↓ ↓ ↓ - Unit Tests Integration Load Testing Monitoring - API Tests UI Tests Security Alerting - DB Tests E2E Tests Performance Backup Tests -``` - -### Comprehensive Testing Plan - -#### Unit Testing -- **Coverage Target**: 80% code coverage minimum -- **Focus Areas**: Business logic, data access layer, API endpoints -- **Test Framework**: xUnit with Moq for dependency injection testing -- **Automated Execution**: Run on every commit and pull request - -#### Integration Testing -- **Database Integration**: Test all repository implementations -- **External Service Integration**: MinIO, Redis, PostgreSQL connectivity -- **API Integration**: Full request/response cycle testing -- **Authentication Testing**: All authentication flows and authorization rules - -#### Load Testing -- **Tools**: k6 or Artillery for load generation -- **Scenarios**: - - Normal load: 100 concurrent users - - Peak load: 500 concurrent users - - Stress test: 1000+ concurrent users -- **Metrics**: Response time, throughput, error rate, resource utilization - -#### Security Testing -- **Container Security**: Scan images for vulnerabilities -- **Network Security**: Validate network policies and isolation -- **Authentication**: Test all authentication and authorization scenarios -- **Data Protection**: Verify encryption at rest and in transit - -#### Disaster Recovery Testing -- **Database Failover**: Test automatic failover scenarios -- **Application Recovery**: Pod failure and recovery testing -- **Backup Restoration**: Full system restoration from backups -- **Network Partitioning**: Test behavior during network issues - -### Performance Testing Scenarios - -**Load Testing Script Example**: -```javascript -import http from 'k6/http'; -import { check, sleep } from 'k6'; - -export let options = { - stages: [ - { duration: '2m', target: 20 }, // Ramp up - { duration: '5m', target: 20 }, // Stay at 20 users - { duration: '2m', target: 50 }, // Ramp up to 50 - { duration: '5m', target: 50 }, // Stay at 50 - { duration: '2m', target: 100 }, // Ramp up to 100 - { duration: '5m', target: 100 }, // Stay at 100 - { duration: '2m', target: 0 }, // Ramp down - ], - thresholds: { - http_req_duration: ['p(95)<500'], // 95% of requests under 500ms - http_req_failed: ['rate<0.1'], // Error rate under 10% - }, -}; - -export default function() { - // Login - let loginResponse = http.post('https://motovault.example.com/api/auth/login', { - username: 'testuser', - password: 'testpass' - }); - - check(loginResponse, { - 'login successful': (r) => r.status === 200, - }); - - let authToken = loginResponse.json('token'); - - // Dashboard load - let dashboardResponse = http.get('https://motovault.example.com/api/dashboard', { - headers: { Authorization: `Bearer ${authToken}` }, - }); - - check(dashboardResponse, { - 'dashboard loaded': (r) => r.status === 200, - 'response time < 500ms': (r) => r.timings.duration < 500, - }); - - sleep(1); -} -``` - -## Operational Procedures - -### Monitoring and Alerting - -#### Application Metrics -```yaml -# Prometheus AlertManager Rules -groups: -- name: motovault.rules - rules: - - alert: HighErrorRate - expr: rate(motovault_http_requests_total{status_code=~"5.."}[5m]) > 0.1 - for: 2m - labels: - severity: critical - annotations: - summary: "High error rate detected" - description: "Error rate is {{ $value }}% for the last 5 minutes" - - - alert: HighResponseTime - expr: histogram_quantile(0.95, rate(motovault_http_request_duration_seconds_bucket[5m])) > 2 - for: 5m - labels: - severity: warning - annotations: - summary: "High response time detected" - description: "95th percentile response time is {{ $value }}s" - - - alert: DatabaseConnectionPoolExhaustion - expr: motovault_active_connections > 80 - for: 2m - labels: - severity: warning - annotations: - summary: "Database connection pool nearly exhausted" - description: "Active connections: {{ $value }}/100" - - - alert: PodCrashLooping - expr: rate(kube_pod_container_status_restarts_total{namespace="motovault"}[15m]) > 0 - for: 5m - labels: - severity: critical - annotations: - summary: "Pod is crash looping" - description: "Pod {{ $labels.pod }} is restarting frequently" -``` - -#### Infrastructure Monitoring -- **Node Resources**: CPU, memory, disk usage across all nodes -- **Network Performance**: Latency, throughput, packet loss -- **Storage Performance**: IOPS, latency for persistent volumes -- **Kubernetes Health**: API server, etcd, scheduler performance - -### Backup and Recovery Procedures - -#### Automated Backup Schedule -```bash -# Daily backup script -#!/bin/bash -set -e - -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_NAMESPACE="motovault" - -# Database backup -echo "Starting database backup at $(date)" -kubectl exec -n $BACKUP_NAMESPACE motovault-postgres-1 -- \ - pg_dump -U postgres motovault | \ - gzip > "database_backup_${TIMESTAMP}.sql.gz" - -# MinIO backup (metadata and small files) -echo "Starting MinIO backup at $(date)" -mc mirror motovault-minio/motovault-files backup/minio_${TIMESTAMP}/ - -# Kubernetes resources backup -echo "Starting Kubernetes backup at $(date)" -velero backup create "motovault-${TIMESTAMP}" \ - --include-namespaces motovault \ - --wait - -# Upload to remote storage -echo "Uploading backups to remote storage" -aws s3 cp "database_backup_${TIMESTAMP}.sql.gz" s3://motovault-backups/daily/ -aws s3 sync "backup/minio_${TIMESTAMP}/" s3://motovault-backups/minio/${TIMESTAMP}/ - -# Cleanup local files older than 7 days -find backup/ -name "*.gz" -mtime +7 -delete -find backup/minio_* -mtime +7 -exec rm -rf {} \; - -echo "Backup completed successfully at $(date)" -``` - -#### Recovery Procedures -```bash -# Full system recovery script -#!/bin/bash -set -e - -BACKUP_DATE=$1 -if [ -z "$BACKUP_DATE" ]; then - echo "Usage: $0 " - echo "Example: $0 20240120_020000" - exit 1 -fi - -# Stop application -echo "Scaling down application..." -kubectl scale deployment motovault-app --replicas=0 -n motovault - -# Restore database -echo "Restoring database from backup..." -aws s3 cp "s3://motovault-backups/daily/database_backup_${BACKUP_DATE}.sql.gz" . -gunzip "database_backup_${BACKUP_DATE}.sql.gz" -kubectl exec -i motovault-postgres-1 -n motovault -- \ - psql -U postgres -d motovault < "database_backup_${BACKUP_DATE}.sql" - -# Restore MinIO data -echo "Restoring MinIO data..." -aws s3 sync "s3://motovault-backups/minio/${BACKUP_DATE}/" /tmp/minio_restore/ -mc mirror /tmp/minio_restore/ motovault-minio/motovault-files/ - -# Restart application -echo "Scaling up application..." -kubectl scale deployment motovault-app --replicas=3 -n motovault - -# Verify health -echo "Waiting for application to be ready..." -kubectl wait --for=condition=ready pod -l app=motovault -n motovault --timeout=300s - -echo "Recovery completed successfully" -``` - -### Maintenance Procedures - -#### Rolling Updates -```yaml -# Zero-downtime deployment strategy -apiVersion: argoproj.io/v1alpha1 -kind: Rollout -metadata: - name: motovault-rollout - namespace: motovault -spec: - replicas: 5 - strategy: - canary: - steps: - - setWeight: 20 - - pause: {duration: 1m} - - setWeight: 40 - - pause: {duration: 2m} - - setWeight: 60 - - pause: {duration: 2m} - - setWeight: 80 - - pause: {duration: 2m} - analysis: - templates: - - templateName: success-rate - args: - - name: service-name - value: motovault-service - canaryService: motovault-canary-service - stableService: motovault-stable-service - selector: - matchLabels: - app: motovault - template: - metadata: - labels: - app: motovault - spec: - containers: - - name: motovault - image: motovault:latest - # ... container spec -``` - -#### Scaling Procedures -- **Horizontal Scaling**: Use HPA for automatic scaling based on metrics -- **Vertical Scaling**: Monitor resource usage and adjust requests/limits -- **Database Scaling**: Add read replicas for read-heavy workloads -- **Storage Scaling**: Monitor MinIO usage and add nodes as needed - -## Implementation Timeline - -### Detailed 16-Week Schedule - -#### Weeks 1-4: Foundation Phase -**Week 1: Environment Setup** -- Day 1-2: Kubernetes cluster setup and configuration -- Day 3-4: Deploy PostgreSQL operator and cluster -- Day 5-7: Deploy MinIO operator and configure HA cluster - -**Week 2: Redis and Monitoring** -- Day 1-3: Deploy Redis cluster with sentinel configuration -- Day 4-5: Set up Prometheus and Grafana -- Day 6-7: Configure initial monitoring dashboards - -**Week 3: Application Changes** -- Day 1-2: Remove LiteDB dependencies -- Day 3-4: Implement configuration externalization -- Day 5-7: Add health check endpoints - -**Week 4: File Storage Abstraction** -- Day 1-3: Implement IFileStorageService interface -- Day 4-5: Create MinIO implementation -- Day 6-7: Add fallback mechanisms - -#### Weeks 5-8: Core Implementation -**Week 5: Database Integration** -- Day 1-3: Optimize PostgreSQL connections -- Day 4-5: Implement connection pooling -- Day 6-7: Add database health checks - -**Week 6: Session and Caching** -- Day 1-2: Implement Redis session storage -- Day 3-4: Add distributed caching layer -- Day 5-7: Implement multi-level caching - -**Week 7: Observability** -- Day 1-3: Add structured logging -- Day 4-5: Implement Prometheus metrics -- Day 6-7: Add distributed tracing - -**Week 8: Security Implementation** -- Day 1-2: Configure Pod Security Standards -- Day 3-4: Implement network policies -- Day 5-7: Set up secret management - -#### Weeks 9-12: Production Deployment -**Week 9: Kubernetes Manifests** -- Day 1-3: Create production Kubernetes manifests -- Day 4-5: Configure HPA and resource limits -- Day 6-7: Set up ingress and TLS - -**Week 10: Backup and Recovery** -- Day 1-3: Implement backup strategies -- Day 4-5: Create recovery procedures -- Day 6-7: Test disaster recovery scenarios - -**Week 11: Load Testing** -- Day 1-3: Create load testing scenarios -- Day 4-5: Execute performance tests -- Day 6-7: Optimize based on results - -**Week 12: Migration Preparation** -- Day 1-3: Create data migration tools -- Day 4-5: Test migration procedures -- Day 6-7: Prepare rollback plans - -#### Weeks 13-16: Advanced Features -**Week 13: Performance Optimization** -- Day 1-3: Implement advanced caching strategies -- Day 4-5: Optimize database queries -- Day 6-7: Fine-tune resource allocation - -**Week 14: Advanced Security** -- Day 1-3: Implement external secret management -- Day 4-5: Add security scanning to CI/CD -- Day 6-7: Configure advanced network policies - -**Week 15: Production Migration** -- Day 1-2: Execute data migration -- Day 3-4: Perform application cutover -- Day 5-7: Monitor and optimize - -**Week 16: Optimization and Documentation** -- Day 1-3: Performance tuning based on production usage -- Day 4-5: Update operational documentation -- Day 6-7: Conduct team training - -### Success Criteria - -#### Technical Success Metrics -- **Availability**: 99.9% uptime (no more than 8.76 hours downtime per year) -- **Performance**: 95th percentile response time under 500ms -- **Scalability**: Ability to handle 10x current user load -- **Recovery**: RTO < 1 hour, RPO < 15 minutes - -#### Operational Success Metrics -- **Deployment Frequency**: Enable weekly deployments with zero downtime -- **Mean Time to Recovery**: < 30 minutes for critical issues -- **Change Failure Rate**: < 5% of deployments require rollback -- **Monitoring Coverage**: 100% of critical services monitored - -#### Business Success Metrics -- **User Satisfaction**: No degradation in user experience -- **Cost Efficiency**: Infrastructure costs within 20% of current spending -- **Maintenance Overhead**: Reduced operational maintenance time by 50% -- **Future Readiness**: Foundation for future enhancements and scaling - ---- - -**Document Version**: 1.0 -**Last Updated**: January 2025 -**Author**: MotoVaultPro Modernization Team -**Status**: Draft for Review - ---- - -This comprehensive plan provides a detailed roadmap for modernizing MotoVaultPro to run efficiently on Kubernetes with high availability, scalability, and operational excellence. The phased approach ensures minimal risk while delivering maximum benefits for future growth and reliability. \ No newline at end of file diff --git a/docs/MOBILE.md b/docs/MOBILE.md deleted file mode 100644 index 3f9b8f4..0000000 --- a/docs/MOBILE.md +++ /dev/null @@ -1,185 +0,0 @@ -# Mobile Experience Improvement Plan for Add Fuel Record Screen - -## Analysis Summary - -The current add fuel record screen has significant mobile UX issues that create pain points for users on mobile devices. The interface feels like a shrunken desktop version rather than a mobile-first experience. - -## Critical Mobile UX Issues Identified - -### 1. Modal Size and Viewport Problems -- Uses Bootstrap's default modal sizing without mobile optimization -- No mobile-specific modal sizing classes or responsive adjustments -- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` - -### 2. Touch Target Size Issues -- Small "+" button for odometer increment (44px minimum not met) -- Small close button in header -- Form switch toggles too small for reliable touch interaction -- **File Locations**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 69, 99, 51, 48, 106, 110) - -### 3. Dense Two-Column Layout Problems -- Advanced mode uses `col-md-6` layout creating cramped display -- Fields become too narrow for comfortable text input -- Second column with file upload becomes nearly unusable -- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 59, 139) - -### 4. Complex Header Layout on Mobile -- Modal header contains multiple elements in cramped flex layout -- Toggle labels may wrap or get cut off -- Mode switch becomes hard to understand and use -- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 44-53) - -### 5. Input Field Accessibility Issues -- Decimal inputs with custom key interceptors interfere with mobile keyboards -- Multi-select dropdown for tags difficult on mobile -- File upload interface unusable in narrow mobile view -- **File Locations**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` (lines 74, 103, 117, 127, 130-135) - - `/wwwroot/js/gasrecord.js` - -### 6. Modal Footer Button Layout -- Multiple buttons including conditional "Delete" button create touch conflicts -- Risk of accidental deletion or difficulty reaching primary action -- **File Location**: `/Views/Vehicle/Gas/_GasModal.cshtml` (line 155) - -### 7. Form Mode Switching UX -- Simple/Advanced mode toggle jarring on mobile -- Content suddenly appears/disappears -- Users might not understand mode switching capability -- **File Location**: `/wwwroot/js/gasrecord.js` (lines 509-536) - -### 8. Keyboard and Input Mode Issues -- Mixed input types with custom JavaScript key handlers -- Mobile keyboards may not behave predictably -- **File Locations**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/js/gasrecord.js` - -### 9. Date Picker Mobile Issues -- Bootstrap datepicker doesn't provide optimal mobile experience -- Native mobile date pickers would be better -- **File Location**: `/wwwroot/js/gasrecord.js` (lines 6, 29) - -### 10. No Progressive Enhancement for Mobile -- No mobile-specific CSS classes or touch-friendly spacing -- No mobile-optimized layouts -- **File Locations**: - - `/wwwroot/css/site.css` - - `/Views/Vehicle/Gas/_GasModal.cshtml` - -## Mobile Experience Improvement Plan - -### Priority 1: Critical Mobile UX Fixes - -#### 1. Mobile-First Modal Design -- Implement full-screen modal on mobile devices -- Add slide-up animation for native app feel -- Create mobile-specific modal header with simplified layout -- **Files to Modify**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/css/site.css` - - `/wwwroot/js/gasrecord.js` - -#### 2. Touch Target Optimization -- Increase all interactive elements to minimum 44px -- Add larger padding around buttons and form controls -- Implement touch-friendly spacing between elements -- **Files to Modify**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/css/site.css` - -#### 3. Single-Column Mobile Layout -- Force single-column layout on mobile regardless of mode -- Stack all form fields vertically with proper spacing -- Move file upload and notes to dedicated sections -- **Files to Modify**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/css/site.css` - -### Priority 2: Input and Interaction Improvements - -#### 4. Mobile-Optimized Inputs -- Replace Bootstrap datepicker with native HTML5 date input on mobile -- Simplify tag selection with mobile-friendly chip input -- Improve number input keyboards with proper `inputmode` attributes -- **Files to Modify**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/js/gasrecord.js` - -#### 5. Form Mode Simplification -- Default to Simple mode on mobile -- Make mode toggle more prominent and clear -- Add smooth transitions between modes -- **Files to Modify**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/js/gasrecord.js` - - `/Controllers/Vehicle/GasController.cs` - -### Priority 3: Enhanced Mobile Features - -#### 6. Bottom Sheet Pattern -- Implement native-style bottom sheet for mobile -- Add swipe-to-dismiss gesture -- Include pull handle for better UX -- **Files to Modify**: - - `/Views/Vehicle/Gas/_GasModal.cshtml` - - `/wwwroot/css/site.css` - - `/wwwroot/js/gasrecord.js` - -#### 7. Mobile-Specific CSS Improvements -- Add mobile breakpoint styles -- Implement proper touch feedback -- Optimize form field sizing for mobile keyboards -- **Files to Modify**: - - `/wwwroot/css/site.css` - -#### 8. Progressive Enhancement -- Add mobile detection for conditional features -- Implement haptic feedback where supported -- Add mobile-specific validation styling -- **Files to Modify**: - - `/wwwroot/js/gasrecord.js` - - `/wwwroot/js/shared.js` - - `/Views/Shared/_Layout.cshtml` - -## Implementation Strategy - -### Phase 1: Modal and Layout Fixes (Priority 1 items) -- Focus on making the most impactful changes first -- Ensure mobile modal feels native and intuitive -- Implement proper touch targets and single-column layout - -### Phase 2: Input Optimizations (Priority 2 items) -- Optimize form inputs for mobile interaction -- Simplify complex form elements -- Improve mode switching experience - -### Phase 3: Advanced Mobile Features (Priority 3 items) -- Add sophisticated mobile interaction patterns -- Implement progressive enhancement -- Add mobile-specific features and feedback - -## Key Files for Mobile Improvements - -### Primary Files: -- `/Views/Vehicle/Gas/_GasModal.cshtml` - Main modal template -- `/wwwroot/js/gasrecord.js` - Modal behavior and form handling -- `/wwwroot/css/site.css` - Styling and responsive design - -### Supporting Files: -- `/Controllers/Vehicle/GasController.cs` - Server-side logic -- `/Views/Shared/_Layout.cshtml` - Global mobile configuration -- `/wwwroot/js/shared.js` - Shared JavaScript utilities - -## Success Metrics - -- Touch target compliance (minimum 44px) -- Single-column layout on mobile breakpoints -- Native mobile input patterns -- Improved task completion rates on mobile -- Reduced user friction and abandonment - -## Notes - -This plan maintains existing functionality while transforming the mobile experience from a desktop-centric interface to a mobile-first, touch-optimized experience that feels native and intuitive on mobile devices. \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 0eb6c9e..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,351 +0,0 @@ -# MotoVaultPro Architecture Documentation - -## Table of Contents -1. [Overview](#overview) -2. [Technology Stack](#technology-stack) -3. [Architecture Patterns](#architecture-patterns) -4. [Project Structure](#project-structure) -5. [Data Layer](#data-layer) -6. [Business Logic Layer](#business-logic-layer) -7. [Controller Layer](#controller-layer) -8. [Frontend Architecture](#frontend-architecture) -9. [Authentication & Security](#authentication--security) -10. [Deployment & Configuration](#deployment--configuration) -11. [API Design](#api-design) -12. [Performance Considerations](#performance-considerations) -13. [Development Guidelines](#development-guidelines) -14. [Future Enhancements](#future-enhancements) - -## Overview - -MotoVaultPro is a self-hosted, open-source vehicle maintenance and fuel mileage tracking application built with ASP.NET Core 8.0. The application follows a traditional Model-View-Controller (MVC) architecture with modern patterns including dependency injection, repository pattern, and dual-database support. - -### Key Features -- Vehicle maintenance tracking and reminders -- Fuel efficiency monitoring and analysis -- Multi-user support with role-based access control -- Dual database support (LiteDB and PostgreSQL) -- RESTful API endpoints -- Progressive Web App (PWA) capabilities -- Internationalization support -- OpenID Connect integration -- File attachment and document management - -## Technology Stack - -### Core Framework -- **ASP.NET Core 8.0** - Web application framework -- **C# 12** - Programming language -- **Razor Pages** - Server-side rendering engine - -### Database Support -- **LiteDB 5.0.17** - Embedded NoSQL database (default) -- **PostgreSQL** - External relational database (via Npgsql 9.0.3) - -### Frontend Technologies -- **Bootstrap 5** - UI framework and responsive design -- **jQuery** - DOM manipulation and AJAX -- **Chart.js** - Data visualization -- **SweetAlert2** - Modal dialogs and notifications - -### Additional Libraries -- **MailKit 4.11.0** - Email functionality -- **CsvHelper 33.0.1** - CSV import/export -- **System.IdentityModel.Tokens.Jwt 7.3.1** - JWT authentication -- **Bootstrap Extensions** - Date pickers, tag inputs, etc. - -## Architecture Patterns - -### 1. Model-View-Controller (MVC) -Clean separation of concerns with: -- **Models**: Data representation and business logic -- **Views**: User interface and presentation -- **Controllers**: Request handling and application flow - -### 2. Repository Pattern -Data access abstraction through interfaces: -- Interface contracts in `External/Interfaces/` -- Dual implementations for LiteDB and PostgreSQL -- Dependency injection for provider selection - -### 3. Dependency Injection -Comprehensive DI container usage: -- Service registration in `Program.cs` -- Constructor injection throughout application -- Singleton lifetime for most services - -### 4. Generic Record Pattern -Base class inheritance for consistency: -- `GenericRecord` base class with common properties -- Specialized record types for different data types -- Promotes code reuse and maintainability - -### 5. Helper/Service Layer Pattern -Business logic separation: -- Helper classes for specific functionality -- Service classes for complex operations -- Clear separation from controllers - -## Project Structure - -``` -MotoVaultPro/ -├── Controllers/ # MVC Controllers -│ ├── Vehicle/ # Vehicle-specific controllers -│ ├── HomeController.cs -│ ├── LoginController.cs -│ └── AdminController.cs -├── Models/ # Data models and ViewModels -│ ├── API/ # API-specific models -│ ├── Shared/ # Common models -│ └── Vehicle/ # Vehicle-related models -├── Views/ # Razor views -│ ├── Shared/ # Common layouts and partials -│ └── Vehicle/ # Vehicle-specific views -├── External/ # Data access layer -│ ├── Interfaces/ # Repository interfaces -│ └── Implementations/ # Database implementations -├── Logic/ # Business logic classes -├── Helper/ # Utility classes -├── Middleware/ # Custom middleware -├── wwwroot/ # Static web assets -└── docs/ # Documentation -``` - -## Data Layer - -### Database Architecture -The application supports dual database backends through a unified repository pattern: - -#### LiteDB Implementation -- **File-based storage**: Single database file (`data/cartracker.db`) -- **NoSQL document storage**: Entities stored as JSON documents -- **Embedded database**: No external dependencies -- **Performance**: Excellent for single-user scenarios - -#### PostgreSQL Implementation -- **Relational database**: External PostgreSQL server -- **Hybrid schema**: Combines relational structure with JSONB flexibility -- **Scalability**: Supports multi-user scenarios with better performance -- **Advanced features**: Full-text search, complex queries, transactions - -### Entity Model -``` -Vehicle (Root Entity) -├── ServiceRecord (maintenance and repairs) -├── GasRecord (fuel tracking) -├── CollisionRecord (accident records) -├── UpgradeRecord (modifications) -├── TaxRecord (registration and taxes) -├── SupplyRecord (parts and supplies) -├── PlanRecord (maintenance planning) -├── OdometerRecord (mileage tracking) -├── ReminderRecord (maintenance reminders) -└── Note (general notes) -``` - -### Data Access Interfaces -Each entity type has a dedicated interface: -- `IVehicleDataAccess` - Vehicle management -- `IServiceRecordDataAccess` - Service records -- `IGasRecordDataAccess` - Fuel tracking -- `IUserRecordDataAccess` - User management -- And more... - -## Business Logic Layer - -### Logic Classes -- **VehicleLogic**: Core vehicle operations and aggregations -- **UserLogic**: User management and access control -- **LoginLogic**: Authentication and user registration -- **OdometerLogic**: Mileage tracking and validation - -### Helper Classes -- **ReminderHelper**: Reminder calculations and urgency -- **ReportHelper**: Data aggregation and reporting -- **StaticHelper**: Common utilities and constants -- **TranslationHelper**: Internationalization support -- **ConfigHelper**: Configuration management - -### Business Rules -- **Mileage Progression**: Automatic odometer record management -- **Reminder Urgency**: Complex urgency calculations based on date/mileage -- **Access Control**: Vehicle-level permissions with collaborator system -- **Data Validation**: Input validation and business rule enforcement - -## Controller Layer - -### Main Controllers -- **HomeController**: Dashboard, settings, and kiosk mode -- **VehicleController**: Vehicle management hub (partial class) -- **LoginController**: Authentication and user management -- **AdminController**: Administrative functions -- **APIController**: RESTful API endpoints - -### Vehicle Controller Architecture -The `VehicleController` uses a partial class pattern with specialized controllers: -- **GasController**: Fuel record management -- **ServiceController**: Service record operations -- **ReminderController**: Reminder management -- **ImportController**: CSV import/export -- And more... - -### Security and Authorization -- **Authentication Middleware**: Custom authentication handler -- **Role-based Authorization**: Admin, root user, and regular user roles -- **Vehicle-level Permissions**: CollaboratorFilter for fine-grained access -- **API Authentication**: Supports both cookie and Basic Auth - -## Frontend Architecture - -### View Organization -- **Razor Views**: Server-side rendered with strong typing -- **Partial Views**: Modular components for reusability -- **Layout System**: Single layout with sections for customization -- **Modal-heavy UI**: Extensive use of Bootstrap modals - -### JavaScript Architecture -- **jQuery-based**: DOM manipulation and AJAX requests -- **Feature-specific files**: Organized by functionality -- **Global functions**: Accessible throughout the application -- **Event-driven**: Extensive event handling for user interactions - -### Styling Approach -- **Bootstrap 5**: Primary CSS framework -- **Custom CSS**: Application-specific styling in `site.css` -- **Responsive Design**: Mobile-first approach with media queries -- **Dark Mode Support**: CSS variables for theme switching - -### Progressive Web App (PWA) -- **Web App Manifest**: Proper configuration for app installation -- **Service Worker Ready**: Architecture supports offline functionality -- **Multiple Icons**: Various sizes for different devices -- **Responsive Design**: Optimized for mobile and desktop - -## Authentication & Security - -### Authentication Modes -- **Cookie-based**: Encrypted session cookies for web interface -- **Basic Authentication**: HTTP Basic Auth for API access -- **OpenID Connect**: External provider integration -- **Token-based**: Registration and password reset tokens - -### Authorization Levels -- **Root User**: Full system access (user ID -1) -- **Admin User**: Administrative privileges -- **Regular User**: Standard application access -- **Vehicle Collaborators**: Vehicle-specific permissions - -### Security Features -- **Password Hashing**: SHA256 with UTF-8 encoding -- **Data Protection**: ASP.NET Core encryption for cookies -- **PKCE Support**: Enhanced OIDC security -- **File Security**: Authenticated file access and uploads -- **SQL Injection Protection**: Parameterized queries throughout - -## Deployment & Configuration - -### Database Selection -Configuration-driven database selection: -```csharp -if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])) -{ - // Use PostgreSQL -} -else -{ - // Use LiteDB (default) -} -``` - -### Configuration Management -- **Environment Variables**: Database connections and external services -- **JSON Configuration**: Application settings and user preferences -- **Feature Flags**: Enable/disable authentication and features -- **Multi-environment**: Development, staging, production configurations - -### Docker Support -- **Multi-stage build**: Optimized Docker image creation -- **ASP.NET Core 8.0 runtime**: Lightweight production image -- **Port 8080**: Standard web application port -- **Volume mounting**: Persistent data storage - -## API Design - -### RESTful Endpoints -- **Conventional routing**: `/api/{controller}/{action}` -- **JSON responses**: Consistent response format -- **HTTP status codes**: Proper error handling -- **Authentication required**: All endpoints require authentication - -### API Features -- **Vehicle data**: CRUD operations for all record types -- **User management**: Authentication and authorization -- **File operations**: Upload and download capabilities -- **Reporting**: Data aggregation and analysis endpoints - -## Performance Considerations - -### Database Performance -- **Connection pooling**: Efficient connection management (PostgreSQL) -- **Lazy loading**: On-demand data retrieval -- **Caching**: Configuration and translation caching -- **Batch operations**: Efficient bulk data operations - -### Frontend Performance -- **Partial views**: Reduced page load times -- **AJAX updates**: Dynamic content without full page reloads -- **Responsive images**: Optimized for different screen sizes -- **Minification ready**: Architecture supports asset bundling - -## Development Guidelines - -### Code Organization -- **Separation of concerns**: Clear layer boundaries -- **Consistent naming**: Descriptive class and method names -- **Interface-based design**: Abstraction for testability -- **Dependency injection**: Loose coupling between components - -### Best Practices -- **Error handling**: Comprehensive exception management -- **Logging**: Structured logging throughout application -- **Validation**: Input validation at multiple layers -- **Security**: Security-first approach to all features - -### Testing Strategy -- **Unit testing**: Business logic and data access layers -- **Integration testing**: API endpoints and workflows -- **UI testing**: Frontend functionality and user flows -- **Security testing**: Authentication and authorization - -## Future Enhancements - -### Scalability Improvements -- **Microservices**: Potential service decomposition -- **Caching layer**: Redis or in-memory caching -- **Load balancing**: Multi-instance deployment support -- **Database sharding**: Data partitioning for scale - -### Technology Upgrades -- **Modern JavaScript**: ES6+ modules and TypeScript -- **Frontend framework**: React, Vue, or Angular integration -- **Real-time features**: SignalR for live updates -- **API versioning**: Structured API evolution - -### Feature Enhancements -- **Mobile applications**: Native iOS and Android apps -- **Advanced reporting**: Business intelligence features -- **Integration APIs**: Third-party service connections -- **Automated backups**: Scheduled data protection - -### Security Enhancements -- **Multi-factor authentication**: Enhanced security options -- **OAuth 2.0**: Extended authentication provider support -- **API rate limiting**: Protection against abuse -- **Audit logging**: Comprehensive security tracking - ---- - -**Last Updated**: January 2025 -**Version**: 1.0 -**Maintainer**: MotoVaultPro Development Team \ No newline at end of file diff --git a/docs/data-model-prompt.md b/docs/data-model-prompt.md deleted file mode 100644 index 4923b8d..0000000 --- a/docs/data-model-prompt.md +++ /dev/null @@ -1 +0,0 @@ -Analyze and document the data model of this application. It is a vehicle fleet management application. I don't believe the data model was structured properly for our desired outcome. The desired data records will be a high level "Maintenance" "Fuel" "Documents". Under Maintenance there will be different catagories for Service, Repairs and Upgrades. Fuel will have different types of Gasoline, Diesel and Electric. Documents will have Insurance, Registration, Notes. \ No newline at end of file diff --git a/docs/generic-record.md b/docs/generic-record.md deleted file mode 100644 index 83d9a2f..0000000 --- a/docs/generic-record.md +++ /dev/null @@ -1,122 +0,0 @@ -# GenericRecord Data Model Analysis - -## Overview - -**GenericRecord** is a **C# class** defined in `Models/Shared/GenericRecord.cs` that serves as a **base class** in MotoVaultPro's architecture, implementing the Generic Record Pattern for shared vehicle maintenance record properties. - -## Properties - -The GenericRecord class has **10 properties**: - -### Core Properties -- `int Id` - Primary key identifier -- `int VehicleId` - Foreign key linking to vehicle -- `DateTime Date` - When the record occurred -- `int Mileage` - Vehicle odometer reading -- `string Description` - Human-readable description -- `decimal Cost` - Monetary cost associated with record -- `string Notes` - Additional notes/comments - -### Collection Properties -- `List Files` - File attachments (Name, Location, IsPending) -- `List Tags` - Categorization tags -- `List ExtraFields` - Custom fields (Name, Value, IsRequired, FieldType) -- `List RequisitionHistory` - Supply usage tracking - -## Architecture Role - -GenericRecord implements the **Generic Record Pattern** mentioned in the architecture documentation. It serves as a **base class** that provides common properties shared across different record types, promoting code reuse and maintainability. - -## Inheritance Pattern - -### Classes that inherit from GenericRecord -- `ServiceRecord` - Maintenance and repair records -- `UpgradeRecord` - Vehicle modification records - -### Classes with similar structure (not inheriting) -- `GasRecord` - Fuel tracking with additional properties (Gallons, IsFillToFull, MissedFuelUp) -- `TaxRecord` - Registration and tax records with recurring reminder fields -- `SupplyRecord` - Parts and supplies with specific properties (PartNumber, PartSupplier, Quantity) -- `PlanRecord` - Maintenance planning with priority/progress tracking -- `OdometerRecord` - Simplified for mileage tracking only -- `ReminderRecord` - Focused on reminder scheduling and intervals - -## Supporting Classes - -### UploadedFiles -Located in `Models/Shared/UploadedFiles.cs`: -- `string Name` - File name -- `string Location` - File storage path -- `bool IsPending` - Upload status flag - -### ExtraField -Located in `Models/Shared/ExtraField.cs`: -- `string Name` - Field name -- `string Value` - Field value -- `bool IsRequired` - Required field flag -- `ExtraFieldType FieldType` - Field data type - -### SupplyUsageHistory -Located in `Models/Supply/SupplyUsageHistory.cs`: -- `int Id` - Record identifier -- `DateTime Date` - Usage date -- `string PartNumber` - Part identifier -- `string Description` - Part description -- `decimal Quantity` - Amount used -- `decimal Cost` - Cost of usage - -### ExtraFieldType Enum -Located in `Enum/ExtraFieldType.cs`: -- `Text` (0) - Text input -- `Number` (1) - Integer input -- `Decimal` (2) - Decimal input -- `Date` (3) - Date picker -- `Time` (4) - Time picker -- `Location` (5) - Location input - -## Usage Throughout Application - -GenericRecord is utilized extensively across MotoVaultPro: - -### API Layer -- **GenericRecordExportModel** - JSON serialization for import/export operations -- **API endpoints** - Service and upgrade record CRUD operations -- **Webhook notifications** - WebHookPayload.FromGenericRecord for external integrations - -### UI Layer -- **GenericRecordEditModel** - Bulk editing operations across multiple records -- **_GenericRecordModal** - Modal interface for multi-record editing -- **Sticker generation** - Vehicle maintenance stickers with record data - -### Data Access Layer -- **Repository pattern** - Common interface for record operations -- **Database abstraction** - Unified storage across LiteDB and PostgreSQL -- **Data transformation** - Conversion between record types - -### Business Logic -- **Record management** - Create, update, delete operations -- **Cost calculations** - Financial reporting and analysis -- **Mileage tracking** - Odometer progression validation - -## Design Benefits - -1. **Code Reuse** - Common properties defined once and inherited -2. **Maintainability** - Centralized structure for vehicle records -3. **Consistency** - Uniform data model across record types -4. **Extensibility** - Easy addition of new record types -5. **Bulk Operations** - Unified interface for multi-record editing -6. **API Consistency** - Standardized data exchange format - -## Implementation Notes - -- GenericRecord serves as both a base class and a standalone model -- Not all record types inherit from GenericRecord due to specialized requirements -- The pattern balances common functionality with type-specific needs -- File attachments, tags, and extra fields provide extensibility -- Supply usage tracking enables parts management integration - ---- - -**Document Version**: 1.0 -**Last Updated**: August 2025 -**Related Documentation**: [Architecture Documentation](architecture.md), [Record Architecture](record-architecture.md) \ No newline at end of file diff --git a/docs/record-architecture.md b/docs/record-architecture.md deleted file mode 100644 index 0b61403..0000000 --- a/docs/record-architecture.md +++ /dev/null @@ -1,268 +0,0 @@ -This UX Design Summary serves as a comprehensive reference for future development efforts, providing specific recommendations and code examples for continued enhancement of the MotoVaultPro - -# Record Entity Removal Guide -## Complete Entity Removal Process -This section documents the comprehensive process used to remove the Collision/Repair record entities from MotoVaultPro, serving as a reference for future AI-assisted entity removals. - -### Pre-Removal Analysis -Before removing any record entity, perform a thorough analysis: - -1. **Identify all entity references** across the entire codebase -2. **Map data flow** from database → data access → business logic → controllers → views → frontend -3. **Document dependencies** including translation keys, navigation elements, and configuration - -### Systematic Removal Checklist - -#### 1. Data Models & Core Entities ✅ **COMPLETED** -**Files Removed:** -- `/Models/CollisionRecord/` - Entire model directory ✅ -- `CollisionRecord.cs`, `CollisionRecordInput.cs`, `CollisionRecordViewModel.cs` ✅ - -**Files Updated:** -- `/Models/Shared/VehicleRecords.cs` - Removed CollisionRecords property ✅ -- `/Enum/ImportMode.cs` - Removed RepairRecord enum entry ✅ -- `/Models/UserConfig.cs` - Removed RepairRecord from VisibleTabs list ✅ -- Report models in `/Models/Report/` - Removed CollisionRecordSum properties ✅ - -#### 2. Data Access Layer ✅ **COMPLETED** -**Files Removed:** -- `/External/Interfaces/ICollisionRecordDataAccess.cs` ✅ -- `/External/Implementations/Litedb/CollisionRecordDataAccess.cs` ✅ -- `/External/Implementations/Postgres/CollisionRecordDataAccess.cs` ✅ - -**Files Updated:** -- `/Program.cs` - Removed dependency injection registrations for both LiteDB and PostgreSQL ✅ -- All controller constructors - Removed ICollisionRecordDataAccess parameters ✅ - -#### 3. Controllers & Business Logic ✅ **COMPLETED** -**Files Removed:** -- `/Controllers/Vehicle/CollisionController.cs` ✅ - -**Files Updated:** -- `/Controllers/VehicleController.cs` - Removed all RepairRecord case statements and _collisionRecordDataAccess usage ✅ -- `/Controllers/APIController.cs` - Removed RepairRecord API endpoints and collision record references ✅ -- `/Logic/VehicleLogic.cs` - Removed CollisionRecords properties and GetOwnershipDays parameter ✅ -- `/Helper/ReportHelper.cs` - Removed GetRepairRecordSum methods ✅ -- `/Helper/StaticHelper.cs` - Removed RepairRecord ImportMode case and GenericToRepairRecord method ✅ -- `/Controllers/Vehicle/ReportController.cs` - Removed all collision record cost calculations and references ✅ -- `/Controllers/Vehicle/ImportController.cs` - Removed RepairRecord import/export functionality ✅ -- `/Controllers/Vehicle/PlanController.cs` - Removed RepairRecord case from plan conversion ✅ -- `/Controllers/MigrationController.cs` - Removed collision record migration code ✅ - -#### 4. Views & Frontend Components ✅ -**Files to Remove:** -- `/Views/Vehicle/{EntityName}/` - Entire view directory -- `/wwwroot/js/{entityname}record.js` - Entity-specific JavaScript - -**Files to Update:** -- `/Views/Vehicle/Index.cshtml` - Remove script references, navigation tabs, and tab panes -- All view files with context menus - Remove "Move To" options -- All view files with modal dropdowns - Remove entity references -- Configuration views (`/Views/Home/_Settings.cshtml`) - Remove tab visibility options - -#### 5. JavaScript & Client-Side ✅ -**Files to Update:** -- `/wwwroot/js/shared.js` - Remove entity cases from move/duplicate/delete functions -- `/wwwroot/js/vehicle.js` - Remove entity tab loading logic and function calls -- Remove all `getVehicle{EntityName}Records` function calls - -#### 6. Translations & Localization ✅ -**Files to Update:** -- `/wwwroot/defaults/en_US.json` - Remove all entity-related translation keys -- Update context-sensitive translations (e.g., "Service/Repair/Upgrade" → "Service/Upgrade") - -#### 7. Configuration & Settings ✅ -**Files to Update:** -- User configuration systems that reference entity tabs -- Kiosk mode displays that show entity counts -- Dashboard metrics that include entity data -- Report configurations that aggregate entity costs - -### Critical C# Code Patterns to Find - -#### Constructor Parameter Removal -```csharp -// BEFORE -public VehicleController( - IServiceRecordDataAccess serviceRecordDataAccess, - ICollisionRecordDataAccess collisionRecordDataAccess, // ← REMOVE - IUpgradeRecordDataAccess upgradeRecordDataAccess) - -// AFTER -public VehicleController( - IServiceRecordDataAccess serviceRecordDataAccess, - IUpgradeRecordDataAccess upgradeRecordDataAccess) -``` - -#### Field Declaration Removal -```csharp -// REMOVE these patterns -private readonly ICollisionRecordDataAccess _collisionRecordDataAccess; -``` - -#### Constructor Assignment Removal -```csharp -// REMOVE these patterns -_collisionRecordDataAccess = collisionRecordDataAccess; -``` - -#### Method Parameter Cleanup -```csharp -// BEFORE -int GetOwnershipDays(string purchaseDate, string soldDate, int year, - List serviceRecords, - List repairRecords, // ← REMOVE - List upgradeRecords) - -// AFTER -int GetOwnershipDays(string purchaseDate, string soldDate, int year, - List serviceRecords, - List upgradeRecords) -``` - -#### Data Access Usage Removal -```csharp -// REMOVE these patterns -var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId); -numbersArray.AddRange(repairRecords.Select(x => x.Mileage)); -``` - -#### API Endpoint Removal -```csharp -// REMOVE entire #region blocks -#region RepairRecord -[HttpGet] -[Route("/api/vehicle/repairrecords")] -public IActionResult RepairRecords(int vehicleId) { ... } -#endregion -``` - -#### Model Property Removal -```csharp -// REMOVE properties from report models -public decimal CollisionRecordSum { get; set; } -public decimal CollisionRecordPerMile { get { return ... CollisionRecordSum ... } } -``` - -### JavaScript Patterns to Remove - -#### Tab Loading Logic -```javascript -// REMOVE these cases -case "accident-tab": - getVehicleCollisionRecords(vehicleId); - break; -``` - -#### Function Definitions -```javascript -// REMOVE entire functions -function getVehicleCollisionRecords(vehicleId) { ... } -``` - -#### Switch Statement Cases -```javascript -// REMOVE these cases from move/duplicate/delete operations -case "RepairRecord": - friendlySource = "Repairs"; - refreshDataCallBack = getVehicleCollisionRecords; - break; -``` - -### View/HTML Patterns to Remove - -#### Navigation Tabs -```html - - -``` - -#### Tab Panes -```html - -
-``` - -#### Context Menu Options -```html - -
  • -
    - Repairs -
    -
  • -``` - -### Translation Cleanup - -#### Systematic Key Removal -```bash -# Use sed to remove translation keys -sed -i 's/,"Repairs":"Repairs"//g; s/,"Add_Repair_Record":"Add Repair Record"//g' en_US.json -``` - -### Post-Removal Verification ✅ **COMPLETED** - -#### Build Verification ✅ -1. **Compilation Check**: ✅ Build completed successfully with no compilation errors -2. **Missing Reference Scan**: ✅ No remaining CollisionRecord/RepairRecord references found -3. **Database Schema**: ✅ All collision record table creation and migration code removed -4. **UI Testing**: ✅ Application functionality confirmed to work correctly - -#### Search Patterns for Verification -```bash -# Search for any remaining references -grep -r "CollisionRecord\|RepairRecord\|collision.*record\|repair.*record" --include="*.cs" --include="*.js" --include="*.cshtml" . -``` - -### Common Pitfalls to Avoid - -1. **Incomplete Constructor Cleanup**: Missing parameter removal and assignment cleanup -2. **Orphaned JavaScript Functions**: Function definitions that are no longer called -3. **Translation Context**: Missing context updates (e.g., "Service/Repair/Upgrade" references) -4. **Report Model Dependencies**: Cost calculations that reference removed entities -5. **Configuration Arrays**: Missing removal from user configuration lists -6. **API Documentation**: Outdated API endpoint references in documentation - -### Recovery Strategy - -If issues arise during removal: - -1. **Incremental Approach**: Remove one layer at a time (models → data access → controllers → views) -2. **Compilation Gates**: Build after each major layer removal -3. **Reference Tracking**: Maintain a list of files modified for potential rollback -4. **Dependency Mapping**: Use IDE "Find Usages" to ensure complete removal - ---- - -**Document Version**: 1.2 -**Last Updated**: August 2025 -**Analysis Coverage**: Complete frontend and user interaction layers + Entity Removal Guide + **Collision/Repair Record Removal COMPLETED** -**Completion Status**: ✅ All collision/repair record entities successfully removed from MotoVaultPro -**Build Status**: ✅ Application builds and runs successfully -**Complementary Documentation**: [MotoVaultPro Architecture Documentation](architecture.md) - ---- - -## Collision/Repair Record Removal - COMPLETED ✅ - -**Completion Date**: August 5, 2025 -**Total Files Modified**: 15+ files across models, controllers, logic, and data access layers -**Build Status**: ✅ SUCCESS - No compilation errors -**Functionality Status**: ✅ VERIFIED - Application runs correctly without collision record functionality - -### Summary of Changes Made: -1. ✅ Removed all collision record model files and directories -2. ✅ Removed data access interfaces and implementations (LiteDB + PostgreSQL) -3. ✅ Cleaned up all controller logic and case statements -4. ✅ Updated business logic to remove collision record dependencies -5. ✅ Removed collision record properties from report models -6. ✅ Updated user configuration to remove RepairRecord references -7. ✅ Cleaned up migration code and database schema references -8. ✅ Verified successful compilation and application functionality - -**This removal process can serve as a template for future entity removals in MotoVaultPro.** \ No newline at end of file diff --git a/docs/ux-architecture.md b/docs/ux-architecture.md deleted file mode 100644 index d80a7d9..0000000 --- a/docs/ux-architecture.md +++ /dev/null @@ -1,434 +0,0 @@ -# MotoVaultPro UX Design Summary - -## Executive Summary - -MotoVaultPro demonstrates a sophisticated user experience design that successfully balances feature richness with usability through intelligent workflow automation, comprehensive mobile optimization, and consistent interaction patterns. The application employs a modal-centric, tab-based architecture with extensive responsive design considerations and a well-implemented dark mode system. - -**Key UX Strengths:** -- Modal-heavy interface with consistent interaction patterns across all features -- Comprehensive mobile-first responsive design with touch-optimized interfaces -- Sophisticated reminder system with multi-dimensional urgency calculations -- Robust feedback mechanisms using SweetAlert2 for notifications and confirmations -- Intelligent automation features that reduce user cognitive load - -**Critical Areas for Improvement:** -- Accessibility compliance gaps requiring immediate attention -- Complex JavaScript architecture creating potential performance bottlenecks -- Modal state management limitations leading to potential data loss - -## Navigation & Layout - -### Architecture Overview -MotoVaultPro uses a **tabbed single-page application (SPA) architecture** with two primary contexts: - -1. **Home Dashboard Layout** (`/Views/Home/Index.cshtml`) - Vehicle overview and global settings -2. **Vehicle Management Layout** (`/Views/Vehicle/Index.cshtml`) - Detailed vehicle record management - -### Navigation Patterns - -#### Desktop Navigation -- **Horizontal tab-based navigation** with intelligent overflow management -- **User-configurable tab ordering** using CSS `order` properties -- **Dynamic content loading** via AJAX partial views for performance optimization -- **Persistent navigation state** with session storage - -#### Mobile Navigation -- **Full-screen overlay menu** replicating desktop functionality -- **Touch-optimized interface elements** with adequate target sizes (44px minimum) -- **Swipe-to-dismiss modals** with native mobile animations -- **Progressive enhancement** based on device detection - -### Layout Consistency -```html - -
    -
    -
    - -
    -
    -
    -``` - -**File References:** -- Base layout: `/Views/Shared/_Layout.cshtml` -- Navigation logic: `/wwwroot/js/shared.js` (lines 1810-1848) -- Overflow management: `checkNavBarOverflow()` function - -## User Interaction & Behavior - -### jQuery-Based Interaction Framework -The application implements a comprehensive jQuery-based system with approximately 8,000+ lines of JavaScript across 18 files, emphasizing: - -- **AJAX-driven content loading** with intelligent caching -- **Modal management system** with mobile-specific optimizations -- **Real-time form validation** with immediate visual feedback -- **Touch gesture support** including swipe-to-dismiss functionality - -### Key Interactive Components - -#### Modal Management (`/wwwroot/js/shared.js`) -```javascript -function initMobileModal(config) { - if (!isMobileDevice()) return; - - // Convert to native HTML5 inputs on mobile - if (dateInputId) { - $(dateInputId).attr('type', 'date').removeClass('datepicker'); - } - - // Initialize swipe to dismiss - initSwipeToDismiss(modalId); -} -``` - -#### Form Validation Patterns -- **Bootstrap integration** using `is-invalid` class system -- **Multi-field validation** with contextual error aggregation -- **Progressive validation** from basic to advanced field sets -- **Real-time financial input validation** supporting multiple currency formats - -#### Performance Optimizations -- **Memory management** through DOM cleanup when switching tabs -- **Event debouncing** (1-second threshold) for rapid operations -- **Lazy loading** of tab content on demand -- **Session storage** for user preferences and state preservation - -**File References:** -- Core interactions: `/wwwroot/js/shared.js` -- Vehicle management: `/wwwroot/js/vehicle.js` -- Fuel tracking: `/wwwroot/js/gasrecord.js` - -## Visual & Responsive Design - -### Design System Foundation -Built on **Bootstrap 5** with extensive customizations for mobile-first responsive design and comprehensive dark mode support. - -### Mobile-First Architecture -```css -html { - font-size: 14px; /* Base mobile size */ -} - -@media (min-width: 768px) { - html { - font-size: 16px; /* Desktop scaling */ - } -} -``` - -### Dark Mode Implementation -Sophisticated theme system using Bootstrap 5's `data-bs-theme` attribute: - -```html - -``` - -**Theme-aware components** with backdrop blur effects: -```css -html[data-bs-theme="dark"] .table-context-menu { - background-color: rgba(33, 37, 41, 0.7); - backdrop-filter: blur(10px); -} -``` - -### Mobile Modal Excellence -Full-screen mobile modals with hardware-accelerated animations: -```css -@media (max-width: 768px) { - .modal-dialog { - margin: 0; - width: 100vw; - height: 100vh; - max-width: none; - max-height: none; - } - - .modal.fade .modal-dialog { - transform: translateY(100%); - transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); - } -} -``` - -### Progressive Web App (PWA) Features -- **Complete PWA manifest** configuration -- **Theme-aware meta tags** for light/dark system preferences -- **Multiple icon sizes** for various device types -- **Standalone display mode** support - -**File References:** -- Main stylesheet: `/wwwroot/css/site.css` (1,089 lines) -- Loading animations: `/wwwroot/css/loader.css` -- PWA manifest: `/wwwroot/manifest.json` - -## Accessibility & Internationalization - -### Accessibility Assessment -**Current Score: 4/10** - Basic HTML semantics present but lacks essential accessibility features - -#### Strengths -- **Semantic HTML** with proper `
    ` usage -- **Form labels** properly associated with inputs -- **Bootstrap tab components** with basic ARIA support -- **Focus management** with custom focus indicators - -#### Critical Gaps -- **Missing skip navigation links** for keyboard users -- **Limited ARIA implementation** beyond basic tab functionality -- **No focus trapping** in modals -- **Missing alt text** for images throughout the application -- **No ARIA live regions** for dynamic content announcements - -### Internationalization Excellence -**Current Score: 7/10** - Strong server-side translation system with room for UX improvements - -#### Translation System (`/Helper/TranslationHelper.cs`) -```csharp -public string Translate(string userLanguage, string text) -{ - string translationKey = text.Replace(" ", "_"); - var translationFilePath = userLanguage == "en_US" ? - _fileHelper.GetFullFilePath($"/defaults/en_US.json") : - _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false); - // Cached translation lookup with fallback -} -``` - -#### Features -- **500+ translation terms** with comprehensive coverage -- **Memory caching** with sliding expiration (1 hour) -- **Fallback strategy** to English when translations missing -- **Cultural formatting** for dates, numbers, and currency -- **Administrative translation management** interface - -#### Limitations -- **No client-side language switching** - requires server round-trip -- **No RTL (right-to-left) language support** -- **Missing pluralization rules** for number-dependent translations -- **No automatic browser language detection** - -**File References:** -- Translation system: `/Helper/TranslationHelper.cs` -- Language configuration: Culture-aware date/number formatting throughout controllers - -## Core User Flows & Feedback Mechanisms - -### Critical Workflow Analysis - -#### 1. Authentication Flow (`/Controllers/LoginController.cs`) -- **Multi-provider support**: Standard auth, OpenID Connect with PKCE -- **Security features**: Encrypted cookies, state validation, comprehensive logging -- **Mobile optimization**: Touch-friendly login interface with proper input types - -#### 2. Vehicle Management (`/Controllers/VehicleController.cs`) -- **Modal-based CRUD operations** with real-time validation -- **File upload handling** with temporary storage management -- **Collaborative access control** with automatic permission assignment -- **Dashboard metrics configuration** with real-time preview - -#### 3. Fuel Tracking (`/Controllers/Vehicle/GasController.cs`) -**Dual-Mode Interface:** -- **Simple Mode**: Auto-calculated costs, touch-optimized for mobile -- **Advanced Mode**: Comprehensive tracking including electric vehicle support - -```javascript -// Mobile-responsive fuel entry initialization -function initializeGasRecordMobile() { - if (typeof initMobileModal === 'function') { - initMobileModal({ - modalId: '#gasRecordModal', - dateInputId: '#gasRecordDate', - simpleModeDefault: true - }); - } -} -``` - -### Reminder System Excellence (`/Helper/ReminderHelper.cs`) - -#### Sophisticated Urgency Calculation -```csharp -public enum ReminderUrgency -{ - NotUrgent = 0, - Urgent = 1, - VeryUrgent = 2, - PastDue = 3 -} -``` - -**Multi-dimensional evaluation:** -- **Date-based urgency** with configurable thresholds -- **Mileage-based calculations** for distance-dependent maintenance -- **Combined metrics** with intelligent prioritization -- **Automatic refresh system** for recurring reminders - -### Feedback System Architecture - -#### SweetAlert2 Integration (`/wwwroot/js/shared.js`) -```javascript -function successToast(message) { - Swal.fire({ - toast: true, - position: "top-end", - showConfirmButton: false, - timer: 3000, - title: message, - timerProgressBar: true, - icon: "success", - didOpen: (toast) => { - toast.onmouseenter = Swal.stopTimer; - toast.onmouseleave = Swal.resumeTimer; - } - }); -} -``` - -#### Validation Framework -- **Client-side validation** with immediate visual feedback via Bootstrap classes -- **Server-side security** with permission checking and parameterized queries -- **Progressive enhancement** for mobile devices -- **Error categorization** with specific handling patterns - -#### Operation Response Pattern (`/Models/Shared/OperationResponse.cs`) -```csharp -public class OperationResponse -{ - public static OperationResponse Succeed(string message = "") { ... } - public static OperationResponse Failed(string message = "") { ... } - public static OperationResponse Conditional(bool result, string successMessage = "", string errorMessage = "") { ... } -} -``` - -### Workflow Strengths -1. **Intelligent automation**: Auto-odometer insertion, reminder pushback, supply requisitioning -2. **Bulk operations**: Multi-record selection with batch processing capabilities -3. **Global search**: Cross-record-type search with result highlighting -4. **Collaborative features**: Vehicle sharing with granular permission management -5. **Data integrity**: Automatic validation rules and consistency checks - -### Identified Friction Points -1. **Modal state management**: Limited caching can lead to data loss on accidental closure -2. **Mobile input challenges**: Date pickers and complex forms on smaller screens -3. **Progressive loading**: Large datasets may impact performance -4. **Error recovery**: Limited undo functionality for destructive operations - -## Opportunities for UX Optimization - -### Short-Term Improvements (High Impact, Low Effort) - -#### 1. Accessibility Quick Wins -```html - -Skip to main content - - - - - -
    -``` - -#### 2. Modal State Management -```javascript -// Implement auto-save for form data -function enableModalAutoSave(modalId, formId) { - setInterval(() => { - const formData = $(formId).serialize(); - sessionStorage.setItem(`${modalId}_autosave`, formData); - }, 5000); -} -``` - -#### 3. Real-Time Validation Enhancement -```javascript -// Move validation to input events -$('input').on('input blur', function() { - validateField(this); -}); -``` - -### Medium-Term Enhancements (Moderate Impact, Moderate Effort) - -#### 1. Client-Side Language Switching -```javascript -function switchLanguage(lang) { - document.cookie = `language=${lang}; path=/`; - // Implement partial page updates instead of full reload - updateTranslatedElements(lang); -} -``` - -#### 2. Enhanced Mobile Experience -- **Implement Progressive Web App offline capabilities** -- **Add gesture-based navigation** for mobile users -- **Optimize touch interactions** with haptic feedback where available - -#### 3. Performance Optimizations -- **Code splitting** for JavaScript modules -- **Implement virtual scrolling** for large data sets -- **Add skeleton screens** for loading states - -### Long-Term Strategic Improvements (High Impact, High Effort) - -#### 1. Modern JavaScript Architecture -```javascript -// Migrate to ES6+ modules -import { VehicleManager } from './modules/VehicleManager.js'; -import { FuelTracker } from './modules/FuelTracker.js'; - -// Implement state management -const appState = new Proxy({}, { - set(target, property, value) { - target[property] = value; - notifyObservers(property, value); - return true; - } -}); -``` - -#### 2. Advanced Analytics Integration -- **Machine learning** for maintenance predictions -- **Predictive analytics** for fuel efficiency optimization -- **Cost forecasting** based on historical data patterns - -#### 3. Enhanced Accessibility Compliance -- **Full WCAG 2.1 AA compliance** implementation -- **Screen reader optimization** with comprehensive ARIA usage -- **Keyboard navigation shortcuts** for power users -- **Voice input support** for hands-free data entry - -#### 4. Internationalization Expansion -- **RTL language support** with proper CSS and layout handling -- **Advanced pluralization** rules for complex language requirements -- **Cultural customization** beyond date/number formatting -- **Dynamic font loading** for international character sets - -## Conclusion - -MotoVaultPro represents a mature and thoughtfully designed user experience that successfully addresses the complex requirements of vehicle maintenance tracking while maintaining usability across diverse user contexts. The application's strength lies in its consistent interaction patterns, comprehensive mobile optimization, and intelligent workflow automation. - -The modal-centric architecture, while occasionally creating navigation complexity, provides a unified interaction model that users can quickly learn and apply across all application features. The sophisticated reminder system and validation frameworks demonstrate deep understanding of user needs and workflow optimization. - -However, the application would significantly benefit from addressing accessibility gaps and modernizing its JavaScript architecture. The translation system provides an excellent foundation for international expansion, though enhanced client-side capabilities would improve the user experience. - -**Overall UX Assessment: 8/10** -- **Navigation & Layout**: 8/10 - Sophisticated but occasionally complex -- **Interaction & Behavior**: 8/10 - Comprehensive but performance-sensitive -- **Visual & Responsive Design**: 9/10 - Excellent mobile-first implementation -- **Accessibility**: 4/10 - Basic compliance with significant gaps -- **Internationalization**: 7/10 - Strong foundation with UX limitations -- **User Flows & Feedback**: 9/10 - Exceptional workflow design and automation - -This UX Design Summary serves as a comprehensive reference for future development efforts, providing specific recommendations and code examples for continued enhancement of the MotoVaultPro user experience. - ---- - -**Document Version**: 1.0 -**Last Updated**: July 2025 -**Analysis Coverage**: Complete frontend and user interaction layers -**Complementary Documentation**: [MotoVaultPro Architecture Documentation](architecture.md) \ No newline at end of file diff --git a/init.sql b/init.sql deleted file mode 100644 index dc88439..0000000 --- a/init.sql +++ /dev/null @@ -1,6 +0,0 @@ -DO $$ -BEGIN - IF NOT EXISTS (SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app') THEN - CREATE SCHEMA app; - END IF; -END $$; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 53893ae..0000000 --- a/package-lock.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "motovaultpro", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "@playwright/test": "^1.54.1" - } - }, - "node_modules/@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 8ec54d8..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "devDependencies": { - "@playwright/test": "^1.54.1" - } -} diff --git a/wwwroot/.DS_Store b/wwwroot/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/wwwroot/.DS_Store and /dev/null differ diff --git a/wwwroot/css/loader.css b/wwwroot/css/loader.css deleted file mode 100644 index f8eeaf4..0000000 --- a/wwwroot/css/loader.css +++ /dev/null @@ -1,55 +0,0 @@ -.sloader { - position: absolute; - top: 0px; - left: 0px; - width: 100%; - height: 100%; - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(255,255,255,0.5); -} - -.loader { - display: block; - width: 100px; - height: 100px; - border: 3px solid white; - border-radius: 50%; - animation: spin 7s ease-in-out; - animation-iteration-count: infinite; - transition-duration: 0.1s; -} - -.loader:hover { - scale: 0.95; - /*Loader on hover effect*/ -} - -.loader:active { - scale: 2.5; - /*Loader on click effect*/ -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - border-bottom: solid 3px transparent; - border-top: solid 3px transparent; - } - - 50% { - transform: rotate(1800deg); - border: 3px solid white; - border-left: solid 3px transparent; - border-right: solid 3px transparent; - } - - 100% { - /*Reversed spinning*/ - transform: rotate(0deg); - border-bottom: solid 3px transparent; - border-top: solid 3px transparent; - } -} diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css deleted file mode 100644 index af6744f..0000000 --- a/wwwroot/css/site.css +++ /dev/null @@ -1,1118 +0,0 @@ -html { - font-size: 14px; -} - -@media (min-width: 768px) { - html { - font-size: 16px; - } -} - -.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; -} - -html { - position: relative; - min-height: 100%; -} - -.card { - border-radius: 4px; - box-shadow: 0 6px 10px rgba(0,0,0,.08), 0 0 6px rgba(0,0,0,.05); - transition: .3s transform cubic-bezier(.155,1.105,.295,1.12),.3s box-shadow,.3s -webkit-transform cubic-bezier(.155,1.105,.295,1.12); - cursor: pointer; -} - - - .card:hover { - transform: scale(1.05); - box-shadow: 0 10px 20px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06); - } - -.vehicleDetailTabContainer { - max-height: 65vh; - overflow-y: auto; - overflow-x: auto; -} -.reportTabContainer { - overflow-y: auto; - overflow-x: auto; -} - -.vehicleDetailTabContainer.fixed { - height: 65vh; -} - -.swimlane{ - height:100%; -} - -.swimlane:not(:last-child) { - border-right-style: solid; -} - -.showOnPrint { - display: none; -} - -@media print { - .hideOnPrint { - display: none; - } - - .showOnPrint { - display: block !important; - } - - .vehicleDetailTabContainer { - background-color: #fff !important; - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - margin: 0; - font-size: 14px; - line-height: 18px; - color: #000 !important; - overflow: visible; - z-index: 1030; - } - .stickerPrintContainer { - background-color: #fff !important; - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - margin: 0; - font-size: 14px; - line-height: 18px; - color: #000 !important; - overflow: visible; - z-index: 1030; - } - .reminderSticker { - width: 98%; - aspect-ratio: 1/1; - border-style: dashed; - border-width: 2px; - } -} - -/* Mobile-First Modal Improvements */ -@media (max-width: 768px) { - /* Full-screen modal on mobile */ - .modal-dialog { - margin: 0; - width: 100vw; - height: 100vh; - max-width: none; - max-height: none; - } - - .modal-content { - height: 100vh; - border-radius: 0; - border: none; - display: flex; - flex-direction: column; - } - - /* Slide-up animation for mobile modals */ - .modal.fade .modal-dialog { - transform: translateY(100%); - transition: transform 0.3s ease-out; - } - - .modal.show .modal-dialog { - transform: translateY(0); - } - - .modal-body { - flex: 1; - overflow-y: auto; - padding: 1rem; - } - - .modal-header { - flex-shrink: 0; - padding: 1rem; - border-bottom: 1px solid #dee2e6; - } - - .modal-footer { - flex-shrink: 0; - padding: 1rem; - border-top: 1px solid #dee2e6; - } - - /* Touch-friendly targets - minimum 44px */ - .btn { - min-height: 44px; - min-width: 44px; - padding: 0.75rem 1rem; - } - - .btn-sm { - min-height: 44px; - min-width: 44px; - padding: 0.5rem 0.75rem; - } - - .btn-close { - width: 44px; - height: 44px; - padding: 0.75rem; - } - - .form-control { - min-height: 44px; - padding: 0.75rem; - font-size: 16px; /* Prevents zoom on iOS */ - } - - .form-select { - min-height: 44px; - padding: 0.75rem; - font-size: 16px; - } - - .form-check-input { - width: 1.5rem; - height: 1.5rem; - margin-top: 0.25rem; - } - - .form-check-label { - min-height: 44px; - display: flex; - align-items: center; - padding-left: 0.5rem; - } - - .input-group-text { - min-height: 44px; - display: flex; - align-items: center; - justify-content: center; - } - - /* Single column layout on mobile */ - .col-md-6 { - flex: 0 0 100% !important; - max-width: 100% !important; - } - - /* Mobile form spacing */ - .form-group { - margin-bottom: 1.5rem; - } - - .form-group label { - margin-bottom: 0.75rem; - font-weight: 500; - } - - .form-group input, - .form-group select, - .form-group textarea { - margin-bottom: 1rem; - } - - /* Simplified header layout on mobile */ - .modal-header .d-flex { - flex-direction: column; - align-items: stretch !important; - } - - .modal-header .modal-title { - margin-bottom: 1rem; - text-align: center; - } - - .modal-header .form-check { - justify-content: center; - margin-bottom: 0.5rem; - } - - .modal-header .btn-close { - position: absolute; - top: 1rem; - right: 1rem; - margin: 0; - } - - /* Mobile-friendly footer buttons */ - .modal-footer { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - .modal-footer .btn { - width: 100%; - margin: 0; - } - - .modal-footer .btn:first-child { - order: 2; /* Move delete button to bottom */ - } - - .modal-footer .btn:last-child { - order: 1; /* Move primary action to top */ - } - - .modal-footer .btn:nth-child(2) { - order: 3; /* Cancel button in middle */ - } - - /* Bottom sheet pattern for mobile */ - .modal-content { - position: relative; - } - - .modal-content::before { - content: ''; - position: absolute; - top: 8px; - left: 50%; - transform: translateX(-50%); - width: 40px; - height: 4px; - background-color: #dee2e6; - border-radius: 2px; - z-index: 1000; - cursor: grab; - } - - .modal-content::before:active { - cursor: grabbing; - } - - /* Touch feedback for interactive elements */ - .btn:active, - .form-control:active, - .form-check-input:active { - transform: scale(0.98); - transition: transform 0.1s ease; - } - - /* Improved focus states for mobile */ - .form-control:focus, - .form-select:focus { - border-color: #86b7fe; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); - outline: none; - } - - /* Mobile tag input styling */ - .mobile-tag-input .bootstrap-tagsinput { - border-radius: 6px; - border: 1px solid #ced4da; - } - - .mobile-tag-input .bootstrap-tagsinput:focus-within { - border-color: #86b7fe; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); - } - - .mobile-tag-input .tag { - background-color: #0d6efd; - color: white; - border-radius: 4px; - margin: 2px; - padding: 4px 8px; - font-size: 14px; - } - - /* Enhanced mobile breakpoints for better responsiveness */ - .mobile-single-column { - width: 100% !important; - max-width: 100% !important; - flex: 0 0 100% !important; - } - - /* Improved mobile modal animations */ - .modal.fade .modal-dialog { - transform: translateY(100%); - transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); - } - - .modal.show .modal-dialog { - transform: translateY(0); - } - - /* Mobile keyboard adjustments */ - .modal-body { - /* Prevent viewport scaling when keyboard appears */ - position: relative; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - } - - /* Accessibility improvements for mobile */ - .form-label { - font-weight: 600; - color: #212529; - margin-bottom: 0.5rem; - } - - /* Error state improvements for mobile */ - .is-invalid { - border-color: #dc3545 !important; - border-width: 2px !important; - } - - .is-invalid:focus { - box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25) !important; - } - - /* Success state for mobile */ - .is-valid { - border-color: #198754 !important; - border-width: 2px !important; - } - - .is-valid:focus { - box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25) !important; - } - - /* iPhone-specific optimizations for high DPI displays */ - .modal-header .modal-title { - font-size: 1.125rem; - line-height: 1.3; - word-break: break-word; - } - - /* Optimize for very narrow screens (iPhone portrait) */ - .modal-body .form-group { - margin-bottom: 1.25rem; - } - - .modal-body .form-group label { - font-size: 0.9rem; - margin-bottom: 0.5rem; - display: block; - font-weight: 600; - } - - /* Enhanced input styling for high DPI */ - .form-control, - .form-select { - border-width: 1.5px; - border-radius: 8px; - line-height: 1.4; - } - - /* Improved button styling for high DPI touch */ - .btn { - border-radius: 8px; - font-weight: 500; - letter-spacing: 0.025em; - } - - .btn-sm { - border-radius: 6px; - } - - /* Enhanced modal animations for smooth feel */ - .modal.fade .modal-dialog { - transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); - } - - /* Better spacing for very narrow viewports */ - .input-group { - margin-bottom: 1rem; - } - - .input-group-text { - border-width: 1.5px; - border-radius: 0 8px 8px 0; - } - - .input-group .form-control { - border-radius: 8px 0 0 8px; - } - - /* Optimize checkbox and radio sizing for high DPI */ - .form-check-input { - margin-top: 0.125rem; - border-width: 1.5px; - } - - .form-check-label { - font-size: 0.9rem; - padding-left: 0.75rem; - } - - /* Enhanced focus rings for high DPI */ - .form-control:focus, - .form-select:focus, - .btn:focus { - box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15); - border-color: #86b7fe; - } - - /* Better modal pull handle for high DPI */ - .modal-content::before { - width: 48px; - height: 5px; - border-radius: 3px; - background-color: #adb5bd; - top: 10px; - } -} - -/* Ultra-narrow portrait optimization (iPhone-specific) */ -@media (max-width: 280px) { - .modal-header { - padding: 0.75rem; - flex-direction: column; - align-items: stretch; - text-align: center; - } - - .modal-header .btn-close { - position: absolute; - top: 0.75rem; - right: 0.75rem; - width: 36px; - height: 36px; - } - - .modal-header .modal-title { - font-size: 1rem; - margin-bottom: 0.75rem; - padding-right: 50px; /* Account for close button */ - } - - .modal-body { - padding: 0.75rem; - } - - .modal-footer { - padding: 0.75rem; - gap: 0.75rem; - } - - /* Ultra-compact form styling */ - .form-group { - margin-bottom: 1rem; - } - - .form-group label { - font-size: 0.85rem; - margin-bottom: 0.375rem; - } - - .form-control, - .form-select { - padding: 0.625rem 0.75rem; - font-size: 15px; /* Prevent zoom on very narrow iPhones */ - } - - .btn { - padding: 0.625rem 1rem; - font-size: 0.9rem; - } - - /* Compact input groups */ - .input-group-text { - padding: 0.625rem 0.75rem; - font-size: 0.9rem; - } - - /* Smaller modal content spacing */ - .modal-content::before { - width: 40px; - height: 4px; - top: 8px; - } - - /* Optimize for one-handed use */ - .modal-footer .btn { - min-height: 48px; - font-size: 1rem; - } - - /* Better tag input styling for narrow screens */ - .mobile-tag-input .bootstrap-tagsinput { - min-height: 40px; - padding: 6px; - font-size: 15px; - } - - .mobile-tag-input .tag { - font-size: 0.8rem; - padding: 3px 6px; - margin: 1px; - } -} - -.recordSticker { - height: 100%; - page-break-after: always; -} - .stickerNote { - height:100%; - } - - html[data-bs-theme="light"] body { - background-color: #fff !important; - } - - html[data-bs-theme="dark"] body { - background-color: #212529 !important; - } - - html[data-bs-theme="light"] table { - background-color: #fff !important; - } - - html[data-bs-theme="dark"] table { - background-color: var(--bs-dark) !important; - } - - html[data-bs-theme="light"] ul { - border: 0px !important; - background-color: #fff !important; - } - - html[data-bs-theme="dark"] ul { - border: 0px !important; - background-color: var(--bs-dark) !important; - } - - html[data-bs-theme="light"] li { - color: #000 !important; - border: 0px !important; - background-color: #fff !important; - } - - html[data-bs-theme="dark"] li { - color: #fff !important; - border: 0px !important; - background-color: var(--bs-dark) !important; - } - - html[data-bs-theme="light"] td { - color: #000 !important; - border: hidden; - background-color: #fff !important; - } - - html[data-bs-theme="dark"] td { - color: #fff !important; - border: hidden; - background-color: var(--bs-dark) !important; - } - - html[data-bs-theme="light"] th { - color: #000 !important; - background-color: #fff !important; - } - - html[data-bs-theme="dark"] th { - color: #fff !important; - background-color: var(--bs-dark) !important; - } - - tr { - border: hidden; - } -} - -.chartContainer { - height: 30vh; - overflow: auto; -} - -.vehicleNoteContainer { - height: 40vh; -} - -.display-7 { - font-size: calc(1.325rem + 0.9vw); - font-weight: 300; - line-height: 1.2; -} - -@media (min-width: 1200px) { - .display-7 { - font-size: 2rem; - } -} - -.reportsCheckBoxContainer { - padding: 0.375rem 2.25rem 0.375rem 0.75rem; -} - -.bell-shake { - animation: bellshake .5s; - backface-visibility: hidden; - transform-origin: top center; -} - -.tablerow-shake { - animation: tablerowshake 1.2s cubic-bezier(.36, .07, .19, .97) both; - backface-visibility: hidden; - transform: translate3d(0, 0, 0); -} - -@keyframes tablerowshake { - - 10%, 90% { - transform: translate3d(-1px, 0, 0); - } - - 20%, 80% { - transform: translate3d(2px, 0, 0); - } - - 30%, 50%, 70% { - transform: translate3d(-4px, 0, 0); - } - - 40%, 60% { - transform: translate3d(4px, 0, 0); - } -} - -@keyframes bellshake { - 0% { - transform: rotate(0); - } - - 15% { - transform: rotate(5deg); - } - - 30% { - transform: rotate(-5deg); - } - - 45% { - transform: rotate(4deg); - } - - 60% { - transform: rotate(-4deg); - } - - 75% { - transform: rotate(2deg); - } - - 85% { - transform: rotate(-2deg); - } - - 92% { - transform: rotate(1deg); - } - - 100% { - transform: rotate(0); - } -} - -.btn-check:checked + .dropdown-item, :not(.btn-check) + .dropdown-item:active { - color: #fff; - background-color: #0d6efd; -} - -.motovaultpro-navbar-container { - position: fixed; - top: 0; - left: 0; - z-index: 1021; -} - -.motovaultpro-navbar { - align-items: center; -} - -.motovaultpro-tab { - border:none; -} - - .motovaultpro-tab .nav-link { - border: none; - } - -/*Media Queries*/ -@media (max-width: 576px) { - .motovaultpro-tab { - display: none; - } - - .motovaultpro-navbar { - justify-content: space-between; - align-items: center; - } - - .motovaultpro-navbar-button { - display: flex; - align-items: center; - } - - .motovaultpro-navbar-button > button { - padding: 0; - font-size: 2rem; - } - - .motovaultpro-menu-icon { - display: inline-block; - width: 1.5em; - height: 1.5em; - } - - - .motovaultpro-mobile-nav { - background-color: var(--bs-body-bg); - height: 100vh; - width: 100%; - position: fixed; - top: 0; - left: 0; - margin: 0; - display: none; - z-index: 2000; - overflow-y:auto; - } - - .motovaultpro-mobile-nav-show { - display: block; - } - - .motovaultpro-mobile-nav > ul > li > .nav-link.active { - color: #6ea8fe; - } -} - -@media(min-width: 576px) { - .motovaultpro-mobile-nav-show { - display: none; - } - - .motovaultpro-mobile-nav { - display: none; - } - - .motovaultpro-navbar-button { - display: none; - } -} - -.dropdown-menu.show { - z-index: 1030; -} - -.table-context-menu { - z-index: 1030; - box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.24); - position: absolute; -} - -.table-context-menu > li > .dropdown-item:hover { - background-color: rgba(var(--bs-primary-rgb)) !important; - color: #fff !important; -} - -html[data-bs-theme="dark"] .table-context-menu { - background-color: rgba(33, 37, 41, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -html[data-bs-theme="light"] .table-context-menu { - background-color: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -input[type="file"] { - max-width: 100%; -} - -.uploadedFileName { - max-width: 75%; -} - -.taskCard { - max-height: 20vh; - padding:0.5rem; - overflow:hidden; - border-radius: 4px; - box-shadow: 0 6px 10px rgba(0,0,0,.08), 0 0 6px rgba(0,0,0,.05); - transition: .3s transform cubic-bezier(.155,1.105,.295,1.12),.3s box-shadow,.3s -webkit-transform cubic-bezier(.155,1.105,.295,1.12); - cursor: pointer; -} -.taskCard.nodrag{ - cursor:not-allowed; -} -.taskCard-title{ - font-size:1.5rem; - font-weight:300; - max-height:10vh; -} -[data-bs-theme=dark] .taskCard { - background-color: rgba(255,255,255,0.5); -} -[data-bs-theme=light] .taskCard { - background-color: rgba(80,80,80,0.25); -} -.override-hide{ - display: none !important; -} -.reminderCalendarViewContent .datepicker, .reminderCalendarViewContent .datepicker-inline, .reminderCalendarViewContent .datepicker-days, .reminderCalendarViewContent .table-condensed { - width: 100%; - height: 100%; - table-layout: fixed; - cursor: default; -} -.reminder-exist{ - overflow:auto; - vertical-align:top; -} -.reminder-exist p{ - margin:0; -} -.reminderCalendarViewContent .datepicker table tr td.day { - cursor: default; -} -.reminderCalendarViewContent .datepicker table tr td.day:hover { - cursor: default; -} -.reminder-calendar-item{ - cursor: pointer; -} -.zero-y-padding{ - padding-top: 0rem; - padding-bottom: 0rem; -} -.vehicle-sold-banner { - color: #fff; - background-color: rgba(0, 0, 0, 0.6); - width: 100%; - text-align: center; - position: absolute; - z-index: 10; -} -.copyable{ - cursor: pointer; -} -.accordion-button.skinny { - padding: 0.438rem 0rem !important; - background-color: inherit !important; -} -.planner-indicator{ - font-size:1em; - padding:0.2em; -} -html[data-bs-theme="dark"] .btn-adaptive { - --bs-btn-color: #fff; - --bs-btn-bg: #212529; - --bs-btn-border-color: #212529; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #424649; - --bs-btn-hover-border-color: #373b3e; - --bs-btn-focus-shadow-rgb: 66, 70, 73; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #4d5154; - --bs-btn-active-border-color: #373b3e; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #212529; - --bs-btn-disabled-border-color: #212529; -} -html[data-bs-theme="light"] .btn-adaptive { - --bs-btn-color: #000; - --bs-btn-bg: #f8f9fa; - --bs-btn-border-color: #f8f9fa; - --bs-btn-hover-color: #000; - --bs-btn-hover-bg: #d3d4d5; - --bs-btn-hover-border-color: #c6c7c8; - --bs-btn-focus-shadow-rgb: 211, 212, 213; - --bs-btn-active-color: #000; - --bs-btn-active-bg: #c6c7c8; - --bs-btn-active-border-color: #babbbc; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #000; - --bs-btn-disabled-bg: #f8f9fa; - --bs-btn-disabled-border-color: #f8f9fa; -} - -html[data-bs-theme="dark"] .api-method:hover { - background-color: #373b3e; -} - -html[data-bs-theme="light"] .api-method:hover { - background-color: rgba(0,0,0,0.1); -} - -.resizable-nav-link { - padding: 0.5rem; -} - -@media(max-width: 1400px) { - .resizable-nav-link { - padding-left: 0.35rem; - padding-right: 0.35rem; - padding-top: 0.5rem; - padding-bottom:0.5rem; - } -} - -@media(max-width: 1200px) { - .resizable-nav-link { - padding-left: 0.2rem; - padding-right: 0.2rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - } -} - -@media(max-width: 992px) { - .resizable-nav-link { - padding-left: 0.25rem; - padding-right: 0.25rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - } -} - -@media(max-width: 768px) { - .resizable-nav-link { - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - } -} - -.password-visible-button { - padding: 0rem; -} - -.btn-warning { - --bs-btn-color: #fff; - --bs-btn-bg: #DA112A; - --bs-btn-border-color: #DA112A; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #B80E23; - --bs-btn-hover-border-color: #A60C1F; - --bs-btn-focus-shadow-rgb: 218, 17, 42; -} - -.motovaultpro-logo { - min-width: 48px; - max-width: 204px; - object-fit: scale-down; - pointer-events: none; -} -.motovaultpro-logo-sticker { - height: 6rem; - width: auto; - object-fit: scale-down; - pointer-events: none; -} - -::-ms-reveal { - display: none; -} - -.motovaultpro-report-banner { - border-top: thin solid black; - text-align: center; -} - -.attachment-badge-xs { - padding-left: 0.25em; - padding-right: 0.25em; - font-size: 0.6em; - font-weight: 500; - top: 15% -} - -.motovaultpro-vehicle-logo { - height: 48px; - width: 48px; - border-radius: 50%; - object-fit: cover; - pointer-events: none; -} - - .motovaultpro-vehicle-logo.sold { - filter: grayscale(100%); - } - -.motovaultpro-uploader { - border: 1px dashed rgb(73,80,87); - border-radius: 0.375rem; - cursor:pointer; - position:relative; -} -.motovaultpro-link-uploader{ - position:absolute; - right:0; - top:0; -} - .motovaultpro-link-uploader > .btn { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - border-bottom-right-radius: 0; - } - -.motovaultpro-uploader.solid { - border-style: solid; -} - -.setup-wizard-container { - position:relative; - box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.24); - height:85vh; -} - -.setup-wizard-content{ - height: calc(100% - 60px); - overflow-y:auto; - overflow-x:hidden; - padding: 10px 15px; -} - -.setup-wizard-nav{ - position: absolute; - width:100%; - height: 60px; - bottom: 0; - border-top: #E6E6E6 solid 1px; -} - -html[data-bs-theme="dark"] .frosted { - background-color: rgba(33, 37, 41, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -html[data-bs-theme="light"] .frosted { - background-color: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -.motovaultpro-body-container { - padding-top: calc(48px + 2.5rem); - min-height: 100vh; -} - - -.no-top-pad { - margin-top: calc(-48px - 2.5rem); -} -.sticky-top-nav { - top: calc(48px + 2.5rem); -} - diff --git a/wwwroot/defaults/.DS_Store b/wwwroot/defaults/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/wwwroot/defaults/.DS_Store and /dev/null differ diff --git a/wwwroot/defaults/addnew_vehicle.png b/wwwroot/defaults/addnew_vehicle.png deleted file mode 100644 index 05dd8e6..0000000 Binary files a/wwwroot/defaults/addnew_vehicle.png and /dev/null differ diff --git a/wwwroot/defaults/demo_default.zip b/wwwroot/defaults/demo_default.zip deleted file mode 100644 index f58e2a5..0000000 Binary files a/wwwroot/defaults/demo_default.zip and /dev/null differ diff --git a/wwwroot/defaults/en_US.json b/wwwroot/defaults/en_US.json deleted file mode 100644 index 0f9accd..0000000 --- a/wwwroot/defaults/en_US.json +++ /dev/null @@ -1 +0,0 @@ -{"Garage":"Garage","Settings":"Settings","Admin_Panel":"Admin Panel","Logout":"Logout","Dark_Mode":"Dark Mode","Enable_CSV_Imports":"Enable CSV Imports","Use_Imperial_Calculation_for_Fuel_Economy_Calculations(MPG)":"Use Imperial Calculation for Fuel Economy Calculations(MPG)","This_Will_Also_Change_Units_to_Miles_and_Gallons":"This Will Also Change Units to Miles and Gallons","Use_UK_MPG_Calculation":"Use UK MPG Calculation","Input_Gas_Consumption_in_Liters,_it_will_be_converted_to_UK_Gals_for_MPG_Calculation":"Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation","Sort_lists_in_Descending_Order(Newest_to_Oldest)":"Sort lists in Descending Order(Newest to Oldest)","Replace_$0.00_Costs_with_---":"Replace $0.00 Costs with ---","Use_Three_Decimals_For_Fuel_Cost":"Use Three Decimals For Fuel Cost","Display_Saved_Notes_in_Markdown":"Display Saved Notes in Markdown","Auto_Refresh_Lapsed_Recurring_Reminders":"Auto Refresh Lapsed Recurring Reminders","Auto_Insert_Odometer_Records":"Auto Insert Odometer Records","Only_when_Adding_Service/Upgrade/Fuel_Record_or_Completing_a_Plan":"Only when Adding Service/Upgrade/Fuel Record or Completing a Plan","Enable_Authentication":"Enable Authentication","Visible_Tabs":"Visible Tabs","Service_Records":"Service Records","Dashboard":"Dashboard","Upgrades":"Upgrades","Fuel":"Fuel","Odometer":"Odometer","Taxes":"Taxes","Notes":"Notes","Reminder":"Reminder","Supplies":"Supplies","Planner":"Planner","Default_Tab":"Default Tab","Service_Record":"Service Record","Tax":"Tax","Reminders":"Reminders","Backups":"Backups","Make":"Make","Restore":"Restore","About":"About","Add_New_Vehicle":"Add New Vehicle","Year":"Year","Year(must_be_after_1900)":"Year(must be after 1900)","Model":"Model","License_Plate":"License Plate","Electric_Vehicle":"Electric Vehicle","Use_Engine_Hours":"Use Engine Hours","Tags(optional)":"Tags(optional)","Upload_a_picture(optional)":"Upload a picture(optional)","Cancel":"Cancel","Edit_Vehicle":"Edit Vehicle","Delete_Vehicle":"Delete Vehicle","Manage_Vehicle":"Manage Vehicle","Expenses_by_Type":"Expenses by Type","Service":"Service","Expenses_by_Month":"Expenses by Month","As_of_Today":"As of Today","\u002B30_Days":"\u002B30 Days","\u002B60_Days":"\u002B60 Days","\u002B90_Days":"\u002B90 Days","Not_Urgent":"Not Urgent","Urgent":"Urgent","Very_Urgent":"Very Urgent","Past_Due":"Past Due","Reminders_by_Category":"Reminders by Category","Reminders_by_Urgency":"Reminders by Urgency","Collaborators":"Collaborators","Username":"Username","Delete":"Delete","Fuel_Mileage_by_Month":"Fuel Mileage by Month","Vehicle_Maintenance_Report":"Vehicle Maintenance Report","Export_Attachments":"Export Attachments","Gasoline":"Gasoline","Last_Reported_Odometer_Reading":"Last Reported Odometer Reading","Average_Fuel_Economy":"Average Fuel Economy","Total_Spent(excl._fuel)":"Total Spent(excl. fuel)","Total_Spent_on_Fuel":"Total Spent on Fuel","Type":"Type","Date":"Date","Description":"Description","Cost":"Cost","Upgrade":"Upgrade","#_of_Odometer_Records":"# of Odometer Records","Add_Odometer_Record":"Add Odometer Record","Import_via_CSV":"Import via CSV","Export_to_CSV":"Export to CSV","Print":"Print","Add_New_Odometer_Record":"Add New Odometer Record","Date_recorded":"Date recorded","Odometer_reading":"Odometer reading","Notes(optional)":"Notes(optional)","Upload_documents(optional)":"Upload documents(optional)","Max_File_Size:_28.6MB":"Max File Size: 28.6MB","#_of_Service_Records":"# of Service Records","Total":"Total","Add_Service_Record":"Add Service Record","No_data_found,_create_reminders_to_see_visualizations_here.":"No data found, create reminders to see visualizations here.","No_data_found,_insert/select_some_data_to_see_visualizations_here.":"No data found, insert/select some data to see visualizations here.","Edit_Odometer_Record":"Edit Odometer Record","Import_Data_from_CSV":"Import Data from CSV","In_order_for_this_utility_to_function_properly,_your_CSV_file_MUST_be_formatted_exactly_like_the_provided_sample._Dates_must_be_supplied_in_a_string._Numbers_must_be_supplied_as_numbers_without_currency_formatting.":"In order for this utility to function properly, your CSV file MUST be formatted exactly like the provided sample. Dates must be supplied in a string. Numbers must be supplied as numbers without currency formatting.","Failure_to_format_the_data_correctly_can_cause_data_corruption._Please_make_sure_you_make_a_copy_of_the_local_database_before_proceeding.":"Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.","Download_Sample":"Download Sample","Upload_CSV_File":"Upload CSV File","Import":"Import","Edit_Service_Record":"Edit Service Record","Date_service_was_performed":"Date service was performed","Odometer_reading_when_serviced":"Odometer reading when serviced","Description_of_item(s)_serviced(i.e._Oil_Change)":"Description of item(s) serviced(i.e. Oil Change)","Cost_of_the_service":"Cost of the service","Move_To":"Move To","Choose_Supplies":"Choose Supplies","Add_Reminder":"Add Reminder","Select_Supplies":"Select Supplies","No_supplies_with_quantities_greater_than_0_is_found.":"No supplies with quantities greater than 0 is found.","Select":"Select","#_of_Upgrade_Records":"# of Upgrade Records","Add_Upgrade_Record":"Add Upgrade Record","Add_New_Upgrade_Record":"Add New Upgrade Record","Date_upgrade/mods_was_installed":"Date upgrade/mods was installed","Odometer_reading_when_upgraded/modded":"Odometer reading when upgraded/modded","Description_of_item(s)_upgraded/modded":"Description of item(s) upgraded/modded","Cost_of_the_upgrade/mods":"Cost of the upgrade/mods","#_of_Gas_Records":"# of Gas Records","Total_Fuel_Consumed":"Total Fuel Consumed","Total_Cost":"Total Cost","Add_Gas_Record":"Add Gas Record","Date_Refueled":"Date Refueled","Consumption":"Consumption","Fuel_Economy":"Fuel Economy","Unit_Cost":"Unit Cost","#_of_Supply_Records":"# of Supply Records","Add_Supply_Record":"Add Supply Record","Part_#":"Part #","Supplier":"Supplier","Quantity":"Quantity","Add_New_Supply_Record":"Add New Supply Record","Date_purchased":"Date purchased","Part_Number":"Part Number","Part_#/Model_#/SKU_#":"Part #/Model #/SKU #","Description_of_the_Part/Supplies":"Description of the Part/Supplies","Supplier/Vendor":"Supplier/Vendor","Part_Supplier":"Part Supplier","Edit_Supply_Record":"Edit Supply Record","Add_New_Service_Record":"Add New Service Record","In_Stock":"In Stock","Edit_Upgrade_Record":"Edit Upgrade Record","Save_Vehicle":"Save Vehicle","Add_New_Gas_Record":"Add New Gas Record","Date_refueled":"Date refueled","Odometer_Reading":"Odometer Reading","Odometer_reading_when_refueled":"Odometer reading when refueled","Fuel_Consumption":"Fuel Consumption","Amount_of_gas_refueled":"Amount of gas refueled","Is_Filled_To_Full":"Is Filled To Full","Missed_Fuel_Up(Skip_MPG_Calculation)":"Missed Fuel Up(Skip MPG Calculation)","Cost_of_gas_refueled":"Cost of gas refueled","Unit":"Unit","#_of_Tax_Records":"# of Tax Records","Add_Tax_Record":"Add Tax Record","Add_New_Tax_Record":"Add New Tax Record","Date_tax_was_paid":"Date tax was paid","Description_of_tax_paid(i.e._Registration)":"Description of tax paid(i.e. Registration)","Cost_of_tax_paid":"Cost of tax paid","Is_Recurring":"Is Recurring","Month":"Month","1_Month":"1 Month","3_Months":"3 Months","6_Months":"6 Months","1_Year":"1 Year","2_Years":"2 Years","3_Years":"3 Years","5_Years":"5 Years","Edit_Tax_Record":"Edit Tax Record","#_of_Notes":"# of Notes","Add_Note":"Add Note","Note":"Note","Add_New_Note":"Add New Note","Pinned":"Pinned","Description_of_the_note":"Description of the note","Min_Fuel_Economy":"Min Fuel Economy","Max_Fuel_Economy":"Max Fuel Economy","Edit_Gas_Record":"Edit Gas Record","#_of_Plan_Records":"# of Plan Records","Add_Plan_Record":"Add Plan Record","Planned":"Planned","Doing":"Doing","Testing":"Testing","Done":"Done","Add_New_Plan_Record":"Add New Plan Record","Describe_the_Plan":"Describe the Plan","Cost_of_the_Plan":"Cost of the Plan","Priority":"Priority","Critical":"Critical","Normal":"Normal","Low":"Low","Current_Stage":"Current Stage","#_of_Reminders":"# of Reminders","Urgency":"Urgency","Metric":"Metric","Add_New_Reminder":"Add New Reminder","Reminder_Description":"Reminder Description","Remind_me_on":"Remind me on","Future_Date":"Future Date","Future_Odometer_Reading":"Future Odometer Reading","Whichever_comes_first":"Whichever comes first","Other":"Other","Edit_Reminder":"Edit Reminder","Replace_picture(optional)":"Replace picture(optional)","Language":"Language","Manage_Languages":"Manage Languages","Upload":"Upload","Tokens":"Tokens","Generate_User_Token":"Generate User Token","Auto_Notify(via_Email)":"Auto Notify(via Email)","Token":"Token","Issued_To":"Issued To","Users":"Users","Email":"Email","Is_Admin":"Is Admin","An_error_has_occurred,_please_try_again_later":"An error has occurred, please try again later","Edit_Note":"Edit Note","Password":"Password","Remember_Me":"Remember Me","Login":"Login","Forgot_Password":"Forgot Password","Register":"Register","Request":"Request","I_Have_a_Token":"I Have a Token","Back_to_Login":"Back to Login","Email_Address":"Email Address","New_Password":"New Password","Reset_Password":"Reset Password","No_data_found_or_all_records_have_zero_sums,_insert_records_with_non-zero_sums_to_see_visualizations_here.":"No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.","Save_as_Template":"Save as Template","View_Templates":"View Templates","Select_Template":"Select Template","No_templates_are_found.":"No templates are found.","Use":"Use","Edit_Plan_Record":"Edit Plan Record","Date_Created":"Date Created","Last_Modified":"Last Modified","Shop_Supplies":"Shop Supplies","Uploaded_Documents":"Uploaded Documents","Upload_more_documents":"Upload more documents","Database_Migration":"Database Migration","Instructions":"Instructions","To_Postgres":"To Postgres","From_Postgres":"From Postgres","Import_To_Postgres":"Import To Postgres","Export_From_Postgres":"Export From Postgres","Create":"Create","Manage_Extra_Fields":"Manage Extra Fields","Name":"Name","Required":"Required","Add_New_Field":"Add New Field","Close":"Close","Calendar":"Calendar","View_Reminder":"View Reminder","Mark_as_Done":"Mark as Done","Login_via":"Login via","Distance_Traveled_by_Month":"Distance Traveled by Month","Expenses_and_Distance_Traveled_by_Month":"Expenses and Distance Traveled by Month","Select_All":"Select All","Supply_Requisition_History":"Supply Requisition History","No_supply_requisitions_in_history":"No supply requisitions in history","Plan":"Plan","Deselect_All":"Deselect All","Duplicate":"Duplicate","Toggle_Pin":"Toggle Pin","Pin":"Pin","Unpin":"Unpin","Profile":"Profile","Update_Profile":"Update Profile","Account_Username":"Account Username","Send_Token":"Send Token","Update":"Update","Show_Extra_Field_Columns":"Show Extra Field Columns","Enabling_this_may_cause_performance_issues":"Enabling this may cause performance issues","Visible_Columns":"Visible Columns","Edit_Multiple":"Edit Multiple","Edit_Multiple_Records":"Edit Multiple Records","(multiple)":"(multiple)","Tags(use_---_to_clear_all_existing_tags)":"Tags(use --- to clear all existing tags)","Notes(use_---_to_clear_all_existing_notes)":"Notes(use --- to clear all existing notes)","Edit":"Edit","Search":"Search","Delta":"Delta","Vehicle":"Vehicle","Select_Reminder":"Select Reminder","Purchased_Date(optional)":"Purchased Date(optional)","Purchased_Date":"Purchased Date","Sold_Date(optional)":"Sold Date(optional)","Sold_Date":"Sold Date","SOLD":"SOLD","Days":"Days","Statistics":"Statistics","Hide_Sold_Vehicles":"Hide Sold Vehicles","Server-wide_Settings":"Server-wide Settings","Extra_Fields":"Extra Fields","Version":"Version","Configure_Reminder_Urgency_Thresholds":"Configure Reminder Urgency Thresholds","Urgent(Days)":"Urgent(Days)","Very_Urgent(Days)":"Very Urgent(Days)","Urgent(Distance)":"Urgent(Distance)","Very_Urgent(Distance)":"Very Urgent(Distance)","Save":"Save","Initial_Odometer":"Initial Odometer","Distance":"Distance","Initial_Odometer_reading":"Initial Odometer reading","Total_Distance":"Total Distance","Recalculate_Distance":"Recalculate Distance","Edit_Multiple_Odometer_Records":"Edit Multiple Odometer Records","Odometer_Adjustments":"Odometer Adjustments","Odometer_Multiplier":"Odometer Multiplier","Odometer_Difference":"Odometer Difference","Adjust_Odometer":"Adjust Odometer","Edit_Multiple_Gas_Records":"Edit Multiple Gas Records","Multiple":"Multiple","Copy_Attachments":"Copy Attachments","Purchased_Price(optional)":"Purchased Price(optional)","Purchased_Price":"Purchased Price","Sold_Price(optional)":"Sold Price(optional)","Sold_Price":"Sold Price","Purchase/Sold_Information(optional)":"Purchase/Sold Information(optional)","Electric":"Electric","Depreciation":"Depreciation","day":"day","Appreciation":"Appreciation","Incremental_Search":"Incremental Search","Unsaved_Changes":"Unsaved Changes","Edit_Plan_Record_Template":"Edit Plan Record Template","No_Data_Found":"No Data Found","Sponsors":"Sponsors","All_Time":"All Time","Metrics":"Metrics","gallons":"gallons","miles":"miles","liters":"liters","kilometers":"kilometers","Fuel_Type":"Fuel Type","Diesel":"Diesel","Documents_Pending_Upload":"Documents Pending Upload","Vehicle_Cost_Breakdown":"Vehicle Cost Breakdown","Cost_Per_Day":"Cost Per Day","Cost_Per_Mile":"Cost Per Mile","Cost_Per_Kilometer":"Cost Per Kilometer","Cost_Per_Hour":"Cost Per Hour","Dashboard_Metrics":"Dashboard Metrics","Last_Odometer":"Last Odometer","Total_Cost_/_Total_Distance_Driven":"Total Cost / Total Distance Driven","Use_Custom_Thresholds":"Use Custom Thresholds","Disable_Registration":"Disable Registration","Default_Reminder_Email":"Default Reminder Email","Default_Email_for_Reminder":"Default Email for Reminder","Enable_OIDC_for_Root_User":"Enable OIDC for Root User","Adaptive_Color_Mode":"Adaptive Color Mode","Uses_the_Default_Reminder_Email_for_OIDC_Auth":"Uses the Default Reminder Email for OIDC Auth","Odometer_Optional":"Odometer Optional","Manage_Tokens":"Manage Tokens","Generate":"Generate","Notify":"Notify","Start_Recording":"Start Recording","Stop_Recording":"Stop Recording","Current_Odometer":"Current Odometer","Experimental_Feature_-_Do_not_exit_or_minimize_this_app_when_recording._Verify_all_starting_and_ending_odometers._Accuracy_subject_to_hardware_limitations.":"Experimental Feature - Do not exit or minimize this app when recording. Verify all starting and ending odometers. Accuracy subject to hardware limitations.","Identifier":"Identifier","Translation_Editor":"Translation Editor","Save_Translation":"Save Translation","Export_Translation":"Export Translation","Get_Translations":"Get Translations","Available_Translations":"Available Translations","Download_All_Translations":"Download All Translations","Reorder_Tabs":"Reorder Tabs","Reset_Tab_Order":"Reset Tab Order","Save_Tab_Order":"Save Tab Order","Upcoming_Reminder":"Upcoming Reminder","Plans":"Plans","No_records_available_to_display":"No records available to display","Duplicate_To_Vehicle":"Duplicate To Vehicle","Automatically_Format_Decimal_Inputs":"Automatically Format Decimal Inputs","Order_Supplies":"Order Supplies","Missing_Supplies,_Please_Delete_This_Template_and_Recreate_It.":"Missing Supplies, Please Delete This Template and Recreate It.","Additional_Widgets":"Additional Widgets","Custom_Widgets_Editor":"Custom Widgets Editor","No_Vehicles_Available":"No Vehicles Available","Use_this_tool_to_migrate_data_between_LiteDB_and_Postgres":"Use this tool to migrate data between LiteDB and Postgres","Note_that_it_is_recommended_that_the_Postgres_DB_is_empty_when_importing_from_LiteDB_to_prevent_primary_key_errors":"Note that it is recommended that the Postgres DB is empty when importing from LiteDB to prevent primary key errors.","No_Recurring_Reminders_Found":"No Recurring Reminders Found","Use_Three_Decimals_For_Fuel_Consumption":"Use Three Decimals For Fuel Consumption","Choose_Additional_Supplies":"Choose Additional Supplies","Supplies_are_requisitioned_immediately_after_the_record_is_saved.":"Supplies are requisitioned immediately after the record is saved.","Vehicle_Monthly_Cost_Breakdown":"Vehicle Monthly Cost Breakdown","Select_Mode":"Select Mode","hours":"hours","Default_to_Fuel_Unit_Cost":"Default to Fuel Unit Cost","Exclude_Records_with_these_Tags":"Exclude Records with these Tags","Only_Include_Records_with_these_Tags":"Only Include Records with these Tags","Select_Columns":"Select Columns","Filter_by_Tags":"Filter by Tags","Advanced_Filters":"Advanced Filters","Filter_by_Date_Range":"Filter by Date Range","From":"From","Start_Date":"Start Date","To":"To","End_Date":"End Date","Create_Odometer":"Create Odometer","Time":"Time","Attachments":"Attachments","Vehicle_Reminders_From_MotoVaultPro":"Vehicle Reminders From MotoVaultPro","Due":"Due","Your_Password_Reset_Token_for_MotoVaultPro":"Your Password Reset Token for MotoVaultPro","A_token_has_been_generated_on_your_behalf,_please_reset_your_password_for_MotoVaultPro_using_the_token":"A token has been generated on your behalf, please reset your password for MotoVaultPro using the token","Your_Registration_Token_for_MotoVaultPro":"Your Registration Token for MotoVaultPro","A_token_has_been_generated_on_your_behalf,_please_complete_your_registration_for_MotoVaultPro_using_the_token":"A token has been generated on your behalf, please complete your registration for MotoVaultPro using the token","Your_User_Account_Update_Token_for_MotoVaultPro":"Your User Account Update Token for MotoVaultPro","A_token_has_been_generated_on_your_behalf,_please_update_your_account_for_MotoVaultPro_using_the_token":"A token has been generated on your behalf, please update your account for MotoVaultPro using the token","Print_Individual_Records":"Print Individual Records","Review_Server_Configurations":"Review Server Configurations","Postgres_Connection":"Postgres Connection","Not_Configured":"Not Configured","Allowed_File_Extensions":"Allowed File Extensions","Logo_URL":"Logo URL","Message_of_the_Day":"Message of the Day","WebHook_URL":"WebHook URL","Custom_Widgets":"Custom Widgets","Invariant_API":"Invariant API","SMTP_Server":"SMTP Server","SMTP_Server_Port":"SMTP Server Port","SMTP_Sender_Address":"SMTP Sender Address","SMTP_Username":"SMTP Username","SMTP_Password":"SMTP Password","OIDC_Provider":"OIDC Provider","OIDC_Client_ID":"OIDC Client ID","OIDC_Client_Secret":"OIDC Client Secret","OIDC_Auth_URL":"OIDC Auth URL","OIDC_Token_URL":"OIDC Token URL","OIDC_Redirect_URL":"OIDC Redirect URL","OIDC_Scope":"OIDC Scope","OIDC_Logout_URL":"OIDC Logout URL","Enabled":"Enabled","Disabled":"Disabled","OIDC_Validate_State":"OIDC Validate State","OIDC_Use_PKCE":"OIDC Use PKCE","OIDC_Login_Only":"OIDC Login Only","Test_Email_from_MotoVaultPro":"Test Email from MotoVaultPro","If_you_are_seeing_this_email_it_means_your_SMTP_configuration_is_functioning_correctly":"If you are seeing this email it means your SMTP configuration is functioning correctly","Show_Calendar":"Show Calendar","Text":"Text","Number":"Number","Decimal":"Decimal","Location":"Location","OIDC_UserInfo_URL":"OIDC UserInfo URL","Show_Vehicle_Thumbnail_in_Header":"Show Vehicle Thumbnail in Header","Distance_Traveled":"Distance Traveled","Search_by_Keyword":"Search by Keyword","Case_Sensitive":"Case Sensitive","Attach_Link":"Attach Link","Small_Logo_URL":"Small Logo URL","Setup_Wizard":"Setup Wizard","Server_Settings_Configurator":"Server Settings Configurator","By_proceeding,_you_acknowledge_that_you_are_solely_responsible_for_all_consequences_from_utilizing_the_Server_Settings_Configurator":"By proceeding, you acknowledge that you are solely responsible for all consequences from utilizing the Server Settings Configurator","Acknowledge_and_Continue":"Acknowledge and Continue","Server_Settings":"Server Settings","Restart_Required":"Restart Required","Server_URL":"Server URL","SMTP":"SMTP","Test_SMTP_Settings":"Test SMTP Settings","Single_Sign_On":"Single Sign On","Registration":"Registration","Invitation_Only":"Invitation Only","Open_Registration":"Open Registration","Root_User_Email_Address":"Root User Email Address","Reminder_Urgency_Thresholds":"Reminder Urgency Thresholds","Miscellaneous":"Miscellaneous","Server_Settings_Saved":"Server Settings Saved","Restart_Wizard":"Restart Wizard","Return_to_Garage":"Return to Garage","Back":"Back","Next":"Next","Configure":"Configure","Skip":"Skip","Note_that_it_is_recommended_that_the_Postgres_DB_is_empty_when_importing_from_LiteDB_to_prevent_primary_key_errors.":"Note that it is recommended that the Postgres DB is empty when importing from LiteDB to prevent primary key errors.","Due_Days":"Due Days","Due_Distance":"Due Distance"} \ No newline at end of file diff --git a/wwwroot/defaults/garage.png b/wwwroot/defaults/garage.png deleted file mode 100644 index 7413365..0000000 Binary files a/wwwroot/defaults/garage.png and /dev/null differ diff --git a/wwwroot/defaults/garage_narrow.png b/wwwroot/defaults/garage_narrow.png deleted file mode 100644 index 335e83e..0000000 Binary files a/wwwroot/defaults/garage_narrow.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro.png b/wwwroot/defaults/motovaultpro.png deleted file mode 100644 index d56c76d..0000000 Binary files a/wwwroot/defaults/motovaultpro.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_icon_128.png b/wwwroot/defaults/motovaultpro_icon_128.png deleted file mode 100644 index 7a38de2..0000000 Binary files a/wwwroot/defaults/motovaultpro_icon_128.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_icon_144.png b/wwwroot/defaults/motovaultpro_icon_144.png deleted file mode 100644 index 133f1da..0000000 Binary files a/wwwroot/defaults/motovaultpro_icon_144.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_icon_192.png b/wwwroot/defaults/motovaultpro_icon_192.png deleted file mode 100644 index 76c782b..0000000 Binary files a/wwwroot/defaults/motovaultpro_icon_192.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_icon_72.png b/wwwroot/defaults/motovaultpro_icon_72.png deleted file mode 100644 index bfa2df3..0000000 Binary files a/wwwroot/defaults/motovaultpro_icon_72.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_launch.png b/wwwroot/defaults/motovaultpro_launch.png deleted file mode 100644 index d56c76d..0000000 Binary files a/wwwroot/defaults/motovaultpro_launch.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_logo.png b/wwwroot/defaults/motovaultpro_logo.png deleted file mode 100644 index 70505a2..0000000 Binary files a/wwwroot/defaults/motovaultpro_logo.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_logo_small.png b/wwwroot/defaults/motovaultpro_logo_small.png deleted file mode 100644 index ab766a2..0000000 Binary files a/wwwroot/defaults/motovaultpro_logo_small.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_maskable_icon_128.png b/wwwroot/defaults/motovaultpro_maskable_icon_128.png deleted file mode 100644 index 7a38de2..0000000 Binary files a/wwwroot/defaults/motovaultpro_maskable_icon_128.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_maskable_icon_192.png b/wwwroot/defaults/motovaultpro_maskable_icon_192.png deleted file mode 100644 index 76c782b..0000000 Binary files a/wwwroot/defaults/motovaultpro_maskable_icon_192.png and /dev/null differ diff --git a/wwwroot/defaults/motovaultpro_maskable_icon_72.png b/wwwroot/defaults/motovaultpro_maskable_icon_72.png deleted file mode 100644 index bfa2df3..0000000 Binary files a/wwwroot/defaults/motovaultpro_maskable_icon_72.png and /dev/null differ diff --git a/wwwroot/defaults/noimage.png b/wwwroot/defaults/noimage.png deleted file mode 100644 index 8acfa0b..0000000 Binary files a/wwwroot/defaults/noimage.png and /dev/null differ diff --git a/wwwroot/defaults/reminderemailtemplate.txt b/wwwroot/defaults/reminderemailtemplate.txt deleted file mode 100644 index 77e2398..0000000 --- a/wwwroot/defaults/reminderemailtemplate.txt +++ /dev/null @@ -1,41 +0,0 @@ - - - - - -

    {VehicleInformation}

    - - -{TableHeader} - -{TableBody} -
    - - \ No newline at end of file diff --git a/wwwroot/favicon.png b/wwwroot/favicon.png deleted file mode 100644 index c1020a8..0000000 Binary files a/wwwroot/favicon.png and /dev/null differ diff --git a/wwwroot/js/garage.js b/wwwroot/js/garage.js deleted file mode 100644 index ca9d8cd..0000000 --- a/wwwroot/js/garage.js +++ /dev/null @@ -1,473 +0,0 @@ -// Initialize vehicle modal for mobile (using shared mobile framework) -function initializeVehicleMobile() { - // Vehicle modal has multiple date inputs, handle them individually - if (isMobileDevice()) { - // Convert date inputs to native HTML5 on mobile - $('#inputPurchaseDate').attr('type', 'date').removeClass('datepicker'); - $('#inputSoldDate').attr('type', 'date').removeClass('datepicker'); - - // Initialize mobile tag selector - initMobileTagSelector($("#inputTag")); - - // Initialize swipe to dismiss - initSwipeToDismiss('#addVehicleModal'); - } else { - // Desktop initialization - initTagSelector($("#inputTag")); - initDatePicker($('#inputPurchaseDate')); - initDatePicker($('#inputSoldDate')); - } -} - -function showAddVehicleModal() { - uploadedFile = ""; - $.get('/Vehicle/AddVehiclePartialView', function (data) { - if (data) { - $("#addVehicleModalContent").html(data); - - // Initialize mobile experience using shared framework - initializeVehicleMobile(); - - $('#addVehicleModal').modal('show'); - } - }) -} -function hideAddVehicleModal() { - $('#addVehicleModal').modal('hide'); -} -//refreshable function to reload Garage PartialView -function loadGarage() { - $.get('/Home/Garage', function (data) { - $("#garageContainer").html(data); - loadSettings(); - bindTabEvent(); - }); -} -function loadSettings() { - $.get('/Home/Settings', function (data) { - $("#settings-tab-pane").html(data); - }); -} -function getVehicleSupplyRecords() { - $.get(`/Vehicle/GetSupplyRecordsByVehicleId?vehicleId=0`, function (data) { - if (data) { - $("#supply-tab-pane").html(data); - restoreScrollPosition(); - } - }); -} -function GetVehicleId() { - return { vehicleId: 0, hasOdometerAdjustment: false }; -} -function bindTabEvent() { - $('button[data-bs-toggle="tab"]').on('show.bs.tab', function (e) { - switch (e.target.id) { - case "supply-tab": - getVehicleSupplyRecords(); - break; - case "calendar-tab": - getVehicleCalendarEvents(); - break; - } - switch (e.relatedTarget.id) { //clear out previous tabs with grids in them to help with performance - case "supply-tab": - $("#supply-tab-pane").html(""); - break; - case "calendar-tab": - $("#calendar-tab-pane").html(""); - break; - } - $(`.motovaultpro-tab #${e.target.id}`).addClass('active'); - $(`.motovaultpro-mobile-nav #${e.target.id}`).addClass('active'); - $(`.motovaultpro-tab #${e.relatedTarget.id}`).removeClass('active'); - $(`.motovaultpro-mobile-nav #${e.relatedTarget.id}`).removeClass('active'); - }); -} -function getVehicleCalendarEvents() { - $.get('/Home/Calendar', function (data) { - if (data) { - $("#calendar-tab-pane").html(data); - } - }); -} -function showCalendarReminderModal(id) { - event.stopPropagation(); - $.get(`/Home/ViewCalendarReminder?reminderId=${id}`, function (data) { - if (data) { - $("#reminderRecordCalendarModalContent").html(data); - $("#reminderRecordCalendarModal").modal('show'); - $('#reminderRecordCalendarModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("reminderNotes"); - } - }); - } - }) -} -function hideCalendarReminderModal() { - $("#reminderRecordCalendarModal").modal('hide'); -} -function generateReminderItem(id, urgency, description) { - if (description.trim() == '') { - return; - } - switch (urgency) { - case "VeryUrgent": - return `

    ${encodeHTMLInput(description)}

    `; - case "PastDue": - return `

    ${encodeHTMLInput(description)}

    `; - case "Urgent": - return `

    ${encodeHTMLInput(description)}

    `; - case "NotUrgent": - return `

    ${encodeHTMLInput(description)}

    `; - } -} -function markDoneCalendarReminderRecord(reminderRecordId, e) { - event.stopPropagation(); - $.post(`/Vehicle/PushbackRecurringReminderRecord?reminderRecordId=${reminderRecordId}`, function (data) { - if (data) { - hideCalendarReminderModal(); - successToast("Reminder Updated"); - getVehicleCalendarEvents(); - } else { - errorToast(genericErrorMessage()); - } - }); -} -function deleteCalendarReminderRecord(reminderRecordId, e) { - if (e != undefined) { - event.stopPropagation(); - } - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Reminders cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeleteReminderRecordById?reminderRecordId=${reminderRecordId}`, function (data) { - if (data) { - hideCalendarReminderModal(); - successToast("Reminder Deleted"); - getVehicleCalendarEvents(); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function initCalendar() { - if (groupedDates.length == 0) { - //group dates - eventDates.map(x => { - var existingIndex = groupedDates.findIndex(y => y.date == x.date); - if (existingIndex == -1) { - groupedDates.push({ date: x.date, reminders: [`${generateReminderItem(x.id, x.urgency, x.description)}`] }); - } else if (existingIndex > -1) { - groupedDates[existingIndex].reminders.push(`${generateReminderItem(x.id, x.urgency, x.description)}`); - } - }); - } - $(".reminderCalendarViewContent").datepicker({ - startDate: "+0d", - format: getShortDatePattern().pattern, - todayHighlight: true, - weekStart: getGlobalConfig().firstDayOfWeek, - beforeShowDay: function (date) { - var reminderDateIndex = groupedDates.findIndex(x => (x.date == date.getTime() || x.date == (date.getTime() - date.getTimezoneOffset() * 60000))); //take into account server timezone offset - if (reminderDateIndex > -1) { - return { - enabled: true, - classes: 'reminder-exist', - content: `

    ${date.getDate()}

    ${groupedDates[reminderDateIndex].reminders.join('
    ')}
    ` - } - } - } - }); -} -function performLogOut() { - $.post('/Login/LogOut', function (data) { - if (data) { - window.location.href = data; - } - }) -} -function loadPinnedNotes(vehicleId) { - var hoveredGrid = $(`#gridVehicle_${vehicleId}`); - if (hoveredGrid.attr("data-bs-title") != '') { - hoveredGrid.tooltip("show"); - } -} -function hidePinnedNotes(vehicleId) { - if ($(`#gridVehicle_${vehicleId}`).attr('data-bs-title') != '') { - $(`#gridVehicle_${vehicleId}`).tooltip("hide"); - } -} - -function filterGarage(sender) { - var rowData = $(".garage-item"); - if (sender == undefined) { - rowData.removeClass('override-hide'); - return; - } - var tagName = sender.textContent; - if ($(sender).hasClass("bg-primary")) { - rowData.removeClass('override-hide'); - $(sender).removeClass('bg-primary'); - $(sender).addClass('bg-secondary'); - } else { - //hide table rows. - rowData.addClass('override-hide'); - $(`[data-tags~='${tagName}']`).removeClass('override-hide'); - if ($(".tagfilter.bg-primary").length > 0) { - //disabling other filters - $(".tagfilter.bg-primary").addClass('bg-secondary'); - $(".tagfilter.bg-primary").removeClass('bg-primary'); - } - $(sender).addClass('bg-primary'); - $(sender).removeClass('bg-secondary'); - } -} -function sortVehicles(desc) { - //get row data - var rowData = $('.garage-item'); - var sortedRow = rowData.toArray().sort((a, b) => { - var currentVal = globalParseFloat($(a).find(".garage-item-year").attr('data-unit')); - var nextVal = globalParseFloat($(b).find(".garage-item-year").attr('data-unit')); - if (desc) { - return nextVal - currentVal; - } else { - return currentVal - nextVal; - } - }); - sortedRow.push($('.garage-item-add')) - $('.vehiclesContainer').html(sortedRow); -} - -var touchtimer; -var touchduration = 800; -function detectLongTouch(sender) { - if ($(sender).hasClass("active")) { - if (!touchtimer) { - touchtimer = setTimeout(function () { sortGarage(sender, true); detectTouchEndPremature(sender); }, touchduration); - } - } -} -function detectTouchEndPremature(sender) { - if (touchtimer) { - clearTimeout(touchtimer); - touchtimer = null; - } -} - -function sortGarage(sender, isMobile) { - if (event != undefined) { - event.preventDefault(); - } - sender = $(sender); - if (sender.hasClass("active")) { - //do sorting only if garage is the active tab. - var sortColumn = sender.text(); - var garageIcon = ''; - var sortAscIcon = ''; - var sortDescIcon = ''; - if (sender.hasClass('sort-asc')) { - sender.removeClass('sort-asc'); - sender.addClass('sort-desc'); - sender.html(isMobile ? `${garageIcon}${sortColumn}${sortDescIcon}` : `${garageIcon}${sortColumn}${sortDescIcon}`); - sortVehicles(true); - } else if (sender.hasClass('sort-desc')) { - //restore table - sender.removeClass('sort-desc'); - sender.html(isMobile ? `${garageIcon}${sortColumn}` : `${garageIcon}${sortColumn}`); - resetSortGarage(); - } else { - //first time sorting. - //check if table was sorted before by a different column(only relevant to fuel tab) - if ($("[default-sort]").length > 0 && ($(".sort-asc").length > 0 || $(".sort-desc").length > 0)) { - //restore table state. - resetSortGarage(); - //reset other sorted columns - if ($(".sort-asc").length > 0) { - $(".sort-asc").html($(".sort-asc").html().replace(sortAscIcon, "")); - $(".sort-asc").removeClass("sort-asc"); - } - if ($(".sort-desc").length > 0) { - $(".sort-desc").html($(".sort-desc").html().replace(sortDescIcon, "")); - $(".sort-desc").removeClass("sort-desc"); - } - } - sender.addClass('sort-asc'); - sender.html(isMobile ? `${garageIcon}${sortColumn}${sortAscIcon}` : `${garageIcon}${sortColumn}${sortAscIcon}`); - //append sortRowId to the vehicle container - if ($("[default-sort]").length == 0) { - $(`.garage-item`).map((index, elem) => { - $(elem).attr("default-sort", index); - }); - } - sortVehicles(false); - } - } -} -function resetSortGarage() { - var rowData = $(`.garage-item`); - var sortedRow = rowData.toArray().sort((a, b) => { - var currentVal = $(a).attr('default-sort'); - var nextVal = $(b).attr('default-sort'); - return currentVal - nextVal; - }); - $(".garage-item-add").map((index, elem) => { - sortedRow.push(elem); - }) - $(`.vehiclesContainer`).html(sortedRow); -} - -let dragged = null; -let draggedId = 0; -function dragEnter(event) { - event.preventDefault(); -} -function dragStart(event, vehicleId) { - dragged = event.target; - draggedId = vehicleId; - event.dataTransfer.setData('text/plain', draggedId); -} -function dragOver(event) { - event.preventDefault(); -} -function dropBox(event, targetVehicleId) { - if (dragged.parentElement != event.target && event.target != dragged && draggedId != targetVehicleId) { - copyContributors(draggedId, targetVehicleId); - } - event.preventDefault(); -} -function copyContributors(sourceVehicleId, destVehicleId) { - var sourceVehicleName = $(`#gridVehicle_${sourceVehicleId} .card-body`).children('h5').map((index, elem) => { return elem.innerText }).toArray().join(" "); - var destVehicleName = $(`#gridVehicle_${destVehicleId} .card-body`).children('h5').map((index, elem) => { return elem.innerText }).toArray().join(" "); - Swal.fire({ - title: "Copy Collaborators?", - text: `Copy collaborators over from ${sourceVehicleName} to ${destVehicleName}?`, - showCancelButton: true, - confirmButtonText: "Copy", - confirmButtonColor: "#0d6efd" - }).then((result) => { - if (result.isConfirmed) { - $.post('/Vehicle/DuplicateVehicleCollaborators', { sourceVehicleId: sourceVehicleId, destVehicleId: destVehicleId }, function (data) { - if (data.success) { - successToast("Collaborators Copied"); - loadGarage(); - } else { - errorToast(data.message); - } - }) - } else { - $("#workAroundInput").hide(); - } - }); -} - -function showAccountInformationModal() { - $.get('/Home/GetUserAccountInformationModal', function (data) { - $('#accountInformationModalContent').html(data); - $('#accountInformationModal').modal('show'); - }) -} - -function showRootAccountInformationModal() { - $.get('/Home/GetRootAccountInformationModal', function (data) { - $('#accountInformationModalContent').html(data); - $('#accountInformationModal').modal('show'); - }) -} -function validateAndSaveRootUserAccount() { - var hasError = false; - if ($('#inputUsername').val().trim() == '') { - $('#inputUsername').addClass("is-invalid"); - hasError = true; - } else { - $('#inputUsername').removeClass("is-invalid"); - } - if ($('#inputPassword').val().trim() == '') { - $('#inputPassword').addClass("is-invalid"); - hasError = true; - } else { - $('#inputPassword').removeClass("is-invalid"); - } - if (hasError) { - errorToast("Please check the form data"); - return; - } - var userAccountInfo = { - userName: $('#inputUsername').val(), - password: $('#inputPassword').val() - } - $.post('/Login/CreateLoginCreds', { credentials: userAccountInfo }, function (data) { - if (data) { - //hide modal - hideAccountInformationModal(); - successToast('Root Account Updated'); - performLogOut(); - } else { - errorToast(data.message); - } - }); -} - -function hideAccountInformationModal() { - $('#accountInformationModal').modal('hide'); -} -function validateAndSaveUserAccount() { - var hasError = false; - if ($('#inputUsername').val().trim() == '') { - $('#inputUsername').addClass("is-invalid"); - hasError = true; - } else { - $('#inputUsername').removeClass("is-invalid"); - } - if ($('#inputEmail').val().trim() == '') { - $('#inputEmail').addClass("is-invalid"); - hasError = true; - } else { - $('#inputEmail').removeClass("is-invalid"); - } - if ($('#inputToken').val().trim() == '') { - $('#inputToken').addClass("is-invalid"); - hasError = true; - } else { - $('#inputToken').removeClass("is-invalid"); - } - if (hasError) { - errorToast("Please check the form data"); - return; - } - var userAccountInfo = { - userName: $('#inputUsername').val(), - password: $('#inputPassword').val(), - emailAddress: $('#inputEmail').val(), - token: $('#inputToken').val() - } - $.post('/Home/UpdateUserAccount', { userAccount: userAccountInfo }, function (data) { - if (data.success) { - //hide modal - hideAccountInformationModal(); - successToast('Profile Updated'); - performLogOut(); - } else { - errorToast(data.message); - } - }); -} -function generateTokenForUser() { - $.post('/Home/GenerateTokenForUser', function (data) { - if (data) { - successToast('Token sent'); - } else { - errorToast(genericErrorMessage()) - } - }); -} \ No newline at end of file diff --git a/wwwroot/js/gasrecord.js b/wwwroot/js/gasrecord.js deleted file mode 100644 index f811b0a..0000000 --- a/wwwroot/js/gasrecord.js +++ /dev/null @@ -1,667 +0,0 @@ -// Initialize gas record modal for mobile (using shared mobile framework) -function initializeGasRecordMobile() { - try { - console.log('Initializing gas record mobile - isMobile:', typeof isMobileDevice === 'function' ? isMobileDevice() : 'function not found'); - - if (typeof initMobileModal === 'function') { - initMobileModal({ - modalId: '#gasRecordModal', - dateInputId: '#gasRecordDate', - tagSelectorId: '#gasRecordTag', - modeToggleId: '#fuelEntryModeToggle', - simpleModeDefault: true - }); - } else { - console.error('initMobileModal function not found'); - } - - // Handle desktop initialization - if (typeof isMobileDevice === 'function' && !isMobileDevice()) { - console.log('Initializing desktop components'); - initDatePicker($('#gasRecordDate')); - initTagSelector($("#gasRecordTag")); - } - } catch (e) { - console.error('Error in initializeGasRecordMobile:', e); - // Fallback to basic initialization - if (typeof isMobileDevice !== 'function' || !isMobileDevice()) { - try { - initDatePicker($('#gasRecordDate')); - initTagSelector($("#gasRecordTag")); - } catch (fallbackError) { - console.error('Fallback initialization also failed:', fallbackError); - } - } - } -} - -function showAddGasRecordModal() { - try { - var vehicleInfo = GetVehicleId(); - console.log('Getting gas record modal for vehicle:', vehicleInfo.vehicleId); - - $.get(`/Vehicle/GetAddGasRecordPartialView?vehicleId=${vehicleInfo.vehicleId}`, function (data) { - if (data) { - $("#gasRecordModalContent").html(data); - - // Initialize mobile experience using shared framework - console.log('Initializing mobile gas record modal'); - initializeGasRecordMobile(); - - console.log('Showing gas record modal'); - $('#gasRecordModal').modal('show'); - } else { - console.error('No data received from GetAddGasRecordPartialView'); - errorToast('Failed to load gas record form'); - } - }).fail(function(xhr, status, error) { - console.error('AJAX request failed:', status, error); - errorToast('Failed to load gas record form: ' + error); - }); - } catch (e) { - console.error('Error in showAddGasRecordModal:', e); - errorToast('Error opening gas record form: ' + e.message); - } -} -function showEditGasRecordModal(gasRecordId, nocache) { - if (!nocache) { - var existingContent = $("#gasRecordModalContent").html(); - if (existingContent.trim() != '') { - //check if id is same. - var existingId = getGasRecordModelData().id; - if (existingId == gasRecordId && $('[data-changed=true]').length > 0) { - $('#gasRecordModal').modal('show'); - $('.cached-banner').show(); - return; - } - } - } - $.get(`/Vehicle/GetGasRecordForEditById?gasRecordId=${gasRecordId}`, function (data) { - if (data) { - $("#gasRecordModalContent").html(data); - - // Initialize mobile experience using shared framework - initializeGasRecordMobile(); - - $('#gasRecordModal').modal('show'); - bindModalInputChanges('gasRecordModal'); - - $('#gasRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("gasRecordNotes"); - } - }); - } - }); -} -function hideAddGasRecordModal() { - $('#gasRecordModal').modal('hide'); -} -function deleteGasRecord(gasRecordId) { - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Gas Records cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeleteGasRecordById?gasRecordId=${gasRecordId}`, function (data) { - if (data) { - hideAddGasRecordModal(); - successToast("Gas Record deleted"); - var vehicleId = GetVehicleId().vehicleId; - getVehicleGasRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function saveGasRecordToVehicle(isEdit) { - //get values - var isSimpleMode = $("#fuelEntryModeToggle").is(":checked"); - var formValues; - - if (isSimpleMode) { - formValues = getAndValidateGasRecordValuesSimple(); - } else { - formValues = getAndValidateGasRecordValues(); - } - - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SaveGasRecordToVehicleId', { gasRecord: formValues }, function (data) { - if (data) { - successToast(isEdit ? "Gas Record Updated" : "Gas Record Added."); - hideAddGasRecordModal(); - saveScrollPosition(); - getVehicleGasRecords(formValues.vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function getAndValidateGasRecordValues() { - var gasDate = $("#gasRecordDate").val(); - var gasMileage = parseInt(globalParseFloat($("#gasRecordMileage").val())).toString(); - var gasGallons = $("#gasRecordGallons").val(); - var gasCost = $("#gasRecordCost").val(); - var gasCostType = $("#gasCostType").val(); - var gasIsFillToFull = $("#gasIsFillToFull").is(":checked"); - var gasIsMissed = $("#gasIsMissed").is(":checked"); - var gasNotes = $("#gasRecordNotes").val(); - var gasTags = $("#gasRecordTag").val(); - var vehicleId = GetVehicleId().vehicleId; - var gasRecordId = getGasRecordModelData().id; - //Odometer Adjustments - if (isNaN(gasMileage) && GetVehicleId().odometerOptional) { - gasMileage = '0'; - } - gasMileage = GetAdjustedOdometer(gasRecordId, gasMileage); - //validation - var hasError = false; - var extraFields = getAndValidateExtraFields(); - if (extraFields.hasError) { - hasError = true; - } - if (gasDate.trim() == '') { //eliminates whitespace. - hasError = true; - $("#gasRecordDate").addClass("is-invalid"); - } else { - $("#gasRecordDate").removeClass("is-invalid"); - } - if (gasMileage.trim() == '' || isNaN(gasMileage) || parseInt(gasMileage) < 0) { - hasError = true; - $("#gasRecordMileage").addClass("is-invalid"); - } else { - $("#gasRecordMileage").removeClass("is-invalid"); - } - if (gasGallons.trim() == '' || globalParseFloat(gasGallons) <= 0) { - hasError = true; - $("#gasRecordGallons").addClass("is-invalid"); - } else { - $("#gasRecordGallons").removeClass("is-invalid"); - } - if (gasCostType != undefined && gasCostType == 'unit') { - var convertedGasCost = globalParseFloat(gasCost) * globalParseFloat(gasGallons); - if (isNaN(convertedGasCost)) - { - hasError = true; - $("#gasRecordCost").addClass("is-invalid"); - } else { - gasCost = globalFloatToString(convertedGasCost.toFixed(2).toString()); - $("#gasRecordCost").removeClass("is-invalid"); - } - } - if (gasCost.trim() == '' || !isValidMoney(gasCost)) { - hasError = true; - $("#gasRecordCost").addClass("is-invalid"); - } else { - $("#gasRecordCost").removeClass("is-invalid"); - } - return { - id: gasRecordId, - hasError: hasError, - vehicleId: vehicleId, - date: gasDate, - mileage: gasMileage, - gallons: gasGallons, - cost: gasCost, - files: uploadedFiles, - tags: gasTags, - isFillToFull: gasIsFillToFull, - missedFuelUp: gasIsMissed, - notes: gasNotes, - extraFields: extraFields.extraFields - } -} - -function saveUserGasTabPreferences() { - var gasUnit = $("[data-gas='consumption']").attr("data-unit"); - var fuelMileageUnit = $("[data-gas='fueleconomy']").attr("data-unit"); - $.post('/Vehicle/SaveUserGasTabPreferences', { gasUnit: gasUnit, fuelMileageUnit: fuelMileageUnit }, function (data) { - if (!data) { - errorToast("Error Saving User Preferences"); - } - }); -} - -function convertGasConsumptionUnits(currentUnit, destinationUnit, save) { - var sender = $("[data-gas='consumption']"); - if (currentUnit == "US gal") { - switch (destinationUnit) { - case "l": - $("[data-gas-type='consumption']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) * 3.785; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - sender.text(sender.text().replace(sender.attr("data-unit"), "l")); - sender.attr("data-unit", "l"); - }); - $("[data-gas-type='unitcost']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) / 3.785; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`; - }); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - case "imp gal": - $("[data-gas-type='consumption']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) / 1.201; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal")); - sender.attr("data-unit", "imp gal"); - }); - $("[data-gas-type='unitcost']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) * 1.201; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`; - }); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - } - } else if (currentUnit == "l") { - switch (destinationUnit) { - case "US gal": - $("[data-gas-type='consumption']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) / 3.785; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - sender.text(sender.text().replace(sender.attr("data-unit"), "US gal")); - sender.attr("data-unit", "US gal"); - }); - $("[data-gas-type='unitcost']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) * 3.785; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`; - }); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - case "imp gal": - $("[data-gas-type='consumption']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) / 4.546; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - sender.text(sender.text().replace(sender.attr("data-unit"), "imp gal")); - sender.attr("data-unit", "imp gal"); - }); - $("[data-gas-type='unitcost']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) * 4.546; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`; - }); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - } - } else if (currentUnit == "imp gal") { - switch (destinationUnit) { - case "US gal": - $("[data-gas-type='consumption']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) * 1.201; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - sender.text(sender.text().replace(sender.attr("data-unit"), "US gal")); - sender.attr("data-unit", "US gal"); - }); - $("[data-gas-type='unitcost']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) / 1.201; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`; - }); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - case "l": - $("[data-gas-type='consumption']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) * 4.546; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - sender.text(sender.text().replace(sender.attr("data-unit"), "l")); - sender.attr("data-unit", "l"); - }); - $("[data-gas-type='unitcost']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText) / 4.546; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - elem.innerText = `${globalAppendCurrency(globalFloatToString(convertedAmount.toFixed(decimalPoints)))}`; - }); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - } - } - updateMPGLabels(); -} - -function convertFuelMileageUnits(currentUnit, destinationUnit, save) { - var sender = $("[data-gas='fueleconomy']"); - if (currentUnit == "l/100km") { - switch (destinationUnit) { - case "km/l": - $("[data-gas-type='fueleconomy']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText); - if (convertedAmount > 0) { - convertedAmount = 100 / convertedAmount; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - } - }); - sender.text(sender.text().replace(sender.attr("data-unit"), "km/l")); - sender.attr("data-unit", "km/l"); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - } - } else if (currentUnit == "km/l") { - switch (destinationUnit) { - case "l/100km": - $("[data-gas-type='fueleconomy']").map((index, elem) => { - var convertedAmount = globalParseFloat(elem.innerText); - if (convertedAmount > 0) { - convertedAmount = 100 / convertedAmount; - elem.innerText = globalFloatToString(convertedAmount.toFixed(2)); - } - }); - sender.text(sender.text().replace(sender.attr("data-unit"), "l/100km")); - sender.attr("data-unit", "l/100km"); - if (save) { setDebounce(saveUserGasTabPreferences); } - break; - } - } - updateMPGLabels(); -} -function toggleGasFilter(sender) { - filterTable('gas-tab-pane', sender); - updateMPGLabels(); -} -function updateMPGLabels() { - var averageLabel = $("#averageFuelMileageLabel"); - var minLabel = $("#minFuelMileageLabel"); - var maxLabel = $("#maxFuelMileageLabel"); - var totalConsumedLabel = $("#totalFuelConsumedLabel"); - var totalDistanceLabel = $("#totalDistanceLabel"); - if (averageLabel.length > 0 && minLabel.length > 0 && maxLabel.length > 0 && totalConsumedLabel.length > 0 && totalDistanceLabel.length > 0) { - var rowsToAggregate = $("[data-aggregated='true']").parent(":not('.override-hide')"); - var rowsUnaggregated = $("[data-aggregated='false']").parent(":not('.override-hide')"); - var rowMPG = rowsToAggregate.children('[data-gas-type="fueleconomy"]').toArray().map(x => globalParseFloat(x.textContent)); - var rowNonZeroMPG = rowMPG.filter(x => x > 0); - var maxMPG = rowMPG.length > 0 ? rowMPG.reduce((a, b) => a > b ? a : b) : 0; - var minMPG = rowMPG.length > 0 && rowNonZeroMPG.length > 0 ? rowNonZeroMPG.reduce((a, b) => a < b ? a : b) : 0; - var totalMilesTraveled = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="mileage"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0; - var totalGasConsumed = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0; - var totalGasConsumedFV = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0; - var totalUnaggregatedGasConsumedFV = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0; - var totalMilesTraveledUnaggregated = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="mileage"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0; - var fullGasConsumed = totalGasConsumedFV + totalUnaggregatedGasConsumedFV; - var fullDistanceTraveled = totalMilesTraveled + totalMilesTraveledUnaggregated; - if (totalGasConsumed > 0 && rowNonZeroMPG.length > 0) { - var averageMPG = totalMilesTraveled / totalGasConsumed; - if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l' && averageMPG > 0) { - averageMPG = 100 / averageMPG; - } - averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString(averageMPG.toFixed(2))}`); - } else { - averageLabel.text(`${averageLabel.text().split(':')[0]}: ${globalFloatToString('0.00')}`); - } - if (fullDistanceTraveled > 0) { - totalDistanceLabel.text(`${totalDistanceLabel.text().split(':')[0]}: ${fullDistanceTraveled} ${getGasModelData().distanceUnit}`); - } else { - totalDistanceLabel.text(`${totalDistanceLabel.text().split(':')[0]}: 0 ${getGasModelData().distanceUnit}`); - } - if (fullGasConsumed > 0) { - totalConsumedLabel.text(`${totalConsumedLabel.text().split(':')[0]}: ${globalFloatToString(fullGasConsumed.toFixed(2))}`); - } else { - totalConsumedLabel.text(`${totalConsumedLabel.text().split(':')[0]}: ${globalFloatToString('0.00')}`); - } - if (!getGlobalConfig().useMPG && $("[data-gas='fueleconomy']").attr("data-unit") != 'km/l') { - maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(minMPG.toFixed(2))}`); - minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(maxMPG.toFixed(2))}`); - } - else { - minLabel.text(`${minLabel.text().split(':')[0]}: ${globalFloatToString(minMPG.toFixed(2))}`); - maxLabel.text(`${maxLabel.text().split(':')[0]}: ${globalFloatToString(maxMPG.toFixed(2))}`); - } - } -} - -function toggleUnits(sender) { - event.preventDefault(); - //check which column to convert. - sender = $(sender); - if (sender.attr("data-gas") == "consumption") { - switch (sender.attr("data-unit")) { - case "US gal": - convertGasConsumptionUnits("US gal", "l", true); - break; - case "l": - convertGasConsumptionUnits("l", "imp gal", true); - break; - case "imp gal": - convertGasConsumptionUnits("imp gal", "US gal", true); - break; - } - } else if (sender.attr("data-gas") == "fueleconomy") { - switch (sender.attr("data-unit")) { - case "l/100km": - convertFuelMileageUnits("l/100km", "km/l", true); - break; - case "km/l": - convertFuelMileageUnits("km/l", "l/100km", true); - break; - } - } -} - -function searchGasTableRows() { - var tabName = 'gas-tab-pane'; - Swal.fire({ - title: 'Search Records', - html: ` - - `, - confirmButtonText: 'Search', - focusConfirm: false, - preConfirm: () => { - const searchString = $("#inputSearch").val(); - return { searchString } - }, - }).then(function (result) { - if (result.isConfirmed) { - var rowData = $(`#${tabName} table tbody tr`); - var filteredRows = $(`#${tabName} table tbody tr td:contains('${result.value.searchString}')`).parent(); - var splitSearchString = result.value.searchString.split('='); - if (result.value.searchString.includes('=') && splitSearchString.length == 2) { - //column specific search. - //get column index - var columns = $(`#${tabName} table th`).toArray().map(x => x.innerText); - var columnName = splitSearchString[0]; - var colSearchString = splitSearchString[1]; - var colIndex = columns.findIndex(x => x == columnName) + 1; - filteredRows = $(`#${tabName} table tbody tr td:nth-child(${colIndex}):contains('${colSearchString}')`).parent(); - } - if (result.value.searchString.trim() == '') { - rowData.removeClass('override-hide'); - } else { - rowData.addClass('override-hide'); - filteredRows.removeClass('override-hide'); - } - $(".tagfilter.bg-primary").addClass('bg-secondary').removeClass('bg-primary'); - updateAggregateLabels(); - updateMPGLabels(); - } - }); -} -function editMultipleGasRecords(ids) { - if (ids.length < 2) { - return; - } - $.post('/Vehicle/GetGasRecordsEditModal', { recordIds: ids }, function (data) { - if (data) { - $("#gasRecordModalContent").html(data); - - // Initialize mobile experience using shared framework - initializeGasRecordMobile(); - - $('#gasRecordModal').modal('show'); - } - }); -} -function saveMultipleGasRecordsToVehicle() { - var gasDate = $("#gasRecordDate").val(); - var gasMileage = $("#gasRecordMileage").val(); - var gasMileageToParse = parseInt(globalParseFloat($("#gasRecordMileage").val())).toString(); - var gasConsumption = $("#gasRecordConsumption").val(); - var gasCost = $("#gasRecordCost").val(); - var gasNotes = $("#gasRecordNotes").val(); - var gasTags = $("#gasRecordTag").val(); - var gasExtraFields = getAndValidateExtraFields(); - //validation - var hasError = false; - if (gasMileage.trim() != '' && (isNaN(gasMileageToParse) || parseInt(gasMileageToParse) < 0)) { - hasError = true; - $("#gasRecordMileage").addClass("is-invalid"); - } else { - $("#gasRecordMileage").removeClass("is-invalid"); - } - if (gasConsumption.trim() != '' && !isValidMoney(gasConsumption)) { - hasError = true; - $("#gasRecordConsumption").addClass("is-invalid"); - } else { - $("#gasRecordConsumption").removeClass("is-invalid"); - } - if (gasCost.trim() != '' && !isValidMoney(gasCost)) { - hasError = true; - $("#gasRecordCost").addClass("is-invalid"); - } else { - $("#gasRecordCost").removeClass("is-invalid"); - } - if (hasError) { - errorToast("Please check the form data"); - return; - } - var formValues = { - recordIds: recordsToEdit, - editRecord: { - date: gasDate, - mileage: gasMileageToParse, - gallons: gasConsumption, - cost: gasCost, - notes: gasNotes, - tags: gasTags, - extraFields: gasExtraFields.extraFields - } - } - $.post('/Vehicle/SaveMultipleGasRecords', { editModel: formValues }, function (data) { - if (data) { - successToast("Gas Records Updated"); - hideAddGasRecordModal(); - saveScrollPosition(); - getVehicleGasRecords(GetVehicleId().vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }) -} - -// Simple/Advanced fuel entry mode toggle functions -function toggleFuelEntryMode(useSimpleMode) { - if (useSimpleMode) { - // Switch to simple mode - $("#simpleModeFields").show(); - $("#advancedModeFields").hide(); - $("#secondColumnFields").hide(); - - // Initialize simple mode values if editing existing record - var existingCost = $("#gasRecordCost").val(); - var existingGallons = $("#gasRecordGallons").val(); - if (existingCost && existingGallons && globalParseFloat(existingGallons) > 0) { - var unitCost = globalParseFloat(existingCost) / globalParseFloat(existingGallons); - $("#gasRecordUnitCost").val(globalFloatToString(unitCost.toFixed(3))); - } - } else { - // Switch to advanced mode - $("#simpleModeFields").hide(); - $("#advancedModeFields").show(); - $("#secondColumnFields").show(); - } - - // Save user preference - $.post('/Vehicle/SaveSimpleFuelEntryPreference', { useSimpleFuelEntry: useSimpleMode }, function (data) { - if (!data) { - errorToast("Error saving preference"); - } - }); -} - -// Auto-calculate total cost based on unit cost and gallons -function calculateTotalCost() { - var unitCost = globalParseFloat($("#gasRecordUnitCost").val()); - var gallons = globalParseFloat($("#gasRecordGallons").val()); - - if (!isNaN(unitCost) && !isNaN(gallons) && unitCost > 0 && gallons > 0) { - var totalCost = unitCost * gallons; - var decimalPoints = getGlobalConfig().useThreeDecimals ? 3 : 2; - $("#gasRecordCost").val(globalFloatToString(totalCost.toFixed(decimalPoints))); - } else { - $("#gasRecordCost").val(""); - } -} - -// Simple mode validation function -function getAndValidateGasRecordValuesSimple() { - var gasDate = $("#simpleGasRecordDate").val(); - var gasMileage = parseInt(globalParseFloat($("#gasRecordMileage").val())).toString(); - var gasGallons = $("#gasRecordGallons").val(); - var gasCost = $("#gasRecordCost").val(); - var gasIsFillToFull = $("#simpleGasIsFillToFull").val() === "true"; - var gasIsMissed = $("#simpleGasIsMissed").val() === "true"; - var gasNotes = $("#simpleGasRecordNotes").val(); - var gasTags = $("#simpleGasRecordTag").val() ? [$("#simpleGasRecordTag").val()] : []; - var vehicleId = GetVehicleId().vehicleId; - var gasRecordId = getGasRecordModelData().id; - - //Odometer Adjustments - if (isNaN(gasMileage) && GetVehicleId().odometerOptional) { - gasMileage = '0'; - } - gasMileage = GetAdjustedOdometer(gasRecordId, gasMileage); - - // Validation for simple mode - var hasError = false; - - if (gasMileage.trim() == '' || isNaN(gasMileage) || parseInt(gasMileage) < 0) { - hasError = true; - $("#gasRecordMileage").addClass("is-invalid"); - } else { - $("#gasRecordMileage").removeClass("is-invalid"); - } - - if (gasGallons.trim() == '' || globalParseFloat(gasGallons) <= 0) { - hasError = true; - $("#gasRecordGallons").addClass("is-invalid"); - } else { - $("#gasRecordGallons").removeClass("is-invalid"); - } - - if (gasCost.trim() == '' || !isValidMoney(gasCost)) { - hasError = true; - $("#gasRecordCost").addClass("is-invalid"); - } else { - $("#gasRecordCost").removeClass("is-invalid"); - } - - return { - id: gasRecordId, - hasError: hasError, - vehicleId: vehicleId, - date: gasDate, - mileage: gasMileage, - gallons: gasGallons, - cost: gasCost, - files: [], // Simple mode doesn't support file uploads - tags: gasTags, - isFillToFull: gasIsFillToFull, - missedFuelUp: gasIsMissed, - notes: gasNotes, - extraFields: [] // Simple mode doesn't use extra fields - }; -} \ No newline at end of file diff --git a/wwwroot/js/loader.js b/wwwroot/js/loader.js deleted file mode 100644 index c5666e4..0000000 --- a/wwwroot/js/loader.js +++ /dev/null @@ -1,10 +0,0 @@ -const sloader = { - show: function () { - var sLoaderElement = `
    ` - $("body").append(sLoaderElement); - }, - hide: function () { - $(".sloader").remove(); - } - -} \ No newline at end of file diff --git a/wwwroot/js/login.js b/wwwroot/js/login.js deleted file mode 100644 index aa0c534..0000000 --- a/wwwroot/js/login.js +++ /dev/null @@ -1,91 +0,0 @@ -function performLogin() { - var userName = $("#inputUserName").val(); - var userPassword = $("#inputUserPassword").val(); - var isPersistent = $("#inputPersistent").is(":checked"); - $.post('/Login/Login', {userName: userName, password: userPassword, isPersistent: isPersistent}, function (data) { - if (data) { - //check for redirectURL - var redirectURL = getRedirectURL().url; - if (redirectURL.trim() != "") { - window.location.href = redirectURL; - } else { - window.location.href = '/Home'; - } - } else { - errorToast("Invalid Login Credentials, please try again."); - } - }) -} -function performRegistration() { - var token = $("#inputToken").val(); - var userName = $("#inputUserName").val(); - var userPassword = $("#inputUserPassword").val(); - var userEmail = $("#inputEmail").val(); - $.post('/Login/Register', { userName: userName, password: userPassword, token: token, emailAddress: userEmail }, function (data) { - if (data.success) { - successToast(data.message); - setTimeout(function () { window.location.href = '/Login/Index' }, 500); - } else { - errorToast(data.message); - } - }); -} -function requestPasswordReset() { - var userName = $("#inputUserName").val(); - $.post('/Login/RequestResetPassword', { userName: userName }, function (data) { - if (data.success) { - successToast(data.message); - setTimeout(function () { window.location.href = '/Login/Index' }, 500); - } else { - errorToast(data.message); - } - }) -} -function performPasswordReset() { - var token = $("#inputToken").val(); - var userPassword = $("#inputUserPassword").val(); - var userEmail = $("#inputEmail").val(); - $.post('/Login/PerformPasswordReset', { password: userPassword, token: token, emailAddress: userEmail }, function (data) { - if (data.success) { - successToast(data.message); - setTimeout(function () { window.location.href = '/Login/Index' }, 500); - } else { - errorToast(data.message); - } - }); -} - -function remoteLogin() { - $.get('/Login/GetRemoteLoginLink', function (data) { - if (data) { - window.location.href = data; - } - }) -} -function sendRegistrationToken() { - Swal.fire({ - title: 'Please Provide an Email Address', - html: ` - - `, - confirmButtonText: 'Send', - focusConfirm: false, - preConfirm: () => { - const tokenEmail = $("#inputTokenEmail").val(); - if (!tokenEmail || tokenEmail.trim() == '') { - Swal.showValidationMessage(`Please enter a valid email address`); - } - return { tokenEmail } - }, - }).then(function (result) { - if (result.isConfirmed) { - $.post('/Login/SendRegistrationToken', { emailAddress: result.value.tokenEmail }, function (data) { - if (data.success) { - successToast(data.message); - } else { - errorToast(data.message); - } - }); - } - }); -} \ No newline at end of file diff --git a/wwwroot/js/note.js b/wwwroot/js/note.js deleted file mode 100644 index ac1a62d..0000000 --- a/wwwroot/js/note.js +++ /dev/null @@ -1,129 +0,0 @@ -function showAddNoteModal() { - $.get('/Vehicle/GetAddNotePartialView', function (data) { - if (data) { - $("#noteModalContent").html(data); - initTagSelector($("#noteRecordTag")); - $('#noteModal').modal('show'); - } - }); -} -function showEditNoteModal(noteId, nocache) { - if (!nocache) { - var existingContent = $("#noteModalContent").html(); - if (existingContent.trim() != '') { - //check if id is same. - var existingId = getNoteModelData().id; - if (existingId == noteId && $('[data-changed=true]').length > 0) { - $('#noteModal').modal('show'); - $('.cached-banner').show(); - return; - } - } - } - $.get(`/Vehicle/GetNoteForEditById?noteId=${noteId}`, function (data) { - if (data) { - $("#noteModalContent").html(data); - initTagSelector($("#noteRecordTag")); - $('#noteModal').modal('show'); - bindModalInputChanges('noteModal'); - $('#noteModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("noteTextArea"); - } - }); - } - }); -} -function hideAddNoteModal() { - $('#noteModal').modal('hide'); -} -function deleteNote(noteId) { - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Notes cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeleteNoteById?noteId=${noteId}`, function (data) { - if (data) { - hideAddNoteModal(); - successToast("Note Deleted"); - var vehicleId = GetVehicleId().vehicleId; - getVehicleNotes(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function saveNoteToVehicle(isEdit) { - //get values - var formValues = getAndValidateNoteValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SaveNoteToVehicleId', { note: formValues }, function (data) { - if (data) { - successToast(isEdit ? "Note Updated" : "Note Added."); - hideAddNoteModal(); - saveScrollPosition(); - getVehicleNotes(formValues.vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function getAndValidateNoteValues() { - var noteDescription = $("#noteDescription").val(); - var noteText = $("#noteTextArea").val(); - var vehicleId = GetVehicleId().vehicleId; - var noteId = getNoteModelData().id; - var noteIsPinned = $("#noteIsPinned").is(":checked"); - var noteTags = $("#noteRecordTag").val(); - //validation - var hasError = false; - var extraFields = getAndValidateExtraFields(); - if (extraFields.hasError) { - hasError = true; - } - if (noteDescription.trim() == '') { //eliminates whitespace. - hasError = true; - $("#noteDescription").addClass("is-invalid"); - } else { - $("#noteDescription").removeClass("is-invalid"); - } - if (noteText.trim() == '') { - hasError = true; - $("#noteTextArea").addClass("is-invalid"); - } else { - $("#noteTextArea").removeClass("is-invalid"); - } - return { - id: noteId, - hasError: hasError, - vehicleId: vehicleId, - description: noteDescription, - noteText: noteText, - files: uploadedFiles, - pinned: noteIsPinned, - tags: noteTags, - extraFields: extraFields.extraFields - } -} -function pinNotes(ids, toggle, pinStatus) { - $.post('/Vehicle/PinNotes', { noteIds: ids, isToggle: toggle, pinStatus: pinStatus }, function (data) { - if (data) { - successToast(ids.length > 1 ? `${ids.length} Notes Updated` : "Note Updated."); - getVehicleNotes(GetVehicleId().vehicleId); - } - }) -} \ No newline at end of file diff --git a/wwwroot/js/odometerrecord.js b/wwwroot/js/odometerrecord.js deleted file mode 100644 index 497b89d..0000000 --- a/wwwroot/js/odometerrecord.js +++ /dev/null @@ -1,394 +0,0 @@ -function showAddOdometerRecordModal() { - $.get(`/Vehicle/GetAddOdometerRecordPartialView?vehicleId=${GetVehicleId().vehicleId}`, function (data) { - if (data) { - $("#odometerRecordModalContent").html(data); - //initiate datepicker - initDatePicker($('#odometerRecordDate')); - initTagSelector($("#odometerRecordTag")); - $('#odometerRecordModal').modal('show'); - } - }); -} -function showEditOdometerRecordModal(odometerRecordId, nocache) { - if (!nocache) { - var existingContent = $("#odometerRecordModalContent").html(); - if (existingContent.trim() != '') { - //check if id is same. - var existingId = getOdometerRecordModelData().id; - if (existingId == odometerRecordId && $('[data-changed=true]').length > 0) { - $('#odometerRecordModal').modal('show'); - $('.cached-banner').show(); - return; - } - } - } - $.get(`/Vehicle/GetOdometerRecordForEditById?odometerRecordId=${odometerRecordId}`, function (data) { - if (data) { - $("#odometerRecordModalContent").html(data); - //initiate datepicker - initDatePicker($('#odometerRecordDate')); - initTagSelector($("#odometerRecordTag")); - $('#odometerRecordModal').modal('show'); - bindModalInputChanges('odometerRecordModal'); - $('#odometerRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("odometerRecordNotes"); - } - }); - } - }); -} -function hideAddOdometerRecordModal() { - $('#odometerRecordModal').modal('hide'); -} -function deleteOdometerRecord(odometerRecordId) { - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Odometer Records cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeleteOdometerRecordById?odometerRecordId=${odometerRecordId}`, function (data) { - if (data) { - hideAddOdometerRecordModal(); - successToast("Odometer Record Deleted"); - var vehicleId = GetVehicleId().vehicleId; - getVehicleOdometerRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function saveOdometerRecordToVehicle(isEdit) { - //get values - var formValues = getAndValidateOdometerRecordValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SaveOdometerRecordToVehicleId', { odometerRecord: formValues }, function (data) { - if (data) { - successToast(isEdit ? "Odometer Record Updated" : "Odometer Record Added."); - hideAddOdometerRecordModal(); - saveScrollPosition(); - getVehicleOdometerRecords(formValues.vehicleId); - if (formValues.addReminderRecord) { - setTimeout(function () { showAddReminderModal(formValues); }, 500); - } - } else { - errorToast(genericErrorMessage()); - } - }) -} -function getAndValidateOdometerRecordValues() { - var serviceDate = $("#odometerRecordDate").val(); - var initialOdometerMileage = parseInt(globalParseFloat($("#initialOdometerRecordMileage").val())).toString(); - var serviceMileage = parseInt(globalParseFloat($("#odometerRecordMileage").val())).toString(); - var serviceNotes = $("#odometerRecordNotes").val(); - var serviceTags = $("#odometerRecordTag").val(); - var vehicleId = GetVehicleId().vehicleId; - var odometerRecordId = getOdometerRecordModelData().id; - //Odometer Adjustments - serviceMileage = GetAdjustedOdometer(odometerRecordId, serviceMileage); - //validation - var hasError = false; - var extraFields = getAndValidateExtraFields(); - if (extraFields.hasError) { - hasError = true; - } - if (serviceDate.trim() == '') { //eliminates whitespace. - hasError = true; - $("#odometerRecordDate").addClass("is-invalid"); - } else { - $("#odometerRecordDate").removeClass("is-invalid"); - } - if (serviceMileage.trim() == '' || isNaN(serviceMileage) || parseInt(serviceMileage) < 0) { - hasError = true; - $("#odometerRecordMileage").addClass("is-invalid"); - } else { - $("#odometerRecordMileage").removeClass("is-invalid"); - } - if (isNaN(initialOdometerMileage) || parseInt(initialOdometerMileage) < 0) { - hasError = true; - $("#initialOdometerRecordMileage").addClass("is-invalid"); - } else { - $("#initialOdometerRecordMileage").removeClass("is-invalid"); - } - return { - id: odometerRecordId, - hasError: hasError, - vehicleId: vehicleId, - date: serviceDate, - initialMileage: initialOdometerMileage, - mileage: serviceMileage, - notes: serviceNotes, - tags: serviceTags, - files: uploadedFiles, - extraFields: extraFields.extraFields - } -} - -function recalculateDistance() { - //force distance recalculation - //reserved for when data is incoherent with negative distances due to non-chronological order of odometer records. - var vehicleId = GetVehicleId().vehicleId - $.post(`/Vehicle/ForceRecalculateDistanceByVehicleId?vehicleId=${vehicleId}`, function (data) { - if (data) { - successToast("Odometer Records Updated") - getVehicleOdometerRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); -} - -function editMultipleOdometerRecords(ids) { - if (ids.length < 2) { - return; - } - $.post('/Vehicle/GetOdometerRecordsEditModal', { recordIds: ids }, function (data) { - if (data) { - $("#odometerRecordModalContent").html(data); - //initiate datepicker - initDatePicker($('#odometerRecordDate')); - initTagSelector($("#odometerRecordTag")); - $('#odometerRecordModal').modal('show'); - } - }); -} -function saveMultipleOdometerRecordsToVehicle() { - var odometerDate = $("#odometerRecordDate").val(); - var initialOdometerMileage = $("#initialOdometerRecordMileage").val(); - var odometerMileage = $("#odometerRecordMileage").val(); - var initialOdometerMileageToParse = parseInt(globalParseFloat($("#initialOdometerRecordMileage").val())).toString(); - var odometerMileageToParse = parseInt(globalParseFloat($("#odometerRecordMileage").val())).toString(); - var odometerNotes = $("#odometerRecordNotes").val(); - var odometerTags = $("#odometerRecordTag").val(); - var odometerExtraFields = getAndValidateExtraFields(); - //validation - var hasError = false; - if (odometerMileage.trim() != '' && (isNaN(odometerMileageToParse) || parseInt(odometerMileageToParse) < 0)) { - hasError = true; - $("#odometerRecordMileage").addClass("is-invalid"); - } else { - $("#odometerRecordMileage").removeClass("is-invalid"); - } - if (initialOdometerMileage.trim() != '' && (isNaN(initialOdometerMileageToParse) || parseInt(initialOdometerMileageToParse) < 0)) { - hasError = true; - $("#odometerRecordMileage").addClass("is-invalid"); - } else { - $("#odometerRecordMileage").removeClass("is-invalid"); - } - if (hasError) { - errorToast("Please check the form data"); - return; - } - var formValues = { - recordIds: recordsToEdit, - editRecord: { - date: odometerDate, - initialMileage: initialOdometerMileageToParse, - mileage: odometerMileageToParse, - notes: odometerNotes, - tags: odometerTags, - extraFields: odometerExtraFields.extraFields - } - } - $.post('/Vehicle/SaveMultipleOdometerRecords', { editModel: formValues }, function (data) { - if (data) { - successToast("Odometer Records Updated"); - hideAddOdometerRecordModal(); - saveScrollPosition(); - getVehicleOdometerRecords(GetVehicleId().vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function toggleInitialOdometerEnabled() { - if ($("#initialOdometerRecordMileage").prop("disabled")) { - $("#initialOdometerRecordMileage").prop("disabled", false); - } else { - $("#initialOdometerRecordMileage").prop("disabled", true); - } - -} -function showTripModal() { - $(".odometer-modal").addClass('d-none'); - $(".trip-modal").removeClass('d-none'); - //set current odometer - $(".trip-odometer").text($("#initialOdometerRecordMileage").val()); -} -function hideTripModal() { - //check if recording is in progress - if (tripTimer != undefined || tripWakeLock != undefined) { - Swal.fire({ - title: "Confirm Exit?", - text: "Recording in Progress, Exit?", - showCancelButton: true, - confirmButtonText: "Exit", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - stopRecording(); - $(".odometer-modal").removeClass('d-none'); - $(".trip-modal").addClass('d-none'); - } - }); - } else { - $(".odometer-modal").removeClass('d-none'); - $(".trip-modal").addClass('d-none'); - } -} -function startRecording() { - if (navigator.geolocation && navigator.wakeLock) { - try { - navigator.wakeLock.request('screen').then((wl) => { - tripWakeLock = wl; - tripTimer = setInterval(() => { - navigator.geolocation.getCurrentPosition(recordPosition, stopRecording, { maximumAge: 1000, timeout: 4000, enableHighAccuracy: true }); - }, 5000); - $(".trip-start").addClass('d-none'); - $(".trip-stop").removeClass('d-none'); - //modify modal to prevent closing - $("#odometerRecordModal").on("hide.bs.modal", function (event) { - event.preventDefault(); - hideTripModal(); - }); - }); - } catch (err) { - errorToast('Location Services not Enabled'); - } - } else { - errorToast('Browser does not support GeoLocation and/or WakeLock API'); - } -} -function recordPosition(position) { - var currentLat = position.coords.latitude; - var currentLong = position.coords.longitude; - if (tripLastPosition == undefined) { - tripLastPosition = { - latitude: currentLat, - longitude: currentLong - } - tripCoordinates.push(`${currentLat},${currentLong}`); - } else { - //calculate distance - var distanceTraveled = calculateDistance(tripLastPosition.latitude, tripLastPosition.longitude, currentLat, currentLong); - var recordedTotalOdometer = getRecordedOdometer(); - if (distanceTraveled >= 0.1) { //if greater than 0.1 mile or KM then it's significant - recordedTotalOdometer += distanceTraveled; - var recordedOdometerString = recordedTotalOdometer.toString().split('.'); - $(".trip-odometer").html(recordedOdometerString[0]); - if (recordedOdometerString.length == 2) { - if (recordedOdometerString[1].toString().length > 3) { - $(".trip-odometer-sub").html(recordedOdometerString[1].toString().substring(0, 3)); - } else { - $(".trip-odometer-sub").html(recordedOdometerString[1].toString()); - } - $(".trip-odometer-sub").attr("data-value", recordedOdometerString[1]); - } - //update last position - tripLastPosition = { - latitude: currentLat, - longitude: currentLong - } - tripCoordinates.push(`${currentLat},${currentLong}`); - } - } -} -function stopRecording(errMsg) { - if (errMsg && errMsg.code) { - switch (errMsg.code) { - case 1: - errorToast(errMsg.message); - break; - case 2: - errorToast("Location Unavailable"); - break; - } - } - if (tripTimer != undefined) { - clearInterval(tripTimer); - tripTimer = undefined; - } - if (tripWakeLock != undefined) { - tripWakeLock.release(); - tripWakeLock = undefined; - } - if (tripLastPosition != undefined) { - tripLastPosition = undefined; - } - $(".trip-start").removeClass('d-none'); - $(".trip-stop").addClass('d-none'); - $("#odometerRecordModal").off("hide.bs.modal"); - if (parseInt(getRecordedOdometer()) != $("#initialOdometerRecordMileage").val()) { - $(".trip-save").removeClass('d-none'); - } -} -// Converts numeric degrees to radians -function toRad(Value) { - return Value * Math.PI / 180; -} -//haversine -function calculateDistance(lat1, lon1, lat2, lon2) { - var earthRadius = 6371; // km radius of the earth - var dLat = toRad(lat2 - lat1); - var dLon = toRad(lon2 - lon1); - var lat1 = toRad(lat1); - var lat2 = toRad(lat2); - - var sinOne = Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); - var tanOne = 2 * Math.atan2(Math.sqrt(sinOne), Math.sqrt(1 - sinOne)); - var calculatedDistance = earthRadius * tanOne; - if (getGlobalConfig().useMPG) { - calculatedDistance *= 0.621; //convert to mile if needed. - } - return Math.abs(calculatedDistance); -} -function getRecordedOdometer() { - var recordedOdometer = $(".trip-odometer").html(); - var recordedSubOdometer = $(".trip-odometer-sub").attr("data-value"); - return parseFloat(`${recordedOdometer}.${recordedSubOdometer}`); -} -function saveRecordedOdometer() { - //save coordinates into a CSV file and upload - if (tripCoordinates.length > 1) { - //update current odometer value - $("#odometerRecordMileage").val(parseInt(getRecordedOdometer()).toString()); - //generate attachment - $.post('/Files/UploadCoordinates', { coordinates: tripCoordinates }, function (response) { - uploadedFiles.push(response); - $.post('/Vehicle/GetFilesPendingUpload', { uploadedFiles: uploadedFiles }, function (viewData) { - $("#filesPendingUpload").html(viewData); - tripCoordinates = ["Latitude,Longitude"]; - }); - }); - } - hideTripModal(); -} -function toggleSubOdometer() { - if ($(".trip-odometer-sub").hasClass("d-none")) { - $(".trip-odometer-sub").removeClass("d-none"); - } else { - $(".trip-odometer-sub").addClass("d-none"); - } -} -function checkTripRecorder() { - //check if connection is https, browser supports required API, and that vehicle does not use engine hours - if (location.protocol != 'https:' || !navigator.geolocation || !navigator.wakeLock || GetVehicleId().useEngineHours) { - $(".trip-show").remove(); - } else { - $(".trip-show").removeClass('d-none'); - } -} \ No newline at end of file diff --git a/wwwroot/js/planrecord.js b/wwwroot/js/planrecord.js deleted file mode 100644 index abc0322..0000000 --- a/wwwroot/js/planrecord.js +++ /dev/null @@ -1,460 +0,0 @@ -function showAddPlanRecordModal() { - $.get('/Vehicle/GetAddPlanRecordPartialView', function (data) { - if (data) { - $("#planRecordModalContent").html(data); - //initiate datepicker - initDatePicker($('#planRecordDate')); - $('#planRecordModal').modal('show'); - } - }); -} -function showEditPlanRecordModal(planRecordId, nocache) { - if (!nocache) { - var existingContent = $("#planRecordModalContent").html(); - if (existingContent.trim() != '') { - //check if id is same. - var existingId = getPlanRecordModelData().id; - var isNotTemplate = !getPlanRecordModelData().isTemplate; - if (existingId == planRecordId && isNotTemplate && $('[data-changed=true]').length > 0) { - $('#planRecordModal').modal('show'); - $('.cached-banner').show(); - return; - } - } - } - $.get(`/Vehicle/GetPlanRecordForEditById?planRecordId=${planRecordId}`, function (data) { - if (data) { - $("#planRecordModalContent").html(data); - //initiate datepicker - initDatePicker($('#planRecordDate')); - $('#planRecordModal').modal('show'); - bindModalInputChanges('planRecordModal'); - $('#planRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("planRecordNotes"); - } - }); - } - }); -} -function showEditPlanRecordTemplateModal(planRecordTemplateId, nocache) { - hidePlanRecordTemplatesModal(); - if (!nocache) { - var existingContent = $("#planRecordModalContent").html(); - if (existingContent.trim() != '') { - //check if id is same. - var existingId = getPlanRecordModelData().id; - var isTemplate = getPlanRecordModelData().isTemplate; - if (existingId == planRecordTemplateId && isTemplate && $('[data-changed=true]').length > 0) { - $('#planRecordModal').modal('show'); - $('.cached-banner').show(); - return; - } - } - } - $.get(`/Vehicle/GetPlanRecordTemplateForEditById?planRecordTemplateId=${planRecordTemplateId}`, function (data) { - if (data) { - $("#planRecordModalContent").html(data); - //initiate datepicker - initDatePicker($('#planRecordDate')); - $('#planRecordModal').modal('show'); - bindModalInputChanges('planRecordModal'); - $('#planRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("planRecordNotes"); - } - }); - } - }); -} -function hideAddPlanRecordModal() { - $('#planRecordModal').modal('hide'); - if (getPlanRecordModelData().createdFromReminder) { - //show reminder Modal - $("#reminderRecordModal").modal("show"); - } - if (getPlanRecordModelData().isTemplate) { - showPlanRecordTemplatesModal(); - } -} -function deletePlanRecord(planRecordId, noModal) { - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Plan Records cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeletePlanRecordById?planRecordId=${planRecordId}`, function (data) { - if (data) { - if (!noModal) { - hideAddPlanRecordModal(); - } - successToast("Plan Record Deleted"); - var vehicleId = GetVehicleId().vehicleId; - getVehiclePlanRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function savePlanRecordToVehicle(isEdit) { - //get values - var formValues = getAndValidatePlanRecordValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SavePlanRecordToVehicleId', { planRecord: formValues }, function (data) { - if (data) { - successToast(isEdit ? "Plan Record Updated" : "Plan Record Added."); - hideAddPlanRecordModal(); - if (!getPlanRecordModelData().createdFromReminder) { - saveScrollPosition(); - getVehiclePlanRecords(formValues.vehicleId); - if (formValues.addReminderRecord) { - setTimeout(function () { showAddReminderModal(formValues); }, 500); - } - } - } else { - errorToast(genericErrorMessage()); - } - }) -} -function showPlanRecordTemplatesModal() { - var vehicleId = GetVehicleId().vehicleId; - $.get(`/Vehicle/GetPlanRecordTemplatesForVehicleId?vehicleId=${vehicleId}`, function (data) { - if (data) { - $("#planRecordTemplateModalContent").html(data); - $('#planRecordTemplateModal').modal('show'); - } - }); -} -function hidePlanRecordTemplatesModal() { - $('#planRecordTemplateModal').modal('hide'); -} -function usePlannerRecordTemplate(planRecordTemplateId) { - $.post(`/Vehicle/ConvertPlanRecordTemplateToPlanRecord?planRecordTemplateId=${planRecordTemplateId}`, function (data) { - if (data.success) { - var vehicleId = GetVehicleId().vehicleId; - successToast(data.message); - hidePlanRecordTemplatesModal(); - saveScrollPosition(); - getVehiclePlanRecords(vehicleId); - } else { - if (data.message == "Insufficient Supplies") { - data.message += `

    Order Required Supplies` - } - errorToast(data.message); - } - }); -} - -function deletePlannerRecordTemplate(planRecordTemplateId) { - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Plan Templates cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeletePlanRecordTemplateById?planRecordTemplateId=${planRecordTemplateId}`, function (data) { - $("#workAroundInput").hide(); - if (data) { - successToast("Plan Template Deleted"); - hideAddPlanRecordModal(); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function savePlanRecordTemplate(isEdit) { - //get values - var formValues = getAndValidatePlanRecordValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SavePlanRecordTemplateToVehicleId', { planRecord: formValues }, function (data) { - if (data.success) { - if (isEdit) { - hideAddPlanRecordModal(); - showPlanRecordTemplatesModal(); - $('[data-changed=true]').attr('data-changed', false) - successToast('Plan Template Updated'); - } else { - successToast('Plan Template Added'); - } - } else { - errorToast(data.message); - } - }) -} -function getAndValidatePlanRecordValues() { - var planDescription = $("#planRecordDescription").val(); - var planCost = $("#planRecordCost").val(); - var planNotes = $("#planRecordNotes").val(); - var planType = $("#planRecordType").val(); - var planPriority = $("#planRecordPriority").val(); - var planProgress = $("#planRecordProgress").val(); - var planDateCreated = getPlanRecordModelData().dateCreated; - var vehicleId = GetVehicleId().vehicleId; - var planRecordId = getPlanRecordModelData().id; - var reminderRecordId = getPlanRecordModelData().reminderRecordId; - //validation - var hasError = false; - var extraFields = getAndValidateExtraFields(); - if (extraFields.hasError) { - hasError = true; - } - if (planDescription.trim() == '') { - hasError = true; - $("#planRecordDescription").addClass("is-invalid"); - } else { - $("#planRecordDescription").removeClass("is-invalid"); - } - if (planCost.trim() == '' || !isValidMoney(planCost)) { - hasError = true; - $("#planRecordCost").addClass("is-invalid"); - } else { - $("#planRecordCost").removeClass("is-invalid"); - } - return { - id: planRecordId, - hasError: hasError, - vehicleId: vehicleId, - dateCreated: planDateCreated, - description: planDescription, - cost: planCost, - notes: planNotes, - files: uploadedFiles, - supplies: selectedSupplies, - priority: planPriority, - progress: planProgress, - importMode: planType, - extraFields: extraFields.extraFields, - requisitionHistory: supplyUsageHistory, - deletedRequisitionHistory: deletedSupplyUsageHistory, - reminderRecordId: reminderRecordId, - copySuppliesAttachment: copySuppliesAttachments - } -} -//drag and drop stuff. - -let dragged = null; -let draggedId = 0; -function dragEnter(event) { - event.preventDefault(); -} -function dragStart(event, planRecordId) { - dragged = event.target; - draggedId = planRecordId; - event.dataTransfer.setData('text/plain', draggedId); -} -function dragOver(event) { - event.preventDefault(); - if (planTouchTimer) { - clearTimeout(planTouchTimer); - planTouchTimer = null; - } -} -function dropBox(event, newProgress) { - var targetSwimLane = $(event.target).hasClass("swimlane") ? event.target : $(event.target).parents(".swimlane")[0]; - var draggedSwimLane = $(dragged).parents(".swimlane")[0]; - if (targetSwimLane != draggedSwimLane) { - updatePlanRecordProgress(newProgress); - } - event.preventDefault(); -} -function updatePlanRecordProgress(newProgress) { - if (draggedId > 0) { - if (newProgress == 'Done') { - //if user is marking the task as done, we will want them to enter the mileage so that we can auto-convert it. - Swal.fire({ - title: 'Mark Task as Done?', - html: `

    To confirm, please enter the current odometer reading on your vehicle, as we also need the current odometer to auto convert the task into the relevant record.

    - - `, - confirmButtonText: 'Confirm', - showCancelButton: true, - focusConfirm: false, - preConfirm: () => { - var odometer = $("#inputOdometer").val(); - if (odometer.trim() == '' && GetVehicleId().odometerOptional) { - odometer = '0'; - } - if (!odometer || isNaN(odometer)) { - Swal.showValidationMessage(`Please enter an odometer reading`) - } - return { odometer } - }, - }).then(function (result) { - if (result.isConfirmed) { - //Odometer Adjustments - var adjustedOdometer = GetAdjustedOdometer(0, result.value.odometer); - $.post('/Vehicle/UpdatePlanRecordProgress', { planRecordId: draggedId, planProgress: newProgress, odometer: adjustedOdometer }, function (data) { - if (data) { - successToast("Plan Progress Updated"); - var vehicleId = GetVehicleId().vehicleId; - getVehiclePlanRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } - draggedId = 0; - }); - } else { - $.post('/Vehicle/UpdatePlanRecordProgress', { planRecordId: draggedId, planProgress: newProgress }, function (data) { - if (data) { - successToast("Plan Progress Updated"); - var vehicleId = GetVehicleId().vehicleId; - getVehiclePlanRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - draggedId = 0; - } - } -} -function orderPlanSupplies(planRecordTemplateId, closeSwal) { - if (closeSwal) { - Swal.close(); - } - $.get(`/Vehicle/OrderPlanSupplies?planRecordTemplateId=${planRecordTemplateId}`, function (data) { - if (data.success != undefined && !data.success) { - //success is provided. - errorToast(data.message); - } else { - //hide plan record template modal. - hidePlanRecordTemplatesModal(); - $("#planRecordTemplateSupplyOrderModalContent").html(data); - $("#planRecordTemplateSupplyOrderModal").modal('show'); - } - }) -} -function hideOrderSupplyModal() { - $("#planRecordTemplateSupplyOrderModal").modal('hide'); - showPlanRecordTemplatesModal(); -} -function configurePlanTableContextMenu(planRecordId, currentSwimLane) { - //clear any bound actions - $(".context-menu-move").off('click'); - //bind context menu actions - $(".context-menu-delete").on('click', () => { - deletePlanRecord(planRecordId, true); - }); - let planRecordIdArray = [planRecordId]; - $(".context-menu-print-tab-sticker").on('click', () => { - printTabStickers(planRecordIdArray, 'PlanRecord'); - }); - $(".context-menu-duplicate").on('click', () => { - duplicateRecords(planRecordIdArray, 'PlanRecord'); - }); - $(".context-menu-duplicate-vehicle").on('click', () => { - duplicateRecordsToOtherVehicles(planRecordIdArray, 'PlanRecord'); - }); - $(".context-menu-move.move-planned").on('click', () => { - draggedId = planRecordId; - updatePlanRecordProgress('Backlog'); - draggedId = 0; - }); - $(".context-menu-move.move-doing").on('click', () => { - draggedId = planRecordId; - updatePlanRecordProgress('InProgress'); - }); - $(".context-menu-move.move-testing").on('click', () => { - draggedId = planRecordId; - updatePlanRecordProgress('Testing'); - }); - $(".context-menu-move.move-done").on('click', () => { - draggedId = planRecordId; - updatePlanRecordProgress('Done'); - }); - //hide all move buttons - $(".context-menu-move").hide(); - $(".context-menu-delete").show(); //delete is always visible. - switch (currentSwimLane) { - case 'Backlog': - $(".context-menu-move.move-header").show(); - $(".context-menu-move.move-doing").show(); - $(".context-menu-move.move-testing").show(); - $(".context-menu-move.move-done").show(); - break; - case 'InProgress': - $(".context-menu-move.move-header").show(); - $(".context-menu-move.move-planned").show(); - $(".context-menu-move.move-testing").show(); - $(".context-menu-move.move-done").show(); - break; - case 'Testing': - $(".context-menu-move.move-header").show(); - $(".context-menu-move.move-planned").show(); - $(".context-menu-move.move-doing").show(); - $(".context-menu-move.move-done").show(); - break; - case 'Done': - break; - } -} -function showPlanTableContextMenu(e, planRecordId, currentSwimLane) { - if (event != undefined) { - event.preventDefault(); - } - if (getDeviceIsTouchOnly()) { - return; - } - if (planRecordId == 0) { - return; - } - $(".table-context-menu").fadeIn("fast"); - $(".table-context-menu").css({ - left: getMenuPosition(event.clientX, 'width', 'scrollLeft'), - top: getMenuPosition(event.clientY, 'height', 'scrollTop') - }); - configurePlanTableContextMenu(planRecordId, currentSwimLane); -} -function showPlanTableContextMenuForMobile(e, xPosition, yPosition, planRecordId, currentSwimLane) { - $(".table-context-menu").fadeIn("fast"); - $(".table-context-menu").css({ - left: getMenuPosition(xPosition, 'width', 'scrollLeft'), - top: getMenuPosition(yPosition, 'height', 'scrollTop') - }); - configurePlanTableContextMenu(planRecordId, currentSwimLane); - if (planTouchTimer) { - clearTimeout(planTouchTimer); - planTouchTimer = null; - } -} -var planTouchTimer; -var planTouchDuration = 3000; -function detectPlanItemLongTouch(sender, planRecordId, currentSwimLane) { - var touchX = event.touches[0].clientX; - var touchY = event.touches[0].clientY; - if (!planTouchTimer) { - planTouchTimer = setTimeout(function () { showPlanTableContextMenuForMobile(sender, touchX, touchY, planRecordId, currentSwimLane); detectPlanItemTouchEndPremature(sender); }, planTouchDuration); - } -} -function detectPlanItemTouchEndPremature(sender) { - if (planTouchTimer) { - clearTimeout(planTouchTimer); - planTouchTimer = null; - } -} \ No newline at end of file diff --git a/wwwroot/js/reminderrecord.js b/wwwroot/js/reminderrecord.js deleted file mode 100644 index 3cb08a4..0000000 --- a/wwwroot/js/reminderrecord.js +++ /dev/null @@ -1,378 +0,0 @@ -// Initialize reminder record modal for mobile (using shared mobile framework) -function initializeReminderRecordMobile() { - initMobileModal({ - modalId: '#reminderRecordModal', - dateInputId: '#reminderDate', - tagSelectorId: '#reminderRecordTag' - }); - - // Handle desktop initialization - if (!isMobileDevice()) { - initDatePicker($('#reminderDate'), true); - initTagSelector($("#reminderRecordTag")); - } -} - -function showEditReminderRecordModal(reminderId) { - $.get(`/Vehicle/GetReminderRecordForEditById?reminderRecordId=${reminderId}`, function (data) { - if (data) { - $("#reminderRecordModalContent").html(data); - - // Initialize mobile experience using shared framework - initializeReminderRecordMobile(); - - $("#reminderRecordModal").modal("show"); - $('#reminderRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("reminderNotes"); - } - }); - } - }); -} -function hideAddReminderRecordModal() { - $('#reminderRecordModal').modal('hide'); -} -function checkCustomMonthInterval() { - var selectedValue = $("#reminderRecurringMonth").val(); - if (selectedValue == "Other") { - $("#workAroundInput").show(); - Swal.fire({ - title: 'Specify Custom Time Interval', - html: ` - - - `, - confirmButtonText: 'Set', - focusConfirm: false, - preConfirm: () => { - const customMonth = $("#inputCustomMonth").val(); - if (!customMonth || isNaN(parseInt(customMonth)) || parseInt(customMonth) <= 0) { - Swal.showValidationMessage(`Please enter a valid number`); - } - const customMonthUnit = $("#inputCustomMonthUnit").val(); - return { customMonth, customMonthUnit } - }, - }).then(function (result) { - if (result.isConfirmed) { - customMonthInterval = result.value.customMonth; - customMonthIntervalUnit = result.value.customMonthUnit; - $("#reminderRecurringMonth > option[value='Other']").text(`Other: ${result.value.customMonth} ${result.value.customMonthUnit}`); - } else { - $("#reminderRecurringMonth").val(getReminderRecordModelData().monthInterval); - } - $("#workAroundInput").hide(); - }); - } -} -function checkCustomMileageInterval() { - var selectedValue = $("#reminderRecurringMileage").val(); - if (selectedValue == "Other") { - $("#workAroundInput").show(); - Swal.fire({ - title: 'Specify Custom Mileage Interval', - html: ` - - `, - confirmButtonText: 'Set', - focusConfirm: false, - preConfirm: () => { - const customMileage = $("#inputCustomMileage").val(); - if (!customMileage || isNaN(parseInt(customMileage)) || parseInt(customMileage) <= 0) { - Swal.showValidationMessage(`Please enter a valid number`); - } - return { customMileage } - }, - }).then(function (result) { - if (result.isConfirmed) { - customMileageInterval = result.value.customMileage; - $("#reminderRecurringMileage > option[value='Other']").text(`Other: ${result.value.customMileage}`); - } else { - $("#reminderRecurringMileage").val(getReminderRecordModelData().mileageInterval); - } - $("#workAroundInput").hide(); - }); - } -} -function deleteReminderRecord(reminderRecordId, e) { - if (e != undefined) { - event.stopPropagation(); - } - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Reminders cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeleteReminderRecordById?reminderRecordId=${reminderRecordId}`, function (data) { - if (data) { - hideAddReminderRecordModal(); - successToast("Reminder Deleted"); - var vehicleId = GetVehicleId().vehicleId; - getVehicleReminders(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function toggleCustomThresholds() { - var isChecked = $("#reminderUseCustomThresholds").is(':checked'); - if (isChecked) { - $("#reminderCustomThresholds").collapse('show'); - } else { - $("#reminderCustomThresholds").collapse('hide'); - } -} -function saveReminderRecordToVehicle(isEdit) { - //get values - var formValues = getAndValidateReminderRecordValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SaveReminderRecordToVehicleId', { reminderRecord: formValues }, function (data) { - if (data) { - successToast(isEdit ? "Reminder Updated" : "Reminder Added."); - hideAddReminderRecordModal(); - saveScrollPosition(); - getVehicleReminders(formValues.vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function appendMileageToOdometer(increment) { - var reminderMileage = $("#reminderMileage").val(); - var reminderMileageIsInvalid = reminderMileage.trim() == '' || parseInt(reminderMileage) < 0; - if (reminderMileageIsInvalid) { - reminderMileage = 0; - } else { - reminderMileage = parseInt(reminderMileage); - } - reminderMileage += increment; - $("#reminderMileage").val(reminderMileage); -} - -function enableRecurring() { - var reminderIsRecurring = $("#reminderIsRecurring").is(":checked"); - if (reminderIsRecurring) { - //check selected metric - var reminderMetric = $('#reminderOptions input:radio:checked').val(); - if (reminderMetric == "Date") { - $("#reminderRecurringMonth").attr('disabled', false); - $("#reminderRecurringMileage").attr('disabled', true); - } - else if (reminderMetric == "Odometer") { - $("#reminderRecurringMileage").attr('disabled', false); - $("#reminderRecurringMonth").attr('disabled', true); - } - else if (reminderMetric == "Both") { - $("#reminderRecurringMonth").attr('disabled', false); - $("#reminderRecurringMileage").attr('disabled', false); - } - } else { - $("#reminderRecurringMileage").attr('disabled', true); - $("#reminderRecurringMonth").attr('disabled', true); - } -} - -function markDoneReminderRecord(reminderRecordId, e) { - event.stopPropagation(); - var vehicleId = GetVehicleId().vehicleId; - $.post(`/Vehicle/PushbackRecurringReminderRecord?reminderRecordId=${reminderRecordId}`, function (data) { - if (data) { - successToast("Reminder Updated"); - getVehicleReminders(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); -} - -function getAndValidateReminderRecordValues() { - var reminderDate = $("#reminderDate").val(); - var reminderMileage = parseInt(globalParseFloat($("#reminderMileage").val())).toString(); - var reminderDescription = $("#reminderDescription").val(); - var reminderNotes = $("#reminderNotes").val(); - var reminderOption = $('#reminderOptions input:radio:checked').val(); - var reminderIsRecurring = $("#reminderIsRecurring").is(":checked"); - var reminderRecurringMonth = $("#reminderRecurringMonth").val(); - var reminderRecurringMileage = $("#reminderRecurringMileage").val(); - var reminderTags = $("#reminderRecordTag").val(); - var vehicleId = GetVehicleId().vehicleId; - var reminderId = getReminderRecordModelData().id; - var reminderUseCustomThresholds = $("#reminderUseCustomThresholds").is(":checked"); - var reminderUrgentDays = $("#reminderUrgentDays").val(); - var reminderVeryUrgentDays = $("#reminderVeryUrgentDays").val(); - var reminderUrgentDistance = $("#reminderUrgentDistance").val(); - var reminderVeryUrgentDistance = $("#reminderVeryUrgentDistance").val(); - //validation - var hasError = false; - var reminderDateIsInvalid = reminderDate.trim() == ''; //eliminates whitespace. - var reminderMileageIsInvalid = reminderMileage.trim() == '' || isNaN(reminderMileage) || parseInt(reminderMileage) < 0; - if ((reminderOption == "Both" || reminderOption == "Date") && reminderDateIsInvalid) { - hasError = true; - $("#reminderDate").addClass("is-invalid"); - } else if (reminderOption == "Date") { - $("#reminderDate").removeClass("is-invalid"); - } - if ((reminderOption == "Both" || reminderOption == "Odometer") && reminderMileageIsInvalid) { - hasError = true; - $("#reminderMileage").addClass("is-invalid"); - } else if (reminderOption == "Odometer") { - $("#reminderMileage").removeClass("is-invalid"); - } - if (reminderDescription.trim() == '') { - hasError = true; - $("#reminderDescription").addClass("is-invalid"); - } else { - $("#reminderDescription").removeClass("is-invalid"); - } - if (reminderUseCustomThresholds) { - //validate custom threshold values - if (reminderUrgentDays.trim() == '' || isNaN(reminderUrgentDays) || parseInt(reminderUrgentDays) < 0) { - hasError = true; - $("#reminderUrgentDays").addClass("is-invalid"); - } else { - $("#reminderUrgentDays").removeClass("is-invalid"); - } - if (reminderVeryUrgentDays.trim() == '' || isNaN(reminderVeryUrgentDays) || parseInt(reminderVeryUrgentDays) < 0) { - hasError = true; - $("#reminderVeryUrgentDays").addClass("is-invalid"); - } else { - $("#reminderVeryUrgentDays").removeClass("is-invalid"); - } - if (reminderUrgentDistance.trim() == '' || isNaN(reminderUrgentDistance) || parseInt(reminderUrgentDistance) < 0) { - hasError = true; - $("#reminderUrgentDistance").addClass("is-invalid"); - } else { - $("#reminderUrgentDistance").removeClass("is-invalid"); - } - if (reminderVeryUrgentDistance.trim() == '' || isNaN(reminderVeryUrgentDistance) || parseInt(reminderVeryUrgentDistance) < 0) { - hasError = true; - $("#reminderVeryUrgentDistance").addClass("is-invalid"); - } else { - $("#reminderVeryUrgentDistance").removeClass("is-invalid"); - } - } - if (reminderOption == undefined) { - hasError = true; - $("#reminderMetricDate").addClass("is-invalid"); - $("#reminderMetricOdometer").addClass("is-invalid"); - $("#reminderMetricBoth").addClass("is-invalid"); - } else { - $("#reminderMetricDate").removeClass("is-invalid"); - $("#reminderMetricOdometer").removeClass("is-invalid"); - $("#reminderMetricBoth").removeClass("is-invalid"); - } - - return { - id: reminderId, - hasError: hasError, - vehicleId: vehicleId, - date: reminderDate, - mileage: reminderMileage, - description: reminderDescription, - notes: reminderNotes, - metric: reminderOption, - isRecurring: reminderIsRecurring, - useCustomThresholds: reminderUseCustomThresholds, - customThresholds: { - urgentDays: reminderUrgentDays, - veryUrgentDays: reminderVeryUrgentDays, - urgentDistance: reminderUrgentDistance, - veryUrgentDistance: reminderVeryUrgentDistance - }, - reminderMileageInterval: reminderRecurringMileage, - reminderMonthInterval: reminderRecurringMonth, - customMileageInterval: customMileageInterval, - customMonthInterval: customMonthInterval, - customMonthIntervalUnit: customMonthIntervalUnit, - tags: reminderTags - } -} -function createPlanRecordFromReminder(reminderRecordId) { - //get values - var formValues = getAndValidateReminderRecordValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - var planModelInput = { - id: 0, - createdFromReminder: true, - vehicleId: formValues.vehicleId, - reminderRecordId: reminderRecordId, - description: formValues.description, - notes: formValues.notes - }; - $.post('/Vehicle/GetAddPlanRecordPartialView', { planModel: planModelInput }, function (data) { - $("#reminderRecordModal").modal("hide"); - $("#planRecordModalContent").html(data); - $("#planRecordModal").modal("show"); - }); -} - -function filterReminderTable(sender) { - var rowData = $(`#reminder-tab-pane table tbody tr`); - if (sender == undefined) { - rowData.removeClass('override-hide'); - return; - } - var tagName = sender.textContent; - //check for other applied filters - if ($(sender).hasClass("bg-primary")) { - rowData.removeClass('override-hide'); - $(sender).removeClass('bg-primary'); - $(sender).addClass('bg-secondary'); - updateReminderAggregateLabels(); - } else { - //hide table rows. - rowData.addClass('override-hide'); - $(`[data-tags~='${tagName}']`).removeClass('override-hide'); - updateReminderAggregateLabels(); - if ($(".tagfilter.bg-primary").length > 0) { - //disabling other filters - $(".tagfilter.bg-primary").addClass('bg-secondary'); - $(".tagfilter.bg-primary").removeClass('bg-primary'); - } - $(sender).addClass('bg-primary'); - $(sender).removeClass('bg-secondary'); - } -} -function updateReminderAggregateLabels() { - //update main count - var newCount = $("[data-record-type='cost']").parent(":not('.override-hide')").length; - var countLabel = $("[data-aggregate-type='count']"); - countLabel.text(`${countLabel.text().split(':')[0]}: ${newCount}`); - //update labels - //paste due - var pastDueCount = $("tr td span.badge.text-bg-secondary").parents("tr:not('.override-hide')").length; - var pastDueLabel = $('[data-aggregate-type="pastdue-count"]'); - pastDueLabel.text(`${pastDueLabel.text().split(':')[0]}: ${pastDueCount}`); - //very urgent - var veryUrgentCount = $("tr td span.badge.text-bg-danger").parents("tr:not('.override-hide')").length; - var veryUrgentLabel = $('[data-aggregate-type="veryurgent-count"]'); - veryUrgentLabel.text(`${veryUrgentLabel.text().split(':')[0]}: ${veryUrgentCount}`); - //urgent - var urgentCount = $("tr td span.badge.text-bg-warning").parents("tr:not('.override-hide')").length; - var urgentLabel = $('[data-aggregate-type="urgent-count"]'); - urgentLabel.text(`${urgentLabel.text().split(':')[0]}: ${urgentCount}`); - //not urgent - var notUrgentCount = $("tr td span.badge.text-bg-success").parents("tr:not('.override-hide')").length; - var notUrgentLabel = $('[data-aggregate-type="noturgent-count"]'); - notUrgentLabel.text(`${notUrgentLabel.text().split(':')[0]}: ${notUrgentCount}`); -} \ No newline at end of file diff --git a/wwwroot/js/reports.js b/wwwroot/js/reports.js deleted file mode 100644 index 13e19da..0000000 --- a/wwwroot/js/reports.js +++ /dev/null @@ -1,414 +0,0 @@ -function getYear() { - return $("#yearOption").val() ?? '0'; -} -function getAndValidateSelectedColumns() { - var reportVisibleColumns = []; - var reportExtraFields = []; - var tagFilterMode = $("#tagSelector").val(); - var tagsToFilter = $("#tagSelectorInput").val(); - var filterByDateRange = $("#dateRangeSelector").is(":checked"); - var printIndividualRecords = $("#printIndividualRecordsCheck").is(":checked"); - var startDate = $("#dateRangeStartDate").val(); - var endDate = $("#dateRangeEndDate").val(); - $("#columnSelector :checked").map(function () { - if ($(this).hasClass('column-default')) { - reportVisibleColumns.push(this.value); - } else { - reportExtraFields.push(this.value); - } - }); - var hasValidationError = false; - var validationErrorMessage = ""; - if (reportVisibleColumns.length + reportExtraFields.length == 0) { - hasValidationError = true; - validationErrorMessage = "You must select at least one column"; - } - if (filterByDateRange) { - //validate date range - let startDateTicks = $("#dateRangeStartDate").datepicker('getDate')?.getTime(); - let endDateTicks = $("#dateRangeEndDate").datepicker('getDate')?.getTime(); - if (!startDateTicks || !endDateTicks || startDateTicks > endDateTicks) { - hasValidationError = true; - validationErrorMessage = "Invalid date range"; - } - } - - if (hasValidationError) { - return { - hasError: true, - errorMessage: validationErrorMessage, - visibleColumns: [], - extraFields: [], - tagFilter: tagFilterMode, - tags: [], - filterByDateRange: filterByDateRange, - startDate: '', - endDate: '', - printIndividualRecords: printIndividualRecords - } - } else { - return { - hasError: false, - errorMessage: '', - visibleColumns: reportVisibleColumns, - extraFields: reportExtraFields, - tagFilter: tagFilterMode, - tags: tagsToFilter, - filterByDateRange: filterByDateRange, - startDate: startDate, - endDate: endDate, - printIndividualRecords: printIndividualRecords - } - } -} -function getSavedReportParameters() { - var vehicleId = GetVehicleId().vehicleId; - var selectedReportColumns = sessionStorage.getItem(`${vehicleId}_selectedReportColumns`); - if (selectedReportColumns != null) { - selectedReportColumns = JSON.parse(selectedReportColumns); - //unselected everything - $(".column-extrafield").prop('checked', false); - $(".column-default").prop('checked', false); - //load selected checkboxes - selectedReportColumns.extraFields.map(x => { - $(`[value='${x}'].column-extrafield`).prop('checked', true); - }); - selectedReportColumns.visibleColumns.map(x => { - $(`[value='${x}'].column-default`).prop('checked', true); - }); - $("#tagSelector").val(selectedReportColumns.tagFilter); - selectedReportColumns.tags.map(x => { - $("#tagSelectorInput").append(``) - }); - $("#dateRangeSelector").prop('checked', selectedReportColumns.filterByDateRange); - $("#dateRangeStartDate").val(selectedReportColumns.startDate); - $("#dateRangeEndDate").val(selectedReportColumns.endDate); - $("#printIndividualRecordsCheck").prop('checked', selectedReportColumns.printIndividualRecords); - } -} -function generateVehicleHistoryReport() { - var vehicleId = GetVehicleId().vehicleId; - $.get(`/Vehicle/GetReportParameters`, function (data) { - if (data) { - //prompt user to select columns - Swal.fire({ - html: data, - confirmButtonText: 'Generate Report', - focusConfirm: false, - preConfirm: () => { - //validate - var selectedColumnsData = getAndValidateSelectedColumns(); - if (selectedColumnsData.hasError) { - Swal.showValidationMessage(selectedColumnsData.errorMessage); - } - return { selectedColumnsData } - }, - didOpen: () => { - getSavedReportParameters(); - initTagSelector($("#tagSelectorInput")); - initDatePicker($('#dateRangeStartDate')); - initDatePicker($('#dateRangeEndDate')); - } - }).then(function (result) { - if (result.isConfirmed) { - //save params in sessionStorage - sessionStorage.setItem(`${vehicleId}_selectedReportColumns`, JSON.stringify(result.value.selectedColumnsData)); - //post params - $.post(`/Vehicle/GetVehicleHistory?vehicleId=${vehicleId}`, { - reportParameter: result.value.selectedColumnsData - }, function (data) { - if (data) { - printContainer(data); - } - }) - } - }); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function updateCheckAll() { - var isChecked = $("#selectAllExpenseCheck").is(":checked"); - $(".reportCheckBox").prop('checked', isChecked); - setDebounce(refreshBarChart); -} -function updateCheck() { - setDebounce(refreshBarChart); - var allIsChecked = $(".reportCheckBox:checked").length == 6; - $("#selectAllExpenseCheck").prop("checked", allIsChecked); -} -function refreshMPGChart() { - var vehicleId = GetVehicleId().vehicleId; - var year = getYear(); - $.post('/Vehicle/GetMonthMPGByVehicle', {vehicleId: vehicleId, year: year}, function (data) { - $("#monthFuelMileageReportContent").html(data); - refreshReportHeader(); - }) -} -function refreshReportHeader() { - var vehicleId = GetVehicleId().vehicleId; - var year = getYear(); - $.post('/Vehicle/GetSummaryForVehicle', { vehicleId: vehicleId, year: year }, function (data) { - $("#reportHeaderContent").html(data); - }) -} -function setSelectedMetrics() { - var selectedMetricCheckBoxes = []; - $(".reportCheckBox:checked").map((index, elem) => { - selectedMetricCheckBoxes.push(elem.id); - }); - var yearMetric = $('#yearOption').val(); - var reminderMetric = $("#reminderOption").val(); - var vehicleId = GetVehicleId().vehicleId; - sessionStorage.setItem(`${vehicleId}_selectedMetricCheckBoxes`, JSON.stringify(selectedMetricCheckBoxes)); - sessionStorage.setItem(`${vehicleId}_yearMetric`, yearMetric); - sessionStorage.setItem(`${vehicleId}_reminderMetric`, reminderMetric); -} -function getSelectedMetrics() { - var vehicleId = GetVehicleId().vehicleId; - var selectedMetricCheckBoxes = sessionStorage.getItem(`${vehicleId}_selectedMetricCheckBoxes`); - var yearMetric = sessionStorage.getItem(`${vehicleId}_yearMetric`); - var reminderMetric = sessionStorage.getItem(`${vehicleId}_reminderMetric`); - if (selectedMetricCheckBoxes != null && yearMetric != null && reminderMetric != null) { - selectedMetricCheckBoxes = JSON.parse(selectedMetricCheckBoxes); - $(".reportCheckBox").prop('checked', false); - $("#selectAllExpenseCheck").prop("checked", false); - selectedMetricCheckBoxes.map(x => { - $(`#${x}`).prop('checked', true); - }); - if (selectedMetricCheckBoxes.length == 6) { - $("#selectAllExpenseCheck").prop("checked", true); - } - //check if option is available - if ($("#yearOption").has(`option[value=${yearMetric}]`).length > 0) { - $('#yearOption').val(yearMetric); - } - $("#reminderOption").val(reminderMetric); - //retrieve data. - yearUpdated(); - updateReminderPie(); - return true; - } - return false; -} -function refreshBarChart() { - var selectedMetrics = []; - var vehicleId = GetVehicleId().vehicleId; - var year = getYear(); - - if ($("#serviceExpenseCheck").is(":checked")) { - selectedMetrics.push('ServiceRecord'); - } - if ($("#repairExpenseCheck").is(":checked")) { - selectedMetrics.push('RepairRecord'); - } - if ($("#upgradeExpenseCheck").is(":checked")) { - selectedMetrics.push('UpgradeRecord'); - } - if ($("#gasExpenseCheck").is(":checked")) { - selectedMetrics.push('GasRecord'); - } - if ($("#taxExpenseCheck").is(":checked")) { - selectedMetrics.push('TaxRecord'); - } - if ($("#odometerExpenseCheck").is(":checked")) { - selectedMetrics.push('OdometerRecord'); - } - - $.post('/Vehicle/GetCostByMonthByVehicle', - { - vehicleId: vehicleId, - selectedMetrics: selectedMetrics, - year: year - }, function (data) { - $("#gasCostByMonthReportContent").html(data); - refreshMPGChart(); - }); - setSelectedMetrics(); -} -function showBarChartTable(elemClicked) { - var selectedMetrics = []; - var vehicleId = GetVehicleId().vehicleId; - var year = getYear(); - - if ($("#serviceExpenseCheck").is(":checked")) { - selectedMetrics.push('ServiceRecord'); - } - if ($("#repairExpenseCheck").is(":checked")) { - selectedMetrics.push('RepairRecord'); - } - if ($("#upgradeExpenseCheck").is(":checked")) { - selectedMetrics.push('UpgradeRecord'); - } - if ($("#gasExpenseCheck").is(":checked")) { - selectedMetrics.push('GasRecord'); - } - if ($("#taxExpenseCheck").is(":checked")) { - selectedMetrics.push('TaxRecord'); - } - if ($("#odometerExpenseCheck").is(":checked")) { - selectedMetrics.push('OdometerRecord'); - } - - $.post('/Vehicle/GetCostByMonthAndYearByVehicle', - { - vehicleId: vehicleId, - selectedMetrics: selectedMetrics, - year: year - }, function (data) { - $("#vehicleDataTableModalContent").html(data); - $("#vehicleDataTableModal").modal('show'); - //highlight clicked row. - if (elemClicked.length > 0) { - var rowClickedIndex = elemClicked[0].index + 1; - var rowToHighlight = $("#vehicleDataTableModalContent").find(`tbody > tr:nth-child(${rowClickedIndex})`); - if (rowToHighlight.length > 0) { - rowToHighlight.addClass('table-info'); - } - } - }); -} -function toggleBarChartTableData() { - //find out which column data type is shown - if (!$('[report-data="cost"]').hasClass('d-none')) { - //currently cost is shown. - $('[report-data="cost"]').addClass('d-none'); - $('[report-data="distance"]').removeClass('d-none'); - } - else if (!$('[report-data="distance"]').hasClass('d-none')) { - //currently distance is shown. - $('[report-data="distance"]').addClass('d-none'); - $('[report-data="costperdistance"]').removeClass('d-none'); - } - else if (!$('[report-data="costperdistance"]').hasClass('d-none')) { - //currently cost per distance is shown. - $('[report-data="costperdistance"]').addClass('d-none'); - $('[report-data="cost"]').removeClass('d-none'); - } -} -function toggleCostTableHint() { - if ($(".cost-table-hint").hasClass("d-none")) { - $(".cost-table-hint").removeClass("d-none"); - } else { - $(".cost-table-hint").addClass("d-none"); - } -} -function updateReminderPie() { - var vehicleId = GetVehicleId().vehicleId; - var daysToAdd = $("#reminderOption").val(); - setSelectedMetrics(); - $.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) { - $("#reminderMakeUpReportContent").html(data); - }); -} -//called when year selected is changed. -function yearUpdated() { - var vehicleId = GetVehicleId().vehicleId; - var year = getYear(); - $.get(`/Vehicle/GetCostMakeUpForVehicle?vehicleId=${vehicleId}`, { year: year }, function (data) { - $("#costMakeUpReportContent").html(data); - refreshBarChart(); - }) -} -function refreshCollaborators() { - var vehicleId = GetVehicleId().vehicleId; - $.get(`/Vehicle/GetCollaboratorsForVehicle?vehicleId=${vehicleId}`, function (data) { - $("#collaboratorContent").html(data); - }); -} -function exportAttachments() { - Swal.fire({ - title: 'Export Attachments', - html: ` -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - `, - confirmButtonText: 'Export', - showCancelButton: true, - focusConfirm: false, - preConfirm: () => { - var selectedExportTabs = $("#attachmentTabs :checked").map(function () { - return this.value; - }); - if (selectedExportTabs.toArray().length == 0) { - Swal.showValidationMessage(`Please make at least one selection`) - } - return { selectedTabs: selectedExportTabs.toArray() } - }, - }).then(function (result) { - if (result.isConfirmed) { - var vehicleId = GetVehicleId().vehicleId; - $.post('/Vehicle/GetVehicleAttachments', { vehicleId: vehicleId, exportTabs: result.value.selectedTabs }, function (data) { - if (data.success) { - window.location.href = data.message; - } else { - errorToast(data.message); - } - }) - } - }); -} -function showDataTable(elemClicked) { - var vehicleId = GetVehicleId().vehicleId; - var year = getYear(); - $.get(`/Vehicle/GetCostTableForVehicle?vehicleId=${vehicleId}`, { year: year }, function (data) { - $("#vehicleDataTableModalContent").html(data); - $("#vehicleDataTableModal").modal('show'); - if (elemClicked.length > 0) { - var rowClickedIndex = elemClicked[0].index + 1; - var rowToHighlight = $("#vehicleDataTableModalContent").find(`tbody > tr:nth-child(${rowClickedIndex})`); - if (rowToHighlight.length > 0) { - rowToHighlight.addClass('table-info'); - } - } - }); -} -function hideDataTable() { - $("#vehicleDataTableModal").modal('hide'); -} -function loadCustomWidgets() { - $.get('/Vehicle/GetAdditionalWidgets', function (data) { - $("#vehicleCustomWidgetsModalContent").html(data); - $("#vehicleCustomWidgetsModal").modal('show'); - }) -} -function hideCustomWidgetsModal() { - $("#vehicleCustomWidgetsModal").modal('hide'); -} - -function showReportAdvancedParameters() { - if ($(".report-advanced-parameters").hasClass("d-none")) { - $(".report-advanced-parameters").removeClass("d-none"); - } else { - $(".report-advanced-parameters").addClass("d-none"); - } -} \ No newline at end of file diff --git a/wwwroot/js/serversettings.js b/wwwroot/js/serversettings.js deleted file mode 100644 index 950ba72..0000000 --- a/wwwroot/js/serversettings.js +++ /dev/null @@ -1,160 +0,0 @@ -function loadSetupPage(pageNumber) { - let pageElem = $(`.setup-wizard-content[data-page="${pageNumber}"]`); - if (pageElem.length > 0) { - $('.setup-wizard-content').hide(); - pageElem.show(); - } - determineSetupButtons(); -} -function determineSetupButtons() { - let currentVisiblePage = $(".setup-wizard-content:visible").attr('data-page'); - switch (currentVisiblePage) { - case '0': - case '5': - $(".setup-wizard-nav").hide(); - break; - case '1': - case '2': - case '3': - $(".setup-wizard-nav").show(); - $(".btn-prev").show(); - $(".btn-next").show(); - $(".btn-save").hide(); - break; - case '4': - $(".setup-wizard-nav").show(); - $(".btn-prev").show(); - $(".btn-next").hide(); - $(".btn-save").show(); - break; - } -} -function nextSetupPage() { - let currentVisiblePage = $(".setup-wizard-content:visible").attr('data-page'); - let nextPage = parseInt(currentVisiblePage) + 1; - loadSetupPage(nextPage); -} -function previousSetupPage() { - let currentVisiblePage = $(".setup-wizard-content:visible").attr('data-page'); - let prevPage = parseInt(currentVisiblePage) - 1; - loadSetupPage(prevPage); -} -function saveSetup() { - let setupData = { - PostgresConnection: $("#inputPostgres").val(), - AllowedFileExtensions: $("#inputFileExt").val(), - CustomLogoURL: $("#inputLogoURL").val(), - CustomSmallLogoURL: $("#inputSmallLogoURL").val(), - MessageOfTheDay: $("#inputMOTD").val(), - WebHookURL: $("#inputWebHook").val(), - ServerURL: $("#inputDomain").val(), - CustomWidgetsEnabled: $("#inputCustomWidget").val(), - InvariantAPIEnabled: $("#inputInvariantAPI").val(), - SMTPConfig: { - EmailServer: $("#inputSMTPServer").val(), - EmailFrom: $("#inputSMTPFrom").val(), - Port: $("#inputSMTPPort").val(), - Username: $("#inputSMTPUsername").val(), - Password: $("#inputSMTPPassword").val() - }, - OIDCConfig: { - Name: $("#inputOIDCProvider").val(), - ClientId: $("#inputOIDCClient").val(), - ClientSecret: $("#inputOIDCSecret").val(), - AuthURL: $("#inputOIDCAuth").val(), - TokenURL: $("#inputOIDCToken").val(), - RedirectURL: $("#inputOIDCRedirect").val(), - Scope: $("#inputOIDCScope").val(), - ValidateState: $("#inputOIDCState").val(), - DisableRegularLogin: $("#inputOIDCDisable").val(), - UsePKCE: $("#inputOIDCPKCE").val(), - LogOutURL: $("#inputOIDCLogout").val(), - UserInfoURL: $("#inputOIDCUserInfo").val() - }, - ReminderUrgencyConfig: { - UrgentDays: $("#inputUrgentDays").val(), - VeryUrgentDays: $("#inputVeryUrgentDays").val(), - UrgentDistance: $("#inputUrgentDistance").val(), - VeryUrgentDistance: $("#inputVeryUrgentDistance").val() - }, - DefaultReminderEmail: $("#inputDefaultReminderEmail").val(), - EnableRootUserOIDC: $("#inputOIDCRootUser").val() - }; - let registrationMode = $("#inputRegistrationMode"); - if (registrationMode.length > 0) { - switch (registrationMode.val()) { - case '0': - setupData["DisableRegistration"] = 'false'; - setupData["OpenRegistration"] = 'false' - break; - case '1': - setupData["DisableRegistration"] = 'true'; - setupData["OpenRegistration"] = 'false' - break; - case '2': - setupData["DisableRegistration"] = 'false'; - setupData["OpenRegistration"] = 'true' - break; - } - } - //nullify skipped settings - if ($("#skipSMTP").is(":checked")) { - setupData["SMTPConfig"] = null; - } - if ($("#skipOIDC").is(":checked")) { - setupData["OIDCConfig"] = null; - } - if ($("#skipPostgres").is(":checked")) { - setupData["PostgresConnection"] = null; - } - let rootUserOIDC = $("#inputOIDCRootUser"); - if (rootUserOIDC.length > 0) { - setupData["EnableRootUserOIDC"] = $("#inputOIDCRootUser").val(); - } - $.post('/Home/WriteServerConfiguration', { serverConfig: setupData }, function (data) { - if (data) { - nextSetupPage(); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function sendTestEmail() { - let mailConfig = { - EmailServer: $("#inputSMTPServer").val(), - EmailFrom: $("#inputSMTPFrom").val(), - Port: $("#inputSMTPPort").val(), - Username: $("#inputSMTPUsername").val(), - Password: $("#inputSMTPPassword").val() - } - Swal.fire({ - title: 'Send Test Email', - html: ` - - `, - confirmButtonText: 'Send', - focusConfirm: false, - preConfirm: () => { - const emailRecipient = $("#testEmailRecipient").val(); - if (!emailRecipient || emailRecipient.trim() == '') { - Swal.showValidationMessage(`Please enter a valid email address`); - } - return { emailRecipient } - }, - }).then(function (result) { - if (result.isConfirmed) { - $.post('/Home/SendTestEmail', { emailAddress: result.value.emailRecipient, mailConfig: mailConfig }, function (data) { - if (data.success) { - successToast(data.message); - } else { - errorToast(data.message); - } - }); - } - }); -} -function nextOnSkip(sender) { - if ($(sender).is(":checked")) { - nextSetupPage(); - } -} \ No newline at end of file diff --git a/wwwroot/js/servicerecord.js b/wwwroot/js/servicerecord.js deleted file mode 100644 index 3902542..0000000 --- a/wwwroot/js/servicerecord.js +++ /dev/null @@ -1,173 +0,0 @@ -// Initialize service record modal for mobile (using shared mobile framework) -function initializeServiceRecordMobile() { - initMobileModal({ - modalId: '#serviceRecordModal', - dateInputId: '#serviceRecordDate', - tagSelectorId: '#serviceRecordTag' - }); - - // Handle desktop initialization - if (!isMobileDevice()) { - initDatePicker($('#serviceRecordDate')); - initTagSelector($("#serviceRecordTag")); - } -} - -function showAddServiceRecordModal() { - $.get('/Vehicle/GetAddServiceRecordPartialView', function (data) { - if (data) { - $("#serviceRecordModalContent").html(data); - - // Initialize mobile experience using shared framework - initializeServiceRecordMobile(); - - $('#serviceRecordModal').modal('show'); - } - }); -} -function showEditServiceRecordModal(serviceRecordId, nocache) { - if (!nocache) { - var existingContent = $("#serviceRecordModalContent").html(); - if (existingContent.trim() != '') { - //check if id is same. - var existingId = getServiceRecordModelData().id; - if (existingId == serviceRecordId && $('[data-changed=true]').length > 0) { - $('#serviceRecordModal').modal('show'); - $('.cached-banner').show(); - return; - } - } - } - $.get(`/Vehicle/GetServiceRecordForEditById?serviceRecordId=${serviceRecordId}`, function (data) { - if (data) { - $("#serviceRecordModalContent").html(data); - - // Initialize mobile experience using shared framework - initializeServiceRecordMobile(); - - $('#serviceRecordModal').modal('show'); - bindModalInputChanges('serviceRecordModal'); - $('#serviceRecordModal').off('shown.bs.modal').on('shown.bs.modal', function () { - if (getGlobalConfig().useMarkDown) { - toggleMarkDownOverlay("serviceRecordNotes"); - } - }); - } - }); -} -function hideAddServiceRecordModal() { - $('#serviceRecordModal').modal('hide'); -} -function deleteServiceRecord(serviceRecordId) { - $("#workAroundInput").show(); - Swal.fire({ - title: "Confirm Deletion?", - text: "Deleted Service Records cannot be restored.", - showCancelButton: true, - confirmButtonText: "Delete", - confirmButtonColor: "#dc3545" - }).then((result) => { - if (result.isConfirmed) { - $.post(`/Vehicle/DeleteServiceRecordById?serviceRecordId=${serviceRecordId}`, function (data) { - if (data) { - hideAddServiceRecordModal(); - successToast("Service Record Deleted"); - var vehicleId = GetVehicleId().vehicleId; - getVehicleServiceRecords(vehicleId); - } else { - errorToast(genericErrorMessage()); - } - }); - } else { - $("#workAroundInput").hide(); - } - }); -} -function saveServiceRecordToVehicle(isEdit) { - //get values - var formValues = getAndValidateServiceRecordValues(); - //validate - if (formValues.hasError) { - errorToast("Please check the form data"); - return; - } - //save to db. - $.post('/Vehicle/SaveServiceRecordToVehicleId', { serviceRecord: formValues }, function (data) { - if (data) { - successToast(isEdit ? "Service Record Updated" : "Service Record Added."); - hideAddServiceRecordModal(); - saveScrollPosition(); - getVehicleServiceRecords(formValues.vehicleId); - if (formValues.addReminderRecord) { - setTimeout(function () { showAddReminderModal(formValues); }, 500); - } - } else { - errorToast(genericErrorMessage()); - } - }) -} -function getAndValidateServiceRecordValues() { - var serviceDate = $("#serviceRecordDate").val(); - var serviceMileage = parseInt(globalParseFloat($("#serviceRecordMileage").val())).toString(); - var serviceDescription = $("#serviceRecordDescription").val(); - var serviceCost = $("#serviceRecordCost").val(); - var serviceNotes = $("#serviceRecordNotes").val(); - var serviceTags = $("#serviceRecordTag").val(); - var vehicleId = GetVehicleId().vehicleId; - var serviceRecordId = getServiceRecordModelData().id; - var addReminderRecord = $("#addReminderCheck").is(":checked"); - //Odometer Adjustments - if (isNaN(serviceMileage) && GetVehicleId().odometerOptional) { - serviceMileage = '0'; - } - serviceMileage = GetAdjustedOdometer(serviceRecordId, serviceMileage); - //validation - var hasError = false; - var extraFields = getAndValidateExtraFields(); - if (extraFields.hasError) { - hasError = true; - } - if (serviceDate.trim() == '') { //eliminates whitespace. - hasError = true; - $("#serviceRecordDate").addClass("is-invalid"); - } else { - $("#serviceRecordDate").removeClass("is-invalid"); - } - if (serviceMileage.trim() == '' || isNaN(serviceMileage) || parseInt(serviceMileage) < 0) { - hasError = true; - $("#serviceRecordMileage").addClass("is-invalid"); - } else { - $("#serviceRecordMileage").removeClass("is-invalid"); - } - if (serviceDescription.trim() == '') { - hasError = true; - $("#serviceRecordDescription").addClass("is-invalid"); - } else { - $("#serviceRecordDescription").removeClass("is-invalid"); - } - if (serviceCost.trim() == '' || !isValidMoney(serviceCost)) { - hasError = true; - $("#serviceRecordCost").addClass("is-invalid"); - } else { - $("#serviceRecordCost").removeClass("is-invalid"); - } - return { - id: serviceRecordId, - hasError: hasError, - vehicleId: vehicleId, - date: serviceDate, - mileage: serviceMileage, - description: serviceDescription, - cost: serviceCost, - notes: serviceNotes, - files: uploadedFiles, - supplies: selectedSupplies, - tags: serviceTags, - addReminderRecord: addReminderRecord, - extraFields: extraFields.extraFields, - requisitionHistory: supplyUsageHistory, - deletedRequisitionHistory: deletedSupplyUsageHistory, - reminderRecordId: recurringReminderRecordId, - copySuppliesAttachment: copySuppliesAttachments - } -} \ No newline at end of file diff --git a/wwwroot/js/settings.js b/wwwroot/js/settings.js deleted file mode 100644 index e559b0c..0000000 --- a/wwwroot/js/settings.js +++ /dev/null @@ -1,420 +0,0 @@ -function showExtraFieldModal() { - $.get(`/Home/GetExtraFieldsModal?importMode=0`, function (data) { - $("#extraFieldModalContent").html(data); - $("#extraFieldModal").modal('show'); - }); -} -function hideExtraFieldModal() { - $("#extraFieldModal").modal('hide'); -} -function getCheckedTabs() { - var visibleTabs = $("#visibleTabs :checked").map(function () { - return this.value; - }); - return visibleTabs.toArray(); -} -function deleteLanguage() { - var languageFileLocation = `/translations/${$("#defaultLanguage").val()}.json`; - $.post('/Files/DeleteFiles', { fileLocation: languageFileLocation }, function (data) { - //reset user language back to en_US - $("#defaultLanguage").val('en_US'); - updateSettings(); - }); -} -function updateColorModeSettings(e) { - var colorMode = $(e).prop("id"); - switch (colorMode) { - case "enableDarkMode": - //uncheck system prefernce - $("#useSystemColorMode").prop('checked', false); - // Apply theme immediately - if ($("#enableDarkMode").is(':checked')) { - $(document.documentElement).attr("data-bs-theme", "dark"); - } else { - $(document.documentElement).attr("data-bs-theme", "light"); - } - updateSettings(); - break; - case "useSystemColorMode": - $("#enableDarkMode").prop('checked', false); - // Apply system theme immediately - if ($("#useSystemColorMode").is(':checked')) { - setThemeBasedOnDevice(); - } else { - $(document.documentElement).attr("data-bs-theme", "light"); - } - updateSettings(); - break; - } -} -function updateSettings() { - var visibleTabs = getCheckedTabs(); - var defaultTab = $("#defaultTab").val(); - if (!visibleTabs.includes(defaultTab)) { - defaultTab = "Dashboard"; //default to dashboard. - } - var tabOrder = getTabOrder(); - - var userConfigObject = { - useDarkMode: $("#enableDarkMode").is(':checked'), - useSystemColorMode: $("#useSystemColorMode").is(':checked'), - enableCsvImports: $("#enableCsvImports").is(':checked'), - useMPG: $("#useMPG").is(':checked'), - useDescending: $("#useDescending").is(':checked'), - hideZero: $("#hideZero").is(":checked"), - automaticDecimalFormat: $("#automaticDecimalFormat").is(":checked"), - useUKMpg: $("#useUKMPG").is(":checked"), - useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"), - useThreeDecimalGasConsumption: $("#useThreeDecimalGasConsumption").is(":checked"), - useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"), - enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"), - enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"), - enableShopSupplies: $("#enableShopSupplies").is(":checked"), - showCalendar: $("#showCalendar").is(":checked"), - showVehicleThumbnail: $("#showVehicleThumbnail").is(":checked"), - enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"), - hideSoldVehicles: $("#hideSoldVehicles").is(":checked"), - preferredGasUnit: $("#preferredGasUnit").val(), - preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(), - userLanguage: $("#defaultLanguage").val(), - useUnitForFuelCost: $("#useUnitForFuelCost").is(":checked"), - visibleTabs: visibleTabs, - defaultTab: defaultTab, - tabOrder: tabOrder - } - sloader.show(); - $.post('/Home/WriteToSettings', { userConfig: userConfigObject }, function (data) { - sloader.hide(); - if (data) { - setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function makeBackup() { - $.get('/Files/MakeBackup', function (data) { - window.location.href = data; - }); -} -function openUploadLanguage() { - $("#inputLanguage").trigger('click'); -} -function openRestoreBackup() { - $("#inputBackup").trigger('click'); -} -function uploadLanguage(event) { - let formData = new FormData(); - formData.append("file", event.files[0]); - sloader.show(); - $.ajax({ - url: "/Files/HandleTranslationFileUpload", - data: formData, - cache: false, - processData: false, - contentType: false, - type: 'POST', - success: function (response) { - sloader.hide(); - if (response.success) { - setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500); - } else { - errorToast(response.message); - } - }, - error: function () { - sloader.hide(); - errorToast("An error has occurred, please check the file size and try again later."); - } - }); -} -function restoreBackup(event) { - let formData = new FormData(); - formData.append("file", event.files[0]); - console.log('MotoVaultPro - DB Restoration Started'); - sloader.show(); - $.ajax({ - url: "/Files/HandleFileUpload", - data: formData, - cache: false, - processData: false, - contentType: false, - type: 'POST', - success: function (response) { - if (response.trim() != '') { - $.post('/Files/RestoreBackup', { fileName: response }, function (data) { - sloader.hide(); - if (data) { - console.log('MotoVaultPro - DB Restoration Completed'); - successToast("Backup Restored"); - setTimeout(function () { window.location.href = '/Home/Index' }, 500); - } else { - errorToast(genericErrorMessage()); - console.log('MotoVaultPro - DB Restoration Failed - Failed to process backup file.'); - } - }); - } else { - console.log('MotoVaultPro - DB Restoration Failed - Failed to upload backup file.'); - } - }, - error: function () { - sloader.hide(); - console.log('MotoVaultPro - DB Restoration Failed - Request failed to reach backend, please check file size.'); - errorToast("An error has occurred, please check the file size and try again later."); - } - }); -} - -function loadSponsors() { - $.get('/Home/Sponsors', function (data) { - $("#sponsorsContainer").html(data); - }) -} - -function showTranslationEditor() { - $.get(`/Home/GetTranslatorEditor?userLanguage=${$("#defaultLanguage").val()}`, function (data) { - $('#translationEditorModalContent').html(data); - $('#translationEditorModal').modal('show'); - }) -} -function hideTranslationEditor() { - $('#translationEditorModal').modal('hide'); -} -function saveTranslation() { - var currentLanguage = $("#defaultLanguage").val(); - var translationData = []; - $(".translation-keyvalue").map((index, elem) => { - var translationKey = $(elem).find('.translation-key'); - var translationValue = $(elem).find('.translation-value textarea'); - translationData.push({ key: translationKey.text().replaceAll(' ', '_').trim(), value: translationValue.val().trim() }); - }); - if (translationData.length == 0) { - errorToast(genericErrorMessage()); - return; - } - var userCanDelete = $(".translation-delete").length > 0; - Swal.fire({ - title: 'Save Translation', - html: ` - - `, - confirmButtonText: 'Save', - focusConfirm: false, - preConfirm: () => { - const translationFileName = $("#translationFileName").val(); - if (!translationFileName || translationFileName.trim() == '') { - Swal.showValidationMessage(`Please enter a valid file name`); - } else if (translationFileName.trim() == 'en_US' && !userCanDelete) { - Swal.showValidationMessage(`en_US is reserved, please enter a different name`); - } - return { translationFileName } - }, - }).then(function (result) { - if (result.isConfirmed) { - $.post('/Home/SaveTranslation', { userLanguage: result.value.translationFileName, translationData: translationData }, function (data) { - if (data.success) { - successToast("Translation Updated"); - updateSettings(); - } else { - errorToast(genericErrorMessage()); - } - }); - } - }); -} -function exportTranslation(){ - var translationData = []; - $(".translation-keyvalue").map((index, elem) => { - var translationKey = $(elem).find('.translation-key'); - var translationValue = $(elem).find('.translation-value textarea'); - translationData.push({ key: translationKey.text().replaceAll(' ', '_').trim(), value: translationValue.val().trim() }); - }); - if (translationData.length == 0) { - errorToast(genericErrorMessage()); - return; - } - $.post('/Home/ExportTranslation', { translationData: translationData }, function (data) { - if (!data) { - errorToast(genericErrorMessage()); - } else { - window.location.href = data; - } - }); -} -function showTranslationDownloader() { - $.get('/Home/GetAvailableTranslations', function(data){ - $('#translationDownloadModalContent').html(data); - $('#translationDownloadModal').modal('show'); - }) -} -function hideTranslationDownloader() { - $('#translationDownloadModal').modal('hide'); -} -function downloadTranslation(continent, name) { - sloader.show(); - $.get(`/Home/DownloadTranslation?continent=${continent}&name=${name}`, function (data) { - sloader.hide(); - if (data) { - successToast("Translation Downloaded"); - updateSettings(); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function downloadAllTranslations() { - sloader.show(); - $.get('/Home/DownloadAllTranslations', function (data) { - sloader.hide(); - if (data.success) { - successToast(data.message); - updateSettings(); - } else { - errorToast(data.message); - } - }) -} -function deleteTranslationKey(e) { - $(e).parent().parent().remove(); -} -//tabs reorder -function showTabReorderModal() { - //reorder the list items based on the CSS attribute - var sortedOrderedTabs = $(".motovaultpro-tab-groups > li").toArray().sort((a, b) => { - var currentVal = $(a).css("order"); - var nextVal = $(b).css("order"); - return currentVal - nextVal; - }); - $(".motovaultpro-tab-groups").html(sortedOrderedTabs); - $("#tabReorderModal").modal('show'); - bindTabReorderEvents(); -} -function hideTabReorderModal() { - $("#tabReorderModal").modal('hide'); -} -var tabDraggedToReorder = undefined; -function handleTabDragStart(e) { - tabDraggedToReorder = $(e.target).closest('.list-group-item'); - //clear out order attribute. - $(".motovaultpro-tab-groups > li").map((index, elem) => { - $(elem).css('order', 0); - }) -} -function handleTabDragOver(e) { - if (tabDraggedToReorder == undefined || tabDraggedToReorder == "") { - return; - } - var potentialDropTarget = $(e.target).closest('.list-group-item').attr("data-tab"); - var draggedTarget = tabDraggedToReorder.closest('.list-group-item').attr("data-tab"); - if (draggedTarget != potentialDropTarget) { - var targetObj = $(e.target).closest('.list-group-item'); - var draggedOrder = tabDraggedToReorder.index(); - var targetOrder = targetObj.index(); - if (draggedOrder < targetOrder) { - tabDraggedToReorder.insertAfter(targetObj); - } else { - tabDraggedToReorder.insertBefore(targetObj); - } - } - else { - event.preventDefault(); - } -} -function bindTabReorderEvents() { - $(".motovaultpro-tab-groups > li").on('dragstart', event => { - handleTabDragStart(event); - }); - $(".motovaultpro-tab-groups > li").on('dragover', event => { - handleTabDragOver(event); - }); - $(".motovaultpro-tab-groups > li").on('dragend', event => { - //reset order attribute - $(".motovaultpro-tab-groups > li").map((index, elem) => { - $(elem).css('order', $(elem).index()); - }) - }); -} -function getTabOrder() { - var tabOrderArray = []; - //check if any tabs have -1 order - var resetTabs = $(".motovaultpro-tab-groups > li").filter((index, elem) => $(elem).css('order') == -1).length > 0; - if (resetTabs) { - return tabOrderArray; //return empty array. - } - var sortedOrderedTabs = $(".motovaultpro-tab-groups > li").toArray().sort((a, b) => { - var currentVal = $(a).css("order"); - var nextVal = $(b).css("order"); - return currentVal - nextVal; - }); - sortedOrderedTabs.map(elem => { - var elemName = $(elem).attr("data-tab"); - tabOrderArray.push(elemName); - }); - return tabOrderArray; -} -function resetTabOrder() { - //set all orders to -1 - $(".motovaultpro-tab-groups > li").map((index, elem) => { - $(elem).css('order', -1); - }) - updateSettings(); -} - -function hideCustomWidgets() { - $("#customWidgetModal").modal('hide'); -} -function saveCustomWidgets() { - $.post('/Home/SaveCustomWidgets', { widgetsData: $("#widgetEditor").val() }, function (data) { - if (data) { - successToast("Custom Widgets Saved!"); - updateSettings(); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function deleteCustomWidgets() { - $.post('/Home/DeleteCustomWidgets', function (data) { - if (data) { - successToast("Custom Widgets Deleted!"); - updateSettings(); - } else { - errorToast(genericErrorMessage()); - } - }) -} -function showCustomWidgets() { - Swal.fire({ - title: 'Warning', - icon: "warning", - html: ` - - You are about to use the Custom Widgets Editor, this is a developer-focused feature that can lead to security vulnerabilities if you don't understand what you're doing. -
    Zero support will be provided from the developer(s) of MotoVaultPro regarding Custom Widgets, Read the Documentation. -
    By proceeding, you acknowledge that you are solely responsible for all consequences from utilizing the Custom Widgets Editor. -
    To proceed, enter 'acknowledge' into the text field below. -
    - - `, - confirmButtonText: 'Proceed', - focusConfirm: false, - preConfirm: () => { - const userAcknowledge = $("#inputAcknowledge").val(); - if (!userAcknowledge || userAcknowledge != 'acknowledge') { - Swal.showValidationMessage(`Please acknowledge before proceeding.`) - } - return { userAcknowledge } - }, - }).then(function (result) { - if (result.isConfirmed) { - $.get('/Home/GetCustomWidgetEditor', function (data) { - if (data.trim() != '') { - $("#customWidgetModalContent").html(data); - $("#customWidgetModal").modal('show'); - } else { - errorToast("Custom Widgets Not Enabled"); - } - }); - } - }); -} \ No newline at end of file diff --git a/wwwroot/js/shared.js b/wwwroot/js/shared.js deleted file mode 100644 index 05b2472..0000000 --- a/wwwroot/js/shared.js +++ /dev/null @@ -1,1827 +0,0 @@ -function returnToGarage() { - window.location.href = '/Home'; -} -function successToast(message) { - Swal.fire({ - toast: true, - position: "top-end", - showConfirmButton: false, - timer: 3000, - title: message, - timerProgressBar: true, - icon: "success", - didOpen: (toast) => { - toast.onmouseenter = Swal.stopTimer; - toast.onmouseleave = Swal.resumeTimer; - } - }) -} -function errorToast(message) { - Swal.fire({ - toast: true, - position: "top-end", - showConfirmButton: false, - timer: 3000, - title: message, - timerProgressBar: true, - icon: "error", - didOpen: (toast) => { - toast.onmouseenter = Swal.stopTimer; - toast.onmouseleave = Swal.resumeTimer; - } - }) -} -function infoToast(message) { - Swal.fire({ - toast: true, - position: "top-end", - showConfirmButton: false, - timer: 3000, - title: message, - timerProgressBar: true, - icon: "info", - didOpen: (toast) => { - toast.onmouseenter = Swal.stopTimer; - toast.onmouseleave = Swal.resumeTimer; - } - }) -} -function viewVehicle(vehicleId) { - window.location.href = `/Vehicle/Index?vehicleId=${vehicleId}`; -} -function saveVehicle(isEdit) { - var vehicleId = getVehicleModelData().id; - var vehicleYear = $("#inputYear").val(); - var vehicleMake = $("#inputMake").val(); - var vehicleModel = $("#inputModel").val(); - var vehicleTags = $("#inputTag").val(); - var vehiclePurchaseDate = $("#inputPurchaseDate").val(); - var vehicleSoldDate = $("#inputSoldDate").val(); - var vehicleLicensePlate = $("#inputLicensePlate").val(); - var vehicleVinNumber = $("#inputVinNumber").val(); - var vehicleIsElectric = $("#inputFuelType").val() == 'Electric'; - var vehicleIsDiesel = $("#inputFuelType").val() == 'Diesel'; - var vehicleUseHours = $("#inputUseHours").is(":checked"); - var vehicleOdometerOptional = $("#inputOdometerOptional").is(":checked"); - var vehicleHasOdometerAdjustment = $("#inputHasOdometerAdjustment").is(':checked'); - var vehicleOdometerMultiplier = $("#inputOdometerMultiplier").val(); - var vehicleOdometerDifference = parseInt(globalParseFloat($("#inputOdometerDifference").val())).toString(); - var vehiclePurchasePrice = $("#inputPurchasePrice").val(); - var vehicleSoldPrice = $("#inputSoldPrice").val(); - var vehicleIdentifier = $("#inputIdentifier").val(); - var vehicleDashboardMetrics = $("#collapseMetricInfo :checked").map(function () { - return this.value; - }).toArray(); - var extraFields = getAndValidateExtraFields(); - //validate - var hasError = false; - if (extraFields.hasError) { - hasError = true; - } - if (vehicleYear.trim() == '' || parseInt(vehicleYear) < 1900) { - hasError = true; - $("#inputYear").addClass("is-invalid"); - } else { - $("#inputYear").removeClass("is-invalid"); - } - if (vehicleMake.trim() == '') { - hasError = true; - $("#inputMake").addClass("is-invalid"); - } else { - $("#inputMake").removeClass("is-invalid"); - } - if (vehicleModel.trim() == '') { - hasError = true; - $("#inputModel").addClass("is-invalid"); - } else { - $("#inputModel").removeClass("is-invalid"); - } - if (vehicleIdentifier == "LicensePlate") { - if (vehicleLicensePlate.trim() == '') { - hasError = true; - $("#inputLicensePlate").addClass("is-invalid"); - } else { - $("#inputLicensePlate").removeClass("is-invalid"); - } - } else { - $("#inputLicensePlate").removeClass("is-invalid"); - //check if extra fields have value. - var vehicleIdentifierExtraField = extraFields.extraFields.filter(x => x.name == vehicleIdentifier); - //check if extra field exists. - if (vehicleIdentifierExtraField.length == 0) { - $(".modal.fade.show").find(`.extra-field [placeholder='${vehicleIdentifier}']`).addClass("is-invalid"); - hasError = true; - } else { - $(".modal.fade.show").find(`.extra-field [placeholder='${vehicleIdentifier}']`).removeClass("is-invalid"); - } - } - - if (vehicleHasOdometerAdjustment) { - //validate odometer adjustments - //validate multiplier - if (vehicleOdometerMultiplier.trim() == '' || !isValidMoney(vehicleOdometerMultiplier)) { - hasError = true; - $("#inputOdometerMultiplier").addClass("is-invalid"); - } else { - $("#inputOdometerMultiplier").removeClass("is-invalid"); - } - //validate difference - if (vehicleOdometerDifference.trim() == '' || isNaN(vehicleOdometerDifference)) { - hasError = true; - $("#inputOdometerDifference").addClass("is-invalid"); - } else { - $("#inputOdometerDifference").removeClass("is-invalid"); - } - } - if (vehiclePurchasePrice.trim() != '' && !isValidMoney(vehiclePurchasePrice)) { - hasError = true; - $("#inputPurchasePrice").addClass("is-invalid"); - $("#collapsePurchaseInfo").collapse('show'); - } else { - $("#inputPurchasePrice").removeClass("is-invalid"); - } - if (vehicleSoldPrice.trim() != '' && !isValidMoney(vehicleSoldPrice)) { - hasError = true; - $("#inputSoldPrice").addClass("is-invalid"); - $("#collapsePurchaseInfo").collapse('show'); - } else { - $("#inputSoldPrice").removeClass("is-invalid"); - } - if (hasError) { - return; - } - $.post('/Vehicle/SaveVehicle', { - id: vehicleId, - imageLocation: uploadedFile, - year: vehicleYear, - make: vehicleMake, - model: vehicleModel, - licensePlate: vehicleLicensePlate, - vinNumber: vehicleVinNumber, - isElectric: vehicleIsElectric, - isDiesel: vehicleIsDiesel, - tags: vehicleTags, - useHours: vehicleUseHours, - extraFields: extraFields.extraFields, - purchaseDate: vehiclePurchaseDate, - soldDate: vehicleSoldDate, - odometerOptional: vehicleOdometerOptional, - hasOdometerAdjustment: vehicleHasOdometerAdjustment, - odometerMultiplier: vehicleOdometerMultiplier, - odometerDifference: vehicleOdometerDifference, - purchasePrice: vehiclePurchasePrice, - soldPrice: vehicleSoldPrice, - dashboardMetrics: vehicleDashboardMetrics, - vehicleIdentifier: vehicleIdentifier - }, function (data) { - if (data) { - if (!isEdit) { - successToast("Vehicle Added"); - hideAddVehicleModal(); - loadGarage(); - } - else { - successToast("Vehicle Updated"); - hideEditVehicleModal(); - viewVehicle(vehicleId); - } - } else { - errorToast(genericErrorMessage()); - } - }); -} -function toggleOdometerAdjustment() { - var isChecked = $("#inputHasOdometerAdjustment").is(':checked'); - if (isChecked) { - $("#odometerAdjustments").collapse('show'); - } else { - $("#odometerAdjustments").collapse('hide'); - } -} -function uploadThumbnail(event) { - var originalImage = event.files[0]; - var maxHeight = 290; - try { - //load image and perform Hermite resize - var img = new Image(); - img.onload = function () { - URL.revokeObjectURL(img.src); - var imgWidth = img.width; - var imgHeight = img.height; - if (imgHeight > maxHeight) { - //only scale if height is greater than threshold - var imgScale = maxHeight / imgHeight; - var newImgWidth = imgWidth * imgScale; - var newImgHeight = imgHeight * imgScale; - var resizedCanvas = hermiteResize(img, newImgWidth, newImgHeight); - resizedCanvas.toBlob((blob) => { - let file = new File([blob], originalImage.name, { type: "image/jpeg" }); - uploadFileAsync(file); - }, 'image/jpeg'); - } else { - uploadFileAsync(originalImage); - } - } - img.src = URL.createObjectURL(originalImage); - } catch (error) { - console.log(`Error while attempting to upload and resize thumbnail - ${error}`); - uploadFileAsync(originalImage); - } -} -//Resize method using Hermite interpolation -//JS implementation by viliusle -function hermiteResize(origImg, width, height) { - var canvas = document.createElement("canvas"); - var ctx = canvas.getContext("2d"); - canvas.width = origImg.width; - canvas.height = origImg.height; - ctx.drawImage(origImg, 0, 0); - - var width_source = canvas.width; - var height_source = canvas.height; - width = Math.round(width); - height = Math.round(height); - - var ratio_w = width_source / width; - var ratio_h = height_source / height; - var ratio_w_half = Math.ceil(ratio_w / 2); - var ratio_h_half = Math.ceil(ratio_h / 2); - - - var img = ctx.getImageData(0, 0, width_source, height_source); - var img2 = ctx.createImageData(width, height); - var data = img.data; - var data2 = img2.data; - - for (var j = 0; j < height; j++) { - for (var i = 0; i < width; i++) { - var x2 = (i + j * width) * 4; - var weight = 0; - var weights = 0; - var weights_alpha = 0; - var gx_r = 0; - var gx_g = 0; - var gx_b = 0; - var gx_a = 0; - var center_y = (j + 0.5) * ratio_h; - var yy_start = Math.floor(j * ratio_h); - var yy_stop = Math.ceil((j + 1) * ratio_h); - for (var yy = yy_start; yy < yy_stop; yy++) { - var dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half; - var center_x = (i + 0.5) * ratio_w; - var w0 = dy * dy; //pre-calc part of w - var xx_start = Math.floor(i * ratio_w); - var xx_stop = Math.ceil((i + 1) * ratio_w); - for (var xx = xx_start; xx < xx_stop; xx++) { - var dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half; - var w = Math.sqrt(w0 + dx * dx); - if (w >= 1) { - //pixel too far - continue; - } - //hermite filter - weight = 2 * w * w * w - 3 * w * w + 1; - var pos_x = 4 * (xx + yy * width_source); - //alpha - gx_a += weight * data[pos_x + 3]; - weights_alpha += weight; - //colors - if (data[pos_x + 3] < 255) - weight = weight * data[pos_x + 3] / 250; - gx_r += weight * data[pos_x]; - gx_g += weight * data[pos_x + 1]; - gx_b += weight * data[pos_x + 2]; - weights += weight; - } - } - data2[x2] = gx_r / weights; - data2[x2 + 1] = gx_g / weights; - data2[x2 + 2] = gx_b / weights; - data2[x2 + 3] = gx_a / weights_alpha; - } - } - //clear and resize canvas - canvas.width = width; - canvas.height = height; - ctx.clearRect(0, 0, width, height); - - //draw - ctx.putImageData(img2, 0, 0); - return canvas; -} -function uploadFileAsync(event) { - let formData = new FormData(); - if (event.files != undefined && event.files.length > 0) { - formData.append("file", event.files[0]); - } else { - formData.append("file", event); - } - sloader.show(); - $.ajax({ - url: "/Files/HandleFileUpload", - data: formData, - cache: false, - processData: false, - contentType: false, - type: 'POST', - success: function (response) { - sloader.hide(); - if (response.trim() != '') { - uploadedFile = response; - } - }, - error: function () { - sloader.hide(); - errorToast("An error has occurred, please check the file size and try again later.") - } - }); -} -function isValidMoney(input) { - const euRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((\.\d{3}){0,8}|(\d{3}){0,8})(,\d{1,3}?)?\)?$/; - const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/; - return (euRegex.test(input) || usRegex.test(input)); -} -function initExtraFieldDatePicker(fieldName) { - let inputField = $(`#${fieldName}`); - if (inputField.length > 0) { - inputField.datepicker({ - format: getShortDatePattern().pattern, - autoclose: true, - weekStart: getGlobalConfig().firstDayOfWeek - }); - } -} -function initDatePicker(input, futureOnly) { - if (futureOnly) { - input.datepicker({ - startDate: "+0d", - format: getShortDatePattern().pattern, - autoclose: true, - weekStart: getGlobalConfig().firstDayOfWeek - }); - } else { - input.datepicker({ - endDate: "+0d", - format: getShortDatePattern().pattern, - autoclose: true, - weekStart: getGlobalConfig().firstDayOfWeek - }); - } -} -function initTagSelector(input, noDataList) { - if (noDataList) { - input.tagsinput({ - useDataList: false - }); - } else { - input.tagsinput(); - } -} - -// ===== MOBILE EXPERIENCE FRAMEWORK ===== - -// Mobile detection utility -function isMobileDevice() { - return window.matchMedia("(max-width: 768px)").matches || - /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); -} - -// Mobile-optimized tag selector -function initMobileTagSelector(input) { - if (input.length) { - // Initialize with mobile-friendly options - input.tagsinput({ - maxTags: 5, // Limit tags on mobile - trimValue: true, - confirmKeys: [13, 44, 32], // Enter, comma, space - focusClass: 'focus', - freeInput: true - }); - - // Add mobile-specific styling - input.parent().addClass('mobile-tag-input'); - - // Increase touch target size for mobile - input.parent().find('.bootstrap-tagsinput').css({ - 'min-height': '44px', - 'padding': '8px', - 'font-size': '16px' // Prevent zoom on iOS - }); - - // Style the input within tags - input.parent().find('.bootstrap-tagsinput input').css({ - 'font-size': '16px', - 'min-height': '30px' - }); - } -} - -// Universal mobile modal initializer -function initMobileModal(config) { - if (!isMobileDevice()) return; - - var modalId = config.modalId || '#modal'; - var dateInputId = config.dateInputId; - var tagSelectorId = config.tagSelectorId; - var modeToggleId = config.modeToggleId; - var simpleModeDefault = config.simpleModeDefault || false; - - // Convert date input to native HTML5 on mobile - if (dateInputId) { - var dateInput = $(dateInputId); - if (dateInput.length) { - dateInput.attr('type', 'date'); - dateInput.removeClass('datepicker'); // Remove Bootstrap datepicker class - } - } - - // Default to simple mode on mobile if toggle exists - if (modeToggleId && simpleModeDefault) { - var modeToggle = $(modeToggleId); - if (modeToggle.length && !modeToggle.is(':checked')) { - modeToggle.prop('checked', true); - // Try to call the mode toggle function if it exists - if (typeof toggleFuelEntryMode === 'function') { - toggleFuelEntryMode(true); - } - } - } - - // Initialize mobile-friendly tag selector - if (tagSelectorId) { - initMobileTagSelector($(tagSelectorId)); - } - - // Initialize swipe to dismiss - initSwipeToDismiss(modalId); -} - -// Swipe to dismiss functionality for mobile modals -function initSwipeToDismiss(modalSelector) { - if (!isMobileDevice()) return; - - modalSelector = modalSelector || '#modal'; - var modal = $(modalSelector); - var modalContent = modal.find('.modal-content'); - var startY = 0; - var currentY = 0; - var isDragging = false; - var threshold = 100; // Minimum swipe distance to dismiss - - // Remove any existing touch handlers to avoid conflicts - modalContent.off('touchstart touchmove touchend'); - - // Touch start - modalContent.on('touchstart', function(e) { - startY = e.originalEvent.touches[0].clientY; - isDragging = true; - modalContent.css('transition', 'none'); - }); - - // Touch move - modalContent.on('touchmove', function(e) { - if (!isDragging) return; - - currentY = e.originalEvent.touches[0].clientY; - var deltaY = currentY - startY; - - // Only allow downward swipes - if (deltaY > 0) { - modalContent.css('transform', `translateY(${deltaY}px)`); - } - }); - - // Touch end - modalContent.on('touchend', function(e) { - if (!isDragging) return; - - isDragging = false; - var deltaY = currentY - startY; - - modalContent.css('transition', 'transform 0.3s ease-out'); - - if (deltaY > threshold) { - // Dismiss modal - modalContent.css('transform', 'translateY(100%)'); - setTimeout(function() { - modal.modal('hide'); - modalContent.css('transform', ''); - }, 300); - } else { - // Snap back - modalContent.css('transform', ''); - } - }); -} - -// Initialize form inputs based on device type (legacy compatibility) -function initializeFormInputs() { - // This function is maintained for backward compatibility with gasrecord.js - // New modals should use initMobileModal() instead - console.warn('initializeFormInputs() is deprecated. Use initMobileModal() instead.'); -} -function getAndValidateSelectedVehicle() { - var selectedVehiclesArray = []; - $("#vehicleSelector :checked").map(function () { - selectedVehiclesArray.push(this.value); - }); - if (selectedVehiclesArray.length == 0) { - return { - hasError: true, - ids: [] - } - } else { - return { - hasError: false, - ids: selectedVehiclesArray - } - } -} -function showMobileNav() { - $(".motovaultpro-mobile-nav").addClass("motovaultpro-mobile-nav-show"); - //hide body scrollbar - $("body").css('overflow-y', 'hidden'); - $("body").css('position', 'fixed'); //iOS SafariWebKit hack fix -} -function hideMobileNav() { - $(".motovaultpro-mobile-nav").removeClass("motovaultpro-mobile-nav-show"); - //re-enable scrollbar. - $("body").css('overflow-y', 'auto'); - $("body").css('position', ''); //iOS SafariWebKit hack fix -} -var windowWidthForCompare = 0; -function bindWindowResize() { - windowWidthForCompare = window.innerWidth; - $(window).on('resize', function () { - if (window.innerWidth != windowWidthForCompare) { - hideMobileNav(); - checkNavBarOverflow(); - windowWidthForCompare = window.innerWidth; - } - }); -} -function encodeHTMLInput(input) { - const encoded = document.createElement('div'); - encoded.innerText = input; - return encoded.innerHTML; -} -function decodeHTMLEntities(text) { - return $(""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // Support: IE <=9 only - // IE <=9 replaces "; - support.option = !!div.lastChild; -} )(); - - -// We have to close these tags to support XHTML (#13200) -var wrapMap = { - - // XHTML parsers do not magically insert elements in the - // same way that tag soup parsers do. So we cannot shorten - // this by omitting or other required elements. - thead: [ 1, "", "
    " ], - col: [ 2, "", "
    " ], - tr: [ 2, "", "
    " ], - td: [ 3, "", "
    " ], - - _default: [ 0, "", "" ] -}; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: IE <=9 only -if ( !support.option ) { - wrapMap.optgroup = wrapMap.option = [ 1, "" ]; -} - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (#15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Ensure the created nodes are orphaned (#12392) - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; -} - - -var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -// Support: IE <=9 - 11+ -// focus() and blur() are asynchronous, except when they are no-op. -// So expect focus to be synchronous when the element is already active, -// and blur to be synchronous when the element is not already active. -// (focus and blur are always synchronous in other supported browsers, -// this just defines when we can count on it). -function expectSync( elem, type ) { - return ( elem === safeActiveElement() ) === ( type === "focus" ); -} - -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Only attach events to objects that accept data - if ( !acceptData( elem ) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = Object.create( null ); - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( nativeEvent ), - - handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // If the event is namespaced, then each handler is only invoked if it is - // specially universal or its namespaces are a superset of the event's. - if ( !event.rnamespace || handleObj.namespace === false || - event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, handleObj, sel, matchedHandlers, matchedSelectors, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - if ( delegateCount && - - // Support: IE <=9 - // Black-hole SVG instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 - // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) - // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only - // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) - !( event.type === "click" && event.button >= 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { - matchedHandlers = []; - matchedSelectors = {}; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matchedSelectors[ sel ] === undefined ) { - matchedSelectors[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matchedSelectors[ sel ] ) { - matchedHandlers.push( handleObj ); - } - } - if ( matchedHandlers.length ) { - handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - cur = this; - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - - // Utilize native event to ensure correct state for checkable inputs - setup: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Claim the first handler - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - // dataPriv.set( el, "click", ... ) - leverageNative( el, "click", returnTrue ); - } - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Force setup before triggering a click - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - leverageNative( el, "click" ); - } - - // Return non-false to allow normal event-path propagation - return true; - }, - - // For cross-browser consistency, suppress native .click() on links - // Also prevent it if we're currently inside a leveraged native-event stack - _default: function( event ) { - var target = event.target; - return rcheckableType.test( target.type ) && - target.click && nodeName( target, "input" ) && - dataPriv.get( target, "click" ) || - nodeName( target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -// Ensure the presence of an event listener that handles manually-triggered -// synthetic events by interrupting progress until reinvoked in response to -// *native* events that it fires directly, ensuring that state changes have -// already occurred before other listeners are invoked. -function leverageNative( el, type, expectSync ) { - - // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add - if ( !expectSync ) { - if ( dataPriv.get( el, type ) === undefined ) { - jQuery.event.add( el, type, returnTrue ); - } - return; - } - - // Register the controller as a special universal handler for all event namespaces - dataPriv.set( el, type, false ); - jQuery.event.add( el, type, { - namespace: false, - handler: function( event ) { - var notAsync, result, - saved = dataPriv.get( this, type ); - - if ( ( event.isTrigger & 1 ) && this[ type ] ) { - - // Interrupt processing of the outer synthetic .trigger()ed event - // Saved data should be false in such cases, but might be a leftover capture object - // from an async native handler (gh-4350) - if ( !saved.length ) { - - // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. - saved = slice.call( arguments ); - dataPriv.set( this, type, saved ); - - // Trigger the native event and capture its result - // Support: IE <=9 - 11+ - // focus() and blur() are asynchronous - notAsync = expectSync( this, type ); - this[ type ](); - result = dataPriv.get( this, type ); - if ( saved !== result || notAsync ) { - dataPriv.set( this, type, false ); - } else { - result = {}; - } - if ( saved !== result ) { - - // Cancel the outer synthetic event - event.stopImmediatePropagation(); - event.preventDefault(); - - // Support: Chrome 86+ - // In Chrome, if an element having a focusout handler is blurred by - // clicking outside of it, it invokes the handler synchronously. If - // that handler calls `.remove()` on the element, the data is cleared, - // leaving `result` undefined. We need to guard against this. - return result && result.value; - } - - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering the - // native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. - } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { - event.stopPropagation(); - } - - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved.length ) { - - // ...and capture the result - dataPriv.set( this, type, { - value: jQuery.event.trigger( - - // Support: IE <=9 - 11+ - // Extend with the prototype to reset the above stopImmediatePropagation() - jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), - saved.slice( 1 ), - this - ) - } ); - - // Abort handling of the native event - event.stopImmediatePropagation(); - } - } - } ); -} - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (#504, #13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || Date.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - code: true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - which: true -}, jQuery.event.addProp ); - -jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { - jQuery.event.special[ type ] = { - - // Utilize native event if possible so blur/focus sequence is correct - setup: function() { - - // Claim the first handler - // dataPriv.set( this, "focus", ... ) - // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, expectSync ); - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function() { - - // Force setup before trigger - leverageNative( this, type ); - - // Return non-false to allow normal event-path propagation - return true; - }, - - // Suppress native focus or blur as it's already being fired - // in leverageNative. - _default: function() { - return true; - }, - - delegateType: delegateType - }; -} ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; - -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { - elem.type = elem.type.slice( 5 ); - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.get( src ); - events = pdataOld.events; - - if ( events ) { - dataPriv.remove( dest, "handle events" ); - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = flat( args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - valueIsFunction = isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl && !node.noModule ) { - jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) - }, doc ); - } - } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && isAttached( node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html; - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = isAttached( elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - -var swap = function( elem, options, callback ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.call( elem ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - container.style.cssText = "position:absolute;left:-11111px;width:60px;" + - "margin-top:1px;padding:0;border:0"; - div.style.cssText = - "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + - "margin:auto;border:1px;padding:1px;" + - "width:60%;top:1%"; - documentElement.appendChild( container ).appendChild( div ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - - // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 - // Some styles come back with percentage values, even though they shouldn't - div.style.right = "60%"; - pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; - - // Support: IE 9 - 11 only - // Detect misreporting of content dimensions for box-sizing:border-box elements - boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; - - // Support: IE 9 only - // Detect overflow:scroll screwiness (gh-3699) - // Support: Chrome <=64 - // Don't get tricked when zoom affects offsetWidth (gh-4029) - div.style.position = "absolute"; - scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - function roundPixelMeasures( measure ) { - return Math.round( parseFloat( measure ) ); - } - - var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableTrDimensionsVal, reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (#8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - jQuery.extend( support, { - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelBoxStyles: function() { - computeStyleTests(); - return pixelBoxStylesVal; - }, - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - }, - scrollboxSize: function() { - computeStyleTests(); - return scrollboxSizeVal; - }, - - // Support: IE 9 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Behavior in IE 9 is more subtle than in newer versions & it passes - // some versions of this test; make sure not to make it pass there! - // - // Support: Firefox 70+ - // Only Firefox includes border widths - // in computed dimensions. (gh-4529) - reliableTrDimensions: function() { - var table, tr, trChild, trStyle; - if ( reliableTrDimensionsVal == null ) { - table = document.createElement( "table" ); - tr = document.createElement( "tr" ); - trChild = document.createElement( "div" ); - - table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; - tr.style.cssText = "border:1px solid"; - - // Support: Chrome 86+ - // Height set through cssText does not get applied. - // Computed height then comes back as 0. - tr.style.height = "1px"; - trChild.style.height = "9px"; - - // Support: Android 8 Chrome 86+ - // In our bodyBackground.html iframe, - // display for all div elements is set to "inline", - // which causes a problem only in Android 8 Chrome 86. - // Ensuring the div is display: block - // gets around this issue. - trChild.style.display = "block"; - - documentElement - .appendChild( table ) - .appendChild( tr ) - .appendChild( trChild ); - - trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + - parseInt( trStyle.borderTopWidth, 10 ) + - parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; - - documentElement.removeChild( table ); - } - return reliableTrDimensionsVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - - // Support: Firefox 51+ - // Retrieving style before computed somehow - // fixes an issue with getting wrong values - // on detached elements - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is needed for: - // .css('filter') (IE 9 only, #12537) - // .css('--customProperty) (#3144) - if ( computed ) { - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( ret === "" && !isAttached( elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style, - vendorProps = {}; - -// Return a vendor-prefixed property or undefined -function vendorPropName( name ) { - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -// Return a potentially-mapped jQuery.cssProps or vendor prefixed property -function finalPropName( name ) { - var final = jQuery.cssProps[ name ] || vendorProps[ name ]; - - if ( final ) { - return final; - } - if ( name in emptyStyle ) { - return name; - } - return vendorProps[ name ] = vendorPropName( name ) || name; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rcustomProp = /^--/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }; - -function setPositiveNumber( _elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { - var i = dimension === "width" ? 1 : 0, - extra = 0, - delta = 0; - - // Adjustment may not be necessary - if ( box === ( isBorderBox ? "border" : "content" ) ) { - return 0; - } - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin - if ( box === "margin" ) { - delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); - } - - // If we get here with a content-box, we're seeking "padding" or "border" or "margin" - if ( !isBorderBox ) { - - // Add padding - delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // For "border" or "margin", add border - if ( box !== "padding" ) { - delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - - // But still keep track of it otherwise - } else { - extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - - // If we get here with a border-box (content + padding + border), we're seeking "content" or - // "padding" or "margin" - } else { - - // For "content", subtract padding - if ( box === "content" ) { - delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // For "content" or "padding", subtract border - if ( box !== "margin" ) { - delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - // Account for positive content-box scroll gutter when requested by providing computedVal - if ( !isBorderBox && computedVal >= 0 ) { - - // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border - // Assuming integer scroll gutter, subtract the rest and round down - delta += Math.max( 0, Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - computedVal - - delta - - extra - - 0.5 - - // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter - // Use an explicit zero to avoid NaN (gh-3964) - ) ) || 0; - } - - return delta; -} - -function getWidthOrHeight( elem, dimension, extra ) { - - // Start with computed style - var styles = getStyles( elem ), - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). - // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox, - - val = curCSS( elem, dimension, styles ), - offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - - // Support: Firefox <=54 - // Return a confounding non-pixel value or feign ignorance, as appropriate. - if ( rnumnonpx.test( val ) ) { - if ( !extra ) { - return val; - } - val = "auto"; - } - - - // Support: IE 9 - 11 only - // Use offsetWidth/offsetHeight for when box sizing is unreliable. - // In those cases, the computed value can be trusted to be border-box. - if ( ( !support.boxSizingReliable() && isBorderBox || - - // Support: IE 10 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Interestingly, in some cases IE 9 doesn't suffer from this issue. - !support.reliableTrDimensions() && nodeName( elem, "tr" ) || - - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - val === "auto" || - - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && - - // Make sure the element is visible & connected - elem.getClientRects().length ) { - - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Where available, offsetWidth/offsetHeight approximate border box dimensions. - // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the - // retrieved value as a content box dimension. - valueIsBorderBox = offsetProp in elem; - if ( valueIsBorderBox ) { - val = elem[ offsetProp ]; - } - } - - // Normalize "" and auto - val = parseFloat( val ) || 0; - - // Adjust for the element's box model - return ( val + - boxModelAdjustment( - elem, - dimension, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles, - - // Provide the current computed size to request scroll gutter calculation (gh-3589) - val - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "animationIterationCount": true, - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "gridArea": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnStart": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowStart": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ), - style = elem.style; - - // Make sure that we're working with the right name. We don't - // want to query the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (#7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (#7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - if ( isCustomProp ) { - style.setProperty( name, value ); - } else { - style[ name ] = value; - } - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ); - - // Make sure that we're working with the right name. We don't - // want to modify the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( _i, dimension ) { - jQuery.cssHooks[ dimension ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = getStyles( elem ), - - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra ? - boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ) : - 0; - - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ dimension ] = value; - value = jQuery.css( elem, dimension ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( prefix !== "margin" ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( Array.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || jQuery.easing._default; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - // Use a property on the element directly when it is not a DOM element, - // or when there is no matching style property that exists. - if ( tween.elem.nodeType !== 1 || - tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { - return tween.elem[ tween.prop ]; - } - - // Passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails. - // Simple values such as "10px" are parsed to Float; - // complex values such as "rotate(1rad)" are returned as-is. - result = jQuery.css( tween.elem, tween.prop, "" ); - - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - - // Use step hook for back compat. - // Use cssHook if its there. - // Use .style if available and use plain properties where available. - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || - tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 only -// Panic based approach to setting things on disconnected nodes -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - }, - _default: "swing" -}; - -jQuery.fx = Tween.prototype.init; - -// Back compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, inProgress, - rfxtypes = /^(?:toggle|show|hide)$/, - rrun = /queueHooks$/; - -function schedule() { - if ( inProgress ) { - if ( document.hidden === false && window.requestAnimationFrame ) { - window.requestAnimationFrame( schedule ); - } else { - window.setTimeout( schedule, jQuery.fx.interval ); - } - - jQuery.fx.tick(); - } -} - -// Animations created synchronously will run synchronously -function createFxNow() { - window.setTimeout( function() { - fxNow = undefined; - } ); - return ( fxNow = Date.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - i = 0, - attrs = { height: type }; - - // If we include width, step value is 1 to do all cssExpand values, - // otherwise step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { - - // We're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, - isBox = "width" in props || "height" in props, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHiddenWithinTree( elem ), - dataShow = dataPriv.get( elem, "fxshow" ); - - // Queue-skipping animations hijack the fx hooks - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always( function() { - - // Ensure the complete handler is called before this completes - anim.always( function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - } ); - } ); - } - - // Detect show/hide animations - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.test( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // Pretend to be hidden if this is a "show" and - // there is still data from a stopped show/hide - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - - // Ignore all other no-op show/hide data - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } - } - - // Bail out if this is a no-op like .hide().hide() - propTween = !jQuery.isEmptyObject( props ); - if ( !propTween && jQuery.isEmptyObject( orig ) ) { - return; - } - - // Restrict "overflow" and "display" styles during box animations - if ( isBox && elem.nodeType === 1 ) { - - // Support: IE <=9 - 11, Edge 12 - 15 - // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY and Edge just mirrors - // the overflowX value there. - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Identify a display type, preferring old show/hide data over the CSS cascade - restoreDisplay = dataShow && dataShow.display; - if ( restoreDisplay == null ) { - restoreDisplay = dataPriv.get( elem, "display" ); - } - display = jQuery.css( elem, "display" ); - if ( display === "none" ) { - if ( restoreDisplay ) { - display = restoreDisplay; - } else { - - // Get nonempty value(s) by temporarily forcing visibility - showHide( [ elem ], true ); - restoreDisplay = elem.style.display || restoreDisplay; - display = jQuery.css( elem, "display" ); - showHide( [ elem ] ); - } - } - - // Animate inline elements as inline-block - if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { - if ( jQuery.css( elem, "float" ) === "none" ) { - - // Restore the original display value at the end of pure show/hide animations - if ( !propTween ) { - anim.done( function() { - style.display = restoreDisplay; - } ); - if ( restoreDisplay == null ) { - display = style.display; - restoreDisplay = display === "none" ? "" : display; - } - } - style.display = "inline-block"; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - anim.always( function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - } ); - } - - // Implement show/hide animations - propTween = false; - for ( prop in orig ) { - - // General show/hide setup for this element animation - if ( !propTween ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); - } - - // Store hidden/visible for toggle so `.stop().toggle()` "reverses" - if ( toggle ) { - dataShow.hidden = !hidden; - } - - // Show elements before animating them - if ( hidden ) { - showHide( [ elem ], true ); - } - - /* eslint-disable no-loop-func */ - - anim.done( function() { - - /* eslint-enable no-loop-func */ - - // The final step of a "hide" animation is actually hiding the element - if ( !hidden ) { - showHide( [ elem ] ); - } - dataPriv.remove( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - } ); - } - - // Per-property setup - propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = propTween.start; - if ( hidden ) { - propTween.end = propTween.start; - propTween.start = 0; - } - } - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( Array.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // Not quite $.extend, this won't overwrite existing keys. - // Reusing 'index' because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = Animation.prefilters.length, - deferred = jQuery.Deferred().always( function() { - - // Don't match elem in the :animated selector - delete tick.elem; - } ), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - - // Support: Android 2.3 only - // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ] ); - - // If there's more to do, yield - if ( percent < 1 && length ) { - return remaining; - } - - // If this was an empty animation, synthesize a final progress notification - if ( !length ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - } - - // Resolve the animation and report its conclusion - deferred.resolveWith( elem, [ animation ] ); - return false; - }, - animation = deferred.promise( { - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { - specialEasing: {}, - easing: jQuery.easing._default - }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - - // If we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // Resolve when we played the last frame; otherwise, reject - if ( gotoEnd ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - } ), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length; index++ ) { - result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - if ( isFunction( result.stop ) ) { - jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - result.stop.bind( result ); - } - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - // Attach callbacks from options - animation - .progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - } ) - ); - - return animation; -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweeners: { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ); - adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); - return tween; - } ] - }, - - tweener: function( props, callback ) { - if ( isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.match( rnothtmlwhite ); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length; index++ ) { - prop = props[ index ]; - Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; - Animation.tweeners[ prop ].unshift( callback ); - } - }, - - prefilters: [ defaultPrefilter ], - - prefilter: function( callback, prepend ) { - if ( prepend ) { - Animation.prefilters.unshift( callback ); - } else { - Animation.prefilters.push( callback ); - } - } -} ); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !isFunction( easing ) && easing - }; - - // Go to the end state if fx are off - if ( jQuery.fx.off ) { - opt.duration = 0; - - } else { - if ( typeof opt.duration !== "number" ) { - if ( opt.duration in jQuery.fx.speeds ) { - opt.duration = jQuery.fx.speeds[ opt.duration ]; - - } else { - opt.duration = jQuery.fx.speeds._default; - } - } - } - - // Normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend( { - fadeTo: function( speed, to, easing, callback ) { - - // Show any hidden elements after setting opacity to 0 - return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() - - // Animate to the value specified - .end().animate( { opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || dataPriv.get( this, "finish" ) ) { - anim.stop( true ); - } - }; - - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue ) { - this.queue( type || "fx", [] ); - } - - return this.each( function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = dataPriv.get( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && - ( type == null || timers[ index ].queue === type ) ) { - - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // Start the next in the queue if the last step wasn't forced. - // Timers currently will call their complete callbacks, which - // will dequeue but only if they were gotoEnd. - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - } ); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each( function() { - var index, - data = dataPriv.get( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // Enable finishing flag on private data - data.finish = true; - - // Empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // Look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // Look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // Turn off finishing flag - delete data.finish; - } ); - } -} ); - -jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -} ); - -// Generate shortcuts for custom animations -jQuery.each( { - slideDown: genFx( "show" ), - slideUp: genFx( "hide" ), - slideToggle: genFx( "toggle" ), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -} ); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - i = 0, - timers = jQuery.timers; - - fxNow = Date.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - - // Run the timer and safely remove it when done (allowing for external removal) - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - jQuery.fx.start(); -}; - -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( inProgress ) { - return; - } - - inProgress = true; - schedule(); -}; - -jQuery.fx.stop = function() { - inProgress = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); - } - - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) > -1 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - return this.each( function() { - var className, i, self, classNames; - - if ( isValidValue ) { - - // Toggle individual class names - i = 0; - self = jQuery( this ); - classNames = classesToArray( value ); - - while ( ( className = classNames[ i++ ] ) ) { - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (#14686, #14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -support.focusin = "onfocusin" in window; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -// Support: Firefox <=44 -// Firefox doesn't have focus(in | out) events -// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 -// -// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 -// focus(in | out) events fire after focus & blur events, -// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order -// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 -if ( !support.focusin ) { - jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - - // Handle: regular nodes (via `this.ownerDocument`), window - // (via `this.document`) & document (via `this`). - var doc = this.ownerDocument || this.document || this, - attaches = dataPriv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this.document || this, - attaches = dataPriv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - dataPriv.remove( doc, fix ); - - } else { - dataPriv.access( doc, fix, attaches ); - } - } - }; - } ); -} -var location = window.location; - -var nonce = { guid: Date.now() }; - -var rquery = ( /\?/ ); - - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, parserErrorElem; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) {} - - parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; - if ( !xml || parserErrorElem ) { - jQuery.error( "Invalid XML: " + ( - parserErrorElem ? - jQuery.map( parserErrorElem.childNodes, function( el ) { - return el.textContent; - } ).join( "\n" ) : - data - ) ); - } - return xml; -}; - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( Array.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && toType( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - if ( a == null ) { - return ""; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ).filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ).map( function( _i, elem ) { - var val = jQuery( this ).val(); - - if ( val == null ) { - return null; - } - - if ( Array.isArray( val ) ) { - return jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ); - } - - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -var - r20 = /%20/g, - rhash = /#.*$/, - rantiCache = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, - - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat( "*" ), - - // Anchor tag for parsing the document origin - originAnchor = document.createElement( "a" ); - -originAnchor.href = location.href; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - - if ( isFunction( func ) ) { - - // For each dataType in the dataTypeExpression - while ( ( dataType = dataTypes[ i++ ] ) ) { - - // Prepend if requested - if ( dataType[ 0 ] === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); - - // Otherwise append - } else { - ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && - !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - } ); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var key, deep, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - - var ct, type, finalDataType, firstDataType, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s.throws ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { - state: "parsererror", - error: conv ? e : "No conversion from " + prev + " to " + current - }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend( { - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: location.href, - type: "GET", - isLocal: rlocalProtocol.test( location.protocol ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /\bxml\b/, - html: /\bhtml/, - json: /\bjson\b/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": JSON.parse, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var transport, - - // URL without anti-cache param - cacheURL, - - // Response headers - responseHeadersString, - responseHeaders, - - // timeout handle - timeoutTimer, - - // Url cleanup var - urlAnchor, - - // Request state (becomes false upon send and true upon completion) - completed, - - // To know if global events are to be dispatched - fireGlobals, - - // Loop variable - i, - - // uncached part of the url - uncached, - - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - - // Callbacks context - callbackContext = s.context || s, - - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && - ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks( "once memory" ), - - // Status-dependent callbacks - statusCode = s.statusCode || {}, - - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - - // Default abort message - strAbort = "canceled", - - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( completed ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() + " " ] = - ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) - .concat( match[ 2 ] ); - } - } - match = responseHeaders[ key.toLowerCase() + " " ]; - } - return match == null ? null : match.join( ", " ); - }, - - // Raw string - getAllResponseHeaders: function() { - return completed ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - if ( completed == null ) { - name = requestHeadersNames[ name.toLowerCase() ] = - requestHeadersNames[ name.toLowerCase() ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( completed == null ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( completed ) { - - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } else { - - // Lazy-add the new callbacks in a way that preserves old ones - for ( code in map ) { - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ); - - // Add protocol if not provided (prefilters might expect it) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || location.href ) + "" ) - .replace( rprotocol, location.protocol + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; - - // A cross-domain request is in order when the origin doesn't match the current origin. - if ( s.crossDomain == null ) { - urlAnchor = document.createElement( "a" ); - - // Support: IE <=8 - 11, Edge 12 - 15 - // IE throws exception on accessing the href property if url is malformed, - // e.g. http://example.com:80x/ - try { - urlAnchor.href = s.url; - - // Support: IE <=8 - 11 only - // Anchor's host property isn't correctly set when s.url is relative - urlAnchor.href = urlAnchor.href; - s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== - urlAnchor.protocol + "//" + urlAnchor.host; - } catch ( e ) { - - // If there is an error parsing the URL, assume it is crossDomain, - // it can be rejected by the transport if it is invalid - s.crossDomain = true; - } - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( completed ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - // Remove hash to simplify url manipulation - cacheURL = s.url.replace( rhash, "" ); - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // Remember the hash so we can put it back - uncached = s.url.slice( cacheURL.length ); - - // If data is available and should be processed, append data to url - if ( s.data && ( s.processData || typeof s.data === "string" ) ) { - cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; - - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add or update anti-cache param if needed - if ( s.cache === false ) { - cacheURL = cacheURL.replace( rantiCache, "$1" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + - uncached; - } - - // Put hash and anti-cache on the URL that will be requested (gh-1732) - s.url = cacheURL + uncached; - - // Change '%20' to '+' if this is encoded form body content (gh-2658) - } else if ( s.data && s.processData && - ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { - s.data = s.data.replace( r20, "+" ); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + - ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && - ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { - - // Abort if not done already and return - return jqXHR.abort(); - } - - // Aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - completeDeferred.add( s.complete ); - jqXHR.done( s.success ); - jqXHR.fail( s.error ); - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - - // If request was aborted inside ajaxSend, stop there - if ( completed ) { - return jqXHR; - } - - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = window.setTimeout( function() { - jqXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - completed = false; - transport.send( requestHeaders, done ); - } catch ( e ) { - - // Rethrow post-completion exceptions - if ( completed ) { - throw e; - } - - // Propagate others as results - done( -1, e ); - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Ignore repeat invocations - if ( completed ) { - return; - } - - completed = true; - - // Clear timeout if it exists - if ( timeoutTimer ) { - window.clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Use a noop converter for missing script but not if jsonp - if ( !isSuccess && - jQuery.inArray( "script", s.dataTypes ) > -1 && - jQuery.inArray( "json", s.dataTypes ) < 0 ) { - s.converters[ "text script" ] = function() {}; - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader( "Last-Modified" ); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader( "etag" ); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - - // Extract error from statusText and normalize for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger( "ajaxStop" ); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -} ); - -jQuery.each( [ "get", "post" ], function( _i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - - // Shift arguments if data argument was omitted - if ( isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - // The url can be an options object (which then must have .url) - return jQuery.ajax( jQuery.extend( { - url: url, - type: method, - dataType: type, - data: data, - success: callback - }, jQuery.isPlainObject( url ) && url ) ); - }; -} ); - -jQuery.ajaxPrefilter( function( s ) { - var i; - for ( i in s.headers ) { - if ( i.toLowerCase() === "content-type" ) { - s.contentType = s.headers[ i ] || ""; - } - } -} ); - - -jQuery._evalUrl = function( url, options, doc ) { - return jQuery.ajax( { - url: url, - - // Make this explicit, since user can override this through ajaxSetup (#11264) - type: "GET", - dataType: "script", - cache: true, - async: false, - global: false, - - // Only evaluate the response if it is successful (gh-4126) - // dataFilter is not invoked for failure responses, so using it instead - // of the default converter is kludgy but it works. - converters: { - "text script": function() {} - }, - dataFilter: function( response ) { - jQuery.globalEval( response, options, doc ); - } - } ); -}; - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -jQuery.ajaxSettings.xhr = function() { - try { - return new window.XMLHttpRequest(); - } catch ( e ) {} -}; - -var xhrSuccessStatus = { - - // File protocol always yields status code 0, assume 200 - 0: 200, - - // Support: IE <=9 only - // #1450: sometimes IE returns 1223 when it should be 204 - 1223: 204 - }, - xhrSupported = jQuery.ajaxSettings.xhr(); - -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -support.ajax = xhrSupported = !!xhrSupported; - -jQuery.ajaxTransport( function( options ) { - var callback, errorCallback; - - // Cross domain only allowed if supported through XMLHttpRequest - if ( support.cors || xhrSupported && !options.crossDomain ) { - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(); - - xhr.open( - options.type, - options.url, - options.async, - options.username, - options.password - ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { - headers[ "X-Requested-With" ] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - - // Callback - callback = function( type ) { - return function() { - if ( callback ) { - callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.ontimeout = - xhr.onreadystatechange = null; - - if ( type === "abort" ) { - xhr.abort(); - } else if ( type === "error" ) { - - // Support: IE <=9 only - // On a manual native abort, IE9 throws - // errors on any property access that is not readyState - if ( typeof xhr.status !== "number" ) { - complete( 0, "error" ); - } else { - complete( - - // File: protocol always yields status 0; see #8605, #14207 - xhr.status, - xhr.statusText - ); - } - } else { - complete( - xhrSuccessStatus[ xhr.status ] || xhr.status, - xhr.statusText, - - // Support: IE <=9 only - // IE9 has no XHR2 but throws on binary (trac-11426) - // For XHR2 non-text, let the caller handle it (gh-2498) - ( xhr.responseType || "text" ) !== "text" || - typeof xhr.responseText !== "string" ? - { binary: xhr.response } : - { text: xhr.responseText }, - xhr.getAllResponseHeaders() - ); - } - } - }; - }; - - // Listen to events - xhr.onload = callback(); - errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); - - // Support: IE 9 only - // Use onreadystatechange to replace onabort - // to handle uncaught aborts - if ( xhr.onabort !== undefined ) { - xhr.onabort = errorCallback; - } else { - xhr.onreadystatechange = function() { - - // Check readyState before timeout as it changes - if ( xhr.readyState === 4 ) { - - // Allow onerror to be called first, - // but that will not handle a native abort - // Also, save errorCallback to a variable - // as xhr.onerror cannot be accessed - window.setTimeout( function() { - if ( callback ) { - errorCallback(); - } - } ); - } - }; - } - - // Create the abort callback - callback = callback( "abort" ); - - try { - - // Do send the request (this may raise an exception) - xhr.send( options.hasContent && options.data || null ); - } catch ( e ) { - - // #14683: Only rethrow if this hasn't been notified as an error yet - if ( callback ) { - throw e; - } - } - }, - - abort: function() { - if ( callback ) { - callback(); - } - } - }; - } -} ); - - - - -// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) -jQuery.ajaxPrefilter( function( s ) { - if ( s.crossDomain ) { - s.contents.script = false; - } -} ); - -// Install script dataType -jQuery.ajaxSetup( { - accepts: { - script: "text/javascript, application/javascript, " + - "application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /\b(?:java|ecma)script\b/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -} ); - -// Handle cache's special case and crossDomain -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - } -} ); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function( s ) { - - // This transport only deals with cross domain or forced-by-attrs requests - if ( s.crossDomain || s.scriptAttrs ) { - var script, callback; - return { - send: function( _, complete ) { - script = jQuery( "