Files
ten31-database/preview.html
T
2026-02-27 12:44:50 -06:00

3572 lines
164 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ten31 CRM</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel removed - not needed, no JSX in this file -->
<script>
window.onerror = function(msg, url, line, col, err) {
document.body.style.background = '#1e293b';
document.body.innerHTML = '<div style="padding:40px;color:#ef4444;font-family:monospace;">' +
'<h2 style="color:#ef4444;">Error Loading CRM</h2>' +
'<p style="color:#e2e8f0;">' + msg + '</p>' +
'<p style="color:#94a3b8;">Line: ' + line + ', Col: ' + col + '</p>' +
'<pre style="color:#fbbf24;white-space:pre-wrap;margin-top:16px;">' + (err && err.stack ? err.stack : '') + '</pre>' +
'</div>';
return true;
};
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
}
#root {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 250px;
background: #1e293b;
border-right: 1px solid #334155;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #334155;
font-size: 18px;
font-weight: 700;
color: #6366f1;
}
.sidebar-section {
flex: 1;
padding: 16px 0;
border-bottom: 1px solid #334155;
}
.sidebar-section-title {
padding: 8px 16px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #94a3b8;
}
.view-item {
padding: 10px 16px;
cursor: pointer;
font-size: 14px;
color: #cbd5e1;
transition: all 0.2s;
}
.view-item:hover {
background: #334155;
}
.view-item.active {
background: #334155;
color: #6366f1;
font-weight: 600;
border-left: 3px solid #6366f1;
padding-left: 13px;
}
.view-item.team-member {
padding-left: 32px;
font-size: 13px;
}
.filter-badge {
display: inline-block;
background: #6366f1;
color: #fff;
border-radius: 4px;
padding: 2px 6px;
font-size: 11px;
margin-left: 8px;
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid #334155;
background: #0f172a;
}
.search-input {
flex: 1;
max-width: 300px;
padding: 8px 12px;
background: #1e293b;
border: 1px solid #334155;
border-radius: 6px;
color: #e2e8f0;
font-size: 14px;
}
.search-input::placeholder {
color: #64748b;
}
.search-input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
.add-button {
padding: 8px 16px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.add-button:hover {
background: #4f46e5;
}
/* Table */
.table-container {
flex: 1;
overflow: auto;
position: relative;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.table thead {
background: #1e293b;
position: sticky;
top: 0;
z-index: 10;
}
.table th {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #334155;
color: #94a3b8;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table th:hover {
cursor: pointer;
background: #334155;
}
.table td {
padding: 12px 16px;
border-bottom: 1px solid #334155;
}
.table tbody tr {
transition: background 0.2s;
}
.table tbody tr:hover {
background: #1e293b;
cursor: pointer;
}
.table tbody tr.selected {
background: #334155;
}
.table td.editable {
cursor: cell;
}
.table td.editable:hover {
background: rgba(99, 102, 241, 0.08);
outline: 1px dashed rgba(99, 102, 241, 0.3);
outline-offset: -1px;
}
.table td.editing {
background: rgba(99, 102, 241, 0.15);
outline: 2px solid #6366f1;
outline-offset: -2px;
}
.cell-edit-input, .cell-edit-select {
width: 100%;
padding: 4px 6px;
background: #0f172a;
color: #e2e8f0;
border: 1px solid #6366f1;
border-radius: 3px;
font-size: 13px;
font-family: inherit;
outline: none;
}
.table input[type="text"],
.table input[type="number"],
.table input[type="email"],
.table select {
width: 100%;
padding: 4px 6px;
background: #0f172a;
border: 1px solid #6366f1;
border-radius: 4px;
color: #e2e8f0;
font-size: 14px;
}
.table input[type="text"]:focus,
.table input[type="number"]:focus,
.table input[type="email"]:focus,
.table select:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}
/* Badge Styles */
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.badge.investor {
background: #065f46;
color: #d1fae5;
}
.badge.prospect {
background: #3730a3;
color: #e0e7ff;
}
.badge.lead {
background: #92400e;
color: #fef3c7;
}
.badge.high {
background: #7c2d12;
color: #fed7aa;
}
.badge.medium {
background: #713f12;
color: #fcd34d;
}
.badge.low {
background: #374151;
color: #d1d5db;
}
/* Detail Panel */
.context-menu {
position: fixed;
z-index: 2000;
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 4px 0;
min-width: 200px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.context-menu-item {
padding: 8px 14px;
font-size: 13px;
color: #cbd5e1;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
}
.context-menu-item:hover {
background: #334155;
color: #fff;
}
.context-menu-item.danger {
color: #fca5a5;
}
.context-menu-item.danger:hover {
background: #7f1d1d;
color: #fff;
}
.context-menu-divider {
height: 1px;
background: #334155;
margin: 4px 0;
}
.context-menu-label {
padding: 4px 14px;
font-size: 11px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.context-menu-check {
width: 14px;
text-align: center;
font-size: 12px;
}
.detail-panel {
position: fixed;
right: 0;
top: 0;
height: 100vh;
width: 450px;
background: #1e293b;
border-left: 1px solid #334155;
z-index: 100;
overflow-y: auto;
transform: translateX(450px);
transition: transform 0.3s ease;
}
.detail-panel.open {
transform: translateX(0);
}
.detail-header {
padding: 24px;
border-bottom: 1px solid #334155;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.detail-title {
display: flex;
align-items: center;
gap: 12px;
}
.detail-title h2 {
font-size: 20px;
font-weight: 700;
}
.close-button {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
font-size: 24px;
padding: 0;
transition: color 0.2s;
}
.close-button:hover {
color: #e2e8f0;
}
.detail-body {
padding: 24px;
}
.detail-section {
margin-bottom: 28px;
}
.detail-section-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #94a3b8;
margin-bottom: 12px;
}
.detail-field {
margin-bottom: 16px;
}
.detail-field label {
display: block;
font-size: 13px;
font-weight: 600;
color: #cbd5e1;
margin-bottom: 6px;
}
.detail-field input,
.detail-field select,
.detail-field textarea {
width: 100%;
padding: 8px 12px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
color: #e2e8f0;
font-size: 14px;
}
.detail-field input:focus,
.detail-field select:focus,
.detail-field textarea:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
.detail-field textarea {
resize: vertical;
min-height: 80px;
}
/* Contacts Section */
.contacts-section {
margin-bottom: 28px;
}
.contacts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.add-contact-button {
padding: 6px 12px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.2s;
}
.add-contact-button:hover {
background: #4f46e5;
}
.contact-card {
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.contact-info {
flex: 1;
}
.contact-name {
font-weight: 600;
color: #e2e8f0;
margin-bottom: 4px;
}
.contact-email {
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
.contact-title {
font-size: 12px;
color: #64748b;
}
.delete-contact-button {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 16px;
padding: 0;
transition: color 0.2s;
}
.delete-contact-button:hover {
color: #dc2626;
}
/* Tooltip */
.tooltip {
position: absolute;
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
padding: 12px;
font-size: 13px;
z-index: 200;
max-width: 300px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
}
.tooltip-contact {
padding: 6px 0;
border-bottom: 1px solid #334155;
}
.tooltip-contact:last-child {
border-bottom: none;
}
.tooltip-contact-name {
font-weight: 600;
color: #e2e8f0;
}
.tooltip-contact-email {
font-size: 12px;
color: #94a3b8;
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 300;
}
.modal.open {
display: flex;
}
.modal-content {
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
font-size: 18px;
font-weight: 700;
margin-bottom: 20px;
color: #e2e8f0;
}
.modal-field {
margin-bottom: 16px;
}
.modal-field label {
display: block;
font-size: 13px;
font-weight: 600;
color: #cbd5e1;
margin-bottom: 6px;
}
.modal-field input,
.modal-field select {
width: 100%;
padding: 8px 12px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
color: #e2e8f0;
font-size: 14px;
}
.modal-field input:focus,
.modal-field select:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
.modal-button {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.modal-button.cancel {
background: #334155;
color: #e2e8f0;
}
.modal-button.cancel:hover {
background: #475569;
}
.modal-button.primary {
background: #6366f1;
color: #fff;
}
.modal-button.primary:hover {
background: #4f46e5;
}
/* Dashboard */
.dashboard {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-card-value {
font-size: 28px;
font-weight: 700;
color: #6366f1;
margin-bottom: 4px;
}
.stat-card-label {
font-size: 12px;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Column Visibility Dropdown */
.columns-button {
background: none;
border: 1px solid #475569;
color: #e2e8f0;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.columns-button:hover {
background: #334155;
border-color: #64748b;
}
.columns-dropdown {
position: absolute;
background: #1e293b;
border: 1px solid #475569;
border-radius: 6px;
min-width: 200px;
max-height: 400px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.columns-dropdown-item {
padding: 10px 12px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: #cbd5e1;
border-bottom: 1px solid #334155;
}
.columns-dropdown-item:last-child {
border-bottom: none;
}
.columns-dropdown-item:hover {
background: #334155;
}
.columns-dropdown-item input[type="checkbox"] {
cursor: pointer;
}
/* Save View Button */
.save-view-button {
background: none;
border: 1px solid #6366f1;
color: #6366f1;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.save-view-button:hover {
background: #6366f1;
color: #fff;
}
.save-view-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Save View Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: #1e293b;
border: 1px solid #475569;
border-radius: 8px;
padding: 24px;
min-width: 350px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.modal-content h2 {
font-size: 18px;
margin-bottom: 16px;
color: #e2e8f0;
}
.modal-content input {
width: 100%;
padding: 8px 12px;
background: #0f172a;
border: 1px solid #475569;
border-radius: 4px;
color: #e2e8f0;
margin-bottom: 16px;
font-size: 14px;
}
.modal-content input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
.modal-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.modal-buttons button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.modal-buttons .save {
background: #6366f1;
color: #fff;
}
.modal-buttons .save:hover {
background: #4f46e5;
}
.modal-buttons .cancel {
background: #334155;
color: #cbd5e1;
}
.modal-buttons .cancel:hover {
background: #475569;
}
/* Custom Views Section */
.custom-views-section {
padding: 16px 0;
border-bottom: 1px solid #334155;
}
.view-item-with-delete {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
cursor: pointer;
font-size: 14px;
color: #cbd5e1;
transition: all 0.2s;
}
.view-item-with-delete:hover {
background: #334155;
}
.view-item-with-delete.active {
background: #334155;
color: #6366f1;
font-weight: 600;
border-left: 3px solid #6366f1;
padding-left: 13px;
}
.delete-view-btn {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px 6px;
font-size: 16px;
opacity: 0;
transition: all 0.2s;
}
.view-item-with-delete:hover .delete-view-btn {
opacity: 1;
color: #ef4444;
}
.delete-view-btn:hover {
color: #dc2626;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/javascript">
const React = window.React;
const { useState, useCallback, useMemo, useEffect, useRef } = React;
// Utility functions (OUTSIDE component)
function formatCurrency(value) {
if (!value) return "$0";
return "$" + value.toLocaleString();
}
function getRelativeDate(dateString) {
if (!dateString) return "";
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Yesterday";
if (diffDays < 7) return `${diffDays}d ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)}m ago`;
return `${Math.floor(diffDays / 365)}y ago`;
}
function calculateTotal(items, key) {
return items.reduce((sum, item) => {
const val = parseFloat(item[key]) || 0;
return sum + val;
}, 0);
}
function getTypeColor(type) {
const colors = {
investor: "#065f46",
prospect: "#3730a3",
lead: "#92400e"
};
return colors[type] || "#374151";
}
function getPriorityColor(priority) {
const colors = {
high: "#7c2d12",
medium: "#713f12",
low: "#374151"
};
return colors[priority] || "#374151";
}
// Mock data with new structure
const initialInvestors = [
{
id: 1,
name: "Caprock / Grey Street",
type: "investor",
lead: "Grant",
priority: "high",
followUp: false,
graveyard: false,
leadSource: "conference",
manualLeadSource: "Ten31 Bitcoin Summit 2023",
contacts: [
{ name: "Jeffrey Friedstein", email: "jeff@caprock.com", title: "Managing Director", location: "Austin, TX, USA" },
{ name: "Jay Page", email: "jay@caprock.com", title: "Partner", location: "Austin, TX, USA" }
],
fundI: 500000,
fundII: 750000,
fundIII: 1000000,
tactical: 250000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Strong commitment across funds. Jeff leads portfolio decisions. Quarterly check-ins scheduled.",
lastContact: "2025-02-01"
},
{
id: 2,
name: "Stepstone Group",
type: "investor",
lead: "Matt",
priority: "high",
followUp: true,
graveyard: false,
leadSource: "inbound",
manualLeadSource: "Direct inquiry via website",
contacts: [
{ name: "Michael Chen", email: "mchen@stepstone.com", title: "Director", location: "New York City, NY, USA" },
{ name: "Sarah Williams", email: "swilliams@stepstone.com", title: "VP", location: "New York City, NY, USA" }
],
fundI: 750000,
fundII: 1000000,
fundIII: 1500000,
tactical: 500000,
pawnE4: 250000,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Follow up on fund III allocation by Feb 28. Michael prefers async comms.",
lastContact: "2025-01-28"
},
{
id: 3,
name: "Cashel Capital",
type: "investor",
lead: "JP",
priority: "high",
followUp: false,
graveyard: false,
leadSource: "referral",
manualLeadSource: "Introduced by Carnegie Endowment",
contacts: [
{ name: "Brendan O'Brien", email: "brendan@cashelcap.com", title: "Founder", location: "Dublin, Ireland" },
{ name: "Laura Martinez", email: "laura@cashelcap.com", title: "COO", location: "Dublin, Ireland" }
],
fundI: 2000000,
fundII: 2500000,
fundIII: 3000000,
tactical: 1000000,
pawnE4: 500000,
pawnF4: 250000,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Top investor relationship. Brendan very engaged. Laura handles ops coordination.",
lastContact: "2025-02-10"
},
{
id: 4,
name: "Corbin Capital",
type: "investor",
lead: "Grant",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "conference",
manualLeadSource: "Bitcoin Miami 2024",
contacts: [
{ name: "Craig Bergstrom", email: "craig@corbincap.com", title: "CIO", location: "Greenwich, CT, USA" },
{ name: "Michelle Torres", email: "michelle@corbincap.com", title: "Analyst", location: "Greenwich, CT, USA" }
],
fundI: 300000,
fundII: 400000,
fundIII: 500000,
tactical: 150000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Craig is crypto-curious. Michelle will do detailed DD. Plan follow-up in March.",
lastContact: "2025-01-15"
},
{
id: 5,
name: "Bessemer Trust",
type: "investor",
lead: "Matt",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "outbound",
manualLeadSource: "Cold email campaign",
contacts: [
{ name: "Richard Park", email: "rpark@bessemer.com", title: "SVP", location: "New York City, NY, USA" },
{ name: "Amanda Liu", email: "aliu@bessemer.com", title: "Wealth Advisor", location: "New York City, NY, USA" }
],
fundI: 200000,
fundII: 250000,
fundIII: 300000,
tactical: 100000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Bessemer considering alternative allocation. Richard approval needed. Slow process.",
lastContact: "2025-01-20"
},
{
id: 6,
name: "Carnegie Endowment",
type: "investor",
lead: "JP",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "referral",
manualLeadSource: "Board introduction",
contacts: [
{ name: "David Walsh", email: "dwalsh@carnegie.org", title: "Investment Director", location: "Washington, DC, USA" },
{ name: "Patricia Nguyen", email: "pnguyen@carnegie.org", title: "Portfolio Manager", location: "Washington, DC, USA" }
],
fundI: 1000000,
fundII: 1200000,
fundIII: 1500000,
tactical: 500000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Long-term strategic relationship. David approves big picture, Patricia manages details.",
lastContact: "2025-02-05"
},
{
id: 7,
name: "Stanford Endowment",
type: "investor",
lead: "Grant",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "network",
manualLeadSource: "Stanford alumni event",
contacts: [
{ name: "Robert Kim", email: "rkim@stanford.edu", title: "Director of Alternatives", location: "Palo Alto, CA, USA" },
{ name: "Jennifer Adams", email: "jadams@stanford.edu", title: "Associate", location: "Palo Alto, CA, USA" }
],
fundI: 800000,
fundII: 1000000,
fundIII: 1200000,
tactical: 400000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Strong endowment interest. Robert decision-maker. Jennifer coordinating technical reviews.",
lastContact: "2025-01-30"
},
{
id: 8,
name: "Danny Kramer",
type: "investor",
lead: "Grant",
priority: "high",
followUp: false,
graveyard: false,
leadSource: "personal",
manualLeadSource: "Personal connection",
contacts: [
{ name: "Danny Kramer", email: "danny@kramerinv.com", title: "", location: "Miami, FL, USA" }
],
fundI: 500000,
fundII: 500000,
fundIII: 750000,
tactical: 250000,
pawnE4: 100000,
pawnF4: 50000,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Early supporter. Very active in community. Attends all investor events.",
lastContact: "2025-02-12"
},
{
id: 9,
name: "Howard Love",
type: "investor",
lead: "Matt",
priority: "high",
followUp: false,
graveyard: false,
leadSource: "referral",
manualLeadSource: "Caprock introduction",
contacts: [
{ name: "Howard Love", email: "howard@loveinv.com", title: "Founder", location: "San Francisco, CA, USA" }
],
fundI: 300000,
fundII: 400000,
fundIII: 500000,
tactical: 200000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Family office based in Austin. Very responsive. Casual culture.",
lastContact: "2025-02-08"
},
{
id: 10,
name: "Mark Patterson",
type: "investor",
lead: "JP",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "conference",
manualLeadSource: "Porcfest 2024",
contacts: [
{ name: "Mark Patterson", email: "mark@pattersonwealth.com", title: "", location: "Dallas, TX, USA" }
],
fundI: 150000,
fundII: 150000,
fundIII: 200000,
tactical: 50000,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Bitcoin maxi. Small allocation but very supportive of mission.",
lastContact: "2025-02-02"
},
{
id: 11,
name: "Thistledown Capital",
type: "prospect",
lead: "Grant",
priority: "high",
followUp: true,
graveyard: false,
leadSource: "outbound",
manualLeadSource: "Prospect research list",
contacts: [
{ name: "James McCaffrey", email: "james@thistledown.com", title: "Principal", location: "Boston, MA, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Strong prospect. Need to schedule initial meeting for Feb. James responsive on email.",
lastContact: "2025-01-22"
},
{
id: 12,
name: "Ridgeline Ventures",
type: "prospect",
lead: "Matt",
priority: "high",
followUp: false,
graveyard: false,
leadSource: "inbound",
manualLeadSource: "Warm intro from AngelList",
contacts: [
{ name: "Karen Wu", email: "karen@ridgelinevc.com", title: "GP", location: "Denver, CO, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "VC firm focused on fintech. Karen saw pitch video, wants full deck.",
lastContact: "2025-02-11"
},
{
id: 13,
name: "Northstar Family Office",
type: "prospect",
lead: "JP",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "network",
manualLeadSource: "Ten31 ecosystem",
contacts: [
{ name: "Tom Bradley", email: "tom@northstarfo.com", title: "CIO", location: "Chicago, IL, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Moderate assets under management. Conservative mandate. Long sales cycle expected.",
lastContact: "2025-01-25"
},
{
id: 14,
name: "Briarwood Wealth",
type: "prospect",
lead: "Grant",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "outbound",
manualLeadSource: "LinkedIn outreach",
contacts: [
{ name: "Susan Chen", email: "susan@briarwood.com", title: "Managing Partner", location: "Nashville, TN, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "RIA with crypto-friendly clients. Susan curious about allocation strategy.",
lastContact: "2025-01-18"
},
{
id: 15,
name: "Citadel Peak Advisors",
type: "prospect",
lead: "Matt",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "referral",
manualLeadSource: "Referred by Briarwood",
contacts: [
{ name: "Alex Rivera", email: "alex@citadelpeak.com", title: "Founder", location: "Scottsdale, AZ, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Young, dynamic firm. Alex very interested in Bitcoin-focused products.",
lastContact: "2025-02-03"
},
{
id: 16,
name: "Granite Point Capital",
type: "prospect",
lead: "JP",
priority: "low",
followUp: false,
graveyard: false,
leadSource: "conference",
manualLeadSource: "Consensus 2024",
contacts: [
{ name: "William Foster", email: "wfoster@granitept.com", title: "Portfolio Manager", location: "Salt Lake City, UT, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Met William at conference. Cautious on crypto but open to discussion.",
lastContact: "2025-01-28"
},
{
id: 17,
name: "Blue Ridge Endowment",
type: "prospect",
lead: "Grant",
priority: "low",
followUp: false,
graveyard: false,
leadSource: "outbound",
manualLeadSource: "Endowment database",
contacts: [
{ name: "Margaret Collins", email: "mcollins@blueridge.org", title: "Executive Director", location: "Charlotte, NC, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Early stage outreach. Margaret responded briefly but needs more context.",
lastContact: "2025-01-10"
},
{
id: 18,
name: "Blackthorn Group",
type: "lead",
lead: "Matt",
priority: "high",
followUp: true,
graveyard: false,
leadSource: "inbound",
manualLeadSource: "Webinar signup",
contacts: [
{ name: "Sean Murphy", email: "sean@blackthorn.com", title: "Partner", location: "London, England, UK" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Signed up for webinar, attended live. Need to send follow-up deck.",
lastContact: "2025-02-06"
},
{
id: 19,
name: "Vermillion Partners",
type: "lead",
lead: "JP",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "outbound",
manualLeadSource: "Cold email",
contacts: [
{ name: "Rachel Goldstein", email: "rachel@vermillion.com", title: "Managing Director", location: "Palm Beach, FL, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Rachel replied to cold email. Initial discovery call scheduled for Feb 20.",
lastContact: "2025-02-01"
},
{
id: 20,
name: "Ironwood Associates",
type: "lead",
lead: "Grant",
priority: "medium",
followUp: false,
graveyard: false,
leadSource: "referral",
manualLeadSource: "Investor network",
contacts: [
{ name: "Peter Chang", email: "peter@ironwood.com", title: "CIO", location: "Houston, TX, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Warm intro from Danny Kramer. Peter open to learning about fund structure.",
lastContact: "2025-01-31"
},
{
id: 21,
name: "Summit Rock Advisors",
type: "lead",
lead: "Matt",
priority: "low",
followUp: false,
graveyard: false,
leadSource: "conference",
manualLeadSource: "Bitcoin Miami 2024",
contacts: [
{ name: "Catherine Blake", email: "catherine@summitrock.com", title: "Director", location: "New York City, NY, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Collected business card. Catherine on passive contact list.",
lastContact: "2024-12-20"
},
{
id: 22,
name: "Osprey Capital",
type: "graveyard",
lead: "JP",
priority: "low",
followUp: false,
graveyard: true,
leadSource: "inbound",
manualLeadSource: "Website form",
contacts: [
{ name: "Nathan Drake", email: "nathan@ospreycap.com", title: "GP", location: "Seattle, WA, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Marked graveyard after multiple follow-up attempts. Nathan unresponsive.",
lastContact: "2024-10-15"
},
{
id: 23,
name: "Silverleaf Investments",
type: "graveyard",
lead: "Grant",
priority: "low",
followUp: false,
graveyard: true,
leadSource: "outbound",
manualLeadSource: "LinkedIn outreach",
contacts: [
{ name: "Diana Ross", email: "diana@silverleaf.com", title: "Founder", location: "Aspen, CO, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "No response to 3+ outreach attempts. Archived for now.",
lastContact: "2024-09-05"
},
{
id: 24,
name: "Pacific Grove Trust",
type: "graveyard",
lead: "Matt",
priority: "low",
followUp: false,
graveyard: true,
leadSource: "referral",
manualLeadSource: "Bessemer introduction",
contacts: [
{ name: "Eric Tanaka", email: "eric@pacificgrove.com", title: "VP", location: "San Diego, CA, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Eric took meeting but firm policy prevents crypto allocation. Not revisiting.",
lastContact: "2024-11-30"
},
{
id: 25,
name: "Mountainview Endowment",
type: "prospect",
lead: "JP",
priority: "low",
followUp: false,
graveyard: false,
leadSource: "network",
manualLeadSource: "Annual conference",
contacts: [
{ name: "Lisa Chen", email: "lchen@mountainview.org", title: "Director of Investments", location: "Portland, OR, USA" }
],
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "Lisa expressed interest at conference. Sent preliminary materials.",
lastContact: "2025-01-12"
}
];
// View definitions with column visibility
const defaultViews = [
{
id: 'all',
name: 'All Investors',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'fundI', 'fundII', 'fundIII', 'tactical', 'pawnE4', 'pawnF4', 'ten31Terahash', 'satsAndStats', 'joinTheFold', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: {}
},
{
id: 'investors',
name: 'Investors',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'fundI', 'fundII', 'fundIII', 'tactical', 'pawnE4', 'pawnF4', 'ten31Terahash', 'satsAndStats', 'joinTheFold', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { type: 'investor' }
},
{
id: 'prospects',
name: 'Prospects',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { type: 'prospect' }
},
{
id: 'leads',
name: 'Leads',
builtin: true,
visibleColumns: ['name', 'contacts', 'lead', 'priority', 'status', 'lastContact', 'notes'],
sortColumn: 'priority',
sortOrder: 'asc',
filterCriteria: { type: 'lead' }
},
{
id: 'followUp',
name: 'Follow-Up List',
builtin: true,
visibleColumns: ['name', 'contacts', 'lead', 'priority', 'lastContact', 'notes'],
sortColumn: 'priority',
sortOrder: 'asc',
filterCriteria: { followUp: true }
},
{
id: 'graveyard',
name: 'Graveyard',
builtin: true,
visibleColumns: ['name', 'type', 'lead', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { graveyard: true }
},
{
id: 'fundI',
name: 'Fund I Investors',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'fundI', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { fundFilter: 'fundI' }
},
{
id: 'fundII',
name: 'Fund II Investors',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'fundII', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { fundFilter: 'fundII' }
},
{
id: 'fundIII',
name: 'Fund III Investors',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'fundIII', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { fundFilter: 'fundIII' }
},
{
id: 'tactical',
name: 'Tactical Fund Investors',
builtin: true,
visibleColumns: ['name', 'contacts', 'type', 'lead', 'priority', 'status', 'tactical', 'total', 'lastContact', 'notes'],
sortColumn: 'name',
sortOrder: 'asc',
filterCriteria: { fundFilter: 'tactical' }
}
];
const allColumns = [
{ id: 'name', label: 'Name' },
{ id: 'contacts', label: 'Contacts' },
{ id: 'type', label: 'Type' },
{ id: 'lead', label: 'Lead' },
{ id: 'priority', label: 'Priority' },
{ id: 'status', label: 'Status' },
{ id: 'fundI', label: 'Fund I' },
{ id: 'fundII', label: 'Fund II' },
{ id: 'fundIII', label: 'Fund III' },
{ id: 'tactical', label: 'Tactical Fund' },
{ id: 'pawnE4', label: 'Pawn to E4' },
{ id: 'pawnF4', label: 'Pawn to f4' },
{ id: 'ten31Terahash', label: 'Ten31 Terahash' },
{ id: 'satsAndStats', label: 'Sats and Stats' },
{ id: 'joinTheFold', label: 'Join the Fold' },
{ id: 'total', label: 'Total Invested' },
{ id: 'lastContact', label: 'Last Contact' },
{ id: 'notes', label: 'Notes' }
];
// Utility functions outside App component
const applyFilterCriteria = (investors, criteria) => {
let filtered = investors;
if (criteria.type) {
filtered = filtered.filter(i => i.type === criteria.type);
}
if (criteria.followUp) {
filtered = filtered.filter(i => i.followUp);
}
if (criteria.graveyard) {
filtered = filtered.filter(i => i.graveyard);
}
if (criteria.fundFilter) {
const fundKey = criteria.fundFilter;
filtered = filtered.filter(i => i[fundKey] > 0);
}
return filtered;
};
const getCurrentView = (selectedView, defaultViews, customViews) => {
const built = defaultViews.find(v => v.id === selectedView);
if (built) return built;
return customViews.find(v => v.id === selectedView);
};
const getCurrentVisibleColumns = (selectedView, defaultViews, customViews) => {
const view = getCurrentView(selectedView, defaultViews, customViews);
if (view) return view.visibleColumns;
return defaultViews[0].visibleColumns;
};
// Location autocomplete data
const LOCATIONS = [
"New York City, NY, USA", "Los Angeles, CA, USA", "Chicago, IL, USA", "Houston, TX, USA",
"Phoenix, AZ, USA", "Philadelphia, PA, USA", "San Antonio, TX, USA", "San Diego, CA, USA",
"Dallas, TX, USA", "San Jose, CA, USA", "Austin, TX, USA", "Jacksonville, FL, USA",
"Fort Worth, TX, USA", "Columbus, OH, USA", "Charlotte, NC, USA", "Indianapolis, IN, USA",
"San Francisco, CA, USA", "Seattle, WA, USA", "Denver, CO, USA", "Nashville, TN, USA",
"Oklahoma City, OK, USA", "El Paso, TX, USA", "Washington, DC, USA", "Boston, MA, USA",
"Portland, OR, USA", "Las Vegas, NV, USA", "Memphis, TN, USA", "Louisville, KY, USA",
"Baltimore, MD, USA", "Milwaukee, WI, USA", "Albuquerque, NM, USA", "Tucson, AZ, USA",
"Fresno, CA, USA", "Sacramento, CA, USA", "Mesa, AZ, USA", "Kansas City, MO, USA",
"Atlanta, GA, USA", "Omaha, NE, USA", "Colorado Springs, CO, USA", "Raleigh, NC, USA",
"Long Beach, CA, USA", "Virginia Beach, VA, USA", "Miami, FL, USA", "Oakland, CA, USA",
"Minneapolis, MN, USA", "Tampa, FL, USA", "Tulsa, OK, USA", "Arlington, TX, USA",
"New Orleans, LA, USA", "Cleveland, OH, USA", "Honolulu, HI, USA", "Anchorage, AK, USA",
"Pittsburgh, PA, USA", "Cincinnati, OH, USA", "St. Louis, MO, USA", "Detroit, MI, USA",
"Salt Lake City, UT, USA", "Scottsdale, AZ, USA", "Greenwich, CT, USA", "Stamford, CT, USA",
"Palm Beach, FL, USA", "Naples, FL, USA", "Palo Alto, CA, USA", "Menlo Park, CA, USA",
"Beverly Hills, CA, USA", "Boca Raton, FL, USA", "Aspen, CO, USA", "Jackson Hole, WY, USA",
"Charleston, SC, USA", "Savannah, GA, USA", "Boulder, CO, USA", "Santa Monica, CA, USA",
"Newport Beach, CA, USA", "Westport, CT, USA", "Darien, CT, USA", "New Canaan, CT, USA",
"Short Hills, NJ, USA", "Summit, NJ, USA", "Princeton, NJ, USA", "Montclair, NJ, USA",
"London, England, UK", "Edinburgh, Scotland, UK", "Manchester, England, UK",
"Toronto, ON, Canada", "Vancouver, BC, Canada", "Montreal, QC, Canada", "Calgary, AB, Canada",
"Zurich, Switzerland", "Geneva, Switzerland", "Singapore", "Hong Kong, China",
"Sydney, NSW, Australia", "Melbourne, VIC, Australia", "Dubai, UAE", "Abu Dhabi, UAE",
"Frankfurt, Germany", "Munich, Germany", "Berlin, Germany", "Paris, France",
"Amsterdam, Netherlands", "Luxembourg City, Luxembourg", "Tokyo, Japan", "Seoul, South Korea",
"Tel Aviv, Israel", "São Paulo, Brazil", "Mexico City, Mexico", "Dublin, Ireland",
"Cayman Islands", "Nassau, Bahamas", "Hamilton, Bermuda", "Zug, Switzerland",
"El Salvador", "Panama City, Panama", "Liechtenstein"
];
const LOCATION_ALIASES = {
"new york": "New York City, NY, USA", "nyc": "New York City, NY, USA", "manhattan": "New York City, NY, USA",
"la": "Los Angeles, CA, USA", "sf": "San Francisco, CA, USA", "dc": "Washington, DC, USA",
"vegas": "Las Vegas, NV, USA", "philly": "Philadelphia, PA, USA", "nola": "New Orleans, LA, USA",
"silicon valley": "Palo Alto, CA, USA", "the city": "San Francisco, CA, USA"
};
function LocationInput({ value, onChange, style }) {
const [inputVal, setInputVal] = useState(value || "");
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIdx, setSelectedIdx] = useState(-1);
const handleInputChange = (e) => {
const val = e.target.value;
setInputVal(val);
setSelectedIdx(-1);
if (val.length < 2) {
setSuggestions([]);
setShowSuggestions(false);
return;
}
const lower = val.toLowerCase();
// Check aliases first
const aliasMatch = LOCATION_ALIASES[lower];
let matches = [];
if (aliasMatch) {
matches.push(aliasMatch);
}
// Filter locations
const filtered = LOCATIONS.filter(loc =>
loc.toLowerCase().includes(lower) && loc !== aliasMatch
);
matches = [...matches, ...filtered].slice(0, 8);
setSuggestions(matches);
setShowSuggestions(matches.length > 0);
};
const selectSuggestion = (loc) => {
setInputVal(loc);
onChange(loc);
setShowSuggestions(false);
setSuggestions([]);
};
const handleKeyDown = (e) => {
if (!showSuggestions) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIdx(prev => Math.min(prev + 1, suggestions.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIdx(prev => Math.max(prev - 1, -1));
} else if (e.key === "Enter" && selectedIdx >= 0) {
e.preventDefault();
selectSuggestion(suggestions[selectedIdx]);
} else if (e.key === "Escape") {
setShowSuggestions(false);
}
};
const handleBlur = () => {
// Delay to allow click on suggestion
setTimeout(() => {
setShowSuggestions(false);
if (inputVal !== value) onChange(inputVal);
}, 200);
};
return React.createElement("div", { style: { position: "relative", ...style } },
React.createElement("input", {
type: "text",
placeholder: "City, State, Country",
value: inputVal,
onChange: handleInputChange,
onKeyDown: handleKeyDown,
onBlur: handleBlur,
onFocus: () => { if (suggestions.length > 0) setShowSuggestions(true); },
style: { display: "block", width: "100%", padding: "4px", background: "#0f172a", border: "1px solid #334155", borderRadius: "4px", color: "#e2e8f0", fontSize: "13px" }
}),
showSuggestions && React.createElement("div", {
style: {
position: "absolute", top: "100%", left: 0, right: 0, zIndex: 1000,
background: "#1e293b", border: "1px solid #334155", borderRadius: "4px",
maxHeight: "200px", overflowY: "auto", marginTop: "2px", boxShadow: "0 4px 12px rgba(0,0,0,0.4)"
}
}, suggestions.map((loc, i) =>
React.createElement("div", {
key: loc,
onMouseDown: (e) => { e.preventDefault(); selectSuggestion(loc); },
style: {
padding: "6px 8px", cursor: "pointer", fontSize: "12px",
color: i === selectedIdx ? "#fff" : "#cbd5e1",
background: i === selectedIdx ? "#6366f1" : "transparent"
}
}, loc)
))
);
}
// Main App Component
function App() {
const [investors, setInvestors] = useState(initialInvestors);
const [selectedView, setSelectedView] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState("name");
const [sortOrder, setSortOrder] = useState("asc");
const [selectedInvestor, setSelectedInvestor] = useState(null);
const [editingCell, setEditingCell] = useState(null);
const [showAddModal, setShowAddModal] = useState(false);
const [newInvestor, setNewInvestor] = useState({
name: "",
type: "prospect",
lead: "Grant",
priority: "medium",
contacts: [{ name: "", email: "", title: "", location: "" }]
});
const [tooltipData, setTooltipData] = useState(null);
const [customViews, setCustomViews] = useState([]);
const [visibleColumns, setVisibleColumns] = useState(null);
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
const [showSaveViewModal, setShowSaveViewModal] = useState(false);
const [saveViewName, setSaveViewName] = useState("");
const [viewModified, setViewModified] = useState(false);
const [filterCriteria, setFilterCriteria] = useState({
search: "",
type: "",
graveyard: null,
followUp: null,
fundFilter: ""
});
const [activeFilters, setActiveFilters] = useState([]);
const [showFilterPanel, setShowFilterPanel] = useState(false);
const [customColumns, setCustomColumns] = useState([]);
const [showCustomColumnModal, setShowCustomColumnModal] = useState(false);
const [newColumnName, setNewColumnName] = useState("");
const [newColumnType, setNewColumnType] = useState("text");
const [newColumnOptions, setNewColumnOptions] = useState("");
const [contextMenu, setContextMenu] = useState(null); // { x, y, investor }
// Close context menu on click anywhere
useEffect(() => {
const handleClick = () => setContextMenu(null);
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
}, []);
const handleContextMenu = (e, investor) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, investor });
};
const contextMenuAction = (action) => {
if (!contextMenu) return;
const inv = contextMenu.investor;
const update = (field, value) => {
const updated = { ...inv, [field]: value };
setInvestors(investors.map(i => i.id === inv.id ? updated : i));
if (selectedInvestor && selectedInvestor.id === inv.id) setSelectedInvestor(updated);
};
switch (action) {
case "view":
setSelectedInvestor(inv);
break;
case "edit":
setEditingCell({ investorId: inv.id, field: "name" });
break;
case "followUp":
update("followUp", !inv.followUp);
break;
case "graveyard":
update("graveyard", !inv.graveyard);
break;
case "assignGrant":
update("lead", "Grant");
break;
case "assignMatt":
update("lead", "Matt");
break;
case "assignJP":
update("lead", "JP");
break;
case "priorityHigh":
update("priority", "high");
break;
case "priorityMedium":
update("priority", "medium");
break;
case "priorityLow":
update("priority", "low");
break;
case "duplicate":
const dup = { ...inv, id: Math.max(...investors.map(i => i.id)) + 1, name: inv.name + " (Copy)" };
setInvestors([...investors, dup]);
break;
case "delete":
if (confirm("Delete " + inv.name + "? This cannot be undone.")) {
setInvestors(investors.filter(i => i.id !== inv.id));
if (selectedInvestor && selectedInvestor.id === inv.id) setSelectedInvestor(null);
}
break;
}
setContextMenu(null);
};
// Filter investors
const filteredInvestors = useMemo(() => {
let filtered = investors;
// View filter
if (selectedView === "investors") {
filtered = filtered.filter(i => i.type === "investor");
} else if (selectedView === "prospects") {
filtered = filtered.filter(i => i.type === "prospect");
} else if (selectedView === "leads") {
filtered = filtered.filter(i => i.type === "lead");
} else if (selectedView === "followUp") {
filtered = filtered.filter(i => i.followUp);
} else if (selectedView === "graveyard") {
filtered = filtered.filter(i => i.graveyard);
} else if (selectedView.startsWith("team-")) {
const member = selectedView.replace("team-", "");
filtered = filtered.filter(i => i.lead === member);
}
// Search filter (match against entity name AND contact names/emails)
if (searchTerm) {
const lowerSearch = searchTerm.toLowerCase();
filtered = filtered.filter(i => {
const nameMatch = i.name.toLowerCase().includes(lowerSearch);
const contactMatch = i.contacts.some(c =>
c.name.toLowerCase().includes(lowerSearch) ||
c.email.toLowerCase().includes(lowerSearch)
);
return nameMatch || contactMatch;
});
}
// Advanced filters (ANDed together)
if (activeFilters.length > 0) {
filtered = filtered.filter(investor => {
return activeFilters.every(filter => {
const val = investor[filter.field];
if (filter.field === "contacts" || filter.field === "location") {
// Search in contact names and locations
const contactMatch = investor.contacts.some(c => {
const fieldVal = filter.field === "contacts" ? c.name : c.location;
if (filter.operator === "contains") return fieldVal.toLowerCase().includes(filter.value.toLowerCase());
if (filter.operator === "does not contain") return !fieldVal.toLowerCase().includes(filter.value.toLowerCase());
return true;
});
return contactMatch;
}
// Text fields
if (["name", "lead", "notes"].includes(filter.field)) {
if (filter.operator === "contains") return String(val || "").toLowerCase().includes(filter.value.toLowerCase());
if (filter.operator === "does not contain") return !String(val || "").toLowerCase().includes(filter.value.toLowerCase());
if (filter.operator === "equals") return String(val || "").toLowerCase() === filter.value.toLowerCase();
if (filter.operator === "is empty") return !val || val === "";
if (filter.operator === "is not empty") return val && val !== "";
}
// Number/currency fields
if (["fundI", "fundII", "fundIII", "tactical", "pawnE4", "pawnF4", "ten31Terahash", "satsAndStats", "joinTheFold", "total"].includes(filter.field)) {
const numVal = parseFloat(val) || 0;
const filterNum = parseFloat(filter.value) || 0;
if (filter.operator === "equals") return numVal === filterNum;
if (filter.operator === "greater than") return numVal > filterNum;
if (filter.operator === "less than") return numVal < filterNum;
if (filter.operator === "between") {
const parts = filter.value.split(",");
const min = parseFloat(parts[0]) || 0;
const max = parseFloat(parts[1]) || 0;
return numVal >= min && numVal <= max;
}
if (filter.operator === "is empty") return !val || numVal === 0;
if (filter.operator === "is not empty") return val && numVal !== 0;
}
// Date fields
if (filter.field === "lastContact") {
if (filter.operator === "is") return val === filter.value;
if (filter.operator === "is before") return val < filter.value;
if (filter.operator === "is after") return val > filter.value;
if (filter.operator === "in last N days") {
const daysNum = parseInt(filter.value) || 0;
const filterDate = new Date();
filterDate.setDate(filterDate.getDate() - daysNum);
const valDate = new Date(val);
return valDate >= filterDate;
}
if (filter.operator === "is empty") return !val;
}
// Select fields
if (["type", "priority"].includes(filter.field)) {
if (filter.operator === "is") return val === filter.value;
if (filter.operator === "is not") return val !== filter.value;
}
// Boolean fields
if (["followUp", "graveyard"].includes(filter.field)) {
if (filter.operator === "is true") return val === true;
if (filter.operator === "is false") return val === false;
}
return true;
});
});
}
// Sort
filtered.sort((a, b) => {
let aVal, bVal;
if (sortBy === "name") {
aVal = a.name;
bVal = b.name;
} else if (sortBy === "lead") {
aVal = a.lead;
bVal = b.lead;
} else if (sortBy === "priority") {
const priorityOrder = { high: 0, medium: 1, low: 2 };
aVal = priorityOrder[a.priority] || 999;
bVal = priorityOrder[b.priority] || 999;
} else if (sortBy === "type") {
aVal = a.type;
bVal = b.type;
} else {
aVal = a[sortBy] || 0;
bVal = b[sortBy] || 0;
}
if (typeof aVal === "string") {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
return 0;
});
return filtered;
}, [investors, selectedView, searchTerm, sortBy, sortOrder]);
// Calculate stats
const stats = useMemo(() => {
const totalInvestors = investors.filter(i => i.type === "investor").length;
const totalCapitalCommitted = calculateTotal(
investors.filter(i => i.type === "investor"),
"fundI"
) + calculateTotal(
investors.filter(i => i.type === "investor"),
"fundII"
) + calculateTotal(
investors.filter(i => i.type === "investor"),
"fundIII"
);
const totalProspects = investors.filter(i => i.type === "prospect").length;
const followUpCount = investors.filter(i => i.followUp && !i.graveyard).length;
return { totalInvestors, totalCapitalCommitted, totalProspects, followUpCount };
}, [investors]);
const handleSort = useCallback((column) => {
if (sortBy === column) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortBy(column);
setSortOrder("asc");
}
}, [sortBy, sortOrder]);
const handleCellChange = useCallback((id, field, value) => {
setInvestors(investors.map(inv =>
inv.id === id ? { ...inv, [field]: value } : inv
));
}, [investors]);
const handleAddInvestor = useCallback(() => {
const id = Math.max(...investors.map(i => i.id), 0) + 1;
const newInv = {
...newInvestor,
id,
followUp: false,
graveyard: false,
leadSource: "manual",
manualLeadSource: "",
fundI: 0,
fundII: 0,
fundIII: 0,
tactical: 0,
pawnE4: 0,
pawnF4: 0,
ten31Terahash: 0,
satsAndStats: 0,
joinTheFold: 0,
notes: "",
lastContact: new Date().toISOString().split("T")[0]
};
setInvestors([...investors, newInv]);
setShowAddModal(false);
setNewInvestor({
name: "",
type: "prospect",
lead: "Grant",
priority: "medium",
contacts: [{ name: "", email: "", title: "", location: "" }]
});
}, [investors, newInvestor]);
const handleDeleteInvestor = useCallback((id) => {
if (confirm("Are you sure you want to delete this investor?")) {
setInvestors(investors.filter(i => i.id !== id));
setSelectedInvestor(null);
}
}, [investors]);
const handleAddContact = useCallback(() => {
if (selectedInvestor) {
const updatedInvestor = {
...selectedInvestor,
contacts: [...selectedInvestor.contacts, { name: "", email: "", title: "", location: "" }]
};
setInvestors(investors.map(inv =>
inv.id === selectedInvestor.id ? updatedInvestor : inv
));
setSelectedInvestor(updatedInvestor);
}
}, [investors, selectedInvestor]);
const handleUpdateContact = useCallback((contactIdx, field, value) => {
if (selectedInvestor) {
const updatedContacts = selectedInvestor.contacts.map((c, idx) =>
idx === contactIdx ? { ...c, [field]: value } : c
);
const updatedInvestor = { ...selectedInvestor, contacts: updatedContacts };
setInvestors(investors.map(inv =>
inv.id === selectedInvestor.id ? updatedInvestor : inv
));
setSelectedInvestor(updatedInvestor);
}
}, [investors, selectedInvestor]);
const handleDeleteContact = useCallback((contactIdx) => {
if (selectedInvestor && selectedInvestor.contacts.length > 1) {
const updatedContacts = selectedInvestor.contacts.filter((_, idx) => idx !== contactIdx);
const updatedInvestor = { ...selectedInvestor, contacts: updatedContacts };
setInvestors(investors.map(inv =>
inv.id === selectedInvestor.id ? updatedInvestor : inv
));
setSelectedInvestor(updatedInvestor);
}
}, [investors, selectedInvestor]);
const handleInvestorUpdate = useCallback((field, value) => {
if (selectedInvestor) {
const updatedInvestor = { ...selectedInvestor, [field]: value };
setInvestors(investors.map(inv =>
inv.id === selectedInvestor.id ? updatedInvestor : inv
));
setSelectedInvestor(updatedInvestor);
}
}, [investors, selectedInvestor]);
const handleContactsHover = (e, investor) => {
if (investor.contacts.length > 1) {
const rect = e.currentTarget.getBoundingClientRect();
setTooltipData({
contacts: investor.contacts,
x: rect.right + 10,
y: rect.top
});
}
};
const handleContactsLeave = () => {
setTooltipData(null);
};
return React.createElement(
"div",
{ style: { display: "flex", height: "100vh", overflow: "hidden" } },
// Sidebar
React.createElement(
"div",
{ className: "sidebar" },
React.createElement("div", { className: "sidebar-header" }, "Ten31 CRM"),
React.createElement(
"div",
{ className: "sidebar-section" },
React.createElement("div", { className: "sidebar-section-title" }, "Views"),
React.createElement(
"div",
{
className: `view-item ${selectedView === "all" ? "active" : ""}`,
onClick: () => setSelectedView("all")
},
"All Investors"
),
React.createElement(
"div",
{
className: `view-item ${selectedView === "investors" ? "active" : ""}`,
onClick: () => setSelectedView("investors")
},
"Investors",
React.createElement("span", { className: "filter-badge" }, stats.totalInvestors)
),
React.createElement(
"div",
{
className: `view-item ${selectedView === "prospects" ? "active" : ""}`,
onClick: () => setSelectedView("prospects")
},
"Prospects",
React.createElement("span", { className: "filter-badge" }, stats.totalProspects)
),
React.createElement(
"div",
{
className: `view-item ${selectedView === "leads" ? "active" : ""}`,
onClick: () => setSelectedView("leads")
},
"Leads"
),
React.createElement(
"div",
{
className: `view-item ${selectedView === "followUp" ? "active" : ""}`,
onClick: () => setSelectedView("followUp")
},
"Follow Up",
React.createElement("span", { className: "filter-badge" }, stats.followUpCount)
),
React.createElement(
"div",
{
className: `view-item ${selectedView === "graveyard" ? "active" : ""}`,
onClick: () => setSelectedView("graveyard")
},
"Graveyard"
)
),
React.createElement(
"div",
{ className: "sidebar-section" },
React.createElement("div", { className: "sidebar-section-title" }, "Team"),
["Grant", "Matt", "JP"].map(member =>
React.createElement(
"div",
{
key: member,
className: `view-item team-member ${selectedView === `team-${member}` ? "active" : ""}`,
onClick: () => setSelectedView(`team-${member}`)
},
member
)
)
)
),
// Main Content
React.createElement(
"div",
{ className: "main-content" },
// Top Bar
React.createElement(
"div",
{ className: "top-bar" },
React.createElement("input", {
type: "text",
className: "search-input",
placeholder: "Search investors, contacts, emails...",
value: searchTerm,
onChange: (e) => setSearchTerm(e.target.value)
}),
React.createElement(
"button",
{
className: "add-button",
style: { background: activeFilters.length > 0 ? "#4f46e5" : "#334155" },
onClick: () => setShowFilterPanel(!showFilterPanel)
},
"Filter" + (activeFilters.length > 0 ? " (" + activeFilters.length + ")" : "")
),
React.createElement(
"button",
{
className: "add-button",
onClick: () => setShowAddModal(true)
},
"+ Add Investor"
)
),
// Filter Panel
showFilterPanel && React.createElement(
"div",
{ style: {
background: "#1e293b",
borderTop: "1px solid #334155",
padding: "12px 24px",
maxHeight: "400px",
overflowY: "auto"
} },
React.createElement(
"div",
{ style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "12px" } },
React.createElement("div", { style: { fontSize: "12px", fontWeight: "600", color: "#94a3b8" } }, "Filter Rules"),
activeFilters.length > 0 && React.createElement(
"a",
{
href: "#",
onClick: (e) => { e.preventDefault(); setActiveFilters([]); },
style: { fontSize: "12px", color: "#6366f1", cursor: "pointer" }
},
"Clear All"
)
),
activeFilters.map((filter, idx) => {
const allFields = ["name", "type", "lead", "priority", "contacts", "location", "fundI", "fundII", "fundIII", "tactical", "pawnE4", "pawnF4", "ten31Terahash", "satsAndStats", "joinTheFold", "total", "lastContact", "notes", "followUp", "graveyard", ...customColumns.map(c => c.id)];
const getOperators = (field) => {
if (["name", "lead", "notes", "contacts", "location"].includes(field)) return ["contains", "does not contain", "equals", "is empty", "is not empty"];
if (["fundI", "fundII", "fundIII", "tactical", "pawnE4", "pawnF4", "ten31Terahash", "satsAndStats", "joinTheFold", "total"].includes(field)) return ["equals", "greater than", "less than", "between", "is empty", "is not empty"];
if (field === "lastContact") return ["is", "is before", "is after", "in last N days", "is empty"];
if (["type", "priority"].includes(field)) return ["is", "is not"];
if (["followUp", "graveyard"].includes(field)) return ["is true", "is false"];
return ["contains", "equals"];
};
return React.createElement(
"div",
{ key: idx, style: { display: "flex", gap: "8px", marginBottom: "8px", alignItems: "center", height: "40px" } },
React.createElement(
"select",
{
value: filter.field,
onChange: (e) => {
const updated = [...activeFilters];
updated[idx].field = e.target.value;
updated[idx].operator = getOperators(e.target.value)[0];
updated[idx].value = "";
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "0 1 120px" }
},
allFields.map(f => React.createElement("option", { key: f, value: f }, f))
),
React.createElement(
"select",
{
value: filter.operator,
onChange: (e) => {
const updated = [...activeFilters];
updated[idx].operator = e.target.value;
updated[idx].value = "";
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "0 1 110px" }
},
getOperators(filter.field).map(op => React.createElement("option", { key: op, value: op }, op))
),
["is empty", "is not empty", "is true", "is false"].includes(filter.operator) ? null : (
filter.field === "type" ? React.createElement(
"select",
{
value: filter.value,
onChange: (e) => {
const updated = [...activeFilters];
updated[idx].value = e.target.value;
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "1" }
},
["investor", "prospect", "advisor", "lead"].map(t => React.createElement("option", { key: t, value: t }, t))
) : filter.field === "priority" ? React.createElement(
"select",
{
value: filter.value,
onChange: (e) => {
const updated = [...activeFilters];
updated[idx].value = e.target.value;
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "1" }
},
["high", "medium", "low"].map(p => React.createElement("option", { key: p, value: p }, p))
) : filter.field === "lead" ? React.createElement(
"select",
{
value: filter.value,
onChange: (e) => {
const updated = [...activeFilters];
updated[idx].value = e.target.value;
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "1" }
},
["Grant", "Matt", "JP"].map(l => React.createElement("option", { key: l, value: l }, l))
) : filter.operator === "between" ? React.createElement(
"div",
{ style: { display: "flex", gap: "4px", flex: "1" } },
React.createElement("input", {
type: "text",
inputMode: "numeric",
placeholder: "Min",
value: filter.value.split(",")[0] || "",
onChange: (e) => {
const updated = [...activeFilters];
const parts = filter.value.split(",");
updated[idx].value = e.target.value + "," + (parts[1] || "");
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "1" }
}),
React.createElement("input", {
type: "text",
inputMode: "numeric",
placeholder: "Max",
value: filter.value.split(",")[1] || "",
onChange: (e) => {
const updated = [...activeFilters];
const parts = filter.value.split(",");
updated[idx].value = (parts[0] || "") + "," + e.target.value;
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "1" }
})
) : React.createElement("input", {
type: filter.field === "lastContact" ? "date" : (["fundI", "fundII", "fundIII", "tactical", "pawnE4", "pawnF4", "ten31Terahash", "satsAndStats", "joinTheFold", "total"].includes(filter.field) ? "text" : "text"),
inputMode: ["fundI", "fundII", "fundIII", "tactical", "pawnE4", "pawnF4", "ten31Terahash", "satsAndStats", "joinTheFold", "total"].includes(filter.field) ? "numeric" : undefined,
placeholder: filter.operator === "in last N days" ? "Days" : "Value",
value: filter.value,
onChange: (e) => {
const updated = [...activeFilters];
updated[idx].value = e.target.value;
setActiveFilters(updated);
},
style: { padding: "4px", background: "#0f172a", border: "1px solid #334155", color: "#e2e8f0", borderRadius: "4px", fontSize: "12px", flex: "1" }
})
),
React.createElement(
"button",
{
onClick: () => setActiveFilters(activeFilters.filter((_, i) => i !== idx)),
style: { padding: "4px 8px", background: "#ef4444", border: "none", color: "#fff", borderRadius: "4px", cursor: "pointer", fontSize: "12px", flex: "0 0 30px" }
},
"×"
)
);
}),
React.createElement(
"button",
{
onClick: () => setActiveFilters([...activeFilters, { field: "name", operator: "contains", value: "" }]),
style: { marginTop: "8px", padding: "4px 12px", background: "#6366f1", border: "none", color: "#fff", borderRadius: "4px", cursor: "pointer", fontSize: "12px" }
},
"+ Add Filter"
)
),// Dashboard (if viewing all)
selectedView === "all" && React.createElement(
"div",
{ style: { padding: "16px 24px" } },
React.createElement(
"div",
{ className: "dashboard" },
React.createElement(
"div",
{ className: "stat-card" },
React.createElement("div", { className: "stat-card-value" }, stats.totalInvestors),
React.createElement("div", { className: "stat-card-label" }, "Active Investors")
),
React.createElement(
"div",
{ className: "stat-card" },
React.createElement("div", { className: "stat-card-value" }, formatCurrency(stats.totalCapitalCommitted)),
React.createElement("div", { className: "stat-card-label" }, "Capital Committed")
),
React.createElement(
"div",
{ className: "stat-card" },
React.createElement("div", { className: "stat-card-value" }, stats.totalProspects),
React.createElement("div", { className: "stat-card-label" }, "Prospects")
),
React.createElement(
"div",
{ className: "stat-card" },
React.createElement("div", { className: "stat-card-value" }, stats.followUpCount),
React.createElement("div", { className: "stat-card-label" }, "Follow-Ups")
)
)
),
// Table
React.createElement(
"div",
{ className: "table-container" },
React.createElement(
"table",
{ className: "table" },
React.createElement(
"thead",
null,
React.createElement(
"tr",
null,
React.createElement(
"th",
{ onClick: () => handleSort("name"), style: { cursor: "pointer" } },
"Name " + (sortBy === "name" ? (sortOrder === "asc" ? "↑" : "↓") : "")
),
React.createElement(
"th",
{ onClick: () => handleSort("type"), style: { cursor: "pointer" } },
"Type " + (sortBy === "type" ? (sortOrder === "asc" ? "↑" : "↓") : "")
),
React.createElement(
"th",
{ onClick: () => handleSort("contacts"), style: { cursor: "pointer" } },
"Contacts " + (sortBy === "contacts" ? (sortOrder === "asc" ? "↑" : "↓") : "")
),
React.createElement(
"th",
{ onClick: () => handleSort("lead"), style: { cursor: "pointer" } },
"Lead " + (sortBy === "lead" ? (sortOrder === "asc" ? "↑" : "↓") : "")
),
React.createElement(
"th",
{ onClick: () => handleSort("priority"), style: { cursor: "pointer" } },
"Priority " + (sortBy === "priority" ? (sortOrder === "asc" ? "↑" : "↓") : "")
),
React.createElement("th", null, "Fund I"),
React.createElement("th", null, "Fund II"),
React.createElement("th", null, "Fund III"),
React.createElement("th", null, "Tactical"),
React.createElement(
"th",
null,
"Custom Columns"
),
React.createElement("th", { style: { width: "30px", textAlign: "center" } }, "+"),
React.createElement("th", null, "Last Contact")
)
),
React.createElement(
"tbody",
null,
filteredInvestors.map((investor, rowIdx) => {
const editableColumns = ["name","type","lead","priority","fundI","fundII","fundIII","tactical"];
const isEd = (field) => editingCell && editingCell.investorId === investor.id && editingCell.field === field;
const dblClick = (field) => (e) => { e.stopPropagation(); setEditingCell({ investorId: investor.id, field }); };
const commitEdit = (field, raw) => {
const fundFields = ["fundI","fundII","fundIII","tactical","pawnE4","pawnF4","ten31Terahash","satsAndStats","joinTheFold"];
const val = fundFields.includes(field) ? (parseInt(String(raw).replace(/[^0-9]/g,"")) || 0) : raw;
handleCellChange(investor.id, field, val);
setEditingCell(null);
};
const navigateCell = (field, raw, direction) => {
const fundFields = ["fundI","fundII","fundIII","tactical","pawnE4","pawnF4","ten31Terahash","satsAndStats","joinTheFold"];
const val = fundFields.includes(field) ? (parseInt(String(raw).replace(/[^0-9]/g,"")) || 0) : raw;
handleCellChange(investor.id, field, val);
const colIdx = editableColumns.indexOf(field);
if (direction === "right") {
if (colIdx < editableColumns.length - 1) {
setEditingCell({ investorId: investor.id, field: editableColumns[colIdx + 1] });
} else if (rowIdx < filteredInvestors.length - 1) {
setEditingCell({ investorId: filteredInvestors[rowIdx + 1].id, field: editableColumns[0] });
} else { setEditingCell(null); }
} else if (direction === "left") {
if (colIdx > 0) {
setEditingCell({ investorId: investor.id, field: editableColumns[colIdx - 1] });
} else if (rowIdx > 0) {
setEditingCell({ investorId: filteredInvestors[rowIdx - 1].id, field: editableColumns[editableColumns.length - 1] });
} else { setEditingCell(null); }
} else if (direction === "down") {
if (rowIdx < filteredInvestors.length - 1) {
setEditingCell({ investorId: filteredInvestors[rowIdx + 1].id, field });
} else { setEditingCell(null); }
} else if (direction === "up") {
if (rowIdx > 0) {
setEditingCell({ investorId: filteredInvestors[rowIdx - 1].id, field });
} else { setEditingCell(null); }
}
};
const cellKeyHandler = (field) => (e) => {
if (e.key === "Tab") {
e.preventDefault();
navigateCell(field, e.target.value, e.shiftKey ? "left" : "right");
} else if (e.key === "Enter") {
e.preventDefault();
navigateCell(field, e.target.value, "down");
} else if (e.key === "Escape") {
setEditingCell(null);
}
};
const editInput = (field, val, type) => {
const isNum = type === "num";
return React.createElement("input", {
type: "text",
inputMode: isNum ? "numeric" : "text",
pattern: isNum ? "[0-9]*" : undefined,
className: "cell-edit-input",
defaultValue: isNum ? val : val,
autoFocus: true,
onClick: (e) => e.stopPropagation(),
onBlur: (e) => { if (editingCell && editingCell.field === field && editingCell.investorId === investor.id) commitEdit(field, e.target.value); },
onKeyDown: cellKeyHandler(field)
});
};
const editSelect = (field, val, options) => {
return React.createElement("select", {
className: "cell-edit-select",
defaultValue: val,
autoFocus: true,
onClick: (e) => e.stopPropagation(),
onBlur: (e) => { if (editingCell && editingCell.field === field && editingCell.investorId === investor.id) commitEdit(field, e.target.value); },
onKeyDown: cellKeyHandler(field),
onChange: (e) => commitEdit(field, e.target.value)
}, options.map(o => React.createElement("option", { key: o, value: o }, o)));
};
return React.createElement(
"tr",
{
key: investor.id,
onClick: () => setSelectedInvestor(investor),
onContextMenu: (e) => handleContextMenu(e, investor),
className: (selectedInvestor && selectedInvestor.id === investor.id) ? "selected" : ""
},
// Name
React.createElement("td", {
className: isEd("name") ? "editable editing" : "editable",
onDoubleClick: dblClick("name")
}, isEd("name") ? editInput("name", investor.name, "text") : investor.name),
// Type
React.createElement("td", {
className: isEd("type") ? "editable editing" : "editable",
onDoubleClick: dblClick("type")
}, isEd("type")
? editSelect("type", investor.type, ["investor","prospect","advisor"])
: React.createElement("span", { className: `badge ${investor.type}`, style: { backgroundColor: getTypeColor(investor.type) } }, investor.type)
),
// Contacts (not inline editable)
React.createElement("td", {
onMouseEnter: (e) => handleContactsHover(e, investor),
onMouseLeave: handleContactsLeave,
style: { position: "relative" }
}, (investor.contacts[0] ? investor.contacts[0].name : "") + (investor.contacts.length > 1 ? " +" + (investor.contacts.length - 1) + " more" : "")),
// Lead
React.createElement("td", {
className: isEd("lead") ? "editable editing" : "editable",
onDoubleClick: dblClick("lead")
}, isEd("lead")
? editSelect("lead", investor.lead, ["Grant","Matt","JP"])
: investor.lead
),
// Priority
React.createElement("td", {
className: isEd("priority") ? "editable editing" : "editable",
onDoubleClick: dblClick("priority")
}, isEd("priority")
? editSelect("priority", investor.priority, ["high","medium","low"])
: React.createElement("span", { className: `badge ${investor.priority}`, style: { backgroundColor: getPriorityColor(investor.priority) } }, investor.priority)
),
// Fund I
React.createElement("td", {
className: isEd("fundI") ? "editable editing" : "editable",
onDoubleClick: dblClick("fundI")
}, isEd("fundI") ? editInput("fundI", investor.fundI, "num") : (investor.fundI ? formatCurrency(investor.fundI) : "—")),
// Fund II
React.createElement("td", {
className: isEd("fundII") ? "editable editing" : "editable",
onDoubleClick: dblClick("fundII")
}, isEd("fundII") ? editInput("fundII", investor.fundII, "num") : (investor.fundII ? formatCurrency(investor.fundII) : "—")),
// Fund III
React.createElement("td", {
className: isEd("fundIII") ? "editable editing" : "editable",
onDoubleClick: dblClick("fundIII")
}, isEd("fundIII") ? editInput("fundIII", investor.fundIII, "num") : (investor.fundIII ? formatCurrency(investor.fundIII) : "—")),
// Tactical
React.createElement("td", {
className: isEd("tactical") ? "editable editing" : "editable",
onDoubleClick: dblClick("tactical")
}, isEd("tactical") ? editInput("tactical", investor.tactical, "num") : (investor.tactical ? formatCurrency(investor.tactical) : "—")),
// Last Contact
React.createElement("td", null, getRelativeDate(investor.lastContact),
// Custom columns
customColumns.map(col => {
const val = (investor.customFields && investor.customFields[col.id] !== undefined && investor.customFields[col.id] !== null) ? investor.customFields[col.id] : (col.type === "checkbox" ? false : col.type === "number" || col.type === "currency" ? 0 : "");
const isEd2 = editingCell && editingCell.investorId === investor.id && editingCell.field === col.id;
return React.createElement("td", {
key: col.id,
className: isEd2 ? "editable editing" : "editable",
onDoubleClick: () => { if (col.type !== "checkbox") { setEditingCell({ investorId: investor.id, field: col.id }); } },
style: { cursor: col.type === "checkbox" ? "pointer" : "default" }
},
col.type === "checkbox" ?
React.createElement("input", {
type: "checkbox",
checked: val || false,
onChange: (e) => {
handleCellChange(investor.id, "customFields", { ...(investor.customFields || {}), [col.id]: e.target.checked });
},
onClick: (e) => e.stopPropagation(),
style: { cursor: "pointer" }
}) :
col.type === "currency" ?
isEd2 ? editInput(col.id, val, "num") : (val ? formatCurrency(val) : "—") :
col.type === "date" ?
isEd2 ? React.createElement("input", {
type: "date",
defaultValue: val,
autoFocus: true,
onClick: (e) => e.stopPropagation(),
onBlur: (e) => { handleCellChange(investor.id, "customFields", { ...(investor.customFields || {}), [col.id]: e.target.value }); setEditingCell(null); }
}) : (val ? getRelativeDate(val) : "—") :
col.type === "dropdown" ?
isEd2 ? React.createElement("select", {
defaultValue: val,
autoFocus: true,
onClick: (e) => e.stopPropagation(),
onBlur: (e) => { handleCellChange(investor.id, "customFields", { ...(investor.customFields || {}), [col.id]: e.target.value }); setEditingCell(null); }
}, (col.options || []).map(o => React.createElement("option", { key: o, value: o }, o))) : val :
col.type === "longText" ?
isEd2 ? React.createElement("textarea", {
defaultValue: val,
autoFocus: true,
onClick: (e) => e.stopPropagation(),
onBlur: (e) => { handleCellChange(investor.id, "customFields", { ...(investor.customFields || {}), [col.id]: e.target.value }); setEditingCell(null); }
}) : (val && val.length > 40 ? val.slice(0, 40) + "..." : val) :
col.type === "number" ?
isEd2 ? editInput(col.id, val, "num") : val :
isEd2 ? editInput(col.id, val, "text") : val
);
}),
// + Column button
React.createElement("td", {
style: { width: "30px", textAlign: "center", cursor: "pointer", color: "#6366f1" },
onClick: () => setShowCustomColumnModal(true)
}, "+"))
);
})
)
)
)
),
// Detail Panel
selectedInvestor && React.createElement(
"div",
{ className: "detail-panel open" },
React.createElement(
"div",
{ className: "detail-header" },
React.createElement(
"div",
{ className: "detail-title" },
React.createElement("h2", null, selectedInvestor.name),
React.createElement(
"span",
{
className: `badge ${selectedInvestor.type}`,
style: { backgroundColor: getTypeColor(selectedInvestor.type) }
},
selectedInvestor.type
)
),
React.createElement(
"button",
{
className: "close-button",
onClick: () => setSelectedInvestor(null)
},
"×"
)
),
React.createElement(
"div",
{ className: "detail-body" },
// Contacts Section
React.createElement(
"div",
{ className: "contacts-section" },
React.createElement(
"div",
{ className: "contacts-header" },
React.createElement("div", { className: "detail-section-title" }, "Contacts"),
React.createElement(
"button",
{ className: "add-contact-button", onClick: handleAddContact },
"+ Add Contact"
)
),
selectedInvestor.contacts.map((contact, idx) =>
React.createElement(
"div",
{ key: idx, className: "contact-card" },
React.createElement(
"div",
{ className: "contact-info" },
React.createElement("input", {
type: "text",
placeholder: "Contact name",
value: contact.name,
onChange: (e) => handleUpdateContact(idx, "name", e.target.value),
style: { className: "contact-name", marginBottom: "4px", display: "block", width: "100%", padding: "4px", background: "#0f172a", border: "1px solid #334155", borderRadius: "4px", color: "#e2e8f0" }
}),
React.createElement("input", {
type: "email",
placeholder: "email@example.com",
value: contact.email,
onChange: (e) => handleUpdateContact(idx, "email", e.target.value),
style: { className: "contact-email", marginBottom: "4px", display: "block", width: "100%", padding: "4px", background: "#0f172a", border: "1px solid #334155", borderRadius: "4px", color: "#e2e8f0" }
}),
React.createElement("input", {
type: "text",
placeholder: "Title (optional)",
value: contact.title,
onChange: (e) => handleUpdateContact(idx, "title", e.target.value),
style: { className: "contact-title", display: "block", width: "100%", padding: "4px", marginBottom: "4px", background: "#0f172a", border: "1px solid #334155", borderRadius: "4px", color: "#e2e8f0" }
}),
React.createElement(LocationInput, {
value: contact.location || "",
onChange: (val) => handleUpdateContact(idx, "location", val),
style: { marginBottom: "0" }
})
),
selectedInvestor.contacts.length > 1 && React.createElement(
"button",
{
className: "delete-contact-button",
onClick: () => handleDeleteContact(idx)
},
"×"
)
)
)
),
// Entity Info
React.createElement(
"div",
{ className: "detail-section" },
React.createElement("div", { className: "detail-section-title" }, "Entity Info"),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Type"),
React.createElement(
"select",
{
value: selectedInvestor.type,
onChange: (e) => handleInvestorUpdate("type", e.target.value)
},
React.createElement("option", { value: "investor" }, "Investor"),
React.createElement("option", { value: "prospect" }, "Prospect"),
React.createElement("option", { value: "lead" }, "Lead")
)
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Lead"),
React.createElement(
"select",
{
value: selectedInvestor.lead,
onChange: (e) => handleInvestorUpdate("lead", e.target.value)
},
["Grant", "Matt", "JP"].map(name =>
React.createElement("option", { key: name, value: name }, name)
)
)
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Priority"),
React.createElement(
"select",
{
value: selectedInvestor.priority,
onChange: (e) => handleInvestorUpdate("priority", e.target.value)
},
React.createElement("option", { value: "high" }, "High"),
React.createElement("option", { value: "medium" }, "Medium"),
React.createElement("option", { value: "low" }, "Low")
)
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Status Flags"),
React.createElement(
"div",
{ style: { display: "flex", gap: "12px" } },
React.createElement(
"label",
{ style: { display: "flex", alignItems: "center", gap: "6px", cursor: "pointer" } },
React.createElement("input", {
type: "checkbox",
checked: selectedInvestor.followUp,
onChange: (e) => handleInvestorUpdate("followUp", e.target.checked)
}),
"Follow Up"
),
React.createElement(
"label",
{ style: { display: "flex", alignItems: "center", gap: "6px", cursor: "pointer" } },
React.createElement("input", {
type: "checkbox",
checked: selectedInvestor.graveyard,
onChange: (e) => handleInvestorUpdate("graveyard", e.target.checked)
}),
"Graveyard"
)
)
)
),
// Fund Commitments
// Custom Fields
customColumns.length > 0 && React.createElement(
"div",
{ className: "detail-section" },
React.createElement("div", { className: "detail-section-title" }, "Custom Fields"),
customColumns.map(col =>
React.createElement(
"div",
{ key: col.id, className: "detail-field" },
React.createElement("label", null, col.name),
col.type === "checkbox" ? React.createElement("input", {
type: "checkbox",
checked: (selectedInvestor.customFields && selectedInvestor.customFields[col.id]) || false,
onChange: (e) => handleInvestorUpdate("customFields", { ...(selectedInvestor.customFields || {}), [col.id]: e.target.checked })
}) : col.type === "date" ? React.createElement("input", {
type: "date",
value: (selectedInvestor.customFields && selectedInvestor.customFields[col.id]) || "",
onChange: (e) => handleInvestorUpdate("customFields", { ...(selectedInvestor.customFields || {}), [col.id]: e.target.value })
}) : col.type === "dropdown" ? React.createElement(
"select",
{
value: (selectedInvestor.customFields && selectedInvestor.customFields[col.id]) || "",
onChange: (e) => handleInvestorUpdate("customFields", { ...(selectedInvestor.customFields || {}), [col.id]: e.target.value })
},
React.createElement("option", { value: "" }, "—"),
(col.options || []).map(o => React.createElement("option", { key: o, value: o }, o))
) : col.type === "longText" ? React.createElement("textarea", {
value: (selectedInvestor.customFields && selectedInvestor.customFields[col.id]) || "",
onChange: (e) => handleInvestorUpdate("customFields", { ...(selectedInvestor.customFields || {}), [col.id]: e.target.value })
}) : React.createElement("input", {
type: col.type === "currency" || col.type === "number" ? "text" : "text",
inputMode: (col.type === "currency" || col.type === "number") ? "numeric" : undefined,
pattern: (col.type === "currency" || col.type === "number") ? "[0-9]*" : undefined,
value: (selectedInvestor.customFields && selectedInvestor.customFields[col.id]) || "",
onChange: (e) => handleInvestorUpdate("customFields", { ...(selectedInvestor.customFields || {}), [col.id]: e.target.value })
})
)
)
),
React.createElement(
"div",
{ className: "detail-section" },
React.createElement("div", { className: "detail-section-title" }, "Fund Commitments"),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Fund I"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.fundI,
onChange: (e) => handleInvestorUpdate("fundI", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Fund II"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.fundII,
onChange: (e) => handleInvestorUpdate("fundII", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Fund III"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.fundIII,
onChange: (e) => handleInvestorUpdate("fundIII", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Tactical"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.tactical,
onChange: (e) => handleInvestorUpdate("tactical", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Pawn E4"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.pawnE4,
onChange: (e) => handleInvestorUpdate("pawnE4", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Pawn F4"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.pawnF4,
onChange: (e) => handleInvestorUpdate("pawnF4", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Ten31 Terahash"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.ten31Terahash,
onChange: (e) => handleInvestorUpdate("ten31Terahash", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Sats & Stats"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.satsAndStats,
onChange: (e) => handleInvestorUpdate("satsAndStats", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Join The Fold"),
React.createElement("input", {
type: "text",
inputMode: "numeric",
pattern: "[0-9]*",
value: selectedInvestor.joinTheFold,
onChange: (e) => handleInvestorUpdate("joinTheFold", e.target.value)
})
)
),
// Lead Source
React.createElement(
"div",
{ className: "detail-section" },
React.createElement("div", { className: "detail-section-title" }, "Lead Source"),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Source"),
React.createElement(
"select",
{
value: selectedInvestor.leadSource,
onChange: (e) => handleInvestorUpdate("leadSource", e.target.value)
},
React.createElement("option", { value: "conference" }, "Conference"),
React.createElement("option", { value: "inbound" }, "Inbound"),
React.createElement("option", { value: "outbound" }, "Outbound"),
React.createElement("option", { value: "referral" }, "Referral"),
React.createElement("option", { value: "network" }, "Network"),
React.createElement("option", { value: "personal" }, "Personal"),
React.createElement("option", { value: "manual" }, "Manual")
)
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Manual Notes"),
React.createElement("input", {
type: "text",
value: selectedInvestor.manualLeadSource || "",
onChange: (e) => handleInvestorUpdate("manualLeadSource", e.target.value)
})
)
),
// Notes & Contact Info
React.createElement(
"div",
{ className: "detail-section" },
React.createElement("div", { className: "detail-section-title" }, "Notes & Contact"),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Notes"),
React.createElement("textarea", {
value: selectedInvestor.notes || "",
onChange: (e) => handleInvestorUpdate("notes", e.target.value)
})
),
React.createElement(
"div",
{ className: "detail-field" },
React.createElement("label", null, "Last Contact"),
React.createElement("input", {
type: "text",
value: selectedInvestor.lastContact || "",
onChange: (e) => handleInvestorUpdate("lastContact", e.target.value)
})
),
React.createElement(
"button",
{
onClick: () => handleDeleteInvestor(selectedInvestor.id),
style: {
marginTop: "16px",
padding: "8px 16px",
background: "#ef4444",
color: "#fff",
border: "none",
borderRadius: "6px",
cursor: "pointer",
width: "100%"
}
},
"Delete Investor"
)
)
)
),
// Custom Column Modal
React.createElement(
"div",
{ className: "modal" + (showCustomColumnModal ? " open" : "") },
React.createElement(
"div",
{ className: "modal-content" },
React.createElement("div", { className: "modal-header" }, "Add Custom Column"),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Column Name"),
React.createElement("input", {
type: "text",
value: newColumnName,
onChange: (e) => setNewColumnName(e.target.value),
placeholder: "e.g., Intro Source"
})
),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Data Type"),
React.createElement(
"select",
{
value: newColumnType,
onChange: (e) => setNewColumnType(e.target.value)
},
React.createElement("option", { value: "text" }, "Text"),
React.createElement("option", { value: "number" }, "Number"),
React.createElement("option", { value: "currency" }, "Currency"),
React.createElement("option", { value: "date" }, "Date"),
React.createElement("option", { value: "longText" }, "Long Text"),
React.createElement("option", { value: "checkbox" }, "Checkbox"),
React.createElement("option", { value: "dropdown" }, "Dropdown")
)
),
newColumnType === "dropdown" && React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Options (comma-separated)"),
React.createElement("input", {
type: "text",
value: newColumnOptions,
onChange: (e) => setNewColumnOptions(e.target.value),
placeholder: "Option 1, Option 2, Option 3"
})
),
React.createElement(
"div",
{ className: "modal-buttons" },
React.createElement(
"button",
{
className: "modal-button cancel",
onClick: () => {
setShowCustomColumnModal(false);
setNewColumnName("");
setNewColumnType("text");
setNewColumnOptions("");
}
},
"Cancel"
),
React.createElement(
"button",
{
className: "modal-button primary",
onClick: () => {
if (newColumnName.trim()) {
const colId = "custom_" + (Math.max(...customColumns.map(c => parseInt(c.id.replace("custom_", "")) || 0), 0) + 1);
const options = newColumnType === "dropdown" ? newColumnOptions.split(",").map(o => o.trim()).filter(o => o) : [];
setCustomColumns([...customColumns, { id: colId, name: newColumnName, type: newColumnType, options }]);
setInvestors(investors.map(inv => ({
...inv,
customFields: { ...(inv.customFields || {}), [colId]: newColumnType === "checkbox" ? false : newColumnType === "number" || newColumnType === "currency" ? 0 : "" }
})));
setShowCustomColumnModal(false);
setNewColumnName("");
setNewColumnType("text");
setNewColumnOptions("");
}
},
disabled: !newColumnName.trim()
},
"Create Column"
)
)
)
),
// Add Investor Modal
React.createElement(
"div",
{ className: `modal ${showAddModal ? "open" : ""}` },
React.createElement(
"div",
{ className: "modal-content" },
React.createElement("div", { className: "modal-header" }, "Add New Investor"),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Entity Name"),
React.createElement("input", {
type: "text",
value: newInvestor.name,
onChange: (e) => setNewInvestor({ ...newInvestor, name: e.target.value })
})
),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Type"),
React.createElement(
"select",
{
value: newInvestor.type,
onChange: (e) => setNewInvestor({ ...newInvestor, type: e.target.value })
},
React.createElement("option", { value: "investor" }, "Investor"),
React.createElement("option", { value: "prospect" }, "Prospect"),
React.createElement("option", { value: "lead" }, "Lead")
)
),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Lead"),
React.createElement(
"select",
{
value: newInvestor.lead,
onChange: (e) => setNewInvestor({ ...newInvestor, lead: e.target.value })
},
["Grant", "Matt", "JP"].map(name =>
React.createElement("option", { key: name, value: name }, name)
)
)
),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Priority"),
React.createElement(
"select",
{
value: newInvestor.priority,
onChange: (e) => setNewInvestor({ ...newInvestor, priority: e.target.value })
},
React.createElement("option", { value: "high" }, "High"),
React.createElement("option", { value: "medium" }, "Medium"),
React.createElement("option", { value: "low" }, "Low")
)
),
React.createElement("div", null, React.createElement("div", { className: "detail-section-title", style: { marginBottom: "12px", marginTop: "16px" } }, "First Contact")),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Name"),
React.createElement("input", {
type: "text",
value: newInvestor.contacts[0].name,
onChange: (e) => setNewInvestor({
...newInvestor,
contacts: [{ ...newInvestor.contacts[0], name: e.target.value }, ...newInvestor.contacts.slice(1)]
})
})
),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Email"),
React.createElement("input", {
type: "email",
value: newInvestor.contacts[0].email,
onChange: (e) => setNewInvestor({
...newInvestor,
contacts: [{ ...newInvestor.contacts[0], email: e.target.value }, ...newInvestor.contacts.slice(1)]
})
})
),
React.createElement(
"div",
{ className: "modal-field" },
React.createElement("label", null, "Title (optional)"),
React.createElement("input", {
type: "text",
value: newInvestor.contacts[0].title,
onChange: (e) => setNewInvestor({
...newInvestor,
contacts: [{ ...newInvestor.contacts[0], title: e.target.value }, ...newInvestor.contacts.slice(1)]
})
})
),
React.createElement(
"div",
{ className: "modal-buttons" },
React.createElement(
"button",
{
className: "modal-button cancel",
onClick: () => {
setShowAddModal(false);
setNewInvestor({
name: "",
type: "prospect",
lead: "Grant",
priority: "medium",
contacts: [{ name: "", email: "", title: "", location: "" }]
});
}
},
"Cancel"
),
React.createElement(
"button",
{
className: "modal-button primary",
onClick: handleAddInvestor,
disabled: !newInvestor.name || !newInvestor.contacts[0].email
},
"Add Investor"
)
)
)
),
// Contacts Tooltip
tooltipData && React.createElement(
"div",
{
className: "tooltip",
style: { left: tooltipData.x + "px", top: tooltipData.y + "px" }
},
tooltipData.contacts.map((contact, idx) =>
React.createElement(
"div",
{ key: idx, className: "tooltip-contact" },
React.createElement("div", { className: "tooltip-contact-name" }, contact.name),
React.createElement("div", { className: "tooltip-contact-email" }, contact.email),
contact.title && React.createElement("div", { className: "tooltip-contact-email" }, contact.title),
contact.location && React.createElement("div", { className: "tooltip-contact-email", style: { color: "#64748b" } }, contact.location)
)
)
),
// Context Menu
contextMenu && React.createElement(
"div",
{
className: "context-menu",
style: { left: contextMenu.x + "px", top: contextMenu.y + "px" },
onClick: (e) => e.stopPropagation()
},
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("view")
}, "\u25B6", " View Details"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("edit")
}, "\u270F", " Edit"),
React.createElement("div", { className: "context-menu-divider" }),
React.createElement("div", { className: "context-menu-label" }, "Assign To"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("assignGrant")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.lead === "Grant" ? "\u2713" : ""), " Grant"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("assignMatt")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.lead === "Matt" ? "\u2713" : ""), " Matt"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("assignJP")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.lead === "JP" ? "\u2713" : ""), " JP"),
React.createElement("div", { className: "context-menu-divider" }),
React.createElement("div", { className: "context-menu-label" }, "Priority"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("priorityHigh")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.priority === "high" ? "\u2713" : ""), " High"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("priorityMedium")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.priority === "medium" ? "\u2713" : ""), " Medium"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("priorityLow")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.priority === "low" ? "\u2713" : ""), " Low"),
React.createElement("div", { className: "context-menu-divider" }),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("followUp")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.followUp ? "\u2713" : ""), contextMenu.investor.followUp ? " Remove from Follow-Up" : " Add to Follow-Up"),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("graveyard")
}, React.createElement("span", { className: "context-menu-check" }, contextMenu.investor.graveyard ? "\u2713" : ""), contextMenu.investor.graveyard ? " Restore from Graveyard" : " Move to Graveyard"),
React.createElement("div", { className: "context-menu-divider" }),
React.createElement("div", {
className: "context-menu-item",
onClick: () => contextMenuAction("duplicate")
}, "\u2398", " Duplicate"),
React.createElement("div", {
className: "context-menu-item danger",
onClick: () => contextMenuAction("delete")
}, "\u2716", " Delete")
)
);
}
// Render App
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(React.createElement(App));
</script>
</body>
</html>