feat: Add InvestorInsightCache model and implement caching for investor insights
This commit is contained in:
+147
-144
@@ -7,23 +7,43 @@
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@page {
|
||||
margin: 0;
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: white;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Each page is exactly one A4 sheet */
|
||||
.page {
|
||||
page-break-after: always;
|
||||
min-height: 100vh;
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
position: relative;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
|
||||
/* Adds a break between pages (for print/PDF) */
|
||||
.page-with-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
/* Inner content wrapper for consistent padding */
|
||||
.page-content {
|
||||
box-sizing: border-box;
|
||||
padding: 48px; /* equivalent to Tailwind p-12 */
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
@@ -32,131 +52,132 @@
|
||||
font-size: 12px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
/* Ensure the footer text stays inside page bounds */
|
||||
.page-footer {
|
||||
position: absolute;
|
||||
bottom: 48px;
|
||||
right: 48px;
|
||||
font-size: 10px;
|
||||
color: #9ca3af; /* Tailwind gray-400 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-white">
|
||||
<!-- Page 1: Investor Profile -->
|
||||
<div class="page p-12">
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 mb-2">Investor Profile</p>
|
||||
<h1 class="text-4xl font-bold text-gray-900">
|
||||
{{ investor.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded text-sm"
|
||||
>
|
||||
<body>
|
||||
<!-- Page 1 -->
|
||||
<div class="page page-with-break">
|
||||
<div class="page-content">
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 mb-2">Investor Profile</p>
|
||||
<h1 class="text-4xl font-bold text-gray-900">
|
||||
{{ investor.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<a
|
||||
href="{{ investor.website }}"
|
||||
target="_blank"
|
||||
class="no-underline text-gray-700"
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded text-sm no-underline"
|
||||
>Visit Website →</a
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-8">
|
||||
<!-- Left Column -->
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="text-sm font-bold text-gray-900 uppercase mb-4"
|
||||
>
|
||||
Investor Description
|
||||
</h2>
|
||||
<p class="text-sm text-gray-700 leading-relaxed">
|
||||
{{ investor.description or 'No description
|
||||
available.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="text-sm font-bold text-gray-900 uppercase mb-4"
|
||||
>
|
||||
Portfolio Highlights
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if investor.portfolio_highlights %} {% for
|
||||
company in investor.portfolio_highlights[:5] %}
|
||||
<span class="tag">{{ company }}</span>
|
||||
{% endfor %} {% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
No portfolio highlights available
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="text-sm font-bold text-gray-900 uppercase mb-4"
|
||||
>
|
||||
Senior Leadership
|
||||
</h2>
|
||||
{% if investor.team_members %} {% for member in
|
||||
investor.team_members[:2] %}
|
||||
<div class="mb-3">
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ member.role or member.title or 'Team Member'
|
||||
}}
|
||||
</p>
|
||||
{% if member.email %}
|
||||
<p class="text-xs text-blue-600">
|
||||
{{ member.email }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %} {% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
No team information available
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||
Key Data
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 gap-8 flex-grow">
|
||||
<!-- Left Column -->
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||
Investor Description
|
||||
</h2>
|
||||
<p class="text-sm text-gray-700 leading-relaxed">
|
||||
{{ investor.description or 'No description available.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">Headquarters:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ investor.headquarters or 'N/A' }}
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||
Portfolio Highlights
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if investor.portfolio_highlights %}
|
||||
{% for company in investor.portfolio_highlights[:5] %}
|
||||
<span class="tag">{{ company }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
No portfolio highlights available
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||
Senior Leadership
|
||||
</h2>
|
||||
{% if investor.team_members %}
|
||||
{% for member in investor.team_members[:2] %}
|
||||
<div class="mb-3">
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ member.role or member.title or 'Team Member' }}
|
||||
</p>
|
||||
{% if member.email %}
|
||||
<p class="text-xs text-blue-600">
|
||||
{{ member.email }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No team information available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">Sectors:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.sectors %} {{ investor.sectors |
|
||||
join(', ') }} {% else %} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Right Column -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||
Key Data
|
||||
</h2>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div>
|
||||
<p class="text-xs text-gray-600">Headquarters:</p>
|
||||
<p class="font-semibold text-gray-900">
|
||||
{{ investor.headquarters or 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">DACH Region:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ investor.geographic_focus or 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600">Sectors:</p>
|
||||
<p class="font-semibold text-gray-900">
|
||||
{% if investor.sectors %}
|
||||
{{ investor.sectors | join(', ') }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
AUM: (EUR million) (as of Fund IX)
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.aum %} €{{
|
||||
'{:,.0f}'.format(investor.aum / 1000000) }}M {% else
|
||||
%} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600">DACH Region:</p>
|
||||
<p class="font-semibold text-gray-900">
|
||||
{{ investor.geographic_focus or 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-gray-600">AUM (EUR million):</p>
|
||||
<p class="font-semibold text-gray-900">
|
||||
{% if investor.aum %}
|
||||
€{{ '{:,.0f}'.format(investor.aum / 1000000) }}M
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
@@ -182,41 +203,23 @@
|
||||
1000000) }}M {% elif investor.check_size_lower %}
|
||||
€{{ '{:,.0f}'.format(investor.check_size_lower /
|
||||
1000000) }}M+ {% else %} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
Select Deals, Series A, Series B:
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
Growth
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">Focus Areas:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.investment_thesis %} {{
|
||||
investor.investment_thesis[:3] | join(', ') }} {%
|
||||
else %} Disruptive Technologies, Entrepreneur-led,
|
||||
Sustainability {% endif %}
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-12 right-12 text-xs text-gray-400">
|
||||
Page 1
|
||||
<div class="page-footer">Page 1</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page 2: Mandate Match Analysis -->
|
||||
<!-- Page 2 -->
|
||||
{% if project %}
|
||||
<div class="page p-12">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">
|
||||
{{ investor.name }}: Mandate Match Analysis
|
||||
</h1>
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">
|
||||
{{ investor.name }}: Mandate Match Analysis
|
||||
</h1>
|
||||
|
||||
<!-- Overall Match Circle -->
|
||||
<div class="flex justify-center mb-12">
|
||||
|
||||
Reference in New Issue
Block a user