dealer table added and claim type filter implemented

This commit is contained in:
laxmanhalaki 2025-12-23 19:25:29 +05:30
parent 651576f51d
commit 9c8a8512bc
13 changed files with 1899 additions and 25 deletions

View File

@ -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;

View File

@ -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

View File

@ -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": {

View File

@ -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,

View File

@ -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

View File

@ -0,0 +1,322 @@
import { QueryInterface, DataTypes } from 'sequelize';
import { Sequelize } from 'sequelize';
export async function up(queryInterface: QueryInterface): Promise<void> {
// 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<void> {
// 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');
}

442
src/models/Dealer.ts Normal file
View File

@ -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<DealerAttributes, 'dealerId' | 'isActive' | 'createdAt' | 'updatedAt'> {}
class Dealer extends Model<DealerAttributes, DealerCreationAttributes> 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 };

View File

@ -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

View File

@ -133,6 +133,7 @@ async function runMigrations(): Promise<void> {
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<void> {
{ 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<void> {
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:');

View File

@ -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 },
];
/**

View File

@ -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<void> {
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 };

View File

@ -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 = `

View File

@ -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({