Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions app/discover/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import PropertyMapWithRents from '@/components/shared/PropertyMapWithRents';
import dynamic from 'next/dynamic';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useAlert } from '@/components/shared/AlertModal';
import { getPropertyType } from '@/lib/propertyTypeOverrides';


// Extend Window interface for address validation timeout
Expand Down Expand Up @@ -191,7 +192,7 @@ function DiscoverPageContent() {
},
body: JSON.stringify({
...lastSearchFilters,
property_type: "MFR",
property_type: getPropertyType(lastSearchFilters?.state, lastSearchFilters?.county),
size: 12 * (currentPage - Math.floor(searchResults.length / PROPERTIES_PER_PAGE)),
resultIndex: nextResultIndex,
userId: user?.id || null // Add user ID for search tracking
Expand Down Expand Up @@ -798,12 +799,15 @@ function DiscoverPageContent() {
searchFilters.state = stateZipMatch[1].toUpperCase();
searchFilters.zip = stateZipMatch[2] || (hasZip ? zipMatch[1] : '');
} else {
searchFilters.state = statePart.toUpperCase();
// Convert full state name to abbreviation if possible
const statePartUpper = statePart.toUpperCase();
searchFilters.state = US_STATES[statePartUpper as keyof typeof US_STATES] || statePartUpper;
searchFilters.zip = hasZip ? zipMatch[1] : '';
}
} else {
// Fallback for unusual formats
searchFilters.state = lastPart.toUpperCase();
const lastPartUpper = lastPart.toUpperCase();
searchFilters.state = US_STATES[lastPartUpper as keyof typeof US_STATES] || lastPartUpper;
searchFilters.zip = hasZip ? zipMatch[1] : '';
}
} else if (locationParts.length >= 2) {
Expand Down Expand Up @@ -831,7 +835,8 @@ function DiscoverPageContent() {
searchFilters.state = stateZipMatch[1].toUpperCase();
searchFilters.zip = stateZipMatch[2] || (hasZip ? zipMatch[1] : '');
} else {
searchFilters.state = lastPart.toUpperCase();
const lastPartUpper = lastPart.toUpperCase();
searchFilters.state = US_STATES[lastPartUpper as keyof typeof US_STATES] || lastPartUpper;
searchFilters.zip = hasZip ? zipMatch[1] : '';
}
} else {
Expand All @@ -843,8 +848,9 @@ function DiscoverPageContent() {
searchFilters.state = stateZipMatch[1].toUpperCase();
searchFilters.zip = stateZipMatch[2] || (hasZip ? zipMatch[1] : '');
} else {
// Assume whole part is state and uppercase it
searchFilters.state = lastPart.toUpperCase();
// Convert full state name to abbreviation if possible
const lastPartUpper = lastPart.toUpperCase();
searchFilters.state = US_STATES[lastPartUpper as keyof typeof US_STATES] || lastPartUpper;
searchFilters.zip = hasZip ? zipMatch[1] : '';
}
}
Expand Down Expand Up @@ -878,7 +884,7 @@ function DiscoverPageContent() {

const searchPayload = {
...searchFilters,
property_type: "MFR", // Only multifamily properties
property_type: getPropertyType(searchFilters.state, searchFilters.county),
size: 12, // Load 12 at a time for pagination
resultIndex: 0,
userId: user?.id || null // Add user ID for search tracking
Expand Down Expand Up @@ -1043,7 +1049,7 @@ function DiscoverPageContent() {
const { apiFields } = savedSearch.criteria;

let apiParams: any = {
property_type: "MFR", // Fixed: was propertyType (camelCase), should be property_type (snake_case)
property_type: getPropertyType(savedSearch.criteria?.state, savedSearch.criteria?.county),
count: false,
size: 12,
resultIndex: 0,
Expand Down Expand Up @@ -1712,7 +1718,7 @@ function DiscoverPageContent() {

// Build API params with saved location and filters
let apiParams: any = {
property_type: "MFR",
property_type: getPropertyType(search.filters?.state, search.filters?.county),
count: false,
size: 12,
resultIndex: 0,
Expand Down
128 changes: 125 additions & 3 deletions components/shared/property-details/AIInvestmentAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
*/
'use client';

import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Bot, ChevronDown, ChevronUp, Calculator } from 'lucide-react';
import { Bot, ChevronDown, ChevronUp, Calculator, RefreshCw, Clock } from 'lucide-react';
import { hasAccess } from '@/lib/v2/accessControl';
import type { UserClass } from '@/lib/v2/accessControl';
import { StandardModalWithActions } from '@/components/shared/StandardModal';
import { createSupabaseBrowserClient } from '@/lib/supabase/client';

interface PropertyData {
id?: string;
Expand Down Expand Up @@ -73,6 +74,55 @@ export function AIInvestmentAnalysis({ property, isEngageContext, userClass }: A
const [analysisError, setAnalysisError] = useState<string | null>(null);
const [investmentAnalysis, setInvestmentAnalysis] = useState<AnalysisResult | null>(null);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);

// NEW: Cached analysis state for persistent storage
const [cachedAnalysis, setCachedAnalysis] = useState<{
data: AnalysisResult;
generated_at: string;
} | null>(null);
const [isLoadingCache, setIsLoadingCache] = useState(false);

// NEW: Load cached analysis on component mount (engage context only)
useEffect(() => {
const loadCachedAnalysis = async () => {
if (!isEngageContext || !property?.id) return;

setIsLoadingCache(true);
try {
const supabase = createSupabaseBrowserClient();

const { data, error } = await supabase
.from('saved_properties')
.select('investment_analysis_data, investment_analysis_generated_at')
.eq('property_id', property.id)
.single();

if (error) {
// Property not found in saved_properties table, that's okay
if (error.code !== 'PGRST116') { // PGRST116 = not found
console.error('Error loading cached analysis:', error);
}
return;
}

// If cached analysis exists, set it
if (data?.investment_analysis_data && data?.investment_analysis_generated_at) {
const cachedData = {
data: data.investment_analysis_data,
generated_at: data.investment_analysis_generated_at
};
setCachedAnalysis(cachedData);
setInvestmentAnalysis(data.investment_analysis_data);
}
} catch (error) {
console.error('Failed to load cached analysis:', error);
} finally {
setIsLoadingCache(false);
}
};

loadCachedAnalysis();
}, [isEngageContext, property?.id]);

const handleAnalyzeProperty = async () => {
setIsAnalyzing(true);
Expand Down Expand Up @@ -112,6 +162,40 @@ export function AIInvestmentAnalysis({ property, isEngageContext, userClass }: A
setInvestmentAnalysis(analysisResult);
setIsAIAnalysisExpanded(true);

// NEW: Save analysis to cache after successful generation (engage context only)
if (isEngageContext && analysisResult) {
const timestamp = new Date().toISOString();
const cacheData = {
data: analysisResult,
generated_at: timestamp
};
setCachedAnalysis(cacheData);

// Save to database
try {
const supabase = createSupabaseBrowserClient();

console.log('🔍 Saving analysis for property.id:', property.id);

const { data, error, count } = await supabase
.from('saved_properties')
.update({
investment_analysis_data: analysisResult,
investment_analysis_generated_at: timestamp
})
.eq('property_id', property.id)
.select();

console.log('🔍 Update result - data:', data, 'error:', error, 'count:', count);

if (error) {
console.error('Failed to save analysis to cache:', error);
}
} catch (cacheError) {
console.error('Cache save error:', cacheError);
}
}

// Show upgrade modal immediately for core users
if (!hasAccess(userClass as UserClass, 'discover_investment_analysis')) {
setShowUpgradeModal(true);
Expand Down Expand Up @@ -142,7 +226,8 @@ export function AIInvestmentAnalysis({ property, isEngageContext, userClass }: A
</div>
</div>
<div className="flex items-center space-x-3">
{!isAnalyzing && (
{/* OLD: Simple analyze button - commented out for cached analysis feature */}
{/* {!isAnalyzing && (
<button
onClick={(e) => {
e.stopPropagation();
Expand All @@ -153,6 +238,43 @@ export function AIInvestmentAnalysis({ property, isEngageContext, userClass }: A
<Bot className="h-4 w-4 mr-2" />
Analyze Investment
</button>
)} */}

{/* NEW: Cached analysis buttons with refresh option */}
{!isAnalyzing && (
<>
{cachedAnalysis ? (
// Show refresh button when cached analysis exists
<div className="flex items-center space-x-2">
<div className="flex items-center text-sm text-gray-600">
<Clock className="h-4 w-4 mr-1" />
Generated {new Date(cachedAnalysis.generated_at).toLocaleDateString()}
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleAnalyzeProperty();
}}
className="flex items-center px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all font-medium text-sm"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh Analysis
</button>
</div>
) : (
// Show analyze button when no cached analysis exists
<button
onClick={(e) => {
e.stopPropagation();
handleAnalyzeProperty();
}}
className="flex items-center px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:from-blue-600 hover:to-purple-700 transition-all font-medium"
>
<Bot className="h-4 w-4 mr-2" />
Analyze Investment
</button>
)}
</>
)}
{isAIAnalysisExpanded ? (
<ChevronUp className="h-5 w-5 text-gray-400" />
Expand Down
14 changes: 14 additions & 0 deletions lib/propertyTypeOverrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Counties where MFR properties are listed as OTHER in the RealEstateAPI
const COUNTY_PROPERTY_TYPE_OVERRIDES: Record<string, string[]> = {
"MO": ["Clay County"],
};

export function getPropertyType(state?: string, county?: string): string {
if (state && county) {
const counties = COUNTY_PROPERTY_TYPE_OVERRIDES[state.toUpperCase()];
if (counties?.some(c => county.toUpperCase().includes(c.toUpperCase()))) {
return "OTHER";
}
}
return "MFR";
}