diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 793218f..b0aa044 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -45,6 +45,7 @@ enum FieldType { NUMBER // Number input DATE // Date picker TAG_INPUT // Tag input (add custom tags) + FILE // File upload (documents, images) } diff --git a/prisma/seed.ts b/prisma/seed.ts index e7b1051..41a6276 100644 --- a/prisma/seed.ts +++ b/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(); diff --git a/src/profile-fields/profile-sections.controller.ts b/src/profile-fields/profile-sections.controller.ts index c2757d1..5e5a553 100644 --- a/src/profile-fields/profile-sections.controller.ts +++ b/src/profile-fields/profile-sections.controller.ts @@ -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); + } } diff --git a/src/profile-fields/profile-sections.service.ts b/src/profile-fields/profile-sections.service.ts index c773de9..3c5a40f 100644 --- a/src/profile-fields/profile-sections.service.ts +++ b/src/profile-fields/profile-sections.service.ts @@ -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, }; } }