This commit is contained in:
2026-01-23 20:35:40 +03:00
parent cf3b0e48ec
commit 66c151653e
137 changed files with 41495 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
# Car Dataset System
## Overview
The Car Dataset system provides a structured way to manage vehicle manufacturers, models, and body types. This ensures data consistency and improves user experience when adding new vehicles.
## Database Schema
### CarDataset Table
- `id`: Primary key (auto-increment)
- `manufacturer`: Vehicle manufacturer name (e.g., "Toyota", "Honda")
- `model`: Vehicle model name (e.g., "Camry", "Civic")
- `bodyType`: Vehicle body type (e.g., "Sedan", "SUV", "Hatchback")
- `isActive`: Boolean flag to enable/disable entries
- `createdDate`: Record creation timestamp
- `updateDate`: Record last update timestamp
### Unique Constraint
- Combination of `manufacturer` and `model` must be unique
- This prevents duplicate entries for the same car model
## How It Works
### 1. Vehicle Form Enhancement
The vehicle form now uses autocomplete inputs for:
- **Manufacturer Selection**: Users type to search from available manufacturers
- **Model Selection**: After selecting a manufacturer, users can search models for that manufacturer
- **Body Type Auto-Fill**: When a model is selected, the body type is automatically filled from the dataset
### 2. API Endpoints
- `GET /api/car-dataset?action=manufacturers` - Returns all unique manufacturers
- `GET /api/car-dataset?action=models&manufacturer=Toyota` - Returns models for a specific manufacturer
- `GET /api/car-dataset?action=bodyType&manufacturer=Toyota&model=Camry` - Returns body type for a specific model
### 3. User Experience Flow
1. User enters plate number
2. User starts typing manufacturer name → autocomplete shows matching manufacturers
3. User selects manufacturer → model field becomes enabled
4. User starts typing model name → autocomplete shows models for selected manufacturer
5. User selects model → body type is automatically filled
6. User continues with other vehicle details (year, transmission, etc.)
## Seeding Data
### Initial Dataset
The system comes pre-loaded with popular car manufacturers and models:
- Toyota (10 models)
- Honda (8 models)
- Nissan (9 models)
- Hyundai (8 models)
- Kia (8 models)
- Ford (8 models)
- Chevrolet (8 models)
- BMW (8 models)
- Mercedes-Benz (8 models)
- Audi (8 models)
- Lexus (8 models)
- Mazda (7 models)
- Mitsubishi (5 models)
- Subaru (6 models)
- Volkswagen (6 models)
- Infiniti (5 models)
- Acura (5 models)
### Running the Seed
```bash
# Seed the car dataset
npx tsx prisma/carDatasetSeed.ts
```
## Management Functions
### Server Functions (`app/lib/car-dataset-management.server.ts`)
- `getManufacturers()` - Get all unique manufacturers
- `getModelsByManufacturer(manufacturer)` - Get models for a manufacturer
- `getBodyType(manufacturer, model)` - Get body type for a specific model
- `createCarDataset(data)` - Add new car dataset entry
- `updateCarDataset(id, data)` - Update existing entry
- `deleteCarDataset(id)` - Delete entry
- `bulkImportCarDataset(data[])` - Bulk import multiple entries
### Adding New Cars
To add new cars to the dataset, you can:
1. Use the bulk import function with an array of car data
2. Create individual entries using the create function
3. Manually insert into the database
Example:
```typescript
await createCarDataset({
manufacturer: "Tesla",
model: "Model 3",
bodyType: "Sedan"
});
```
## Benefits
### 1. Data Consistency
- Standardized manufacturer and model names
- Consistent body type classifications
- Prevents typos and variations in naming
### 2. Improved User Experience
- Fast autocomplete search
- Reduced typing for common vehicles
- Automatic body type detection
### 3. Maintenance
- Easy to add new manufacturers and models
- Centralized vehicle data management
- Can disable outdated models without deleting data
### 4. Reporting
- Accurate statistics by manufacturer
- Consistent data for analytics
- Better search and filtering capabilities
## Migration Steps
### 1. Database Migration
```bash
# Apply the schema changes
npx prisma db push
# Or run the SQL migration directly
sqlite3 prisma/dev.db < prisma/migrations/add_car_dataset.sql
```
### 2. Generate Prisma Client
```bash
npx prisma generate
```
### 3. Seed the Dataset
```bash
npx tsx prisma/carDatasetSeed.ts
```
### 4. Update Application
The vehicle form will automatically use the new car dataset system once the migration is complete.
## Future Enhancements
### Possible Additions
- Vehicle trim levels per model
- Engine specifications per model
- Year ranges for model availability
- Regional model variations
- Integration with external vehicle databases
- Admin interface for managing car dataset
### API Extensions
- Search across all fields
- Pagination for large datasets
- Filtering by body type or year
- Export/import functionality
- Validation against external sources

View File

@@ -0,0 +1,141 @@
# Maintenance Types Seeding
This directory contains a dedicated seed file for populating the maintenance types table with comprehensive Arabic maintenance service types.
## Files
- `maintenanceTypeSeed.ts` - Standalone seed file for maintenance types
- `MAINTENANCE_TYPES_README.md` - This documentation file
## Usage
### Run the maintenance types seed
```bash
npm run db:seed:maintenance-types
```
Or run directly with tsx:
```bash
npx tsx prisma/maintenanceTypeSeed.ts
```
### What it does
The seed file will:
1. Create new maintenance types that don't exist
2. Update existing maintenance types with new descriptions
3. Preserve existing data (no deletions)
4. Show a summary of created/updated types
5. Display all maintenance types in the database
## Maintenance Types Included
The seed includes 20 comprehensive maintenance types in Arabic:
### Basic Maintenance
- **صيانة دورية** - Comprehensive periodic maintenance
- **تغيير زيت المحرك** - Engine oil and filter change
- **فحص دوري** - Periodic inspection
- **تنظيف شامل** - Complete cleaning
### Engine & Transmission
- **إصلاح المحرك** - Engine repair and maintenance
- **إصلاح ناقل الحركة** - Transmission repair
- **إصلاح الرادياتير** - Cooling system and radiator repair
### Brakes & Suspension
- **إصلاح الفرامل** - Brake system repair
- **إصلاح التعليق** - Suspension system repair
- **إصلاح الإطارات** - Tire repair and replacement
### Electrical & Electronics
- **إصلاح الكهرباء** - Electrical system repair
- **إصلاح البطارية** - Battery and charging system
- **إصلاح المصابيح** - Lighting system repair
### Body & Interior
- **إصلاح الهيكل** - Body and frame repair
- **إصلاح الزجاج** - Glass repair and replacement
- **إصلاح التكييف** - AC and climate control
### Exhaust & Other
- **إصلاح العادم** - Exhaust system repair
- **فحص ما قبل السفر** - Pre-travel inspection
- **صيانة طارئة** - Emergency maintenance
- **أخرى** - Other maintenance types
## Features
### Smart Upsert Logic
- Creates new maintenance types if they don't exist
- Updates existing types with new descriptions
- Preserves custom maintenance types added by users
### Detailed Descriptions
Each maintenance type includes:
- Arabic name
- Detailed description of what's included
- Active status (all default to active)
### Safe Operation
- No data deletion
- Preserves existing maintenance visits
- Can be run multiple times safely
### Comprehensive Logging
- Shows progress for each maintenance type
- Displays summary statistics
- Lists all maintenance types in the database
## Customization
To add more maintenance types, edit the `maintenanceTypes` array in `maintenanceTypeSeed.ts`:
```typescript
{
name: 'نوع الصيانة الجديد',
description: 'وصف تفصيلي لنوع الصيانة',
isActive: true,
}
```
## Integration with Main Seed
The maintenance types seed is also integrated into the main seed file (`seed.ts`), so running `npm run db:seed` will also populate maintenance types.
## Database Schema
The maintenance types are stored in the `maintenance_types` table with:
- `id` - Auto-increment primary key
- `name` - Unique maintenance type name
- `description` - Optional detailed description
- `isActive` - Boolean flag for active/inactive status
- `createdDate` - Creation timestamp
- `updateDate` - Last update timestamp
## Troubleshooting
### Permission Errors
Make sure your database connection has write permissions.
### Duplicate Name Errors
The seed handles duplicates gracefully by updating existing records.
### TypeScript Errors
Ensure all dependencies are installed:
```bash
npm install
```
### Database Connection Issues
Check your `.env` file has the correct `DATABASE_URL`.
## Future Enhancements
- Add maintenance type categories
- Include pricing suggestions
- Add maintenance intervals
- Support for multiple languages
- Import/export functionality

1448
prisma/carDatasetSeed.ts Normal file

File diff suppressed because it is too large Load Diff

0
prisma/dev.db Normal file
View File

View File

@@ -0,0 +1,264 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const maintenanceTypes = [
{
name: 'صيانة دورية',
description: 'صيانة دورية شاملة للمركبة تشمل فحص جميع الأنظمة الأساسية',
isActive: true,
},
{
name: 'تغيير زيت المحرك',
description: 'تغيير زيت المحرك وفلتر الزيت وفحص مستوى السوائل',
isActive: true,
},
{
name: 'إصلاح الفرامل',
description: 'صيانة وإصلاح نظام الفرامل بما في ذلك الأقراص والتيل والسوائل',
isActive: true,
},
{
name: 'إصلاح المحرك',
description: 'إصلاح وصيانة المحرك وأجزائه الداخلية والخارجية',
isActive: true,
},
{
name: 'إصلاح ناقل الحركة',
description: 'صيانة وإصلاح ناقل الحركة الأوتوماتيكي أو اليدوي',
isActive: true,
},
{
name: 'إصلاح التكييف',
description: 'صيانة وإصلاح نظام التكييف والتبريد في المركبة',
isActive: true,
},
{
name: 'إصلاح الإطارات',
description: 'تغيير وإصلاح الإطارات وضبط الهواء والتوازن',
isActive: true,
},
{
name: 'إصلاح الكهرباء',
description: 'إصلاح الأنظمة الكهربائية والإلكترونية في المركبة',
isActive: true,
},
{
name: 'إصلاح التعليق',
description: 'صيانة وإصلاح نظام التعليق والممتصات',
isActive: true,
},
{
name: 'إصلاح العادم',
description: 'إصلاح وتغيير نظام العادم والكاتم',
isActive: true,
},
{
name: 'إصلاح الرادياتير',
description: 'صيانة وإصلاح نظام التبريد والرادياتير',
isActive: true,
},
{
name: 'إصلاح البطارية',
description: 'فحص وتغيير البطارية ونظام الشحن',
isActive: true,
},
{
name: 'إصلاح المصابيح',
description: 'إصلاح وتغيير المصابيح الأمامية والخلفية',
isActive: true,
},
{
name: 'إصلاح الزجاج',
description: 'إصلاح وتغيير الزجاج الأمامي والخلفي والجانبي',
isActive: true,
},
{
name: 'إصلاح الهيكل',
description: 'إصلاح أضرار الهيكل والصدمات والخدوش',
isActive: true,
},
{
name: 'تنظيف شامل',
description: 'تنظيف شامل للمركبة من الداخل والخارج',
isActive: true,
},
{
name: 'فحص دوري',
description: 'فحص دوري شامل لجميع أنظمة المركبة',
isActive: true,
},
{
name: 'فحص ما قبل السفر',
description: 'فحص شامل للمركبة قبل السفر الطويل',
isActive: true,
},
{
name: 'صيانة طارئة',
description: 'صيانة طارئة لحل مشاكل عاجلة في المركبة',
isActive: true,
},
{
name: 'أخرى',
description: 'أنواع صيانة أخرى غير مدرجة في القائمة',
isActive: true,
},
];
async function seedMaintenanceTypes() {
console.log('🔧 Seeding maintenance types...');
try {
// Check if there are any maintenance visits that might reference maintenance types in JSON
const visitCount = await prisma.maintenanceVisit.count();
if (visitCount > 0) {
console.log(`⚠️ Found ${visitCount} maintenance visits in database.`);
console.log('🔄 Analyzing maintenance jobs in existing visits...');
// Get all maintenance visits to check their JSON maintenance jobs
const visits = await prisma.maintenanceVisit.findMany({
select: { maintenanceJobs: true },
});
const referencedTypeIds = new Set<number>();
// Parse JSON maintenance jobs to find referenced type IDs
visits.forEach(visit => {
try {
const jobs = JSON.parse(visit.maintenanceJobs);
if (Array.isArray(jobs)) {
jobs.forEach(job => {
if (job.typeId && typeof job.typeId === 'number') {
referencedTypeIds.add(job.typeId);
}
});
}
} catch (error) {
// Skip invalid JSON
}
});
if (referencedTypeIds.size > 0) {
console.log(`📋 Found ${referencedTypeIds.size} maintenance type IDs referenced in visits`);
// Delete only maintenance types that are NOT referenced
const deletedTypes = await prisma.maintenanceType.deleteMany({
where: {
id: {
notIn: Array.from(referencedTypeIds),
},
},
});
console.log(`🗑️ Deleted ${deletedTypes.count} unreferenced maintenance types`);
// Update referenced maintenance types to match our seed data
console.log('🔄 Updating referenced maintenance types...');
for (const type of maintenanceTypes) {
const existingType = await prisma.maintenanceType.findUnique({
where: { name: type.name },
});
if (existingType && referencedTypeIds.has(existingType.id)) {
await prisma.maintenanceType.update({
where: { id: existingType.id },
data: {
description: type.description,
isActive: type.isActive,
},
});
console.log(`✅ Updated referenced type: ${type.name}`);
}
}
} else {
// No maintenance types are actually referenced, safe to delete all
const deletedCount = await prisma.maintenanceType.deleteMany();
console.log(`🗑️ Deleted ${deletedCount.count} maintenance types`);
}
} else {
// No maintenance visits exist, safe to delete all maintenance types
const deletedCount = await prisma.maintenanceType.deleteMany();
console.log(`🗑️ Deleted ${deletedCount.count} existing maintenance types`);
}
// Reset the auto-increment counter for SQLite
console.log('🔄 Resetting ID counter...');
await prisma.$executeRaw`DELETE FROM sqlite_sequence WHERE name = 'maintenance_types'`;
console.log('✅ ID counter reset to start from 1');
console.log('📝 Inserting fresh maintenance types...');
let createdCount = 0;
let updatedCount = 0;
for (const type of maintenanceTypes) {
try {
console.log(`Processing: ${type.name}`);
const result = await prisma.maintenanceType.upsert({
where: { name: type.name },
update: {
description: type.description,
isActive: type.isActive,
},
create: {
name: type.name,
description: type.description,
isActive: type.isActive,
},
});
if (result.createdDate.getTime() === result.updateDate.getTime()) {
createdCount++;
console.log(`✅ Created: ${type.name} (ID: ${result.id})`);
} else {
updatedCount++;
console.log(`✅ Updated: ${type.name} (ID: ${result.id})`);
}
} catch (error) {
console.error(`❌ Error processing "${type.name}":`, error);
}
}
console.log(`\n📊 Summary:`);
console.log(` Created: ${createdCount} maintenance types`);
console.log(` Updated: ${updatedCount} maintenance types`);
console.log(` Total: ${maintenanceTypes.length} maintenance types processed`);
// Display all maintenance types
const allTypes = await prisma.maintenanceType.findMany({
orderBy: { name: 'asc' },
});
console.log(`\n📋 All maintenance types in database (${allTypes.length}):`);
allTypes.forEach((type, index) => {
const status = type.isActive ? '🟢' : '🔴';
console.log(` ${index + 1}. ${status} ${type.name}`);
if (type.description) {
console.log(` 📝 ${type.description}`);
}
});
console.log('\n🎉 Maintenance types seeding completed successfully!');
} catch (error) {
console.error('❌ Error during maintenance types seeding:', error);
throw error;
}
}
async function main() {
try {
await seedMaintenanceTypes();
} catch (error) {
console.error('❌ Error seeding maintenance types:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Always run the main function when this file is executed
main();
export { seedMaintenanceTypes };

View File

@@ -0,0 +1,94 @@
-- CreateTable
CREATE TABLE "users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"authLevel" INTEGER NOT NULL,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"editDate" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "customers" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"phone" TEXT,
"email" TEXT,
"address" TEXT,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "vehicles" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"plateNumber" TEXT NOT NULL,
"bodyType" TEXT NOT NULL,
"manufacturer" TEXT NOT NULL,
"model" TEXT NOT NULL,
"trim" TEXT,
"year" INTEGER NOT NULL,
"transmission" TEXT NOT NULL,
"fuel" TEXT NOT NULL,
"cylinders" INTEGER,
"engineDisplacement" REAL,
"useType" TEXT NOT NULL,
"ownerId" INTEGER NOT NULL,
"lastVisitDate" DATETIME,
"suggestedNextVisitDate" DATETIME,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL,
CONSTRAINT "vehicles_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "maintenance_visits" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"vehicleId" INTEGER NOT NULL,
"customerId" INTEGER NOT NULL,
"maintenanceType" TEXT NOT NULL,
"description" TEXT NOT NULL,
"cost" REAL NOT NULL,
"paymentStatus" TEXT NOT NULL DEFAULT 'pending',
"kilometers" INTEGER NOT NULL,
"visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"nextVisitDelay" INTEGER NOT NULL,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL,
CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "expenses" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"category" TEXT NOT NULL,
"amount" REAL NOT NULL,
"expenseDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "income" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"maintenanceVisitId" INTEGER NOT NULL,
"amount" REAL NOT NULL,
"incomeDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL,
CONSTRAINT "income_maintenanceVisitId_fkey" FOREIGN KEY ("maintenanceVisitId") REFERENCES "maintenance_visits" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "vehicles_plateNumber_key" ON "vehicles"("plateNumber");

View File

@@ -0,0 +1,45 @@
/*
Warnings:
- You are about to drop the column `maintenanceType` on the `maintenance_visits` table. All the data in the column will be lost.
- Added the required column `maintenanceTypeId` to the `maintenance_visits` table without a default value. This is not possible if the table is not empty.
*/
-- CreateTable
CREATE TABLE "maintenance_types" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_maintenance_visits" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"vehicleId" INTEGER NOT NULL,
"customerId" INTEGER NOT NULL,
"maintenanceTypeId" INTEGER NOT NULL,
"description" TEXT NOT NULL,
"cost" REAL NOT NULL,
"paymentStatus" TEXT NOT NULL DEFAULT 'pending',
"kilometers" INTEGER NOT NULL,
"visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"nextVisitDelay" INTEGER NOT NULL,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL,
CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "maintenance_visits_maintenanceTypeId_fkey" FOREIGN KEY ("maintenanceTypeId") REFERENCES "maintenance_types" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_maintenance_visits" ("cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate") SELECT "cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate" FROM "maintenance_visits";
DROP TABLE "maintenance_visits";
ALTER TABLE "new_maintenance_visits" RENAME TO "maintenance_visits";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "maintenance_types_name_key" ON "maintenance_types"("name");

View File

@@ -0,0 +1,31 @@
/*
Warnings:
- You are about to drop the column `maintenanceTypeId` on the `maintenance_visits` table. All the data in the column will be lost.
- Added the required column `maintenanceJobs` to the `maintenance_visits` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_maintenance_visits" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"vehicleId" INTEGER NOT NULL,
"customerId" INTEGER NOT NULL,
"maintenanceJobs" TEXT NOT NULL,
"description" TEXT NOT NULL,
"cost" REAL NOT NULL,
"paymentStatus" TEXT NOT NULL DEFAULT 'pending',
"kilometers" INTEGER NOT NULL,
"visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"nextVisitDelay" INTEGER NOT NULL,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL,
CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_maintenance_visits" ("cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate") SELECT "cost", "createdDate", "customerId", "description", "id", "kilometers", "nextVisitDelay", "paymentStatus", "updateDate", "vehicleId", "visitDate" FROM "maintenance_visits";
DROP TABLE "maintenance_visits";
ALTER TABLE "new_maintenance_visits" RENAME TO "maintenance_visits";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "car_dataset" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"manufacturer" TEXT NOT NULL,
"model" TEXT NOT NULL,
"bodyType" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "car_dataset_manufacturer_model_key" ON "car_dataset"("manufacturer", "model");

View File

@@ -0,0 +1,59 @@
-- CreateTable
CREATE TABLE "maintenance_types" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "maintenance_types_name_key" ON "maintenance_types"("name");
-- Add new column to maintenance_visits for JSON maintenance jobs
ALTER TABLE "maintenance_visits" ADD COLUMN "maintenanceJobs" TEXT;
-- Create default maintenance types (will be populated by seed script)
-- Note: The seed script will handle populating maintenance types
-- Convert existing maintenance visits to use JSON format
-- First, let's handle the case where maintenanceType column exists (old format)
UPDATE "maintenance_visits"
SET "maintenanceJobs" = '[{"typeId": 1, "job": "' || COALESCE("maintenanceType", 'صيانة عامة') || '", "notes": ""}]'
WHERE "maintenanceJobs" IS NULL;
-- Set default JSON for any remaining NULL values
UPDATE "maintenance_visits"
SET "maintenanceJobs" = '[{"typeId": 1, "job": "صيانة عامة", "notes": ""}]'
WHERE "maintenanceJobs" IS NULL OR "maintenanceJobs" = '';
-- Make maintenanceJobs NOT NULL and remove old maintenanceType column if it exists
-- Note: SQLite doesn't support ALTER COLUMN, so we need to recreate the table
CREATE TABLE "maintenance_visits_new" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"vehicleId" INTEGER NOT NULL,
"customerId" INTEGER NOT NULL,
"maintenanceJobs" TEXT NOT NULL,
"description" TEXT NOT NULL,
"cost" REAL NOT NULL,
"paymentStatus" TEXT NOT NULL DEFAULT 'pending',
"kilometers" INTEGER NOT NULL,
"visitDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"nextVisitDelay" INTEGER NOT NULL,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL,
CONSTRAINT "maintenance_visits_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "maintenance_visits_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- Copy data from old table to new table (excluding old maintenanceType column)
INSERT INTO "maintenance_visits_new"
SELECT "id", "vehicleId", "customerId", "maintenanceJobs", "description", "cost", "paymentStatus", "kilometers", "visitDate", "nextVisitDelay", "createdDate", "updateDate"
FROM "maintenance_visits";
-- Drop old table and rename new table
DROP TABLE "maintenance_visits";
ALTER TABLE "maintenance_visits_new" RENAME TO "maintenance_visits";
-- Note: Income table foreign key constraint should still work as it references maintenance_visits(id)

View File

@@ -0,0 +1,19 @@
-- Migration: Add Settings Table
-- Description: Creates settings table for application configuration (date format, currency, number format)
CREATE TABLE IF NOT EXISTS "settings" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"key" TEXT NOT NULL UNIQUE,
"value" TEXT NOT NULL,
"description" TEXT,
"createdDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Insert default settings
INSERT OR IGNORE INTO "settings" ("key", "value", "description") VALUES
('dateFormat', 'ar-SA', 'Date format locale (ar-SA or en-US)'),
('currency', 'JOD', 'Currency code (JOD, USD, EUR, etc.)'),
('numberFormat', 'ar-SA', 'Number format locale (ar-SA or en-US)'),
('currencySymbol', 'د.أ', 'Currency symbol display'),
('dateDisplayFormat', 'dd/MM/yyyy', 'Date display format pattern');

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

BIN
prisma/prisma/dev.db Normal file

Binary file not shown.

160
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,160 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// User model for authentication and access control
model User {
id Int @id @default(autoincrement())
name String
username String @unique
email String @unique
password String // hashed password
status String @default("active") // "active" or "inactive"
authLevel Int // 1=superadmin, 2=admin, 3=user
createdDate DateTime @default(now())
editDate DateTime @updatedAt
@@map("users")
}
// Customer model for vehicle owners
model Customer {
id Int @id @default(autoincrement())
name String
phone String?
email String?
address String?
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
// Relationships
vehicles Vehicle[]
maintenanceVisits MaintenanceVisit[]
@@map("customers")
}
// Vehicle model with comprehensive specifications
model Vehicle {
id Int @id @default(autoincrement())
plateNumber String @unique
bodyType String
manufacturer String
model String
trim String?
year Int
transmission String // "Automatic" or "Manual"
fuel String // "Gasoline", "Diesel", "Hybrid", "Mild Hybrid", "Electric"
cylinders Int?
engineDisplacement Float?
useType String // "personal", "taxi", "apps", "loading", "travel"
ownerId Int
lastVisitDate DateTime?
suggestedNextVisitDate DateTime?
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
// Relationships
owner Customer @relation(fields: [ownerId], references: [id], onDelete: Cascade)
maintenanceVisits MaintenanceVisit[]
@@map("vehicles")
}
// Maintenance type model for categorizing maintenance services
model MaintenanceType {
id Int @id @default(autoincrement())
name String @unique
description String?
isActive Boolean @default(true)
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
@@map("maintenance_types")
}
// Car dataset model for storing vehicle manufacturers, models, and body types
model CarDataset {
id Int @id @default(autoincrement())
manufacturer String
model String
bodyType String
isActive Boolean @default(true)
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
// Unique constraint to prevent duplicate manufacturer-model combinations
@@unique([manufacturer, model])
@@map("car_dataset")
}
// Maintenance visit model for tracking service records
model MaintenanceVisit {
id Int @id @default(autoincrement())
vehicleId Int
customerId Int
maintenanceJobs String // JSON field storing array of maintenance jobs: [{"typeId": 1, "job": "تغيير زيت المحرك", "notes": "..."}, ...]
description String
cost Float
paymentStatus String @default("pending")
kilometers Int
visitDate DateTime @default(now())
nextVisitDelay Int // months (1, 2, 3, or 4)
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
// Relationships
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
income Income[]
@@map("maintenance_visits")
}
// Expense model for business expense tracking
model Expense {
id Int @id @default(autoincrement())
description String
category String
amount Float
expenseDate DateTime @default(now())
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
@@map("expenses")
}
// Income model for revenue tracking from maintenance visits
model Income {
id Int @id @default(autoincrement())
maintenanceVisitId Int
amount Float
incomeDate DateTime @default(now())
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
// Relationships
maintenanceVisit MaintenanceVisit @relation(fields: [maintenanceVisitId], references: [id], onDelete: Cascade)
@@map("income")
}
// Settings model for application configuration
model Settings {
id Int @id @default(autoincrement())
key String @unique
value String
description String?
createdDate DateTime @default(now())
updateDate DateTime @updatedAt
@@map("settings")
}

168
prisma/seed.ts Normal file
View File

@@ -0,0 +1,168 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { seedMaintenanceTypes } from './maintenanceTypeSeed';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding database...');
// Check if superadmin already exists
const existingSuperadmin = await prisma.user.findFirst({
where: { authLevel: 1 }
});
if (!existingSuperadmin) {
// Create superadmin account
const hashedPassword = await bcrypt.hash('admin123', 10);
const superadmin = await prisma.user.create({
data: {
name: 'Super Administrator',
username: 'superadmin',
email: 'admin@carmaintenance.com',
password: hashedPassword,
status: 'active',
authLevel: 1,
},
});
console.log('✅ Created superadmin user:', superadmin.username);
} else {
console.log(' Superadmin user already exists');
}
// Seed maintenance types using the dedicated seed function
await seedMaintenanceTypes();
// Seed some sample data for development
const sampleCustomer = await prisma.customer.upsert({
where: { id: 1 },
update: {},
create: {
name: 'أحمد محمد',
phone: '+966501234567',
email: 'ahmed@example.com',
address: 'الرياض، المملكة العربية السعودية',
},
});
const sampleVehicle = await prisma.vehicle.upsert({
where: { id: 1 },
update: {},
create: {
plateNumber: 'ABC-1234',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
trim: 'GLE',
year: 2022,
transmission: 'Automatic',
fuel: 'Gasoline',
cylinders: 4,
engineDisplacement: 2.5,
useType: 'personal',
ownerId: sampleCustomer.id,
},
});
// Get maintenance types for sample visits
const periodicMaintenanceType = await prisma.maintenanceType.findFirst({
where: { name: 'صيانة دورية' }
});
const brakeRepairType = await prisma.maintenanceType.findFirst({
where: { name: 'إصلاح الفرامل' }
});
// Create some sample maintenance visits
const sampleVisit1 = await prisma.maintenanceVisit.upsert({
where: { id: 1 },
update: {},
create: {
vehicleId: sampleVehicle.id,
customerId: sampleCustomer.id,
maintenanceTypeId: periodicMaintenanceType!.id,
description: 'تغيير زيت المحرك وفلتر الزيت وفحص شامل للمركبة',
cost: 250.00,
paymentStatus: 'paid',
kilometers: 45000,
visitDate: new Date('2024-01-15'),
nextVisitDelay: 6,
},
});
const sampleVisit2 = await prisma.maintenanceVisit.upsert({
where: { id: 2 },
update: {},
create: {
vehicleId: sampleVehicle.id,
customerId: sampleCustomer.id,
maintenanceTypeId: brakeRepairType!.id,
description: 'تغيير أقراص الفرامل الأمامية وتيل الفرامل',
cost: 450.00,
paymentStatus: 'paid',
kilometers: 47500,
visitDate: new Date('2024-03-20'),
nextVisitDelay: 12,
},
});
const sampleVisit3 = await prisma.maintenanceVisit.upsert({
where: { id: 3 },
update: {},
create: {
vehicleId: sampleVehicle.id,
customerId: sampleCustomer.id,
maintenanceTypeId: periodicMaintenanceType!.id,
description: 'تغيير زيت المحرك وفلتر الهواء وفحص البطارية',
cost: 180.00,
paymentStatus: 'pending',
kilometers: 52000,
visitDate: new Date('2024-07-15'),
nextVisitDelay: 6,
},
});
// Create income records for the maintenance visits
await prisma.income.upsert({
where: { id: 1 },
update: {},
create: {
maintenanceVisitId: sampleVisit1.id,
amount: sampleVisit1.cost,
incomeDate: sampleVisit1.visitDate,
},
});
await prisma.income.upsert({
where: { id: 2 },
update: {},
create: {
maintenanceVisitId: sampleVisit2.id,
amount: sampleVisit2.cost,
incomeDate: sampleVisit2.visitDate,
},
});
// Update the vehicle with last visit information
await prisma.vehicle.update({
where: { id: sampleVehicle.id },
data: {
lastVisitDate: sampleVisit3.visitDate,
suggestedNextVisitDate: new Date('2025-01-15'), // 6 months after last visit
},
});
console.log('✅ Created sample customer, vehicle, and maintenance visits');
console.log('🎉 Database seeded successfully!');
}
main()
.catch((e) => {
console.error('❌ Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

54
prisma/settingsSeed.ts Normal file
View File

@@ -0,0 +1,54 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function seedSettings() {
console.log('🌱 Seeding settings...');
const defaultSettings = [
{
key: 'dateFormat',
value: 'ar-SA',
description: 'Date format locale (ar-SA or en-US)'
},
{
key: 'currency',
value: 'JOD',
description: 'Currency code (JOD, USD, EUR, etc.)'
},
{
key: 'numberFormat',
value: 'ar-SA',
description: 'Number format locale (ar-SA or en-US)'
},
{
key: 'currencySymbol',
value: 'د.أ',
description: 'Currency symbol display'
},
{
key: 'dateDisplayFormat',
value: 'dd/MM/yyyy',
description: 'Date display format pattern'
}
];
for (const setting of defaultSettings) {
await prisma.settings.upsert({
where: { key: setting.key },
update: {},
create: setting
});
}
console.log('✅ Settings seeded successfully');
}
seedSettings()
.catch((e) => {
console.error('❌ Error seeding settings:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});