feat: Introduce new profile sections and fields with detailed configurations and validation rules.
This commit is contained in:
@@ -45,6 +45,7 @@ enum FieldType {
|
||||
NUMBER // Number input
|
||||
DATE // Date picker
|
||||
TAG_INPUT // Tag input (add custom tags)
|
||||
FILE // File upload (documents, images)
|
||||
}
|
||||
|
||||
|
||||
|
||||
884
prisma/seed.ts
884
prisma/seed.ts
@@ -124,6 +124,794 @@ async function main() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Location',
|
||||
slug: 'location',
|
||||
description: 'Service areas and location information',
|
||||
icon: 'map-pin',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isGlobal: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'State',
|
||||
slug: 'state',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'States where you provide services',
|
||||
placeholder: 'Select states',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Alabama', value: 'AL' },
|
||||
{ label: 'Alaska', value: 'AK' },
|
||||
{ label: 'Arizona', value: 'AZ' },
|
||||
{ label: 'Arkansas', value: 'AR' },
|
||||
{ label: 'California', value: 'CA' },
|
||||
{ label: 'Colorado', value: 'CO' },
|
||||
{ label: 'Connecticut', value: 'CT' },
|
||||
{ label: 'Delaware', value: 'DE' },
|
||||
{ label: 'Florida', value: 'FL' },
|
||||
{ label: 'Georgia', value: 'GA' },
|
||||
{ label: 'Hawaii', value: 'HI' },
|
||||
{ label: 'Idaho', value: 'ID' },
|
||||
{ label: 'Illinois', value: 'IL' },
|
||||
{ label: 'Indiana', value: 'IN' },
|
||||
{ label: 'Iowa', value: 'IA' },
|
||||
{ label: 'Kansas', value: 'KS' },
|
||||
{ label: 'Kentucky', value: 'KY' },
|
||||
{ label: 'Louisiana', value: 'LA' },
|
||||
{ label: 'Maine', value: 'ME' },
|
||||
{ label: 'Maryland', value: 'MD' },
|
||||
{ label: 'Massachusetts', value: 'MA' },
|
||||
{ label: 'Michigan', value: 'MI' },
|
||||
{ label: 'Minnesota', value: 'MN' },
|
||||
{ label: 'Mississippi', value: 'MS' },
|
||||
{ label: 'Missouri', value: 'MO' },
|
||||
{ label: 'Montana', value: 'MT' },
|
||||
{ label: 'Nebraska', value: 'NE' },
|
||||
{ label: 'Nevada', value: 'NV' },
|
||||
{ label: 'New Hampshire', value: 'NH' },
|
||||
{ label: 'New Jersey', value: 'NJ' },
|
||||
{ label: 'New Mexico', value: 'NM' },
|
||||
{ label: 'New York', value: 'NY' },
|
||||
{ label: 'North Carolina', value: 'NC' },
|
||||
{ label: 'North Dakota', value: 'ND' },
|
||||
{ label: 'Ohio', value: 'OH' },
|
||||
{ label: 'Oklahoma', value: 'OK' },
|
||||
{ label: 'Oregon', value: 'OR' },
|
||||
{ label: 'Pennsylvania', value: 'PA' },
|
||||
{ label: 'Rhode Island', value: 'RI' },
|
||||
{ label: 'South Carolina', value: 'SC' },
|
||||
{ label: 'South Dakota', value: 'SD' },
|
||||
{ label: 'Tennessee', value: 'TN' },
|
||||
{ label: 'Texas', value: 'TX' },
|
||||
{ label: 'Utah', value: 'UT' },
|
||||
{ label: 'Vermont', value: 'VT' },
|
||||
{ label: 'Virginia', value: 'VA' },
|
||||
{ label: 'Washington', value: 'WA' },
|
||||
{ label: 'West Virginia', value: 'WV' },
|
||||
{ label: 'Wisconsin', value: 'WI' },
|
||||
{ label: 'Wyoming', value: 'WY' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'City',
|
||||
slug: 'city',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Cities where you provide services (max 5)',
|
||||
placeholder: 'Enter cities',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
validation: {
|
||||
maxSelections: 5,
|
||||
},
|
||||
uiConfig: {
|
||||
allowCustomOptions: true,
|
||||
maxSelections: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Upload Documents',
|
||||
slug: 'upload-documents',
|
||||
description: 'Upload licensing credentials and verification documents',
|
||||
icon: 'document',
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
isGlobal: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'Documents',
|
||||
slug: 'documents',
|
||||
fieldType: FieldType.FILE,
|
||||
description: 'Upload documentation or verification for licensing credentials',
|
||||
placeholder: 'Drag and drop files here or click to upload',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
validation: {
|
||||
allowedTypes: ['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png'],
|
||||
maxFileSize: 10485760, // 10MB
|
||||
maxFiles: 10,
|
||||
},
|
||||
uiConfig: {
|
||||
acceptedFormats: '.pdf, .doc, .docx, .jpg, .png',
|
||||
showPreview: true,
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Certification',
|
||||
slug: 'certification',
|
||||
description: 'Professional certifications and credentials',
|
||||
icon: 'star',
|
||||
sortOrder: 5,
|
||||
isActive: true,
|
||||
isGlobal: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'Certification Name',
|
||||
slug: 'certification_name',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Name of your professional certification',
|
||||
placeholder: 'Enter certification name',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Issuing Organization',
|
||||
slug: 'issuing_organization',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Organization that issued the certification',
|
||||
placeholder: 'Enter issuing organization',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Issue Date',
|
||||
slug: 'issue_date',
|
||||
fieldType: FieldType.DATE,
|
||||
description: 'Date the certification was issued',
|
||||
placeholder: 'Select issue date',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Expiration Date',
|
||||
slug: 'expiration_date',
|
||||
fieldType: FieldType.DATE,
|
||||
description: 'Date the certification expires (if applicable)',
|
||||
placeholder: 'Select expiration date',
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Certification Number',
|
||||
slug: 'certification_number',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Your certification or license number',
|
||||
placeholder: 'Enter certification number',
|
||||
sortOrder: 5,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Demographic',
|
||||
slug: 'demographic',
|
||||
description: 'Demographic and contact information',
|
||||
icon: 'user',
|
||||
sortOrder: 6,
|
||||
isActive: true,
|
||||
isGlobal: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'Name',
|
||||
slug: 'demographic_name',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Your full name',
|
||||
placeholder: 'Enter your name',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Office Name',
|
||||
slug: 'office_name',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Name of your office or brokerage',
|
||||
placeholder: 'Enter office name',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Registered / Licensed',
|
||||
slug: 'registered_licensed',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'Are you registered or licensed?',
|
||||
placeholder: 'Select status',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Registered', value: 'registered' },
|
||||
{ label: 'Licensed', value: 'licensed' },
|
||||
{ label: 'Both', value: 'both' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Office #',
|
||||
slug: 'office_number',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Your office phone number',
|
||||
placeholder: 'Enter office number',
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Cell #',
|
||||
slug: 'cell_number',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Your cell phone number',
|
||||
placeholder: 'Enter cell number',
|
||||
sortOrder: 5,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
},
|
||||
{
|
||||
name: 'Availability',
|
||||
slug: 'availability',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'When are you available?',
|
||||
placeholder: 'Select availability',
|
||||
sortOrder: 6,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'M-F 9-5', value: 'mf_9_5' },
|
||||
{ label: 'Evenings', value: 'evenings' },
|
||||
{ label: 'Weekends', value: 'weekends' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
slug: 'age',
|
||||
fieldType: FieldType.RANGE,
|
||||
description: 'Your age (used for search matching only, not displayed on profile)',
|
||||
placeholder: 'Select age range',
|
||||
sortOrder: 7,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: true,
|
||||
rangeConfig: {
|
||||
min: 18,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Gender',
|
||||
slug: 'gender',
|
||||
fieldType: FieldType.TEXT,
|
||||
description: 'Your gender (used for search matching only, not displayed on profile)',
|
||||
placeholder: 'Enter gender',
|
||||
sortOrder: 8,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: true,
|
||||
},
|
||||
{
|
||||
name: 'Race',
|
||||
slug: 'race',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Your race (used for search matching only, not displayed on profile)',
|
||||
placeholder: 'Select race',
|
||||
sortOrder: 9,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: true,
|
||||
options: [
|
||||
{ label: 'American Indian or Alaska Native', value: 'american_indian_alaska_native' },
|
||||
{ label: 'Asian', value: 'asian' },
|
||||
{ label: 'Black or African American', value: 'black_african_american' },
|
||||
{ label: 'Native Hawaiian or Other Pacific Islander', value: 'native_hawaiian_pacific_islander' },
|
||||
{ label: 'White', value: 'white' },
|
||||
{ label: 'Two or More Races', value: 'two_or_more' },
|
||||
{ label: 'Prefer not to say', value: 'prefer_not_to_say' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ethnicity',
|
||||
slug: 'ethnicity',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Your ethnicity (used for search matching only, not displayed on profile)',
|
||||
placeholder: 'Select ethnicity',
|
||||
sortOrder: 10,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: true,
|
||||
options: [
|
||||
{ label: 'Hispanic or Latino', value: 'hispanic_latino' },
|
||||
{ label: 'Not Hispanic or Latino', value: 'not_hispanic_latino' },
|
||||
{ label: 'Prefer not to say', value: 'prefer_not_to_say' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Language',
|
||||
slug: 'language',
|
||||
fieldType: FieldType.TAG_INPUT,
|
||||
description: 'Languages you speak',
|
||||
placeholder: 'Add languages',
|
||||
sortOrder: 11,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
uiConfig: {
|
||||
allowCustomOptions: true,
|
||||
suggestions: ['English', 'Spanish', 'Chinese', 'French', 'Vietnamese', 'Korean', 'Tagalog', 'Arabic', 'Russian', 'Portuguese'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'LGBTQ+',
|
||||
slug: 'lgbtq',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'LGBTQ+ identification (used for search matching only, not displayed on profile)',
|
||||
placeholder: 'Select option',
|
||||
sortOrder: 12,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: true,
|
||||
options: [
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{ label: 'No', value: 'no' },
|
||||
{ label: 'Prefer not to say', value: 'prefer_not_to_say' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Highest Education Level',
|
||||
slug: 'highest_education_level',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'Your highest level of education',
|
||||
placeholder: 'Select education level',
|
||||
sortOrder: 13,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'High School/GED', value: 'high_school_ged' },
|
||||
{ label: 'Some College', value: 'some_college' },
|
||||
{ label: 'Associates', value: 'associates' },
|
||||
{ label: 'Bachelors', value: 'bachelors' },
|
||||
{ label: 'Masters', value: 'masters' },
|
||||
{ label: 'Doctoral', value: 'doctoral' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Veteran Status',
|
||||
slug: 'veteran_status',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Your military/veteran status',
|
||||
placeholder: 'Select veteran status',
|
||||
sortOrder: 14,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Reserve', value: 'reserve' },
|
||||
{ label: 'Guard', value: 'guard' },
|
||||
{ label: 'Veteran', value: 'veteran' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Military Branch',
|
||||
slug: 'military_branch',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Military branch(es) you served in',
|
||||
placeholder: 'Select military branch',
|
||||
sortOrder: 15,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Army', value: 'army' },
|
||||
{ label: 'Navy', value: 'navy' },
|
||||
{ label: 'Marines', value: 'marines' },
|
||||
{ label: 'Air Force', value: 'air_force' },
|
||||
{ label: 'Space Force', value: 'space_force' },
|
||||
{ label: 'Coast Guard', value: 'coast_guard' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Agent-type-specific sections (NOT global)
|
||||
const agentTypeSections = [
|
||||
{
|
||||
agentTypeName: 'Professional',
|
||||
section: {
|
||||
name: 'Experience',
|
||||
slug: 'professional-experience',
|
||||
description: 'Professional experience and track record for real estate agents',
|
||||
icon: 'star',
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
isGlobal: false,
|
||||
isSystem: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'Years in Business',
|
||||
slug: 'years_in_business',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'How many years have you been in real estate?',
|
||||
placeholder: 'Select years',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Less than 2 years', value: '<2' },
|
||||
{ label: '2+ years', value: '2+' },
|
||||
{ label: '5+ years', value: '5+' },
|
||||
{ label: '10+ years', value: '10+' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Years Operated in Area',
|
||||
slug: 'years_in_area',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'How many years have you operated in your service area?',
|
||||
placeholder: 'Select years',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Less than 2 years', value: '<2' },
|
||||
{ label: '2+ years', value: '2+' },
|
||||
{ label: '5+ years', value: '5+' },
|
||||
{ label: '10+ years', value: '10+' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Number of Contracts Completed',
|
||||
slug: 'contracts_completed',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'Number of contracts completed in the previous year',
|
||||
placeholder: 'Select range',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Less than 3', value: '<3' },
|
||||
{ label: '3-10', value: '3-10' },
|
||||
{ label: '10-20', value: '10-20' },
|
||||
{ label: '20-50', value: '20-50' },
|
||||
{ label: '50+', value: '50+' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
agentTypeName: 'Professional',
|
||||
section: {
|
||||
name: 'Specialization',
|
||||
slug: 'professional-specialization',
|
||||
description: 'Areas of specialization for real estate agents',
|
||||
icon: 'briefcase',
|
||||
sortOrder: 5,
|
||||
isActive: true,
|
||||
isGlobal: false,
|
||||
isSystem: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'Client Specialization',
|
||||
slug: 'client_specialization',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Types of clients you specialize in',
|
||||
placeholder: 'Select client types',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Buyers', value: 'buyers' },
|
||||
{ label: 'Sellers', value: 'sellers' },
|
||||
{ label: 'First Time Buyer', value: 'first_time_buyer' },
|
||||
{ label: 'First Time Seller', value: 'first_time_seller' },
|
||||
{ label: 'Divorce', value: 'divorce' },
|
||||
{ label: 'Estate', value: 'estate' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Price Point',
|
||||
slug: 'price_point',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Price ranges you typically work with',
|
||||
placeholder: 'Select price ranges',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Under $100K', value: '<100K' },
|
||||
{ label: '$100K - $300K', value: '100-300K' },
|
||||
{ label: '$300K - $500K', value: '300-500K' },
|
||||
{ label: '$500K - $700K', value: '500-700K' },
|
||||
{ label: '$700K - $1M', value: '700K-1M' },
|
||||
{ label: '$1M+', value: '1M+' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Property Type',
|
||||
slug: 'property_type',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Types of properties you specialize in',
|
||||
placeholder: 'Select property types',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'SFR (Single Family Residence)', value: 'sfr' },
|
||||
{ label: 'Townhome', value: 'townhome' },
|
||||
{ label: 'Condo', value: 'condo' },
|
||||
{ label: 'Manufactured', value: 'manufactured' },
|
||||
{ label: 'Modular', value: 'modular' },
|
||||
{ label: 'Urban', value: 'urban' },
|
||||
{ label: 'Rural', value: 'rural' },
|
||||
{ label: 'Luxury', value: 'luxury' },
|
||||
{ label: 'Retirement', value: 'retirement' },
|
||||
{ label: 'Agriculture', value: 'agriculture' },
|
||||
{ label: 'Commercial', value: 'commercial' },
|
||||
{ label: 'Investment / Income Producing', value: 'investment' },
|
||||
{ label: 'Historical', value: 'historical' },
|
||||
{ label: 'Mountain Property', value: 'mountain' },
|
||||
{ label: 'Undeveloped Land', value: 'undeveloped_land' },
|
||||
{ label: 'Empty Lots', value: 'empty_lots' },
|
||||
{ label: 'Barndaminium', value: 'barndaminium' },
|
||||
{ label: 'Unique Properties', value: 'unique' },
|
||||
{ label: 'Apartment / Multifamily', value: 'apartment_multifamily' },
|
||||
{ label: 'Duplex / Tri-plex / 4-plex', value: 'multiplex' },
|
||||
{ label: 'New Build', value: 'new_build' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Loan Type',
|
||||
slug: 'loan_type',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Loan types you are familiar with',
|
||||
placeholder: 'Select loan types',
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Conventional', value: 'conventional' },
|
||||
{ label: 'FHA', value: 'fha' },
|
||||
{ label: 'VA', value: 'va' },
|
||||
{ label: 'USDA', value: 'usda' },
|
||||
{ label: 'Downpayment Assistance Programs', value: 'downpayment_assistance' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Special Interests and Hobbies',
|
||||
slug: 'special_interests',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Special property features you have expertise in',
|
||||
placeholder: 'Select special interests',
|
||||
sortOrder: 5,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Workshop / Trade / Craftspace', value: 'workshop' },
|
||||
{ label: 'Boating', value: 'boating' },
|
||||
{ label: 'Automotive', value: 'automotive' },
|
||||
{ label: 'Aviation', value: 'aviation' },
|
||||
{ label: 'Home Theater', value: 'home_theater' },
|
||||
{ label: 'Horses', value: 'horses' },
|
||||
{ label: 'RV', value: 'rv' },
|
||||
{ label: 'Smart Home Features', value: 'smart_home' },
|
||||
{ label: 'Gardening / Landscape', value: 'gardening' },
|
||||
{ label: 'Hobby Farm', value: 'hobby_farm' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
agentTypeName: 'Lender',
|
||||
section: {
|
||||
name: 'Experience',
|
||||
slug: 'lender-experience',
|
||||
description: 'Professional experience and track record for lenders',
|
||||
icon: 'star',
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
isGlobal: false,
|
||||
isSystem: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'Years in Business',
|
||||
slug: 'years_in_business',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'How many years have you been in lending?',
|
||||
placeholder: 'Select years',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Less than 2 years', value: '<2' },
|
||||
{ label: '2+ years', value: '2+' },
|
||||
{ label: '5+ years', value: '5+' },
|
||||
{ label: '10+ years', value: '10+' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Years Operated in Area',
|
||||
slug: 'years_in_area',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'How many years have you operated in your service area?',
|
||||
placeholder: 'Select years',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Less than 2 years', value: '<2' },
|
||||
{ label: '2+ years', value: '2+' },
|
||||
{ label: '5+ years', value: '5+' },
|
||||
{ label: '10+ years', value: '10+' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Annual Loans',
|
||||
slug: 'annual_loans',
|
||||
fieldType: FieldType.RADIO,
|
||||
description: 'Total value of loans processed annually',
|
||||
placeholder: 'Select range',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: '0-1M', value: '0-1M' },
|
||||
{ label: '1-5M', value: '1-5M' },
|
||||
{ label: '5-10M', value: '5-10M' },
|
||||
{ label: '10-30M', value: '10-30M' },
|
||||
{ label: '30M+', value: '30M+' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
agentTypeName: 'Lender',
|
||||
section: {
|
||||
name: 'Specialization',
|
||||
slug: 'lender-specialization',
|
||||
description: 'Areas of specialization for lenders',
|
||||
icon: 'briefcase',
|
||||
sortOrder: 5,
|
||||
isActive: true,
|
||||
isGlobal: false,
|
||||
isSystem: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'Client Specialization',
|
||||
slug: 'client_specialization',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Types of clients you specialize in',
|
||||
placeholder: 'Select client types',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'First Time Buyer', value: 'first_time_buyer' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Property Type',
|
||||
slug: 'property_type',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Types of properties you can finance',
|
||||
placeholder: 'Select property types',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'SFR (Single Family Residence)', value: 'sfr' },
|
||||
{ label: 'Townhome', value: 'townhome' },
|
||||
{ label: 'Condo', value: 'condo' },
|
||||
{ label: 'Manufactured', value: 'manufactured' },
|
||||
{ label: 'Modular', value: 'modular' },
|
||||
{ label: 'Urban', value: 'urban' },
|
||||
{ label: 'Rural', value: 'rural' },
|
||||
{ label: 'Luxury', value: 'luxury' },
|
||||
{ label: 'Retirement', value: 'retirement' },
|
||||
{ label: 'Agriculture', value: 'agriculture' },
|
||||
{ label: 'Commercial', value: 'commercial' },
|
||||
{ label: 'Investment / Income Producing', value: 'investment' },
|
||||
{ label: 'Undeveloped Land', value: 'undeveloped_land' },
|
||||
{ label: 'Construction / Construction-to-Perm', value: 'construction' },
|
||||
{ label: 'Empty Lots', value: 'empty_lots' },
|
||||
{ label: 'Barndaminium', value: 'barndaminium' },
|
||||
{ label: 'Unique Properties', value: 'unique' },
|
||||
{ label: 'Apartment / Multifamily', value: 'apartment_multifamily' },
|
||||
{ label: 'Duplex / Tri-plex / 4-plex', value: 'multiplex' },
|
||||
{ label: 'New Build', value: 'new_build' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Loan Type',
|
||||
slug: 'loan_type',
|
||||
fieldType: FieldType.MULTI_SELECT,
|
||||
description: 'Types of loans you offer',
|
||||
placeholder: 'Select loan types',
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
isRequired: false,
|
||||
isSearchableOnly: false,
|
||||
options: [
|
||||
{ label: 'Conventional', value: 'conventional' },
|
||||
{ label: 'FHA', value: 'fha' },
|
||||
{ label: 'VA', value: 'va' },
|
||||
{ label: 'USDA', value: 'usda' },
|
||||
{ label: 'Downpayment Assistance Programs', value: 'downpayment_assistance' },
|
||||
{ label: 'Rehab Loan - FHA 203k', value: 'rehab_fha_203k' },
|
||||
{ label: 'Rehab Loan - Conventional Home Style', value: 'rehab_conventional' },
|
||||
{ label: 'Rehab Loan - VA Reno', value: 'rehab_va_reno' },
|
||||
{ label: 'Debt Service Cover Ratio (DSCR)', value: 'dscr' },
|
||||
{ label: 'HELOC', value: 'heloc' },
|
||||
{ label: 'Standalone Second', value: 'standalone_second' },
|
||||
{ label: 'Refinancing - FHA Streamline', value: 'refi_fha_streamline' },
|
||||
{ label: 'Refinancing - VA IRRL', value: 'refi_va_irrl' },
|
||||
{ label: 'Refinancing - Conventional', value: 'refi_conventional' },
|
||||
{ label: 'Jumbo / Non-conforming', value: 'jumbo' },
|
||||
{ label: 'Bank Statement', value: 'bank_statement' },
|
||||
{ label: 'ITIN', value: 'itin' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const sectionData of defaultSections) {
|
||||
@@ -172,6 +960,100 @@ async function main() {
|
||||
|
||||
console.log('');
|
||||
|
||||
// =============================================
|
||||
// Seed Agent-Type-Specific Sections
|
||||
// =============================================
|
||||
console.log('📋 Seeding Agent-Type-Specific Sections...');
|
||||
|
||||
// First, delete old global Experience section if exists
|
||||
const oldExperienceSection = await prisma.profileSection.findUnique({
|
||||
where: { slug: 'experience' },
|
||||
});
|
||||
if (oldExperienceSection) {
|
||||
await prisma.profileSection.delete({
|
||||
where: { slug: 'experience' },
|
||||
});
|
||||
console.log(' 🗑️ Deleted old global Experience section');
|
||||
}
|
||||
|
||||
for (const { agentTypeName, section: sectionData } of agentTypeSections) {
|
||||
const { fields, ...sectionInfo } = sectionData;
|
||||
|
||||
// Find the agent type
|
||||
const agentType = await prisma.agentType.findUnique({
|
||||
where: { name: agentTypeName },
|
||||
});
|
||||
|
||||
if (!agentType) {
|
||||
console.log(` ⚠️ Agent type "${agentTypeName}" not found, skipping section`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if section already exists
|
||||
let section = await prisma.profileSection.findUnique({
|
||||
where: { slug: sectionInfo.slug },
|
||||
});
|
||||
|
||||
if (section) {
|
||||
console.log(` ⚠️ Section "${sectionInfo.name}" for ${agentTypeName} already exists`);
|
||||
|
||||
// Still check and create missing fields
|
||||
for (const fieldData of fields) {
|
||||
const existingField = await prisma.profileField.findFirst({
|
||||
where: {
|
||||
sectionId: section.id,
|
||||
slug: fieldData.slug,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingField) {
|
||||
await prisma.profileField.create({
|
||||
data: {
|
||||
...fieldData,
|
||||
sectionId: section.id,
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Created field: ${fieldData.name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create section with fields
|
||||
section = await prisma.profileSection.create({
|
||||
data: {
|
||||
...sectionInfo,
|
||||
fields: {
|
||||
create: fields,
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Created section: ${sectionInfo.name} for ${agentTypeName} with ${fields.length} fields`);
|
||||
}
|
||||
|
||||
// Create AgentTypeSection assignment if not exists
|
||||
const existingAssignment = await prisma.agentTypeSection.findUnique({
|
||||
where: {
|
||||
agentTypeId_sectionId: {
|
||||
agentTypeId: agentType.id,
|
||||
sectionId: section.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingAssignment) {
|
||||
await prisma.agentTypeSection.create({
|
||||
data: {
|
||||
agentTypeId: agentType.id,
|
||||
sectionId: section.id,
|
||||
sortOrder: sectionInfo.sortOrder,
|
||||
isRequired: false,
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Assigned section to ${agentTypeName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// =============================================
|
||||
// Seed Admin User
|
||||
// =============================================
|
||||
@@ -222,6 +1104,7 @@ async function main() {
|
||||
const sectionCount = await prisma.profileSection.count();
|
||||
const fieldCount = await prisma.profileField.count();
|
||||
const systemSectionCount = await prisma.profileSection.count({ where: { isSystem: true } });
|
||||
const agentTypeSectionCount = await prisma.agentTypeSection.count();
|
||||
|
||||
console.log('📊 Database Summary:');
|
||||
console.log(` Total Users: ${userCount}`);
|
||||
@@ -229,6 +1112,7 @@ async function main() {
|
||||
console.log(` Agent Types: ${agentTypeCount}`);
|
||||
console.log(` Profile Sections: ${sectionCount} (${systemSectionCount} system)`);
|
||||
console.log(` Profile Fields: ${fieldCount}`);
|
||||
console.log(` Section Assignments: ${agentTypeSectionCount}`);
|
||||
console.log('\n🌱 Seeding completed!\n');
|
||||
|
||||
await prisma.$disconnect();
|
||||
|
||||
@@ -108,4 +108,29 @@ export class ProfileSectionsController {
|
||||
) {
|
||||
return this.sectionsService.removeFromAgentType(id, agentTypeId);
|
||||
}
|
||||
|
||||
@Get('agent-type/:agentTypeId')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get all sections for an agent type with ordering (Admin only)' })
|
||||
@ApiQuery({ name: 'includeFields', required: false, type: Boolean })
|
||||
getSectionsForAgentType(
|
||||
@Param('agentTypeId', ParseUUIDPipe) agentTypeId: string,
|
||||
@Query('includeFields') includeFields?: string,
|
||||
) {
|
||||
return this.sectionsService.getSectionsForAgentType(agentTypeId, includeFields !== 'false');
|
||||
}
|
||||
|
||||
@Patch('agent-type/:agentTypeId/order')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update section ordering for an agent type (Admin only)' })
|
||||
updateSectionOrderForAgentType(
|
||||
@Param('agentTypeId', ParseUUIDPipe) agentTypeId: string,
|
||||
@Body() body: { sectionOrders: { sectionId: string; sortOrder: number; isRequired?: boolean }[] },
|
||||
) {
|
||||
return this.sectionsService.updateSectionOrderForAgentType(agentTypeId, body.sectionOrders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ export class ProfileSectionsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sections for a specific agent type
|
||||
* Get all sections for a specific agent type with custom ordering support
|
||||
*/
|
||||
async getSectionsForAgentType(agentTypeId: string, includeFields = true) {
|
||||
const agentType = await this.prisma.agentType.findUnique({
|
||||
@@ -267,10 +267,9 @@ export class ProfileSectionsService {
|
||||
throw new NotFoundException(`Agent type with ID ${agentTypeId} not found`);
|
||||
}
|
||||
|
||||
// Get assigned sections
|
||||
// Get ALL AgentTypeSection entries for this agent type (including global sections)
|
||||
const assignments = await this.prisma.agentTypeSection.findMany({
|
||||
where: { agentTypeId },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
include: {
|
||||
section: {
|
||||
include: includeFields
|
||||
@@ -285,7 +284,10 @@ export class ProfileSectionsService {
|
||||
},
|
||||
});
|
||||
|
||||
// Also get global sections
|
||||
// Create a map of section IDs that have custom assignments
|
||||
const assignmentMap = new Map(assignments.map((a) => [a.sectionId, a]));
|
||||
|
||||
// Get all global sections
|
||||
const globalSections = await this.prisma.profileSection.findMany({
|
||||
where: {
|
||||
isGlobal: true,
|
||||
@@ -299,19 +301,104 @@ export class ProfileSectionsService {
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
|
||||
// Combine all sections with their effective sort order
|
||||
const allSections: any[] = [];
|
||||
|
||||
// Add global sections (with custom order if assigned, otherwise use default)
|
||||
for (const section of globalSections) {
|
||||
const assignment = assignmentMap.get(section.id);
|
||||
allSections.push({
|
||||
...section,
|
||||
isRequired: assignment?.isRequired ?? false,
|
||||
isGlobal: true,
|
||||
effectiveSortOrder: assignment?.sortOrder ?? section.sortOrder,
|
||||
hasCustomOrder: !!assignment,
|
||||
});
|
||||
}
|
||||
|
||||
// Add non-global assigned sections
|
||||
for (const assignment of assignments) {
|
||||
if (!assignment.section.isGlobal) {
|
||||
allSections.push({
|
||||
...assignment.section,
|
||||
isRequired: assignment.isRequired,
|
||||
isGlobal: false,
|
||||
effectiveSortOrder: assignment.sortOrder,
|
||||
hasCustomOrder: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by effective sort order
|
||||
allSections.sort((a, b) => a.effectiveSortOrder - b.effectiveSortOrder);
|
||||
|
||||
return {
|
||||
agentType,
|
||||
sections: [
|
||||
...globalSections.map((s) => ({ ...s, isRequired: false, isGlobal: true })),
|
||||
...assignments.map((a) => ({
|
||||
...a.section,
|
||||
isRequired: a.isRequired,
|
||||
isGlobal: false,
|
||||
})),
|
||||
],
|
||||
sections: allSections,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update section ordering for a specific agent type
|
||||
* Creates or updates AgentTypeSection entries with custom sortOrder
|
||||
*/
|
||||
async updateSectionOrderForAgentType(
|
||||
agentTypeId: string,
|
||||
sectionOrders: { sectionId: string; sortOrder: number; isRequired?: boolean }[],
|
||||
) {
|
||||
// Verify agent type exists
|
||||
const agentType = await this.prisma.agentType.findUnique({
|
||||
where: { id: agentTypeId },
|
||||
});
|
||||
|
||||
if (!agentType) {
|
||||
throw new NotFoundException(`Agent type with ID ${agentTypeId} not found`);
|
||||
}
|
||||
|
||||
// Process each section order update
|
||||
const results = await Promise.all(
|
||||
sectionOrders.map(async ({ sectionId, sortOrder, isRequired }) => {
|
||||
// Check if assignment exists
|
||||
const existing = await this.prisma.agentTypeSection.findUnique({
|
||||
where: {
|
||||
agentTypeId_sectionId: {
|
||||
agentTypeId,
|
||||
sectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update existing assignment
|
||||
return this.prisma.agentTypeSection.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
sortOrder,
|
||||
...(isRequired !== undefined && { isRequired }),
|
||||
},
|
||||
include: { section: true },
|
||||
});
|
||||
} else {
|
||||
// Create new assignment (for global sections that need custom ordering)
|
||||
return this.prisma.agentTypeSection.create({
|
||||
data: {
|
||||
agentTypeId,
|
||||
sectionId,
|
||||
sortOrder,
|
||||
isRequired: isRequired ?? false,
|
||||
},
|
||||
include: { section: true },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updated: results.length,
|
||||
assignments: results,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user