Skip to content

Commit 0215229

Browse files
authored
feat: add company registration on portfolio page (#31)
* feat: add company registration on portfolio page * fix: misc clean up
1 parent dee84ac commit 0215229

11 files changed

Lines changed: 855 additions & 69 deletions

File tree

src/App.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import IDRDealDetailPage from "./idr/pages/DealDetailPage";
2626
import { ThemeProvider } from "./shared/contexts/ThemeContext";
2727
import { getConfig } from "./shared/config/runtime";
2828
import { AuthGuard } from "./shared/components/auth/AuthGuard";
29+
import { CompanyProvider } from "./credit/contexts/CompanyContext";
2930

3031
// IDR Context Providers
3132
import { DealsProvider } from "./idr/lib/deals-context";
@@ -64,17 +65,21 @@ function AppContent() {
6465
<Route path="/onboarding" element={<OnboardingPage />} />
6566
<Route path="/dashboard" element={<DashboardPage />} />
6667

67-
{/* Credit-App Routes with ThemeProvider wrapper */}
68+
{/* Credit-App Routes with ThemeProvider and CompanyProvider wrapper */}
6869
<Route path="/credit" element={<Navigate to="/credit/dashboard" replace />} />
6970
<Route path="/credit/dashboard" element={
70-
<ThemeProvider defaultTheme="system" storageKey="arda-theme">
71-
<CreditDashboardPage />
72-
</ThemeProvider>
71+
<CompanyProvider>
72+
<ThemeProvider defaultTheme="system" storageKey="arda-theme">
73+
<CreditDashboardPage />
74+
</ThemeProvider>
75+
</CompanyProvider>
7376
} />
7477
<Route path="/credit/portfolio" element={
75-
<ThemeProvider defaultTheme="system" storageKey="arda-theme">
76-
<CreditPortfolioPage />
77-
</ThemeProvider>
78+
<CompanyProvider>
79+
<ThemeProvider defaultTheme="system" storageKey="arda-theme">
80+
<CreditPortfolioPage />
81+
</ThemeProvider>
82+
</CompanyProvider>
7883
} />
7984

8085
{/* IDR Routes with Context Providers */}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { useState } from 'react';
2+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
3+
import { Button } from '../ui/button';
4+
import { Input } from '../ui/input';
5+
import { Label } from '../ui/label';
6+
import { Alert, AlertDescription } from '../ui/alert';
7+
import { AlertCircle, Building2 } from 'lucide-react';
8+
import { Company } from '../../../shared/lib/types';
9+
import { useCompany } from '../../contexts/CompanyContext';
10+
import { useToast } from '../ui/use-toast';
11+
import { createCompany } from '../../utils/api';
12+
13+
interface CompanyRegistrationModalProps {
14+
isOpen: boolean;
15+
onClose: () => void;
16+
onSuccess?: (company: Company) => void;
17+
}
18+
19+
export function CompanyRegistrationModal({ isOpen, onClose, onSuccess }: CompanyRegistrationModalProps) {
20+
const [companyName, setCompanyName] = useState('');
21+
const [isSubmitting, setIsSubmitting] = useState(false);
22+
const [error, setError] = useState('');
23+
const { setUserCompanies, userCompanies, setSelectedCompany, refreshUserCompanies } = useCompany();
24+
const { toast } = useToast();
25+
26+
const handleSubmit = async (e: React.FormEvent) => {
27+
e.preventDefault();
28+
29+
if (!companyName.trim()) {
30+
setError('Company name is required');
31+
return;
32+
}
33+
34+
setIsSubmitting(true);
35+
setError('');
36+
37+
try {
38+
console.log('🏢 Creating company via API...');
39+
40+
const newCompany = await createCompany({
41+
company_name: companyName.trim(),
42+
company_type: 'originator', // Default to originator, user can change later
43+
description: `${companyName.trim()} company created via portfolio registration`,
44+
});
45+
46+
console.log('✅ Company created successfully:', newCompany);
47+
48+
// Add to user companies and select it
49+
const updatedCompanies = [...userCompanies, newCompany];
50+
setUserCompanies(updatedCompanies);
51+
setSelectedCompany(newCompany);
52+
53+
// Refresh companies from API to ensure consistency
54+
console.log('🔄 Refreshing companies after creation...');
55+
try {
56+
await refreshUserCompanies();
57+
} catch (refreshError) {
58+
console.warn('⚠️ Could not refresh companies after creation:', refreshError);
59+
}
60+
61+
toast({
62+
title: 'Company Created',
63+
description: `${companyName} has been successfully registered.`,
64+
});
65+
66+
if (onSuccess) {
67+
onSuccess(newCompany);
68+
}
69+
70+
// Reset form and close modal
71+
setCompanyName('');
72+
onClose();
73+
} catch (error: any) {
74+
console.error('❌ Error creating company:', error);
75+
const errorMessage = error?.message || 'Failed to create company. Please try again.';
76+
setError(errorMessage);
77+
} finally {
78+
setIsSubmitting(false);
79+
}
80+
};
81+
82+
const handleClose = () => {
83+
if (!isSubmitting) {
84+
setCompanyName('');
85+
setError('');
86+
onClose();
87+
}
88+
};
89+
90+
return (
91+
<Dialog open={isOpen} onOpenChange={handleClose}>
92+
<DialogContent className="sm:max-w-md">
93+
<DialogHeader>
94+
<div className="flex items-center gap-3">
95+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
96+
<Building2 className="h-5 w-5 text-primary" />
97+
</div>
98+
<div>
99+
<DialogTitle>Create Your Company</DialogTitle>
100+
<DialogDescription>
101+
Register your company to access the portfolio features and manage deals.
102+
</DialogDescription>
103+
</div>
104+
</div>
105+
</DialogHeader>
106+
107+
<form onSubmit={handleSubmit} className="space-y-4">
108+
<div className="space-y-2">
109+
<Label htmlFor="companyName">Company Name</Label>
110+
<Input
111+
id="companyName"
112+
type="text"
113+
placeholder="Enter your company name"
114+
value={companyName}
115+
onChange={(e) => setCompanyName(e.target.value)}
116+
disabled={isSubmitting}
117+
className="w-full"
118+
autoFocus
119+
/>
120+
</div>
121+
122+
{error && (
123+
<Alert variant="destructive">
124+
<AlertCircle className="h-4 w-4" />
125+
<AlertDescription>{error}</AlertDescription>
126+
</Alert>
127+
)}
128+
129+
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 space-y-2 space-y-reverse sm:space-y-0">
130+
<Button
131+
type="button"
132+
variant="outline"
133+
onClick={handleClose}
134+
disabled={isSubmitting}
135+
>
136+
Cancel
137+
</Button>
138+
<Button type="submit" disabled={isSubmitting || !companyName.trim()}>
139+
{isSubmitting ? 'Creating...' : 'Create Company'}
140+
</Button>
141+
</div>
142+
</form>
143+
144+
<div className="text-sm text-muted-foreground">
145+
<p>You can update company details and settings after creation.</p>
146+
</div>
147+
</DialogContent>
148+
</Dialog>
149+
);
150+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useState } from 'react';
2+
import { Check, ChevronDown, Building2, Plus } from 'lucide-react';
3+
import { Button } from '../ui/button';
4+
import {
5+
DropdownMenu,
6+
DropdownMenuContent,
7+
DropdownMenuItem,
8+
DropdownMenuSeparator,
9+
DropdownMenuTrigger,
10+
} from '../ui/dropdown-menu';
11+
import { CompanyRegistrationModal } from './CompanyRegistrationModal';
12+
import { useCompany } from '../../contexts/CompanyContext';
13+
import { cn } from '../../../shared/lib/utils';
14+
15+
interface OrganizationSwitcherProps {
16+
className?: string;
17+
showLabel?: boolean;
18+
}
19+
20+
export function OrganizationSwitcher({ className, showLabel = true }: OrganizationSwitcherProps) {
21+
const [isRegistrationModalOpen, setIsRegistrationModalOpen] = useState(false);
22+
const { selectedCompany, userCompanies, setSelectedCompany } = useCompany();
23+
24+
const handleCompanySelect = (company: typeof userCompanies[0]) => {
25+
if (company.id !== selectedCompany?.id) {
26+
setSelectedCompany(company);
27+
}
28+
};
29+
30+
const handleCreateCompany = () => {
31+
setIsRegistrationModalOpen(true);
32+
};
33+
34+
const handleRegistrationSuccess = () => {
35+
setIsRegistrationModalOpen(false);
36+
};
37+
38+
if (!selectedCompany && userCompanies.length === 0) {
39+
return (
40+
<div className={cn("flex items-center", className)}>
41+
<Button
42+
variant="outline"
43+
size="sm"
44+
onClick={handleCreateCompany}
45+
className="h-8 text-xs"
46+
>
47+
<Plus className="h-3 w-3 mr-1" />
48+
Create Company
49+
</Button>
50+
<CompanyRegistrationModal
51+
isOpen={isRegistrationModalOpen}
52+
onClose={() => setIsRegistrationModalOpen(false)}
53+
onSuccess={handleRegistrationSuccess}
54+
/>
55+
</div>
56+
);
57+
}
58+
59+
return (
60+
<div className={cn("flex items-center", className)}>
61+
<DropdownMenu>
62+
<DropdownMenuTrigger asChild>
63+
<Button
64+
variant="ghost"
65+
className="h-8 px-2 py-1 text-xs flex items-center gap-1 hover:bg-accent"
66+
>
67+
<Building2 className="h-3 w-3" />
68+
{showLabel && selectedCompany && (
69+
<span className="max-w-[100px] truncate">
70+
{selectedCompany.company_name}
71+
</span>
72+
)}
73+
<ChevronDown className="h-3 w-3 opacity-50" />
74+
</Button>
75+
</DropdownMenuTrigger>
76+
<DropdownMenuContent align="start" className="w-64">
77+
<div className="px-2 py-1">
78+
<div className="text-xs text-muted-foreground uppercase tracking-wide">
79+
Your Organizations
80+
</div>
81+
</div>
82+
<DropdownMenuSeparator />
83+
{userCompanies.map((company) => (
84+
<DropdownMenuItem
85+
key={company.id}
86+
onClick={() => handleCompanySelect(company)}
87+
className="flex items-center justify-between cursor-pointer"
88+
>
89+
<div className="flex items-center gap-2">
90+
<Building2 className="h-4 w-4" />
91+
<div className="flex flex-col">
92+
<span className="text-sm font-medium">{company.company_name}</span>
93+
<span className="text-xs text-muted-foreground capitalize">
94+
{company.company_type}
95+
</span>
96+
</div>
97+
</div>
98+
{selectedCompany?.id === company.id && (
99+
<Check className="h-4 w-4 text-primary" />
100+
)}
101+
</DropdownMenuItem>
102+
))}
103+
<DropdownMenuSeparator />
104+
<DropdownMenuItem
105+
onClick={handleCreateCompany}
106+
className="flex items-center gap-2 cursor-pointer"
107+
>
108+
<Plus className="h-4 w-4" />
109+
<span className="text-sm">Create New Company</span>
110+
</DropdownMenuItem>
111+
</DropdownMenuContent>
112+
</DropdownMenu>
113+
114+
<CompanyRegistrationModal
115+
isOpen={isRegistrationModalOpen}
116+
onClose={() => setIsRegistrationModalOpen(false)}
117+
onSuccess={handleRegistrationSuccess}
118+
/>
119+
</div>
120+
);
121+
}

src/credit/components/deals/DealsList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ export const DealsList = forwardRef<DealsListRef, DealsListProps>(({ user, onDea
9999
const marketplaceDeals = (Array.isArray(response) ? response : []).map((apiDeal: any) => ({
100100
id: apiDeal.deal_id,
101101
merchantId: apiDeal.originator_id,
102-
title: `Deal ${apiDeal.deal_id.slice(-8)}`, // Marketplace deals don't have title/custom_deal_id
103-
description: apiDeal.description || `Investment opportunity with ${apiDeal.interest_rate_bps / 100}% interest`,
102+
title: apiDeal.title,
103+
description: apiDeal.description,
104104
loanAmountRange: {
105105
min: parseFloat(apiDeal.principal_amount) || 0,
106106
max: parseFloat(apiDeal.principal_amount) || 0,

src/credit/components/layout/Navigation.tsx

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useNavigate, useLocation } from "react-router-dom";
22
import { Button } from "../../../shared/components/ui/button";
3-
import { LogOut, LayoutDashboard, ChevronDown, Menu, TrendingUp, ShieldCheck, UserCog } from "lucide-react";
3+
import { LogOut, LayoutDashboard, ChevronDown, Menu, TrendingUp, UserCog } from "lucide-react";
44
import { User as UserType } from "../../../shared/types";
5+
import { OrganizationSwitcher } from "../company/OrganizationSwitcher";
56
import {
67
DropdownMenu,
78
DropdownMenuContent,
@@ -129,10 +130,13 @@ export function Navigation({ user, onLogout }: NavigationProps) {
129130
</div>
130131
</div>
131132
<DropdownMenuSeparator />
132-
<DropdownMenuItem onClick={() => console.log('Switch Organization clicked')}>
133-
<ShieldCheck className="w-4 h-4 mr-2" />
134-
Switch Organization
135-
</DropdownMenuItem>
133+
<div className="px-2 py-2">
134+
<div className="flex items-center justify-between mb-2">
135+
<span className="text-sm">Organization</span>
136+
<OrganizationSwitcher showLabel={false} />
137+
</div>
138+
</div>
139+
<DropdownMenuSeparator />
136140
<DropdownMenuItem onClick={() => console.log('Switch Role clicked')}>
137141
<UserCog className="w-4 h-4 mr-2" />
138142
Switch Role
@@ -201,18 +205,13 @@ export function Navigation({ user, onLogout }: NavigationProps) {
201205
</div>
202206

203207
{/* Organization & Role Section */}
204-
<div className="pt-4 border-t space-y-1">
205-
<Button
206-
variant="ghost"
207-
className="w-full justify-start"
208-
onClick={() => {
209-
setMobileMenuOpen(false);
210-
console.log('Switch Organization clicked');
211-
}}
212-
>
213-
<ShieldCheck className="w-4 h-4 mr-3" />
214-
Switch Organization
215-
</Button>
208+
<div className="pt-4 border-t space-y-3">
209+
<div className="px-3 py-2 rounded-md bg-muted">
210+
<div className="flex items-center justify-between mb-2">
211+
<span className="text-sm font-medium">Organization</span>
212+
<OrganizationSwitcher showLabel={false} />
213+
</div>
214+
</div>
216215
<Button
217216
variant="ghost"
218217
className="w-full justify-start"

src/credit/components/loans/LoanDetailModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface LoanData {
2929
industry: string;
3030
status: string;
3131
createdAt: string;
32+
lastUpdated: string;
3233
outstandingBalance?: number;
3334
}
3435

0 commit comments

Comments
 (0)