feat: Implement report generation service and add report route for investor profiles

This commit is contained in:
bolade
2025-10-14 12:02:23 +01:00
parent e386ebbdef
commit 9e1ec258f1
10 changed files with 697 additions and 1 deletions
+319
View File
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Investor Profile Report</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@page {
margin: 0;
size: A4;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
}
.page {
page-break-after: always;
min-height: 100vh;
background: white;
}
.page:last-child {
page-break-after: auto;
}
.tag {
display: inline-block;
padding: 4px 12px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
margin: 4px;
}
</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"
>
<a
href="{{ investor.website }}"
target="_blank"
class="no-underline text-gray-700"
>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="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>
<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>
<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 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 class="mb-4">
<p class="text-xs text-gray-600 mb-1">
Investment Stage:
</p>
<p class="text-sm font-semibold text-gray-900">
{% if investor.investment_stages %} {{
investor.investment_stages | join(', ') }} {% else
%} N/A {% endif %}
</p>
</div>
<div class="mb-4">
<p class="text-xs text-gray-600 mb-1">
Est. Investment Size:
</p>
<p class="text-sm font-semibold text-gray-900">
{% if investor.check_size_lower and
investor.check_size_upper %} €{{
'{:,.0f}'.format(investor.check_size_lower /
1000000) }}M - €{{
'{:,.0f}'.format(investor.check_size_upper /
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>
</div>
</div>
</div>
<div class="absolute bottom-12 right-12 text-xs text-gray-400">
Page 3
</div>
</div>
<!-- Page 2: Mandate Match Analysis -->
{% if project %}
<div class="page p-12">
<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">
<div class="text-center">
<p class="text-sm font-bold text-gray-700 uppercase mb-4">
Overall Mandate Match
</p>
<div
class="w-48 h-48 rounded-full border-8 border-green-400 flex items-center justify-center bg-green-50 mx-auto"
>
<span class="text-5xl font-bold text-green-600"
>{{ compatibility_score }}%</span
>
</div>
</div>
</div>
<!-- Mandate Alignment Analysis Table -->
<div class="mb-12">
<h2 class="text-xl font-bold text-gray-900 mb-6">
Mandate Alignment Analysis
</h2>
<table class="w-full border-collapse">
<thead>
<tr class="border-b-2 border-gray-300">
<th
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
>
Criterion
</th>
<th
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
>
Mandate Requirement
</th>
<th
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
>
Investor Evidence (from Database)
</th>
<th
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
>
Match Score
</th>
<th
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
>
Weight
</th>
</tr>
</thead>
<tbody>
{% for criterion in match_criteria %}
<tr class="border-b border-gray-200">
<td class="py-4 px-4 text-sm text-gray-900">
{{ criterion.name }}
</td>
<td class="py-4 px-4 text-sm text-gray-700">
{{ criterion.requirement }}
</td>
<td class="py-4 px-4 text-sm text-gray-700">
{{ criterion.evidence }}
</td>
<td class="py-4 px-4 text-sm">
<span
class="{% if criterion.match == 'Perfect' %}text-green-600{% elif criterion.match == 'Strong' %}text-blue-600{% else %}text-yellow-600{% endif %} font-semibold"
>
{{ criterion.match }}
</span>
</td>
<td class="py-4 px-4 text-sm text-gray-700">
{{ criterion.weight }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Final Recommendation -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 rounded">
<h3 class="text-lg font-bold text-gray-900 mb-3">
Final Recommendation & Rationale
</h3>
<p class="text-sm text-gray-700 leading-relaxed">
{{ recommendation or "High Priority. A strong target due to
exceptional alignment on the most heavily-weighted criteria:
Sector and Stage. The strong geographic fit in the DACH
region further solidifies this recommendation." }}
</p>
</div>
<div class="absolute bottom-12 right-12 text-xs text-gray-400">
Page 4
</div>
</div>
{% endif %}
</body>
</html>