diff --git a/docs/DEALERS_CSV_IMPORT_FIX.sql b/docs/DEALERS_CSV_IMPORT_FIX.sql new file mode 100644 index 0000000..4da9eb6 --- /dev/null +++ b/docs/DEALERS_CSV_IMPORT_FIX.sql @@ -0,0 +1,275 @@ +-- ============================================================ +-- DEALERS CSV IMPORT - WORKING SOLUTION +-- ============================================================ +-- This script provides a working solution for importing dealers +-- from CSV with auto-generated columns (dealer_id, created_at, updated_at, is_active) +-- ============================================================ + +-- METHOD 1: If your CSV does NOT have dealer_id, created_at, updated_at, is_active +-- ============================================================ +-- Use this COPY command if your CSV has exactly 44 columns (without the auto-generated ones) + +\copy public.dealers( + sales_code, + service_code, + gear_code, + gma_code, + region, + dealership, + state, + district, + city, + location, + city_category_pst, + layout_format, + tier_city_category, + on_boarding_charges, + date, + single_format_month_year, + domain_id, + replacement, + termination_resignation_status, + date_of_termination_resignation, + last_date_of_operations, + old_codes, + branch_details, + dealer_principal_name, + dealer_principal_email_id, + dp_contact_number, + dp_contacts, + showroom_address, + showroom_pincode, + workshop_address, + workshop_pincode, + location_district, + state_workshop, + no_of_studios, + website_update, + gst, + pan, + firm_type, + prop_managing_partners_directors, + total_prop_partners_directors, + docs_folder_link, + workshop_gma_codes, + existing_new, + dlrcode +) +FROM 'C:/Users/COMP/Downloads/DEALERS_CLEAN.csv' +WITH ( + FORMAT csv, + HEADER true, + ENCODING 'UTF8' +); + + +-- ============================================================ +-- METHOD 2: If your CSV HAS dealer_id, created_at, updated_at, is_active columns +-- ============================================================ +-- Use this approach if your CSV has 48 columns (including the auto-generated ones) +-- This creates a temporary table, imports, then inserts with defaults + +-- Step 1: Create temporary table matching your CSV structure +-- This accepts ALL columns from CSV (whether 44 or 48 columns) +CREATE TEMP TABLE dealers_temp ( + dealer_id TEXT, + sales_code TEXT, + service_code TEXT, + gear_code TEXT, + gma_code TEXT, + region TEXT, + dealership TEXT, + state TEXT, + district TEXT, + city TEXT, + location TEXT, + city_category_pst TEXT, + layout_format TEXT, + tier_city_category TEXT, + on_boarding_charges TEXT, + date TEXT, + single_format_month_year TEXT, + domain_id TEXT, + replacement TEXT, + termination_resignation_status TEXT, + date_of_termination_resignation TEXT, + last_date_of_operations TEXT, + old_codes TEXT, + branch_details TEXT, + dealer_principal_name TEXT, + dealer_principal_email_id TEXT, + dp_contact_number TEXT, + dp_contacts TEXT, + showroom_address TEXT, + showroom_pincode TEXT, + workshop_address TEXT, + workshop_pincode TEXT, + location_district TEXT, + state_workshop TEXT, + no_of_studios TEXT, + website_update TEXT, + gst TEXT, + pan TEXT, + firm_type TEXT, + prop_managing_partners_directors TEXT, + total_prop_partners_directors TEXT, + docs_folder_link TEXT, + workshop_gma_codes TEXT, + existing_new TEXT, + dlrcode TEXT, + created_at TEXT, + updated_at TEXT, + is_active TEXT +); + +-- Step 2: Import CSV into temporary table +-- This will work whether your CSV has 44 or 48 columns +\copy dealers_temp FROM 'C:/Users/COMP/Downloads/DEALERS_CLEAN.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8'); + +-- Optional: Check what was imported +-- SELECT COUNT(*) FROM dealers_temp; + +-- Step 3: Insert into actual dealers table +-- IMPORTANT: We IGNORE dealer_id, created_at, updated_at, is_active from CSV +-- These will use database DEFAULT values (auto-generated UUID, current timestamp, true) +INSERT INTO public.dealers ( + sales_code, + service_code, + gear_code, + gma_code, + region, + dealership, + state, + district, + city, + location, + city_category_pst, + layout_format, + tier_city_category, + on_boarding_charges, + date, + single_format_month_year, + domain_id, + replacement, + termination_resignation_status, + date_of_termination_resignation, + last_date_of_operations, + old_codes, + branch_details, + dealer_principal_name, + dealer_principal_email_id, + dp_contact_number, + dp_contacts, + showroom_address, + showroom_pincode, + workshop_address, + workshop_pincode, + location_district, + state_workshop, + no_of_studios, + website_update, + gst, + pan, + firm_type, + prop_managing_partners_directors, + total_prop_partners_directors, + docs_folder_link, + workshop_gma_codes, + existing_new, + dlrcode +) +SELECT + NULLIF(sales_code, ''), + NULLIF(service_code, ''), + NULLIF(gear_code, ''), + NULLIF(gma_code, ''), + NULLIF(region, ''), + NULLIF(dealership, ''), + NULLIF(state, ''), + NULLIF(district, ''), + NULLIF(city, ''), + NULLIF(location, ''), + NULLIF(city_category_pst, ''), + NULLIF(layout_format, ''), + NULLIF(tier_city_category, ''), + NULLIF(on_boarding_charges, ''), + NULLIF(date, ''), + NULLIF(single_format_month_year, ''), + NULLIF(domain_id, ''), + NULLIF(replacement, ''), + NULLIF(termination_resignation_status, ''), + NULLIF(date_of_termination_resignation, ''), + NULLIF(last_date_of_operations, ''), + NULLIF(old_codes, ''), + NULLIF(branch_details, ''), + NULLIF(dealer_principal_name, ''), + NULLIF(dealer_principal_email_id, ''), + NULLIF(dp_contact_number, ''), + NULLIF(dp_contacts, ''), + NULLIF(showroom_address, ''), + NULLIF(showroom_pincode, ''), + NULLIF(workshop_address, ''), + NULLIF(workshop_pincode, ''), + NULLIF(location_district, ''), + NULLIF(state_workshop, ''), + CASE WHEN no_of_studios = '' THEN 0 ELSE no_of_studios::INTEGER END, + NULLIF(website_update, ''), + NULLIF(gst, ''), + NULLIF(pan, ''), + NULLIF(firm_type, ''), + NULLIF(prop_managing_partners_directors, ''), + NULLIF(total_prop_partners_directors, ''), + NULLIF(docs_folder_link, ''), + NULLIF(workshop_gma_codes, ''), + NULLIF(existing_new, ''), + NULLIF(dlrcode, '') +FROM dealers_temp; + +-- Step 4: Clean up temporary table +DROP TABLE dealers_temp; + +-- ============================================================ +-- METHOD 3: Using COPY with DEFAULT (PostgreSQL 12+) +-- ============================================================ +-- Alternative approach using a function to set defaults + +-- Create a function to handle the import with defaults +CREATE OR REPLACE FUNCTION import_dealers_from_csv() +RETURNS void AS $$ +BEGIN + -- This will be called from a COPY command that uses a function + -- See METHOD 1 for the actual COPY command +END; +$$ LANGUAGE plpgsql; + +-- ============================================================ +-- VERIFICATION QUERIES +-- ============================================================ + +-- Check import results +SELECT + COUNT(*) as total_dealers, + COUNT(dealer_id) as has_dealer_id, + COUNT(created_at) as has_created_at, + COUNT(updated_at) as has_updated_at, + COUNT(*) FILTER (WHERE is_active = true) as active_count +FROM dealers; + +-- View sample records with auto-generated values +SELECT + dealer_id, + dlrcode, + dealership, + created_at, + updated_at, + is_active +FROM dealers +LIMIT 5; + +-- Check for any issues +SELECT + COUNT(*) FILTER (WHERE dealer_id IS NULL) as missing_dealer_id, + COUNT(*) FILTER (WHERE created_at IS NULL) as missing_created_at, + COUNT(*) FILTER (WHERE updated_at IS NULL) as missing_updated_at +FROM dealers; + diff --git a/docs/DEALERS_CSV_IMPORT_GUIDE.md b/docs/DEALERS_CSV_IMPORT_GUIDE.md new file mode 100644 index 0000000..bb4f2f1 --- /dev/null +++ b/docs/DEALERS_CSV_IMPORT_GUIDE.md @@ -0,0 +1,515 @@ +# Dealers CSV Import Guide + +This guide explains how to format and import dealer data from a CSV file into the PostgreSQL `dealers` table. + +## ⚠️ Important: Auto-Generated Columns + +**DO NOT include these columns in your CSV file** - they are automatically generated by the database: + +- ❌ `dealer_id` - Auto-generated UUID (e.g., `550e8400-e29b-41d4-a716-446655440000`) +- ❌ `created_at` - Auto-generated timestamp (current time on import) +- ❌ `updated_at` - Auto-generated timestamp (current time on import) +- ❌ `is_active` - Defaults to `true` + +Your CSV should have **exactly 44 columns** (the data columns listed below). + +## Table of Contents +- [CSV File Format Requirements](#csv-file-format-requirements) +- [Column Mapping](#column-mapping) +- [Preparing Your CSV File](#preparing-your-csv-file) +- [Import Methods](#import-methods) +- [Troubleshooting](#troubleshooting) + +--- + +## CSV File Format Requirements + +### File Requirements +- **Format**: CSV (Comma-Separated Values) +- **Encoding**: UTF-8 +- **Header Row**: Required (first row must contain column names) +- **Delimiter**: Comma (`,`) +- **Text Qualifier**: Double quotes (`"`) for fields containing commas or special characters + +### Required Columns (in exact order) + +**Important Notes:** +- **DO NOT include** `dealer_id`, `created_at`, `updated_at`, or `is_active` in your CSV file +- These columns will be automatically generated by the database: + - `dealer_id`: Auto-generated UUID + - `created_at`: Auto-generated timestamp (current time) + - `updated_at`: Auto-generated timestamp (current time) + - `is_active`: Defaults to `true` + +Your CSV file must have these **44 columns** in the following order: + +1. `sales_code` +2. `service_code` +3. `gear_code` +4. `gma_code` +5. `region` +6. `dealership` +7. `state` +8. `district` +9. `city` +10. `location` +11. `city_category_pst` +12. `layout_format` +13. `tier_city_category` +14. `on_boarding_charges` +15. `date` +16. `single_format_month_year` +17. `domain_id` +18. `replacement` +19. `termination_resignation_status` +20. `date_of_termination_resignation` +21. `last_date_of_operations` +22. `old_codes` +23. `branch_details` +24. `dealer_principal_name` +25. `dealer_principal_email_id` +26. `dp_contact_number` +27. `dp_contacts` +28. `showroom_address` +29. `showroom_pincode` +30. `workshop_address` +31. `workshop_pincode` +32. `location_district` +33. `state_workshop` +34. `no_of_studios` +35. `website_update` +36. `gst` +37. `pan` +38. `firm_type` +39. `prop_managing_partners_directors` +40. `total_prop_partners_directors` +41. `docs_folder_link` +42. `workshop_gma_codes` +43. `existing_new` +44. `dlrcode` + +--- + +## Column Mapping + +### Column Details + +| Column Name | Type | Required | Notes | +|------------|------|----------|-------| +| `sales_code` | String(50) | No | Sales code identifier | +| `service_code` | String(50) | No | Service code identifier | +| `gear_code` | String(50) | No | Gear code identifier | +| `gma_code` | String(50) | No | GMA code identifier | +| `region` | String(50) | No | Geographic region | +| `dealership` | String(255) | No | Dealership business name | +| `state` | String(100) | No | State name | +| `district` | String(100) | No | District name | +| `city` | String(100) | No | City name | +| `location` | String(255) | No | Location details | +| `city_category_pst` | String(50) | No | City category (PST) | +| `layout_format` | String(50) | No | Layout format | +| `tier_city_category` | String(100) | No | TIER City Category | +| `on_boarding_charges` | Decimal | No | Numeric value (e.g., 1000.50) | +| `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) | +| `single_format_month_year` | String(50) | No | Format: Sep-2014 | +| `domain_id` | String(255) | No | Email domain (e.g., dealer@royalenfield.com) | +| `replacement` | String(50) | No | Replacement status | +| `termination_resignation_status` | String(255) | No | Termination/Resignation status | +| `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD | +| `last_date_of_operations` | Date | No | Format: YYYY-MM-DD | +| `old_codes` | String(255) | No | Old code references | +| `branch_details` | Text | No | Branch information | +| `dealer_principal_name` | String(255) | No | Principal's full name | +| `dealer_principal_email_id` | String(255) | No | Principal's email | +| `dp_contact_number` | String(20) | No | Contact phone number | +| `dp_contacts` | String(20) | No | Additional contacts | +| `showroom_address` | Text | No | Full showroom address | +| `showroom_pincode` | String(10) | No | Showroom postal code | +| `workshop_address` | Text | No | Full workshop address | +| `workshop_pincode` | String(10) | No | Workshop postal code | +| `location_district` | String(100) | No | Location/District | +| `state_workshop` | String(100) | No | State for workshop | +| `no_of_studios` | Integer | No | Number of studios (default: 0) | +| `website_update` | String(10) | No | Yes/No value | +| `gst` | String(50) | No | GST number | +| `pan` | String(50) | No | PAN number | +| `firm_type` | String(100) | No | Type of firm (e.g., Proprietorship) | +| `prop_managing_partners_directors` | String(255) | No | Proprietor/Partners/Directors names | +| `total_prop_partners_directors` | String(255) | No | Total count or names | +| `docs_folder_link` | Text | No | Google Drive or document folder URL | +| `workshop_gma_codes` | String(255) | No | Workshop GMA codes | +| `existing_new` | String(50) | No | Existing/New status | +| `dlrcode` | String(50) | No | Dealer code | + +--- + +## Preparing Your CSV File + +### Step 1: Create/Edit Your CSV File + +1. **Open your CSV file** in Excel, Google Sheets, or a text editor +2. **Remove auto-generated columns** (if present): + - ❌ **DO NOT include**: `dealer_id`, `created_at`, `updated_at`, `is_active` + - ✅ These will be automatically generated by the database +3. **Ensure the header row** matches the column names exactly (see [Column Mapping](#column-mapping)) +4. **Verify column order** - columns must be in the exact order listed above (44 columns total) +5. **Check data formats**: + - Dates: Use `YYYY-MM-DD` format (e.g., `2014-09-30`) + - Numbers: Use decimal format for `on_boarding_charges` (e.g., `1000.50`) + - Empty values: Leave cells empty (don't use "NULL" or "N/A" as text) + +### Step 2: Handle Special Characters + +- **Commas in text**: Wrap the entire field in double quotes + - Example: `"No.335, HVP RR Nagar Sector B"` +- **Quotes in text**: Use double quotes to escape: `""quoted text""` +- **Newlines in text**: Wrap field in double quotes + +### Step 3: Date Formatting + +Ensure dates are in `YYYY-MM-DD` format: +- ✅ Correct: `2014-09-30` +- ❌ Wrong: `30-Sep-14`, `09/30/2014`, `30-09-2014` + +### Step 4: Save the File + +1. **Save as CSV** (UTF-8 encoding) +2. **File location**: Save to an accessible path (e.g., `C:/Users/COMP/Downloads/DEALERS_CLEAN.csv`) +3. **File name**: Use a descriptive name (e.g., `DEALERS_CLEAN.csv`) + +### Sample CSV Format + +**Important:** Your CSV should **NOT** include `dealer_id`, `created_at`, `updated_at`, or `is_active` columns. These are auto-generated. + +```csv +sales_code,service_code,gear_code,gma_code,region,dealership,state,district,city,location,city_category_pst,layout_format,tier_city_category,on_boarding_charges,date,single_format_month_year,domain_id,replacement,termination_resignation_status,date_of_termination_resignation,last_date_of_operations,old_codes,branch_details,dealer_principal_name,dealer_principal_email_id,dp_contact_number,dp_contacts,showroom_address,showroom_pincode,workshop_address,workshop_pincode,location_district,state_workshop,no_of_studios,website_update,gst,pan,firm_type,prop_managing_partners_directors,total_prop_partners_directors,docs_folder_link,workshop_gma_codes,existing_new,dlrcode +5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.royalenfield.com,,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist – Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist – Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386 +``` + +**What gets auto-generated:** +- `dealer_id`: `550e8400-e29b-41d4-a716-446655440000` (example UUID) +- `created_at`: `2025-01-20 10:30:45.123` (current timestamp) +- `updated_at`: `2025-01-20 10:30:45.123` (current timestamp) +- `is_active`: `true` + +--- + +## Import Methods + +### Method 1: PostgreSQL COPY Command (Recommended - If CSV has 44 columns) + +**Use this if your CSV does NOT include `dealer_id`, `created_at`, `updated_at`, `is_active` columns.** + +**Prerequisites:** +- PostgreSQL client (psql) installed +- Access to PostgreSQL server +- CSV file path accessible from PostgreSQL server + +**Steps:** + +1. **Connect to PostgreSQL:** + ```bash + psql -U your_username -d royal_enfield_workflow -h localhost + ``` + +2. **Run the COPY command:** + + **Note:** The COPY command explicitly lists only the columns from your CSV. The following columns are **automatically handled by the database** and should **NOT** be in your CSV: + - `dealer_id` - Auto-generated UUID + - `created_at` - Auto-generated timestamp + - `updated_at` - Auto-generated timestamp + - `is_active` - Defaults to `true` + + ```sql + \copy public.dealers( + sales_code, + service_code, + gear_code, + gma_code, + region, + dealership, + state, + district, + city, + location, + city_category_pst, + layout_format, + tier_city_category, + on_boarding_charges, + date, + single_format_month_year, + domain_id, + replacement, + termination_resignation_status, + date_of_termination_resignation, + last_date_of_operations, + old_codes, + branch_details, + dealer_principal_name, + dealer_principal_email_id, + dp_contact_number, + dp_contacts, + showroom_address, + showroom_pincode, + workshop_address, + workshop_pincode, + location_district, + state_workshop, + no_of_studios, + website_update, + gst, + pan, + firm_type, + prop_managing_partners_directors, + total_prop_partners_directors, + docs_folder_link, + workshop_gma_codes, + existing_new, + dlrcode + ) + FROM 'C:/Users/COMP/Downloads/DEALERS_CLEAN.csv' + WITH ( + FORMAT csv, + HEADER true, + ENCODING 'UTF8' + ); + ``` + + **What happens:** + - `dealer_id` will be automatically generated as a UUID for each row + - `created_at` will be set to the current timestamp + - `updated_at` will be set to the current timestamp + - `is_active` will default to `true` + +3. **Verify import:** + ```sql + SELECT COUNT(*) FROM dealers; + SELECT * FROM dealers LIMIT 5; + ``` + +### Method 2: Using Temporary Table (If CSV has 48 columns including auto-generated ones) + +**Use this if your CSV includes `dealer_id`, `created_at`, `updated_at`, `is_active` columns and you're getting errors.** + +This method uses a temporary table to import the CSV, then inserts into the actual table while ignoring the auto-generated columns: + +```sql +-- Step 1: Create temporary table +CREATE TEMP TABLE dealers_temp ( + dealer_id TEXT, + sales_code TEXT, + service_code TEXT, + -- ... (all 48 columns as TEXT) +); + +-- Step 2: Import CSV into temp table +\copy dealers_temp FROM 'C:/Users/COMP/Downloads/DEALERS_CLEAN.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8'); + +-- Step 3: Insert into actual table (ignoring dealer_id, created_at, updated_at, is_active) +INSERT INTO public.dealers ( + sales_code, + service_code, + -- ... (only the 44 data columns) +) +SELECT + NULLIF(sales_code, ''), + NULLIF(service_code, ''), + -- ... (convert and handle empty strings) +FROM dealers_temp +WHERE sales_code IS NOT NULL OR dealership IS NOT NULL; -- Skip completely empty rows + +-- Step 4: Clean up +DROP TABLE dealers_temp; +``` + +**See `DEALERS_CSV_IMPORT_FIX.sql` for the complete working script.** + +### Method 3: Using pgAdmin + +1. Open pgAdmin and connect to your database +2. Right-click on `dealers` table → **Import/Export Data** +3. Select **Import** +4. Configure: + - **Filename**: Browse to your CSV file + - **Format**: CSV + - **Header**: Yes + - **Encoding**: UTF8 + - **Delimiter**: Comma +5. Click **OK** to import + +### Method 4: Using Node.js Script + +Create a script to import CSV programmatically (useful for automation): + +```typescript +import { sequelize } from '../config/database'; +import { QueryTypes } from 'sequelize'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as csv from 'csv-parser'; + +async function importDealersFromCSV(csvFilePath: string) { + const dealers: any[] = []; + + return new Promise((resolve, reject) => { + fs.createReadStream(csvFilePath) + .pipe(csv()) + .on('data', (row) => { + dealers.push(row); + }) + .on('end', async () => { + try { + // Bulk insert dealers + // Implementation depends on your needs + console.log(`Imported ${dealers.length} dealers`); + resolve(dealers); + } catch (error) { + reject(error); + } + }); + }); +} +``` + +--- + +## Troubleshooting + +### Common Issues and Solutions + +#### 1. **"Column count mismatch" Error** +- **Problem**: CSV has different number of columns than expected +- **Solution**: + - Verify your CSV has exactly **44 columns** (excluding header) + - **Remove** `dealer_id`, `created_at`, `updated_at`, and `is_active` if they exist in your CSV + - These columns are auto-generated and should NOT be in the CSV file + +#### 2. **"Invalid date format" Error** +- **Problem**: Dates not in `YYYY-MM-DD` format +- **Solution**: Convert dates to `YYYY-MM-DD` format (e.g., `2014-09-30`) + +#### 3. **"Encoding error" or "Special characters not displaying correctly** +- **Problem**: CSV file not saved in UTF-8 encoding +- **Solution**: + - In Excel: Save As → CSV UTF-8 (Comma delimited) (*.csv) + - In Notepad++: Encoding → Convert to UTF-8 → Save + +#### 4. **"Permission denied" Error (COPY command)** +- **Problem**: PostgreSQL server cannot access the file path +- **Solution**: + - Use absolute path with forward slashes: `C:/Users/COMP/Downloads/DEALERS_CLEAN.csv` + - Ensure file permissions allow read access + - For remote servers, upload file to server first + +#### 5. **"Duplicate key" Error** +- **Problem**: Trying to import duplicate records +- **Solution**: + - Use `ON CONFLICT` handling in your import + - Or clean CSV to remove duplicates before import + +#### 6. **Empty values showing as "NULL" text** +- **Problem**: CSV contains literal "NULL" or "N/A" strings +- **Solution**: Replace with empty cells in CSV + +#### 7. **Commas in address fields breaking import** +- **Problem**: Address fields contain commas not properly quoted +- **Solution**: Wrap fields containing commas in double quotes: + ```csv + "No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship" + ``` + +### Pre-Import Checklist + +- [ ] CSV file saved in UTF-8 encoding +- [ ] **Removed** `dealer_id`, `created_at`, `updated_at`, and `is_active` columns (if present) +- [ ] Header row matches column names exactly +- [ ] All 44 columns present in correct order +- [ ] Dates formatted as `YYYY-MM-DD` +- [ ] Numeric fields contain valid numbers (or are empty) +- [ ] Text fields with commas are wrapped in quotes +- [ ] File path is accessible from PostgreSQL server +- [ ] Database connection credentials are correct + +### Verification Queries + +After import, run these queries to verify: + +```sql +-- Count total dealers +SELECT COUNT(*) as total_dealers FROM dealers; + +-- Verify auto-generated columns +SELECT + dealer_id, + created_at, + updated_at, + is_active, + dlrcode, + dealership +FROM dealers +LIMIT 5; + +-- Check for null values in key fields +SELECT + COUNT(*) FILTER (WHERE dlrcode IS NULL) as null_dlrcode, + COUNT(*) FILTER (WHERE domain_id IS NULL) as null_domain_id, + COUNT(*) FILTER (WHERE dealership IS NULL) as null_dealership +FROM dealers; + +-- View sample records +SELECT + dealer_id, + dlrcode, + dealership, + city, + state, + domain_id, + created_at, + is_active +FROM dealers +LIMIT 10; + +-- Check date formats +SELECT + dlrcode, + date, + date_of_termination_resignation, + last_date_of_operations +FROM dealers +WHERE date IS NOT NULL +LIMIT 5; + +-- Verify all dealers have dealer_id and timestamps +SELECT + COUNT(*) as total, + COUNT(dealer_id) as has_dealer_id, + COUNT(created_at) as has_created_at, + COUNT(updated_at) as has_updated_at, + COUNT(*) FILTER (WHERE is_active = true) as active_count +FROM dealers; +``` + +--- + +## Additional Notes + +- **Backup**: Always backup your database before bulk imports +- **Testing**: Test import with a small sample (5-10 rows) first +- **Validation**: Validate data quality before import +- **Updates**: Use `UPSERT` logic if you need to update existing records + +--- + +## Support + +For issues or questions: +1. Check the troubleshooting section above +2. Review PostgreSQL COPY documentation +3. Verify CSV format matches the sample provided +4. Check database logs for detailed error messages + +--- + +**Last Updated**: December 2025 +**Version**: 1.0 + diff --git a/package.json b/package.json index 5d104be..a16f04c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "setup": "ts-node -r tsconfig-paths/register src/scripts/auto-setup.ts", "migrate": "ts-node -r tsconfig-paths/register src/scripts/migrate.ts", "seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.ts", - "seed:dealers": "ts-node -r tsconfig-paths/register src/scripts/seed-dealers.ts", "cleanup:dealer-claims": "ts-node -r tsconfig-paths/register src/scripts/cleanup-dealer-claims.ts" }, "dependencies": { diff --git a/src/controllers/dashboard.controller.ts b/src/controllers/dashboard.controller.ts index da5d493..ec9de45 100644 --- a/src/controllers/dashboard.controller.ts +++ b/src/controllers/dashboard.controller.ts @@ -46,6 +46,7 @@ export class DashboardController { const endDate = req.query.endDate as string | undefined; const status = req.query.status as string | undefined; // Status filter (not used in stats - stats show all statuses) const priority = req.query.priority as string | undefined; + const templateType = req.query.templateType as string | undefined; const department = req.query.department as string | undefined; const initiator = req.query.initiator as string | undefined; const approver = req.query.approver as string | undefined; @@ -61,6 +62,7 @@ export class DashboardController { endDate, status, priority, + templateType, department, initiator, approver, diff --git a/src/controllers/workflow.controller.ts b/src/controllers/workflow.controller.ts index 8260867..83cb9bc 100644 --- a/src/controllers/workflow.controller.ts +++ b/src/controllers/workflow.controller.ts @@ -380,6 +380,7 @@ export class WorkflowController { search: req.query.search as string | undefined, status: req.query.status as string | undefined, priority: req.query.priority as string | undefined, + templateType: req.query.templateType as string | undefined, department: req.query.department as string | undefined, initiator: req.query.initiator as string | undefined, approver: req.query.approver as string | undefined, @@ -441,6 +442,7 @@ export class WorkflowController { const search = req.query.search as string | undefined; const status = req.query.status as string | undefined; const priority = req.query.priority as string | undefined; + const templateType = req.query.templateType as string | undefined; const department = req.query.department as string | undefined; const initiator = req.query.initiator as string | undefined; const approver = req.query.approver as string | undefined; @@ -450,7 +452,7 @@ export class WorkflowController { const startDate = req.query.startDate as string | undefined; const endDate = req.query.endDate as string | undefined; - const filters = { search, status, priority, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate }; + const filters = { search, status, priority, templateType, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate }; const result = await workflowService.listParticipantRequests(userId, page, limit, filters); ResponseHandler.success(res, result, 'Participant requests fetched'); @@ -473,13 +475,14 @@ export class WorkflowController { const search = req.query.search as string | undefined; const status = req.query.status as string | undefined; const priority = req.query.priority as string | undefined; + const templateType = req.query.templateType as string | undefined; const department = req.query.department as string | undefined; const slaCompliance = req.query.slaCompliance as string | undefined; const dateRange = req.query.dateRange as string | undefined; const startDate = req.query.startDate as string | undefined; const endDate = req.query.endDate as string | undefined; - const filters = { search, status, priority, department, slaCompliance, dateRange, startDate, endDate }; + const filters = { search, status, priority, templateType, department, slaCompliance, dateRange, startDate, endDate }; const result = await workflowService.listMyInitiatedRequests(userId, page, limit, filters); ResponseHandler.success(res, result, 'My initiated requests fetched'); @@ -499,7 +502,8 @@ export class WorkflowController { const filters = { search: req.query.search as string | undefined, status: req.query.status as string | undefined, - priority: req.query.priority as string | undefined + priority: req.query.priority as string | undefined, + templateType: req.query.templateType as string | undefined }; // Extract sorting parameters @@ -524,7 +528,8 @@ export class WorkflowController { const filters = { search: req.query.search as string | undefined, status: req.query.status as string | undefined, - priority: req.query.priority as string | undefined + priority: req.query.priority as string | undefined, + templateType: req.query.templateType as string | undefined }; // Extract sorting parameters diff --git a/src/migrations/20250120-create-dealers-table.ts b/src/migrations/20250120-create-dealers-table.ts new file mode 100644 index 0000000..f9d08f3 --- /dev/null +++ b/src/migrations/20250120-create-dealers-table.ts @@ -0,0 +1,322 @@ +import { QueryInterface, DataTypes } from 'sequelize'; +import { Sequelize } from 'sequelize'; + +export async function up(queryInterface: QueryInterface): Promise { + // Ensure uuid-ossp extension is enabled (required for uuid_generate_v4()) + await queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); + + // Create dealers table with all fields from sample data + await queryInterface.createTable('dealers', { + dealer_id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.literal('uuid_generate_v4()') + }, + sales_code: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Sales Code' + }, + service_code: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Service Code' + }, + gear_code: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Gear Code' + }, + gma_code: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'GMA CODE' + }, + region: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Region' + }, + dealership: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Dealership name' + }, + state: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'State' + }, + district: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'District' + }, + city: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'City' + }, + location: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Location' + }, + city_category_pst: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'City category (PST)' + }, + layout_format: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Layout format' + }, + tier_city_category: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'TIER City Category' + }, + on_boarding_charges: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'On Boarding Charges (stored as text to allow text values)' + }, + date: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'DATE (stored as text to avoid format validation)' + }, + single_format_month_year: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Single Format of Month/Year (stored as text)' + }, + domain_id: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Domain Id' + }, + replacement: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Replacement (stored as text to allow longer values)' + }, + termination_resignation_status: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Termination / Resignation under Proposal or Evaluation' + }, + date_of_termination_resignation: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Date Of termination/ resignation (stored as text to avoid format validation)' + }, + last_date_of_operations: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Last date of operations (stored as text to avoid format validation)' + }, + old_codes: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Old Codes' + }, + branch_details: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Branch Details' + }, + dealer_principal_name: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Dealer Principal Name' + }, + dealer_principal_email_id: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Dealer Principal Email Id' + }, + dp_contact_number: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'DP CONTACT NUMBER (stored as text to allow multiple numbers)' + }, + dp_contacts: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'DP CONTACTS (stored as text to allow multiple contacts)' + }, + showroom_address: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Showroom Address' + }, + showroom_pincode: { + type: DataTypes.STRING(10), + allowNull: true, + comment: 'Showroom Pincode' + }, + workshop_address: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Workshop Address' + }, + workshop_pincode: { + type: DataTypes.STRING(10), + allowNull: true, + comment: 'Workshop Pincode' + }, + location_district: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'Location / District' + }, + state_workshop: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'State (for workshop)' + }, + no_of_studios: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 0, + comment: 'No Of Studios' + }, + website_update: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Website update (stored as text to allow longer values)' + }, + gst: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'GST' + }, + pan: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'PAN' + }, + firm_type: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'Firm Type' + }, + prop_managing_partners_directors: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Prop. / Managing Partners / Managing Directors' + }, + total_prop_partners_directors: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Total Prop. / Partners / Directors' + }, + docs_folder_link: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'DOCS Folder Link' + }, + workshop_gma_codes: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Workshop GMA Codes' + }, + existing_new: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Existing / New' + }, + dlrcode: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'dlrcode' + }, + is_active: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + comment: 'Whether the dealer is currently active' + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + } + }); + + // Create indexes + await queryInterface.addIndex('dealers', ['sales_code'], { + name: 'idx_dealers_sales_code', + unique: false + }); + + await queryInterface.addIndex('dealers', ['service_code'], { + name: 'idx_dealers_service_code', + unique: false + }); + + await queryInterface.addIndex('dealers', ['gma_code'], { + name: 'idx_dealers_gma_code', + unique: false + }); + + await queryInterface.addIndex('dealers', ['domain_id'], { + name: 'idx_dealers_domain_id', + unique: false + }); + + await queryInterface.addIndex('dealers', ['region'], { + name: 'idx_dealers_region', + unique: false + }); + + await queryInterface.addIndex('dealers', ['state'], { + name: 'idx_dealers_state', + unique: false + }); + + await queryInterface.addIndex('dealers', ['city'], { + name: 'idx_dealers_city', + unique: false + }); + + await queryInterface.addIndex('dealers', ['district'], { + name: 'idx_dealers_district', + unique: false + }); + + await queryInterface.addIndex('dealers', ['dlrcode'], { + name: 'idx_dealers_dlrcode', + unique: false + }); + + await queryInterface.addIndex('dealers', ['is_active'], { + name: 'idx_dealers_is_active', + unique: false + }); +} + +export async function down(queryInterface: QueryInterface): Promise { + // Drop indexes first + await queryInterface.removeIndex('dealers', 'idx_dealers_sales_code'); + await queryInterface.removeIndex('dealers', 'idx_dealers_service_code'); + await queryInterface.removeIndex('dealers', 'idx_dealers_gma_code'); + await queryInterface.removeIndex('dealers', 'idx_dealers_domain_id'); + await queryInterface.removeIndex('dealers', 'idx_dealers_region'); + await queryInterface.removeIndex('dealers', 'idx_dealers_state'); + await queryInterface.removeIndex('dealers', 'idx_dealers_city'); + await queryInterface.removeIndex('dealers', 'idx_dealers_district'); + await queryInterface.removeIndex('dealers', 'idx_dealers_dlrcode'); + await queryInterface.removeIndex('dealers', 'idx_dealers_is_active'); + + // Drop table + await queryInterface.dropTable('dealers'); +} + diff --git a/src/models/Dealer.ts b/src/models/Dealer.ts new file mode 100644 index 0000000..f6394c7 --- /dev/null +++ b/src/models/Dealer.ts @@ -0,0 +1,442 @@ +import { DataTypes, Model, Optional } from 'sequelize'; +import { sequelize } from '../config/database'; + +interface DealerAttributes { + dealerId: string; + salesCode?: string | null; + serviceCode?: string | null; + gearCode?: string | null; + gmaCode?: string | null; + region?: string | null; + dealership?: string | null; + state?: string | null; + district?: string | null; + city?: string | null; + location?: string | null; + cityCategoryPst?: string | null; + layoutFormat?: string | null; + tierCityCategory?: string | null; + onBoardingCharges?: string | null; + date?: string | null; + singleFormatMonthYear?: string | null; + domainId?: string | null; + replacement?: string | null; + terminationResignationStatus?: string | null; + dateOfTerminationResignation?: string | null; + lastDateOfOperations?: string | null; + oldCodes?: string | null; + branchDetails?: string | null; + dealerPrincipalName?: string | null; + dealerPrincipalEmailId?: string | null; + dpContactNumber?: string | null; + dpContacts?: string | null; + showroomAddress?: string | null; + showroomPincode?: string | null; + workshopAddress?: string | null; + workshopPincode?: string | null; + locationDistrict?: string | null; + stateWorkshop?: string | null; + noOfStudios?: number | null; + websiteUpdate?: string | null; + gst?: string | null; + pan?: string | null; + firmType?: string | null; + propManagingPartnersDirectors?: string | null; + totalPropPartnersDirectors?: string | null; + docsFolderLink?: string | null; + workshopGmaCodes?: string | null; + existingNew?: string | null; + dlrcode?: string | null; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +interface DealerCreationAttributes extends Optional {} + +class Dealer extends Model implements DealerAttributes { + public dealerId!: string; + public salesCode?: string | null; + public serviceCode?: string | null; + public gearCode?: string | null; + public gmaCode?: string | null; + public region?: string | null; + public dealership?: string | null; + public state?: string | null; + public district?: string | null; + public city?: string | null; + public location?: string | null; + public cityCategoryPst?: string | null; + public layoutFormat?: string | null; + public tierCityCategory?: string | null; + public onBoardingCharges?: string | null; + public date?: string | null; + public singleFormatMonthYear?: string | null; + public domainId?: string | null; + public replacement?: string | null; + public terminationResignationStatus?: string | null; + public dateOfTerminationResignation?: string | null; + public lastDateOfOperations?: string | null; + public oldCodes?: string | null; + public branchDetails?: string | null; + public dealerPrincipalName?: string | null; + public dealerPrincipalEmailId?: string | null; + public dpContactNumber?: string | null; + public dpContacts?: string | null; + public showroomAddress?: string | null; + public showroomPincode?: string | null; + public workshopAddress?: string | null; + public workshopPincode?: string | null; + public locationDistrict?: string | null; + public stateWorkshop?: string | null; + public noOfStudios?: number | null; + public websiteUpdate?: string | null; + public gst?: string | null; + public pan?: string | null; + public firmType?: string | null; + public propManagingPartnersDirectors?: string | null; + public totalPropPartnersDirectors?: string | null; + public docsFolderLink?: string | null; + public workshopGmaCodes?: string | null; + public existingNew?: string | null; + public dlrcode?: string | null; + public isActive!: boolean; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; +} + +Dealer.init( + { + dealerId: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + field: 'dealer_id' + }, + salesCode: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'sales_code', + comment: 'Sales Code' + }, + serviceCode: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'service_code', + comment: 'Service Code' + }, + gearCode: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'gear_code', + comment: 'Gear Code' + }, + gmaCode: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'gma_code', + comment: 'GMA CODE' + }, + region: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'Region' + }, + dealership: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Dealership name' + }, + state: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'State' + }, + district: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'District' + }, + city: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'City' + }, + location: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Location' + }, + cityCategoryPst: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'city_category_pst', + comment: 'City category (PST)' + }, + layoutFormat: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'layout_format', + comment: 'Layout format' + }, + tierCityCategory: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'tier_city_category', + comment: 'TIER City Category' + }, + onBoardingCharges: { + type: DataTypes.TEXT, + allowNull: true, + field: 'on_boarding_charges', + comment: 'On Boarding Charges (stored as text to allow text values)' + }, + date: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'DATE (stored as text to avoid format validation)' + }, + singleFormatMonthYear: { + type: DataTypes.TEXT, + allowNull: true, + field: 'single_format_month_year', + comment: 'Single Format of Month/Year (stored as text)' + }, + domainId: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'domain_id', + comment: 'Domain Id' + }, + replacement: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Replacement (stored as text to allow longer values)' + }, + terminationResignationStatus: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'termination_resignation_status', + comment: 'Termination / Resignation under Proposal or Evaluation' + }, + dateOfTerminationResignation: { + type: DataTypes.TEXT, + allowNull: true, + field: 'date_of_termination_resignation', + comment: 'Date Of termination/ resignation (stored as text to avoid format validation)' + }, + lastDateOfOperations: { + type: DataTypes.TEXT, + allowNull: true, + field: 'last_date_of_operations', + comment: 'Last date of operations (stored as text to avoid format validation)' + }, + oldCodes: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'old_codes', + comment: 'Old Codes' + }, + branchDetails: { + type: DataTypes.TEXT, + allowNull: true, + field: 'branch_details', + comment: 'Branch Details' + }, + dealerPrincipalName: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'dealer_principal_name', + comment: 'Dealer Principal Name' + }, + dealerPrincipalEmailId: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'dealer_principal_email_id', + comment: 'Dealer Principal Email Id' + }, + dpContactNumber: { + type: DataTypes.TEXT, + allowNull: true, + field: 'dp_contact_number', + comment: 'DP CONTACT NUMBER (stored as text to allow multiple numbers)' + }, + dpContacts: { + type: DataTypes.TEXT, + allowNull: true, + field: 'dp_contacts', + comment: 'DP CONTACTS (stored as text to allow multiple contacts)' + }, + showroomAddress: { + type: DataTypes.TEXT, + allowNull: true, + field: 'showroom_address', + comment: 'Showroom Address' + }, + showroomPincode: { + type: DataTypes.STRING(10), + allowNull: true, + field: 'showroom_pincode', + comment: 'Showroom Pincode' + }, + workshopAddress: { + type: DataTypes.TEXT, + allowNull: true, + field: 'workshop_address', + comment: 'Workshop Address' + }, + workshopPincode: { + type: DataTypes.STRING(10), + allowNull: true, + field: 'workshop_pincode', + comment: 'Workshop Pincode' + }, + locationDistrict: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'location_district', + comment: 'Location / District' + }, + stateWorkshop: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'state_workshop', + comment: 'State (for workshop)' + }, + noOfStudios: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 0, + field: 'no_of_studios', + comment: 'No Of Studios' + }, + websiteUpdate: { + type: DataTypes.TEXT, + allowNull: true, + field: 'website_update', + comment: 'Website update (stored as text to allow longer values)' + }, + gst: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'GST' + }, + pan: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'PAN' + }, + firmType: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'firm_type', + comment: 'Firm Type' + }, + propManagingPartnersDirectors: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'prop_managing_partners_directors', + comment: 'Prop. / Managing Partners / Managing Directors' + }, + totalPropPartnersDirectors: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'total_prop_partners_directors', + comment: 'Total Prop. / Partners / Directors' + }, + docsFolderLink: { + type: DataTypes.TEXT, + allowNull: true, + field: 'docs_folder_link', + comment: 'DOCS Folder Link' + }, + workshopGmaCodes: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'workshop_gma_codes', + comment: 'Workshop GMA Codes' + }, + existingNew: { + type: DataTypes.STRING(50), + allowNull: true, + field: 'existing_new', + comment: 'Existing / New' + }, + dlrcode: { + type: DataTypes.STRING(50), + allowNull: true, + comment: 'dlrcode' + }, + isActive: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + field: 'is_active', + comment: 'Whether the dealer is currently active' + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'created_at' + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'updated_at' + } + }, + { + sequelize, + tableName: 'dealers', + modelName: 'Dealer', + timestamps: true, + underscored: true, + indexes: [ + { + fields: ['sales_code'], + name: 'idx_dealers_sales_code' + }, + { + fields: ['service_code'], + name: 'idx_dealers_service_code' + }, + { + fields: ['gma_code'], + name: 'idx_dealers_gma_code' + }, + { + fields: ['domain_id'], + name: 'idx_dealers_domain_id' + }, + { + fields: ['region'], + name: 'idx_dealers_region' + }, + { + fields: ['state'], + name: 'idx_dealers_state' + }, + { + fields: ['city'], + name: 'idx_dealers_city' + }, + { + fields: ['district'], + name: 'idx_dealers_district' + }, + { + fields: ['dlrcode'], + name: 'idx_dealers_dlrcode' + }, + { + fields: ['is_active'], + name: 'idx_dealers_is_active' + } + ] + } +); + +export { Dealer }; +export type { DealerAttributes, DealerCreationAttributes }; diff --git a/src/models/index.ts b/src/models/index.ts index 37130ce..59578bf 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -23,6 +23,7 @@ import { DealerProposalCostItem } from './DealerProposalCostItem'; import { WorkflowTemplate } from './WorkflowTemplate'; import { InternalOrder } from './InternalOrder'; import { ClaimBudgetTracking } from './ClaimBudgetTracking'; +import { Dealer } from './Dealer'; // Define associations const defineAssociations = () => { @@ -166,7 +167,8 @@ export { DealerProposalCostItem, WorkflowTemplate, InternalOrder, - ClaimBudgetTracking + ClaimBudgetTracking, + Dealer }; // Export default sequelize instance diff --git a/src/scripts/auto-setup.ts b/src/scripts/auto-setup.ts index d4685e5..73b3cc7 100644 --- a/src/scripts/auto-setup.ts +++ b/src/scripts/auto-setup.ts @@ -133,6 +133,7 @@ async function runMigrations(): Promise { const m38 = require('../migrations/20251213-create-claim-invoice-credit-note-tables'); const m39 = require('../migrations/20251214-create-dealer-completion-expenses'); const m40 = require('../migrations/20251218-fix-claim-invoice-credit-note-columns'); + const m41 = require('../migrations/20250120-create-dealers-table'); const migrations = [ { name: '2025103000-create-users', module: m0 }, @@ -178,6 +179,7 @@ async function runMigrations(): Promise { { name: '20251213-create-claim-invoice-credit-note-tables', module: m38 }, { name: '20251214-create-dealer-completion-expenses', module: m39 }, { name: '20251218-fix-claim-invoice-credit-note-columns', module: m40 }, + { name: '20250120-create-dealers-table', module: m41 }, ]; const queryInterface = sequelize.getQueryInterface(); @@ -273,7 +275,8 @@ async function autoSetup(): Promise { console.log('✅ Setup completed successfully!'); console.log('========================================\n'); - console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.\n'); + console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.'); + console.log('📝 Note: Dealers table will be empty - import dealers using CSV import script.\n'); if (wasCreated) { console.log('💡 Next steps:'); diff --git a/src/scripts/migrate.ts b/src/scripts/migrate.ts index 60d6795..43b5cc9 100644 --- a/src/scripts/migrate.ts +++ b/src/scripts/migrate.ts @@ -43,6 +43,7 @@ import * as m37 from '../migrations/20251213-drop-claim-details-invoice-columns' import * as m38 from '../migrations/20251213-create-claim-invoice-credit-note-tables'; import * as m39 from '../migrations/20251214-create-dealer-completion-expenses'; import * as m40 from '../migrations/20251218-fix-claim-invoice-credit-note-columns'; +import * as m41 from '../migrations/20250120-create-dealers-table'; interface Migration { name: string; @@ -100,6 +101,7 @@ const migrations: Migration[] = [ { name: '20251213-create-claim-invoice-credit-note-tables', module: m38 }, { name: '20251214-create-dealer-completion-expenses', module: m39 }, { name: '20251218-fix-claim-invoice-credit-note-columns', module: m40 }, + { name: '20250120-create-dealers-table', module: m41 }, ]; /** diff --git a/src/scripts/seed-dealers-table.ts b/src/scripts/seed-dealers-table.ts new file mode 100644 index 0000000..fd1e822 --- /dev/null +++ b/src/scripts/seed-dealers-table.ts @@ -0,0 +1,185 @@ +/** + * Seed Dealers Table + * Populates the dealers table with sample dealer data + * + * Note: Update this script with your actual dealer data from the Excel/CSV file + */ + +import { sequelize } from '../config/database'; +import { Dealer } from '../models/Dealer'; +import { Op } from 'sequelize'; +import logger from '../utils/logger'; + +interface DealerSeedData { + salesCode?: string | null; + serviceCode?: string | null; + gearCode?: string | null; + gmaCode?: string | null; + region?: string | null; + dealership?: string | null; + state?: string | null; + district?: string | null; + city?: string | null; + location?: string | null; + cityCategoryPst?: string | null; + layoutFormat?: string | null; + tierCityCategory?: string | null; + onBoardingCharges?: string | null; + date?: string | null; + singleFormatMonthYear?: string | null; + domainId?: string | null; + replacement?: string | null; + terminationResignationStatus?: string | null; + dateOfTerminationResignation?: string | null; + lastDateOfOperations?: string | null; + oldCodes?: string | null; + branchDetails?: string | null; + dealerPrincipalName?: string | null; + dealerPrincipalEmailId?: string | null; + dpContactNumber?: string | null; + dpContacts?: string | null; + showroomAddress?: string | null; + showroomPincode?: string | null; + workshopAddress?: string | null; + workshopPincode?: string | null; + locationDistrict?: string | null; + stateWorkshop?: string | null; + noOfStudios?: number | null; + websiteUpdate?: string | null; + gst?: string | null; + pan?: string | null; + firmType?: string | null; + propManagingPartnersDirectors?: string | null; + totalPropPartnersDirectors?: string | null; + docsFolderLink?: string | null; + workshopGmaCodes?: string | null; + existingNew?: string | null; + dlrcode?: string | null; +} + +// Sample data based on the provided table +// TODO: Replace with your actual dealer data from Excel/CSV +const dealersData: DealerSeedData[] = [ + { + salesCode: '5124', + serviceCode: '5125', + gearCode: '5573', + gmaCode: '9430', + region: 'S3', + dealership: 'Accelerate Motors', + state: 'Karnataka', + district: 'Bengaluru', + city: 'Bengaluru', + location: 'RAJA RAJESHWARI NAGAR', + cityCategoryPst: 'A+', + layoutFormat: 'A+', + tierCityCategory: 'Tier 1 City', + onBoardingCharges: null, + date: '2014-09-30', + singleFormatMonthYear: 'Sep-2014', + domainId: 'acceleratemotors.rrnagar@dealer.royalenfield.com', + replacement: null, + terminationResignationStatus: null, + dateOfTerminationResignation: null, + lastDateOfOperations: null, + oldCodes: null, + branchDetails: null, + dealerPrincipalName: 'N. Shyam Charmanna', + dealerPrincipalEmailId: 'shyamcharmanna@yahoo.co.in', + dpContactNumber: '7022049621', + dpContacts: '7022049621', + showroomAddress: 'No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist – Bangalore, Karnataka', + showroomPincode: '560098', + workshopAddress: 'Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist – Bangalore, Karnataka', + workshopPincode: '560098', + locationDistrict: 'Bangalore', + stateWorkshop: 'Karnataka', + noOfStudios: 0, + websiteUpdate: 'Yes', + gst: '29ARCPS1311D1Z6', + pan: 'ARCPS1311D', + firmType: 'Proprietorship', + propManagingPartnersDirectors: 'CHARMANNA SHYAM NELLAMAKADA', + totalPropPartnersDirectors: 'CHARMANNA SHYAM NELLAMAKADA', + docsFolderLink: 'https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb', + workshopGmaCodes: null, + existingNew: null, + dlrcode: '3386' + } + // Add more dealer records here from your Excel/CSV data +]; + +async function seedDealersTable(): Promise { + try { + logger.info('[Seed Dealers Table] Starting dealers table seeding...'); + + for (const dealerData of dealersData) { + // Use dlrcode or domainId as unique identifier if available + const uniqueIdentifier = dealerData.dlrcode || dealerData.domainId || dealerData.salesCode; + + if (!uniqueIdentifier) { + logger.warn('[Seed Dealers Table] Skipping dealer record without unique identifier'); + continue; + } + + // Check if dealer already exists (using dlrcode, domainId, or salesCode) + const whereConditions: any[] = []; + if (dealerData.dlrcode) whereConditions.push({ dlrcode: dealerData.dlrcode }); + if (dealerData.domainId) whereConditions.push({ domainId: dealerData.domainId }); + if (dealerData.salesCode) whereConditions.push({ salesCode: dealerData.salesCode }); + + const existingDealer = whereConditions.length > 0 + ? await Dealer.findOne({ + where: { + [Op.or]: whereConditions + } + }) + : null; + + if (existingDealer) { + logger.info(`[Seed Dealers Table] Dealer ${uniqueIdentifier} already exists, updating...`); + + // Update existing dealer + await existingDealer.update({ + ...dealerData, + isActive: true + }); + + logger.info(`[Seed Dealers Table] ✅ Updated dealer: ${uniqueIdentifier}`); + } else { + // Create new dealer + await Dealer.create({ + ...dealerData, + isActive: true + }); + + logger.info(`[Seed Dealers Table] ✅ Created dealer: ${uniqueIdentifier}`); + } + } + + logger.info('[Seed Dealers Table] ✅ Dealers table seeding completed successfully'); + } catch (error) { + logger.error('[Seed Dealers Table] ❌ Error seeding dealers table:', error); + throw error; + } +} + +// Run if called directly +if (require.main === module) { + sequelize + .authenticate() + .then(() => { + logger.info('[Seed Dealers Table] Database connection established'); + return seedDealersTable(); + }) + .then(() => { + logger.info('[Seed Dealers Table] Seeding completed'); + process.exit(0); + }) + .catch((error) => { + logger.error('[Seed Dealers Table] Seeding failed:', error); + process.exit(1); + }); +} + +export { seedDealersTable, dealersData }; diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts index 7f200e9..07f5d27 100644 --- a/src/services/dashboard.service.ts +++ b/src/services/dashboard.service.ts @@ -122,7 +122,7 @@ export class DashboardService { engagement, aiInsights ] = await Promise.all([ - this.getRequestStats(userId, dateRange, startDate, endDate, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, viewAsUser), + this.getRequestStats(userId, dateRange, startDate, endDate, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, viewAsUser), this.getTATEfficiency(userId, dateRange, startDate, endDate, viewAsUser), this.getApproverLoad(userId, dateRange, startDate, endDate, viewAsUser), this.getEngagementStats(userId, dateRange, startDate, endDate, viewAsUser), @@ -153,6 +153,7 @@ export class DashboardService { endDate?: string, status?: string, priority?: string, + templateType?: string, department?: string, initiator?: string, approver?: string, @@ -162,7 +163,8 @@ export class DashboardService { viewAsUser?: boolean ) { // Check if date range should be applied - const applyDateRange = dateRange !== undefined && dateRange !== null; + // 'all' means no date filter - show all requests regardless of date + const applyDateRange = dateRange !== undefined && dateRange !== null && dateRange !== 'all'; const range = applyDateRange ? this.parseDateRange(dateRange, startDate, endDate) : null; // Check if user is admin or management (has broader access) @@ -205,6 +207,18 @@ export class DashboardService { replacements.priority = priority.toUpperCase(); } + // TemplateType filter + if (templateType && templateType !== 'all') { + const templateTypeUpper = templateType.toUpperCase(); + if (templateTypeUpper === 'CUSTOM') { + // For CUSTOM, include both CUSTOM and null (legacy requests) + filterConditions += ` AND (wf.template_type = 'CUSTOM' OR wf.template_type IS NULL)`; + } else { + filterConditions += ` AND wf.template_type = :templateType`; + replacements.templateType = templateTypeUpper; + } + } + // Department filter (through initiator) if (department && department !== 'all') { filterConditions += ` AND EXISTS ( @@ -279,11 +293,16 @@ export class DashboardService { // Organization Level: Admin/Management see ALL requests across organization // Personal Level: Regular users see requests where they are INVOLVED (initiator, approver, or participant) - // Note: If dateRange is provided, filter by submission_date. Otherwise, show all requests. + // Note: If dateRange is provided, filter by submission_date (or createdAt if submission_date is null). Otherwise, show all requests. // For pending/open requests, if no date range, count ALL pending requests regardless of creation date // For approved/rejected/closed, if date range is provided, count only those submitted in date range + // Match the same logic as listParticipantRequests: include requests where submission_date is in range OR (submission_date is null AND created_at is in range) const dateFilterClause = applyDateRange - ? `wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL` + ? `( + (wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL) + OR + (wf.submission_date IS NULL AND wf.created_at BETWEEN :start AND :end) + )` : `1=1`; // No date filter - show all requests // Build user-level filter: Include requests where user is initiator, approver, or participant @@ -313,8 +332,13 @@ export class DashboardService { // For pending requests, if no date range is applied, don't filter by date at all // This ensures pending requests are always counted regardless of submission date + // Match the same logic as listParticipantRequests: include requests where submission_date is in range OR (submission_date is null AND created_at is in range) const pendingDateFilterClause = applyDateRange - ? `wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL` + ? `( + (wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL) + OR + (wf.submission_date IS NULL AND wf.created_at BETWEEN :start AND :end) + )` : `1=1`; // No date filter for pending requests let whereClauseForPending = ` diff --git a/src/services/workflow.service.ts b/src/services/workflow.service.ts index 4ff5ad3..5268008 100644 --- a/src/services/workflow.service.ts +++ b/src/services/workflow.service.ts @@ -560,7 +560,7 @@ export class WorkflowService { * Shows ALL requests in the organization, including where admin is initiator * Used by: "All Requests" page for admin users */ - async listWorkflows(page: number, limit: number, filters?: { search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; approverType?: 'current' | 'any'; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string }) { + async listWorkflows(page: number, limit: number, filters?: { search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; approverType?: 'current' | 'any'; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string }) { const offset = (page - 1) * limit; // Build where clause with filters @@ -605,6 +605,22 @@ export class WorkflowService { whereConditions.push({ priority: filters.priority.toUpperCase() }); } + // Apply templateType filter + if (filters?.templateType && filters.templateType !== 'all') { + const templateTypeUpper = filters.templateType.toUpperCase(); + // For CUSTOM, also include null values (legacy requests without templateType) + if (templateTypeUpper === 'CUSTOM') { + whereConditions.push({ + [Op.or]: [ + { templateType: 'CUSTOM' }, + { templateType: null } + ] + }); + } else { + whereConditions.push({ templateType: templateTypeUpper }); + } + } + // Apply search filter (title, description, or requestNumber) if (filters?.search && filters.search.trim()) { whereConditions.push({ @@ -1371,6 +1387,7 @@ export class WorkflowService { search?: string; status?: string; priority?: string; + templateType?: string; department?: string; initiator?: string; approver?: string; @@ -1459,6 +1476,22 @@ export class WorkflowService { whereConditions.push({ priority: filters.priority.toUpperCase() }); } + // Apply templateType filter + if (filters?.templateType && filters.templateType !== 'all') { + const templateTypeUpper = filters.templateType.toUpperCase(); + // For CUSTOM, also include null values (legacy requests without templateType) + if (templateTypeUpper === 'CUSTOM') { + whereConditions.push({ + [Op.or]: [ + { templateType: 'CUSTOM' }, + { templateType: null } + ] + }); + } else { + whereConditions.push({ templateType: templateTypeUpper }); + } + } + // Apply search filter (title, description, or requestNumber) if (filters?.search && filters.search.trim()) { whereConditions.push({ @@ -1664,6 +1697,7 @@ export class WorkflowService { search?: string; status?: string; priority?: string; + templateType?: string; department?: string; slaCompliance?: string; dateRange?: string; @@ -1703,6 +1737,22 @@ export class WorkflowService { whereConditions.push({ priority: filters.priority.toUpperCase() }); } + // Apply templateType filter + if (filters?.templateType && filters.templateType !== 'all') { + const templateTypeUpper = filters.templateType.toUpperCase(); + // For CUSTOM, also include null values (legacy requests without templateType) + if (templateTypeUpper === 'CUSTOM') { + whereConditions.push({ + [Op.or]: [ + { templateType: 'CUSTOM' }, + { templateType: null } + ] + }); + } else { + whereConditions.push({ templateType: templateTypeUpper }); + } + } + // Apply search filter (title, description, or requestNumber) if (filters?.search && filters.search.trim()) { whereConditions.push({ @@ -1841,7 +1891,7 @@ export class WorkflowService { return { data, pagination: { page, limit, total: count, totalPages: Math.ceil(count / limit) || 1 } }; } - async listOpenForMe(userId: string, page: number, limit: number, filters?: { search?: string; status?: string; priority?: string }, sortBy?: string, sortOrder?: string) { + async listOpenForMe(userId: string, page: number, limit: number, filters?: { search?: string; status?: string; priority?: string; templateType?: string }, sortBy?: string, sortOrder?: string) { const offset = (page - 1) * limit; // Find all pending/in-progress/paused approval levels across requests ordered by levelNumber // Include PAUSED status so paused requests where user is the current approver are shown @@ -1972,6 +2022,22 @@ export class WorkflowService { baseConditions.push({ priority: filters.priority.toUpperCase() }); } + // Apply templateType filter + if (filters?.templateType && filters.templateType !== 'all') { + const templateTypeUpper = filters.templateType.toUpperCase(); + // For CUSTOM, also include null values (legacy requests without templateType) + if (templateTypeUpper === 'CUSTOM') { + baseConditions.push({ + [Op.or]: [ + { templateType: 'CUSTOM' }, + { templateType: null } + ] + }); + } else { + baseConditions.push({ templateType: templateTypeUpper }); + } + } + // Apply search filter (title, description, or requestNumber) if (filters?.search && filters.search.trim()) { baseConditions.push({ @@ -2076,7 +2142,7 @@ export class WorkflowService { return { data, pagination: { page, limit, total: count, totalPages: Math.ceil(count / limit) || 1 } }; } - async listClosedByMe(userId: string, page: number, limit: number, filters?: { search?: string; status?: string; priority?: string }, sortBy?: string, sortOrder?: string) { + async listClosedByMe(userId: string, page: number, limit: number, filters?: { search?: string; status?: string; priority?: string; templateType?: string }, sortBy?: string, sortOrder?: string) { const offset = (page - 1) * limit; // Get requests where user participated as approver @@ -2151,21 +2217,37 @@ export class WorkflowService { } } - // Apply priority filter - if (filters?.priority && filters.priority !== 'all') { - approverConditionParts.push({ priority: filters.priority.toUpperCase() }); - } - - // Apply search filter (title, description, or requestNumber) - if (filters?.search && filters.search.trim()) { + // Apply priority filter + if (filters?.priority && filters.priority !== 'all') { + approverConditionParts.push({ priority: filters.priority.toUpperCase() }); + } + + // Apply templateType filter + if (filters?.templateType && filters.templateType !== 'all') { + const templateTypeUpper = filters.templateType.toUpperCase(); + // For CUSTOM, also include null values (legacy requests without templateType) + if (templateTypeUpper === 'CUSTOM') { approverConditionParts.push({ [Op.or]: [ - { title: { [Op.iLike]: `%${filters.search.trim()}%` } }, - { description: { [Op.iLike]: `%${filters.search.trim()}%` } }, - { requestNumber: { [Op.iLike]: `%${filters.search.trim()}%` } } + { templateType: 'CUSTOM' }, + { templateType: null } ] }); + } else { + approverConditionParts.push({ templateType: templateTypeUpper }); } + } + + // Apply search filter (title, description, or requestNumber) + if (filters?.search && filters.search.trim()) { + approverConditionParts.push({ + [Op.or]: [ + { title: { [Op.iLike]: `%${filters.search.trim()}%` } }, + { description: { [Op.iLike]: `%${filters.search.trim()}%` } }, + { requestNumber: { [Op.iLike]: `%${filters.search.trim()}%` } } + ] + }); + } const approverCondition = approverConditionParts.length > 0 ? { [Op.and]: approverConditionParts } @@ -2219,6 +2301,22 @@ export class WorkflowService { initiatorConditionParts.push({ priority: filters.priority.toUpperCase() }); } + // Apply templateType filter + if (filters?.templateType && filters.templateType !== 'all') { + const templateTypeUpper = filters.templateType.toUpperCase(); + // For CUSTOM, also include null values (legacy requests without templateType) + if (templateTypeUpper === 'CUSTOM') { + initiatorConditionParts.push({ + [Op.or]: [ + { templateType: 'CUSTOM' }, + { templateType: null } + ] + }); + } else { + initiatorConditionParts.push({ templateType: templateTypeUpper }); + } + } + // Apply search filter (title, description, or requestNumber) if (filters?.search && filters.search.trim()) { initiatorConditionParts.push({