diff --git a/ENHANCED_LOVABLE_SPEC.md b/ENHANCED_LOVABLE_SPEC.md new file mode 100644 index 0000000..7886889 --- /dev/null +++ b/ENHANCED_LOVABLE_SPEC.md @@ -0,0 +1,478 @@ +# Dealspace AI - Complete Lovable Prototype Specification + +> Comprehensive analysis from Chrome DevTools extraction and Lovable development conversation + +## Executive Summary + +Dealspace AI is a sophisticated M&A data room platform with AI-powered diligence assistance. Built on Supabase with React/TypeScript frontend, it provides role-based access for sellers and buyers with comprehensive deal management, document organization, and Atlas AI integration for intelligent diligence support. + +## Demo Data Overview + +The platform contains rich demo data with: +- **4 Deals**: Project Aurora, Beacon, Catalyst, Delta +- **12 Documents** organized in 5 folders per deal +- **23 Diligence Requests** across 2 deals +- **8 Contacts** with buyer/seller/advisor types +- **Demo Organization**: "Apex Capital Partners" +- **Demo Users**: + - Sarah Chen (Seller/Owner role) + - James Park (Buyer/Viewer role) + +## Architecture + +### Frontend (React 18 + TypeScript) +``` +src/ +├── pages/ # Main application pages (11 pages) +│ ├── Analytics.tsx # Deal performance metrics +│ ├── AuditLog.tsx # Activity tracking +│ ├── Auth.tsx # Authentication with demo modes +│ ├── Contacts.tsx # Contact management +│ ├── Dashboard.tsx # Main overview with deal cards +│ ├── DealRoom.tsx # Individual deal room with tabs +│ ├── DealRooms.tsx # Deal listing with timelines +│ ├── ICMemos.tsx # Investment committee memos +│ ├── RequestList.tsx # Diligence request management +│ ├── Settings.tsx # User/org settings +│ └── PlatformGuide.tsx # Documentation +├── components/ # Core reusable components +│ ├── AppLayout.tsx # Main app shell +│ ├── AppShell.tsx # Navigation wrapper +│ ├── AtlasGlobalChat.tsx # AI chat interface +│ ├── AtlasSidebar.tsx # AI assistant sidebar +│ ├── DealRequestsPanel.tsx # Request management +│ └── NavLink.tsx # Navigation components +├── hooks/ # Custom React hooks +│ ├── useAuth.ts # Authentication state +│ ├── useOrganization.ts # Org context +│ ├── useTheme.ts # Dark/light mode +│ └── useUserRole.ts # Role-based access +├── lib/ # Utilities +│ ├── api.ts # API layer +│ └── utils.ts # Helper functions +├── integrations/supabase/ # Backend integration +│ ├── client.ts # Supabase client +│ └── types.ts # TypeScript types +└── components/ui/ # Shadcn/UI components (50+ components) +``` + +### Backend (Supabase) +``` +supabase/ +├── migrations/ # Database schema (5 migration files) +│ ├── 20260214204302_*.sql # Initial schema +│ ├── 20260214210921_*.sql # Demo data seeding +│ ├── 20260214212128_*.sql # Request list enhancements +│ ├── 20260214222633_*.sql # Buyer group segmentation +│ └── 20260214224626_*.sql # Role-based access +└── functions/ # Edge functions (5 functions) + ├── atlas-chat/ # AI document Q&A + ├── compute-deal-score/ # Deal scoring algorithms + ├── demo-login/ # Demo account creation + ├── folder-summary/ # AI folder summaries + └── generate-ic-memo/ # IC memo generation +``` + +## Database Schema (Inferred from Development History) + +### Core Tables +- **organizations** - Multi-tenant organization structure +- **profiles** - User profiles linked to Supabase auth +- **deals** - Deal rooms with metadata +- **folders** - Document organization within deals +- **documents** - File storage and metadata +- **diligence_requests** - Request list items +- **contacts** - Contact management +- **audit_logs** - Activity tracking + +### Key Fields Added During Development +- **diligence_requests**: + - `buyer_comment` (text, nullable) - Buyer notes on requests + - `seller_comment` (text, nullable) - Seller notes on requests + - `buyer_group` (text, nullable) - Segmentation by buyer groups + - `atlas_status` (enum) - fulfilled/partial/missing + - `atlas_note` (text) - AI assessment summary + - `confidence_score` (numeric) - AI confidence rating + +- **deals**: + - `ioi_date` (date) - Indication of Interest date + - `loi_date` (date) - Letter of Intent date + - `exclusivity_days` (integer) - Days remaining in exclusivity + - `stage` (enum) - Deal stage tracking + +### Row Level Security (RLS) +- **Owner Role (Sellers)**: Full access to organization data +- **Viewer Role (Buyers)**: Filtered access based on buyer_group assignments +- **Buyer Group Filtering**: Automatic data scoping based on user assignments + +## Atlas AI System + +### Core AI Functions + +1. **Atlas Chat (`atlas-chat/index.ts`)** + - Document-based Q&A using RAG + - Concise, bullet-point responses + - Document hyperlinking (e.g., `[FileName.pdf](/folder)`) + - Emoji-prefixed status indicators (✅/⚠️/🔴) + - Integrated with markdown parser for rich rendering + +2. **Deal Scoring (`compute-deal-score/index.ts`)** + - Automated deal risk assessment + - Completion probability calculations + - Timeline urgency scoring + +3. **Folder Summarization (`folder-summary/index.ts`)** + - AI-generated folder summaries + - Document completeness scoring (e.g., "85%" for Financial Statements) + - Integration with deal room folder views + +4. **IC Memo Generation (`generate-ic-memo/index.ts`)** + - Investment Committee memo creation + - Data synthesis from deal room contents + - Buyer-role specific functionality + +5. **Demo Login (`demo-login/index.ts`)** + - Separate seller/buyer demo account creation + - Automatic organization assignment + - RLS policy compliance for demo data access + +## User Roles & Access Control + +### Seller (Owner) Role - "Sarah Chen" +**Full Platform Access:** +- Dashboard with all deals and metrics +- Deal Rooms management (create, upload, organize) +- Request Lists with "View All" consolidated view +- Analytics and performance metrics +- Contacts management +- Audit Log visibility +- Settings and configuration +- Platform Guide access +- **Excluded**: IC Memos tab (buyer-only feature) + +**Capabilities:** +- Create new deal rooms +- Upload CSV request lists with optional "Buyer Group" column +- Edit seller comments on requests +- Manage document folders and uploads +- View all buyer group request lists + +### Buyer (Viewer) Role - "James Park" +**Limited Platform Access:** +- Dashboard (filtered to assigned deals only) +- Deal Rooms (access only to assigned deals) +- Request Lists (filtered to buyer group only) +- IC Memos generation tool +- Platform Guide access +- **Excluded**: Analytics, Contacts, Audit Log, Settings + +**Data Filtering:** +- Project Aurora via "Meridian Capital" buyer group +- Project Beacon via "Summit Health Equity" buyer group +- No create/upload actions available +- Can only edit buyer comments on requests + +**Specific Deal Assignments:** +- **Project Aurora**: "Meridian Capital" / "Horizon Partners" buyer groups +- **Project Beacon**: "Summit Health Equity" / "Northstar Health Capital" buyer groups +- **Project Catalyst**: Additional buyer groups as configured +- **Project Delta**: Additional buyer groups as configured + +## Key Features Deep Dive + +### 1. Deal Room Management +**Structure:** +- Deal rooms with tabbed interface (Documents / Request List) +- 5 folders per deal with AI-generated summaries +- Document upload and organization +- Folder completeness scoring (e.g., "Financial Statements: 85%") + +**Timeline Tracking:** +- IOI Date (Indication of Interest) +- LOI Date (Letter of Intent) +- Exclusivity Period with color-coded urgency: + - Green: >30 days remaining + - Yellow: 10-30 days remaining + - Red: <10 days remaining + +**Deal Stages:** +- Stage badges replacing risk percentages +- Visual status indicators +- Progress tracking + +### 2. Request List System +**Segmentation:** +- Buyer group tabs (e.g., "Meridian Capital", "Horizon Partners") +- "View All" tab for seller consolidated view +- 23 demo requests across Project Aurora and Beacon + +**Column Structure:** +1. Request item description +2. Section grouping +3. Buyer comment (inline-editable) +4. Seller comment (inline-editable) +5. Atlas Note (AI assessment summary) +6. Atlas Status (fulfilled/partial/missing with confidence scores) + +**CSV Import/Export:** +- Supports "Buyer Group" column for automatic segmentation +- Bulk request list management +- Data interchange capabilities + +**Atlas Assessment:** +- **11 Fulfilled** requests with ✅ status +- **5 Partial** requests with ⚠️ status +- **7 Missing** requests with 🔴 status +- AI-generated notes for status rationale (blank when missing) + +### 3. Authentication & Demo System +**Demo Mode Implementation:** +- Bifurcated demo buttons on auth page +- "Seller Demo" → Sarah Chen (owner role) +- "Buyer Demo" → James Park (viewer role) +- Real account creation with RLS compliance +- Identical data access as normal login + +**Security:** +- Supabase authentication +- Organization-scoped access +- Role-based navigation filtering +- Comment editing permissions by role + +### 4. Atlas AI Integration +**Response Characteristics:** +- Concise, bullet-point format +- Document hyperlinks: `[FileName.pdf](/folder)` +- Status indicators: ✅ (complete), ⚠️ (partial), 🔴 (missing) +- Markdown rendering with link support +- Context-aware responses based on deal room contents + +**UI Integration:** +- AtlasGlobalChat component for document Q&A +- AtlasSidebar for persistent AI assistance +- Inline status indicators in request lists +- Confidence scoring for assessments + +### 5. UI/UX Features +**Theme System:** +- Light/dark mode toggle in sidebar footer (sun/moon icon) +- Defaults to dark mode +- Persists via localStorage +- Flash-free loading with initial script +- Clean white/gray palette for light mode +- Consistent teal accent color + +**Navigation:** +- Role-based sidebar navigation +- Platform Guide with TOC and expandable accordions +- Role badges displayed in sidebar +- Responsive design for mobile/desktop + +## Technology Stack + +**Frontend:** +- React 18 with TypeScript +- Vite build system +- Tailwind CSS for styling +- Shadcn/UI component library (50+ components) +- Lucide Icons +- State management via React Context API + +**Backend:** +- Supabase (PostgreSQL + Auth + Edge Functions) +- Row Level Security (RLS) policies +- Real-time subscriptions +- File storage integration + +**AI Integration:** +- OpenAI API via Supabase Edge Functions +- RAG (Retrieval Augmented Generation) for document Q&A +- Custom prompt engineering for concise responses +- Document embedding and vector search + +## Detailed Development History + +Based on the extracted conversation, the platform evolved through these key phases: + +### Phase 1: Initial Demo Data Population +- Rich demo data creation with 4 deals +- 12 documents across 5 folders per deal +- 23 diligence requests with AI assessments +- 8 contacts with type classifications +- Platform Guide documentation + +### Phase 2: Atlas AI Refinement +- Rewrote system prompts for concise responses +- Added document hyperlinking capability +- Implemented emoji status indicators +- Enhanced markdown parser for link rendering + +### Phase 3: UI/UX Enhancements +- Light/dark mode toggle implementation +- Request list integration within deal rooms +- Tabbed interface (Documents / Request List) +- Responsive design improvements + +### Phase 4: Demo Authentication System +- Demo mode implementation for testing +- Real demo account creation via edge function +- RLS compliance for demo data access +- Bypass authentication for UI testing + +### Phase 5: Request List Enhancements +- Buyer/seller comment columns (inline-editable) +- Buyer group segmentation with tabs +- CSV import with "Buyer Group" column support +- Atlas Note column for AI assessments + +### Phase 6: Buyer Group Segmentation +- Multiple request lists per deal by buyer group +- Demo data for buyer group assignments +- "View All" seller perspective +- Filtered buyer access by group + +### Phase 7: Atlas Assessment Integration +- Atlas Note column implementation +- Brief AI summaries for request status +- Blank notes for missing items +- Status-based conditional display + +### Phase 8: Deal Timeline Features +- Removed close percentage and risk metrics +- IOI/LOI date tracking implementation +- Exclusivity period countdown with color coding +- Stage-based deal progression + +### Phase 9: Role-Based Access Control +- Bifurcated demo login (Seller/Buyer) +- Role-based sidebar navigation +- Data filtering by buyer group assignments +- Comment editing permissions by role +- Role badge display in sidebar + +## Migration to Go - Implementation Guide + +### 1. Database Migration +```sql +-- Example table structure based on features +CREATE TABLE organizations ( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE deals ( + id UUID PRIMARY KEY, + organization_id UUID REFERENCES organizations(id), + name TEXT NOT NULL, + stage TEXT, + ioi_date DATE, + loi_date DATE, + exclusivity_days INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE diligence_requests ( + id UUID PRIMARY KEY, + deal_id UUID REFERENCES deals(id), + description TEXT NOT NULL, + section TEXT, + buyer_group TEXT, + buyer_comment TEXT, + seller_comment TEXT, + atlas_status TEXT CHECK (atlas_status IN ('fulfilled', 'partial', 'missing')), + atlas_note TEXT, + confidence_score DECIMAL(3,2), + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### 2. Authentication System +- JWT-based authentication with organization scoping +- Role-based access control (owner/viewer) +- Demo account generation endpoints +- Session management with refresh tokens + +### 3. AI Integration Architecture +```go +// Example Go structure for Atlas AI +type AtlasService struct { + openaiClient *openai.Client + vectorStore VectorStore +} + +type DocumentQuery struct { + Question string `json:"question"` + DealID string `json:"deal_id"` + Context []string `json:"context"` +} + +type AtlasResponse struct { + Answer string `json:"answer"` + Confidence float64 `json:"confidence"` + Sources []DocumentReference `json:"sources"` + Status string `json:"status"` // fulfilled, partial, missing +} +``` + +### 4. API Endpoints +``` +POST /api/auth/demo-login +POST /api/atlas/chat +POST /api/atlas/folder-summary +POST /api/atlas/ic-memo +GET /api/deals +GET /api/deals/{id}/requests +POST /api/deals/{id}/requests/csv-import +PATCH /api/requests/{id}/comments +``` + +### 5. Role-Based Filtering +```go +func FilterRequestsByBuyerGroup(requests []Request, userRole string, buyerGroup string) []Request { + if userRole == "owner" { + return requests // Sellers see all + } + + var filtered []Request + for _, req := range requests { + if req.BuyerGroup == buyerGroup || req.BuyerGroup == "" { + filtered = append(filtered, req) + } + } + return filtered +} +``` + +## Business Logic Patterns + +### Demo Data Seeding +- 4 predefined deals with realistic names +- Buyer group assignments per deal +- AI-generated folder summaries +- Request list segmentation by buyer groups + +### Atlas AI Prompt Engineering +- System prompts optimized for concise responses +- Document context injection for RAG +- Status classification logic (fulfilled/partial/missing) +- Confidence scoring algorithms + +### Audit Logging +- Activity tracking across all user actions +- Role-based log filtering +- Timeline view of deal progression +- User interaction monitoring + +## Critical Implementation Notes + +1. **RLS Compliance**: All data access must respect buyer group assignments +2. **AI Response Format**: Maintain emoji status indicators and document linking +3. **Demo Mode**: Ensure demo accounts have identical data access to real accounts +4. **CSV Import**: Support buyer group column for request segmentation +5. **Timeline Urgency**: Implement color-coded exclusivity period warnings +6. **Comment System**: Separate buyer/seller comment fields with role-based editing +7. **Theme Persistence**: Implement localStorage-based theme switching +8. **Responsive Design**: Mobile-first approach with sidebar collapsing + +This comprehensive specification captures the complete feature set, data model, and implementation patterns from the Dealspace AI Lovable prototype, providing a complete blueprint for Go migration. \ No newline at end of file diff --git a/LOVABLE-ANALYSIS.md b/LOVABLE-ANALYSIS.md new file mode 100644 index 0000000..3c5abad --- /dev/null +++ b/LOVABLE-ANALYSIS.md @@ -0,0 +1,498 @@ +# Lovable Prototype Analysis — Dealspace AI + +> Generated from source code dump of Misha's 49-iteration Lovable prototype. +> Source: `/home/johan/shared/dealspace-lovable/` + +## Data Model (Supabase/PostgreSQL) + +### Tables + +#### 1. `organizations` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| name | text | | +| slug | text | URL-safe identifier | +| domain | text? | | +| logo_url | text? | | +| settings | jsonb? | | +| created_at | timestamp | | +| updated_at | timestamp | | + +#### 2. `profiles` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| user_id | uuid | FK to auth.users | +| email | text? | | +| full_name | text? | | +| title | text? | Job title | +| phone | text? | | +| avatar_url | text? | | +| organization_id | uuid? | FK to organizations | +| onboarding_completed | boolean? | | +| settings | jsonb? | | +| created_at/updated_at | timestamp | | + +#### 3. `user_roles` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| user_id | uuid | FK to auth.users | +| organization_id | uuid | FK to organizations | +| role | enum | `owner`, `admin`, `member`, `viewer` | +| created_at | timestamp | | + +#### 4. `deals` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| name | text | Deal/project codename | +| description | text? | | +| organization_id | uuid | FK to organizations | +| created_by | uuid? | | +| stage | enum | `pipeline`, `loi`, `initial_review`, `due_diligence`, `final_negotiation`, `closed`, `dead` | +| target_company | text? | | +| deal_size | numeric? | | +| currency | text? | | +| close_probability | numeric? | 0-100 | +| expected_close_date | date? | | +| ioi_date | date? | Indication of Interest date | +| loi_date | date? | Letter of Intent date | +| exclusivity_end_date | date? | | +| is_archived | boolean? | | +| settings | jsonb? | | +| created_at/updated_at | timestamp | | + +#### 5. `folders` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| parent_id | uuid? | FK to folders (self-referential tree) | +| name | text | | +| description | text? | | +| sort_order | integer? | | +| created_by | uuid? | | +| ai_summary | text? | Atlas-generated folder summary | +| ai_summary_updated_at | timestamp? | | +| completeness_score | numeric? | 0-100, AI-computed | +| created_at/updated_at | timestamp | | + +#### 6. `files` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| folder_id | uuid? | FK to folders | +| name | text | Original filename | +| file_path | text? | Supabase storage path | +| file_size | bigint? | | +| mime_type | text? | | +| uploaded_by | uuid? | | +| status | enum | `uploaded`, `processing`, `reviewed`, `flagged`, `archived` | +| version | integer? | | +| ai_summary | text? | | +| ai_tags | text[]? | | +| is_sensitive | boolean? | | +| download_disabled | boolean? | | +| watermark_enabled | boolean? | | +| created_at/updated_at | timestamp | | + +#### 7. `diligence_requests` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| section | text | Category (Financial, Legal, etc.) | +| item_number | text? | e.g., "1.1", "2.3" | +| description | text | What's being requested | +| priority | enum | `high`, `medium`, `low` | +| created_by | uuid? | | +| buyer_group | text? | Which buyer group this request is for | +| buyer_comment | text? | Inline comment from buyer | +| seller_comment | text? | Inline comment from seller | +| linked_file_ids | uuid[]? | Files that fulfill this request | +| atlas_status | enum | `fulfilled`, `partial`, `missing`, `not_applicable` | +| atlas_confidence | numeric? | 0-100 | +| atlas_note | text? | AI explanation | +| atlas_assessed_at | timestamp? | | +| created_at/updated_at | timestamp | | + +#### 8. `deal_activity` (Audit Log) +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid? | FK to deals | +| organization_id | uuid? | FK to organizations | +| user_id | uuid? | | +| activity_type | enum | `view`, `download`, `upload`, `edit`, `delete`, `share`, `comment`, `permission_change`, `nda_signed`, `login` | +| resource_type | text? | | +| resource_id | uuid? | | +| resource_name | text? | | +| details | jsonb? | | +| ip_address | inet | | +| user_agent | text? | | +| created_at | timestamp | | + +#### 9. `buyer_engagement` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| user_id | uuid? | | +| file_id | uuid? | FK to files | +| folder_id | uuid? | FK to folders | +| event_type | text | | +| dwell_time_seconds | numeric? | | +| page_views | integer? | | +| metadata | jsonb? | | +| created_at | timestamp | | + +#### 10. `deal_scores` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| close_probability | numeric | 0-100 | +| risk_tier | text | `low`, `medium`, `high` | +| diligence_completion | numeric? | | +| buyer_engagement_score | numeric? | | +| response_velocity | numeric? | | +| red_flag_count | integer? | | +| recommendations | jsonb? | Array of strings | +| computed_at | timestamp | | +| created_at | timestamp | | + +#### 11. `ai_insights` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| insight_type | text | | +| title | text | | +| content | text? | | +| severity | text? | | +| is_dismissed | boolean? | | +| source_file_ids | uuid[]? | | +| created_at | timestamp | | + +#### 12. `contacts` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| organization_id | uuid | FK to organizations | +| full_name | text | | +| email | text? | | +| phone | text? | | +| company | text? | | +| title | text? | | +| contact_type | text? | buyer, advisor, internal | +| tags | text[]? | | +| notes | text? | | +| last_activity_at | timestamp? | | +| created_at/updated_at | timestamp | | + +#### 13. `ic_memos` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| organization_id | uuid | FK to organizations | +| created_by | uuid? | | +| title | text | | +| content | jsonb | Structured: summary, business_model, financials, key_risks[], diligence_gaps[], valuation_considerations, recommendation, source_documents[] | +| status | text | `draft`, `review`, `final` | +| version | integer | | +| created_at/updated_at | timestamp | | + +#### 14. `nda_records` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| deal_id | uuid | FK to deals | +| signer_name | text | | +| signer_email | text | | +| signer_company | text? | | +| user_id | uuid? | | +| status | text? | | +| signed_at | timestamp? | | +| expires_at | timestamp? | | +| nda_document_url | text? | | +| ip_address | inet | | +| created_at | timestamp | | + +#### 15. `tasks` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| organization_id | uuid | FK to organizations | +| deal_id | uuid? | FK to deals | +| title | text | | +| description | text? | | +| assigned_to | uuid? | | +| created_by | uuid? | | +| status | text? | | +| priority | enum | `high`, `medium`, `low` | +| due_date | date? | | +| completed_at | timestamp? | | +| created_at/updated_at | timestamp | | + +#### 16. `workflow_rules` +| Field | Type | Notes | +|-------|------|-------| +| id | uuid PK | | +| organization_id | uuid | FK to organizations | +| name | text | | +| trigger_type | text | | +| trigger_config | jsonb | | +| action_type | text | | +| action_config | jsonb | | +| is_active | boolean | | +| created_by | uuid? | | +| created_at/updated_at | timestamp | | + +### Enums +- **deal_stage:** pipeline, loi, initial_review, due_diligence, final_negotiation, closed, dead +- **file_status:** uploaded, processing, reviewed, flagged, archived +- **request_priority:** high, medium, low +- **request_status:** fulfilled, partial, missing, not_applicable +- **activity_type:** view, download, upload, edit, delete, share, comment, permission_change, nda_signed, login +- **app_role:** owner, admin, member, viewer + +### DB Functions +- `has_role(org_id, role, user_id)` → boolean +- `is_org_member(org_id, user_id)` → boolean + +--- + +## Role/Permission Model + +From `useUserRole.ts`: +- **Seller view** = `owner` or `admin` role → full platform access +- **Buyer view** = `member` or `viewer` role → restricted access + +Key differences: +| Feature | Seller | Buyer | +|---------|--------|-------| +| Create deal rooms | ✅ | ❌ | +| Upload files | ✅ | ❌ | +| Upload CSV request lists | ✅ | ❌ | +| View analytics/engagement | ✅ | ❌ | +| Generate IC memos | ✅ | ❌ | +| View all deals | ✅ | Only deals with their buyer_group | +| View all requests | ✅ | Only requests for their buyer_group | +| Add buyer/seller comments | ✅ | ✅ | + +Buyer groups are hardcoded demo values: `["Meridian Capital", "Summit Health Equity"]` + +--- + +## API Operations (from `api.ts`) + +| Function | Table | Operation | +|----------|-------|-----------| +| createOrganization | organizations | INSERT | +| getOrganization | organizations | SELECT by id | +| assignRole | user_roles | INSERT | +| getUserOrg | user_roles + organizations | SELECT (join) | +| getProfile | profiles | SELECT by user_id | +| updateProfile | profiles | UPDATE | +| getDeals | deals | SELECT by org_id | +| getDeal | deals | SELECT by id | +| createDeal | deals | INSERT | +| updateDeal | deals | UPDATE | +| getFolders | folders | SELECT by deal_id | +| createFolder | folders | INSERT | +| updateFolder | folders | UPDATE | +| getFiles | files | SELECT by deal_id + folder_id | +| getAllFiles | files | SELECT by deal_id | +| getFileCount | files | COUNT by deal_id | +| createFileRecord | files | INSERT | +| uploadFile | storage + files | Upload to Supabase storage + INSERT | +| getRequests | diligence_requests | SELECT by deal_id | +| getAllRequests | diligence_requests | SELECT all for org (via deals) | +| createRequest | diligence_requests | INSERT | +| bulkCreateRequests | diligence_requests | INSERT (batch) | +| updateRequest | diligence_requests | UPDATE | +| getActivity | deal_activity | SELECT by org_id | +| logActivity | deal_activity | INSERT | +| getContacts | contacts | SELECT by org_id | +| getInsights | ai_insights | SELECT by deal_id (non-dismissed) | + +### Supabase Edge Functions (called via fetch, not in api.ts) +- `folder-summary` — POST: generates AI summary for a folder +- `compute-deal-score` — POST: computes deal close probability score +- `generate-ic-memo` — POST: generates IC memo from deal documents +- `demo-login` — POST: creates demo session with seller/buyer role + +--- + +## Feature Inventory by Page + +### Routes (from App.tsx) +| Path | Component | Auth Required | +|------|-----------|---------------| +| `/auth` | Auth | No | +| `/` | Dashboard | Yes | +| `/deals` | DealRooms | Yes | +| `/deals/:id` | DealRoom | Yes | +| `/requests` | RequestList | Yes | +| `/analytics` | Analytics | Yes | +| `/ic-memos` | ICMemos | Yes | +| `/audit` | AuditLog | Yes | +| `/contacts` | Contacts | Yes | +| `/settings` | Settings | Yes | +| `/guide` | PlatformGuide | Yes | +| `*` | NotFound | No | + +### Dashboard +- **Stats cards:** Active Rooms, Documents, Active Deals, Avg Close Probability +- **Active Deal Rooms list** (top 5) with stage badges, doc counts, links +- **Recent Activity feed** (last 8 events) with timestamps +- All data is LIVE from Supabase + +### DealRoom (single deal view) +- **Header:** Deal name, stage badge, doc count, target company, back link +- **Tabs:** Documents | Request List +- **Documents tab:** + - Left sidebar: folder tree (hierarchical, expandable) + - Breadcrumb navigation + - **Atlas Folder Summary** panel (AI-generated, with refresh button and completeness score) + - File table: name, size, modified date, status badge, actions menu + - File upload (multi-file) + - Create folder (inline form, supports nesting under selected folder) +- **Request List tab:** Embedded `DealRequestsPanel` component (missing from dump) +- **Atlas AI sidebar** toggle — passes deal context (dealId, name, stage, docs, folder, completeness, recent files) + +### DealRooms (list) +- Table with: Deal name + target company, Stage badge, Size ($M), IOI Date, LOI Date, Exclusivity countdown +- Search/filter +- Create new deal dialog (name, target company, size, stage) +- **Buyer filtering:** buyers only see deals that have diligence requests for their buyer_group +- Exclusivity countdown with color-coded warnings (expired/expiring) + +### RequestList (cross-deal view) +- **Atlas Assessment Summary bar:** Total, Fulfilled, Partial, Missing counts +- Grouped by deal room, each expandable +- Per-request table columns: #, Section, Description, Priority, Atlas Status, Confidence bar, Buyer Comment (inline editable), Seller Comment (inline editable), Atlas Note +- **Buyer group tabs** — when requests have buyer_group, shows tab per group + "View All" +- **CSV Upload dialog** — parse CSV → preview → bulk import to selected deal +- **Buyer filtering:** buyers only see requests for their buyer_group + +### Analytics +- **KPI row:** Deal Rooms, Total Documents, Engagement Events +- **Deal Close Probability table:** per-deal with diligence %, engagement %, red flags, recommendations, compute button +- **Buyer Engagement timeline:** recent events with event type, dwell time, page views +- Calls Supabase edge function `compute-deal-score` to generate scores +- **Seller-only page** (not explicitly gated in code but designed for sellers) + +### ICMemos +- **Left panel:** Generate new memo (select deal → click Generate), List of all memos +- **Right panel:** Memo viewer with structured sections: + - Executive Summary, Business Model, Financial Analysis, Key Risks, Diligence Gaps, Valuation Considerations, Recommendation, Source Documents +- Memo statuses: Draft, In Review, Final +- Version tracking +- Calls Supabase edge function `generate-ic-memo` + +### Contacts +- **HARDCODED mock data** — not connected to database +- Table: Name, Company, Email, Type (buyer/advisor/internal), Tags, Last Active +- Search bar (not functional) +- Add Contact button (not functional) + +### AuditLog +- Full activity log from `deal_activity` table +- Filterable by action/resource name +- Color-coded action badges (view, upload, download, NDA signed, etc.) +- Shows: Timestamp, Action, Resource, Details + +### Settings +- **Sidebar navigation** with sections: + - **Profile** — name, email, title, phone (LIVE from profiles table) + - **Organization** — org name (LIVE) + - **Team Members** — placeholder ("coming soon") + - **Security** — toggle switches for MFA, IP allowlisting, session timeout, download watermarking, view-only mode (MOCK — toggles don't persist) + - **Notifications** — placeholder + - **Integrations** — Salesforce, HubSpot, Affinity, DocuSign, Slack (MOCK — all "not connected", configure buttons non-functional) + - **Workflows** — 4 sample rules with triggers/actions (MOCK — toggles don't persist) + - **Appearance** — placeholder + +### Auth +- Email/password login and signup (via Supabase Auth) +- On signup: auto-creates organization and assigns owner role +- On first login without org: auto-creates org +- **Demo login buttons:** Seller Demo / Buyer Demo (calls `demo-login` edge function) +- Right panel: marketing copy about the platform + +### PlatformGuide +- Comprehensive documentation page with accordion sections +- Covers: Overview, Dashboard, Deal Rooms, Request Lists, Analytics, IC Memos, Contacts, Atlas AI, Audit Log, Settings, Security +- Table of contents with anchor links +- Role badges (Seller/Buyer/Both) per section + +--- + +## Atlas AI Capabilities + +Atlas AI is referenced throughout but the core components (`AtlasSidebar`, `AtlasGlobalChat`) are **missing from the dump**. Based on usage: + +1. **Deal Room Sidebar** — Context-aware chat panel receiving: + - dealId, dealName, stage, totalDocs, currentFolder, completeness, recentFiles +2. **Folder Summaries** — via edge function `folder-summary` +3. **Diligence Request Assessment** — auto-status with confidence scores + notes +4. **Deal Score Computation** — via edge function `compute-deal-score` +5. **IC Memo Generation** — via edge function `generate-ic-memo` +6. **AI Insights** — stored in `ai_insights` table, shown on dashboard (code references but UI not fully visible) +7. **Document AI Processing** — files have `ai_summary` and `ai_tags` fields + +--- + +## What's Real vs Mock/Placeholder + +### Real (connected to Supabase) +- Authentication (email/password + demo login) +- Organization creation + role assignment +- Deal CRUD (create, read, update) +- Folder CRUD (create, read, tree structure) +- File upload to Supabase storage + metadata +- Diligence request CRUD + bulk CSV import +- Inline buyer/seller comments on requests +- Activity/audit log (read + write) +- Profile management +- AI edge functions (folder summary, deal score, IC memo generation) +- IC Memo CRUD + viewer + +### Mock/Placeholder +- **Contacts page** — entirely hardcoded data +- **Settings > Security** — toggle switches don't persist +- **Settings > Integrations** — CRM buttons are non-functional +- **Settings > Workflows** — rules are hardcoded, toggles don't persist +- **Settings > Team Members, Notifications, Appearance** — "coming soon" +- **Buyer engagement tracking** — table exists but no code writing to it +- **NDA records** — table exists but no UI for NDA flow +- **Tasks** — table exists but no dedicated UI +- **Workflow rules** — table exists but no execution engine +- **AI Insights dismissal** — read from DB but no dismiss UI visible + +### Missing Components (not in dump) +- `AppLayout` — main layout shell with sidebar navigation +- `AppShell` — possibly alternate layout +- `AtlasGlobalChat` — global AI chat component +- `AtlasSidebar` — deal-scoped AI sidebar +- `DealRequestsPanel` — embedded request list for deal room +- `NavLink` — navigation link component +- `useAuth` hook +- `useOrganization` hook +- All `ui/` shadcn components (Button, Dialog, Tabs, etc.) + +--- + +## Key Architectural Observations + +1. **Multi-tenant by design** — everything scoped to `organization_id` +2. **Seller/Buyer distinction** is thin — just role mapping, not separate user types +3. **AI is the differentiator** — Atlas does diligence assessment, folder summarization, deal scoring, IC memo generation +4. **Buyer group concept** — requests can be tagged per buyer group, enabling multi-buyer processes +5. **Deal stages are IB-specific** — pipeline → LOI → initial review → due diligence → final negotiation → closed/dead +6. **File security features designed but not implemented** — watermarking, download disable, sensitivity flags are in the schema but UI is minimal diff --git a/LOVABLE_SPEC.md b/LOVABLE_SPEC.md new file mode 100644 index 0000000..0102d1b --- /dev/null +++ b/LOVABLE_SPEC.md @@ -0,0 +1,155 @@ +# Dealspace AI - Lovable Prototype Specification + +> Generated from file tree analysis of Misha's Lovable prototype + +## Overview + +Dealspace AI is a data room platform for M&A transactions with AI-powered diligence assistance. It provides role-based access for sellers and buyers with different views and capabilities. + +## Architecture + +### Frontend (React/TypeScript) +- **Pages**: Analytics, AuditLog, Auth, Contacts, Dashboard, DealRoom, DealRooms, ICMemos, RequestList, Settings, PlatformGuide +- **Components**: AppLayout, AppShell, AtlasGlobalChat, AtlasSidebar, DealRequestsPanel, NavLink +- **Hooks**: useAuth, useOrganization, useTheme, useUserRole +- **Lib**: api, utils +- **UI Framework**: Shadcn/UI components (extensive component library) + +### Backend (Supabase) +- **Database**: PostgreSQL with RLS (Row Level Security) +- **Edge Functions**: 5 serverless functions for AI and business logic +- **Auth**: Supabase authentication with demo mode + +## Database Schema (Inferred) + +Based on migration files found: +1. `20260214204302_ecf07ab9-408c-4285-aaa7-fa863ab0bf44.sql` +2. `20260214210921_76c3250f-a80f-48c6-811e-bd63c5df73b9.sql` +3. `20260214212128_b9b50ed5-01a1-452c-8980-08cf8f01de07.sql` +4. `20260214222633_87b2d0bd-eb15-4756-8276-688939090180.sql` +5. `20260214224626_6cd97efe-10ea-46f3-9127-62bbc9d89cba.sql` + +The database likely contains tables for: +- Organizations +- Users and roles (seller/buyer/admin) +- Deals and deal rooms +- Documents and folders +- Diligence requests +- Comments and audit logs +- Buyer groups and permissions + +## Atlas AI System + +Edge functions identified: +1. **atlas-chat**: AI chat interface for document questions +2. **compute-deal-score**: Calculate deal risk/completion scores +3. **demo-login**: Demo account creation for testing +4. **folder-summary**: AI-generated folder/document summaries +5. **generate-ic-memo**: Investment Committee memo generation + +## User Roles & Features + +### Seller (Owner) Role +- Full dashboard access +- Create and manage deal rooms +- Upload documents and organize folders +- Manage diligence request lists +- View analytics and audit logs +- Manage contacts and settings +- **No IC Memo access** + +### Buyer (Viewer) Role +- Limited dashboard (only assigned deals) +- View deal rooms they have access to +- See only their buyer group's request lists +- IC Memo generation tool +- Platform guide access +- **No analytics, contacts, audit logs, or settings** + +## Key Features + +### 1. Deal Management +- Deal rooms with folder organization +- Document upload and management +- Deal timelines (IOI, LOI, exclusivity tracking) +- Deal status and stage tracking + +### 2. Request Lists +- Buyer group segmentation +- Seller and buyer comment columns +- Atlas AI status tracking (fulfilled/partial/missing) +- CSV import/export capability +- Confidence scoring + +### 3. Atlas AI Integration +- Document chat interface +- Folder summaries +- Deal scoring algorithms +- IC memo generation +- Concise responses with document links + +### 4. Role-Based Security +- Row-level security in database +- Role-based sidebar navigation +- Filtered data access per buyer group +- Audit logging of all actions + +### 5. UI/UX Features +- Light/dark mode toggle +- Responsive design +- Real-time updates +- Toast notifications +- Modal dialogs and forms + +## Authentication System + +- Supabase auth integration +- Demo accounts for testing (separate seller/buyer logins) +- Organization-based access control +- Session management + +## File Structure + +``` +src/ +├── pages/ # Main application pages +├── components/ # Reusable components +├── hooks/ # Custom React hooks +├── lib/ # Utilities and API layer +├── integrations/ # Supabase client and types +└── components/ui/ # Shadcn UI components + +supabase/ +├── migrations/ # Database schema changes +└── functions/ # Edge functions for AI/backend logic +``` + +## Technology Stack + +- **Frontend**: React 18, TypeScript, Vite +- **UI**: Tailwind CSS, Shadcn/UI, Lucide Icons +- **State**: React hooks, Context API +- **Backend**: Supabase (PostgreSQL, Auth, Edge Functions) +- **AI**: OpenAI integration via edge functions +- **Deployment**: Lovable hosting platform + +## Notes + +This specification was generated from file tree analysis. The actual implementation details, database schema, and API contracts would need to be extracted from the source code for a complete rebuild in Go. + +Key areas needing deeper investigation: +1. Complete database schema from migration files +2. API endpoint definitions from edge functions +3. Component props and state management patterns +4. Authentication flow and RLS policies +5. AI prompt engineering and model configurations + +## Migration Considerations for Go + +1. **Database**: Migrate PostgreSQL schema, implement similar RLS patterns +2. **Auth**: Implement JWT-based auth with organization scoping +3. **AI Integration**: Port edge function logic to Go handlers +4. **Role System**: Recreate buyer/seller role distinctions +5. **Real-time**: Consider WebSocket implementation for live updates +6. **File Upload**: Implement document storage and management +7. **Audit**: Create comprehensive audit logging system diff --git a/SPEC.md b/SPEC.md index 8a4cec2..c80961e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -5,6 +5,9 @@ **Tech Lead:** James **Architecture Pattern:** inou-portal pattern +> **Note:** Items marked with 🆕 were discovered from the Lovable prototype analysis and added to this spec. +> Items marked with 📝 were modified based on the prototype's implementation. + ## Executive Summary Deal Room is a secure, invite-only document sharing platform designed for Investment Banking deal teams. It provides role-based access control, encrypted file storage, AI-powered document analysis, and comprehensive audit trails for sensitive financial transactions. @@ -13,7 +16,7 @@ Deal Room is a secure, invite-only document sharing platform designed for Invest ### Core Principles - **Single binary deployment** - Zero runtime dependencies -- **Data-centric design** - All entities stored as typed JSON in unified entries table +- **Data-centric design** - All entities stored in normalized SQLite tables - **Security-first** - Encryption at rest, RBAC, audit logging - **AI-enhanced** - Document analysis and embeddings for intelligent search - **Production-grade** - Battle-tested patterns from inou-portal @@ -25,80 +28,358 @@ Deal Room is a secure, invite-only document sharing platform designed for Invest - **Database:** SQLite with encryption at rest - **File Storage:** Encrypted (AES-256-GCM) + Compressed (zstd) - **AI/ML:** K2.5 for document analysis and embeddings -- **Authentication:** Magic link + session cookies +- **Authentication:** Email/password + magic link + session cookies ## Database Schema ### Core Tables -#### users +#### organizations 🆕 +```sql +CREATE TABLE organizations ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + domain TEXT, + logo_url TEXT, + settings TEXT, -- JSON + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +``` + +#### users (was: profiles + auth.users) ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, - name TEXT NOT NULL, - role TEXT NOT NULL CHECK (role IN ('admin', 'user')), + full_name TEXT NOT NULL, + title TEXT, -- 🆕 Job title + phone TEXT, -- 🆕 + password_hash TEXT NOT NULL, avatar_url TEXT, + organization_id TEXT, -- 🆕 FK to organizations + onboarding_completed BOOLEAN NOT NULL DEFAULT 0, -- 🆕 + settings TEXT, -- 🆕 JSON user preferences created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, last_login INTEGER, - is_active BOOLEAN NOT NULL DEFAULT 1 + is_active BOOLEAN NOT NULL DEFAULT 1, + + FOREIGN KEY (organization_id) REFERENCES organizations(id) ); CREATE INDEX idx_users_email ON users(email); -CREATE INDEX idx_users_active ON users(is_active); +CREATE INDEX idx_users_org ON users(organization_id); ``` -#### entries -**Unified data table storing all content types as typed JSON** +#### user_roles 🆕 ```sql -CREATE TABLE entries ( +CREATE TABLE user_roles ( id TEXT PRIMARY KEY, - parent_id TEXT, -- For threading/hierarchy - deal_room_id TEXT NOT NULL, -- Links to deal room entry - entry_type TEXT NOT NULL CHECK (entry_type IN ('deal_room', 'document', 'note', 'message', 'analysis')), - title TEXT NOT NULL, - content TEXT NOT NULL, -- JSON payload, schema varies by type - file_path TEXT, -- For documents: encrypted file path - file_size INTEGER, -- Original file size - file_hash TEXT, -- SHA-256 of original file - embedding BLOB, -- AI embeddings for search - created_by TEXT NOT NULL, + user_id TEXT NOT NULL, + organization_id TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')), + created_at INTEGER NOT NULL, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + UNIQUE(user_id, organization_id) +); +``` + +#### 📝 deals (was: entries with type=deal_room) +```sql +CREATE TABLE deals ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + organization_id TEXT NOT NULL, + created_by TEXT, + stage TEXT NOT NULL DEFAULT 'pipeline' + CHECK (stage IN ('pipeline', 'loi', 'initial_review', 'due_diligence', 'final_negotiation', 'closed', 'dead')), + target_company TEXT, + deal_size REAL, -- 🆕 + currency TEXT DEFAULT 'USD', -- 🆕 + close_probability REAL, -- 🆕 0-100 + expected_close_date TEXT, -- 🆕 ISO date + ioi_date TEXT, -- 🆕 Indication of Interest date + loi_date TEXT, -- 🆕 Letter of Intent date + exclusivity_end_date TEXT, -- 🆕 + is_archived BOOLEAN DEFAULT 0, -- 🆕 + settings TEXT, -- 🆕 JSON created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, - - FOREIGN KEY (created_by) REFERENCES users(id), - FOREIGN KEY (parent_id) REFERENCES entries(id), - FOREIGN KEY (deal_room_id) REFERENCES entries(id) + + FOREIGN KEY (organization_id) REFERENCES organizations(id), + FOREIGN KEY (created_by) REFERENCES users(id) ); -CREATE INDEX idx_entries_deal_room ON entries(deal_room_id); -CREATE INDEX idx_entries_type ON entries(entry_type); -CREATE INDEX idx_entries_parent ON entries(parent_id); -CREATE INDEX idx_entries_created ON entries(created_at); -CREATE INDEX idx_entries_creator ON entries(created_by); +CREATE INDEX idx_deals_org ON deals(organization_id); +CREATE INDEX idx_deals_stage ON deals(stage); ``` -#### access -**RBAC permissions using bitmask** +#### 📝 folders (was: implicit in entries hierarchy) ```sql -CREATE TABLE access ( +CREATE TABLE folders ( id TEXT PRIMARY KEY, - entry_id TEXT NOT NULL, - user_id TEXT NOT NULL, - permissions INTEGER NOT NULL DEFAULT 1, -- Bitmask: read=1, write=2, delete=4, manage=8 - granted_by TEXT NOT NULL, - granted_at INTEGER NOT NULL, - - FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (granted_by) REFERENCES users(id), - - UNIQUE(entry_id, user_id) + deal_id TEXT NOT NULL, + parent_id TEXT, -- Self-referential tree + name TEXT NOT NULL, + description TEXT, + sort_order INTEGER DEFAULT 0, + created_by TEXT, + ai_summary TEXT, -- 🆕 Atlas-generated summary + ai_summary_updated_at INTEGER, -- 🆕 + completeness_score REAL, -- 🆕 0-100, AI-computed + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES folders(id), + FOREIGN KEY (created_by) REFERENCES users(id) ); -CREATE INDEX idx_access_entry ON access(entry_id); -CREATE INDEX idx_access_user ON access(user_id); -CREATE INDEX idx_access_permissions ON access(permissions); +CREATE INDEX idx_folders_deal ON folders(deal_id); +CREATE INDEX idx_folders_parent ON folders(parent_id); +``` + +#### 📝 files (was: entries with type=document) +```sql +CREATE TABLE files ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + folder_id TEXT, + name TEXT NOT NULL, -- Original filename + file_path TEXT, -- Encrypted storage path + file_size INTEGER, + file_hash TEXT, -- SHA-256 + mime_type TEXT, + uploaded_by TEXT, + status TEXT DEFAULT 'uploaded' + CHECK (status IN ('uploaded', 'processing', 'reviewed', 'flagged', 'archived')), + version INTEGER DEFAULT 1, + ai_summary TEXT, -- 🆕 + ai_tags TEXT, -- 🆕 JSON array + is_sensitive BOOLEAN DEFAULT 0, -- 🆕 + download_disabled BOOLEAN DEFAULT 0, -- 🆕 + watermark_enabled BOOLEAN DEFAULT 0, -- 🆕 + embedding BLOB, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE, + FOREIGN KEY (folder_id) REFERENCES folders(id), + FOREIGN KEY (uploaded_by) REFERENCES users(id) +); + +CREATE INDEX idx_files_deal ON files(deal_id); +CREATE INDEX idx_files_folder ON files(folder_id); +CREATE INDEX idx_files_status ON files(status); +``` + +#### diligence_requests 🆕 +```sql +CREATE TABLE diligence_requests ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + section TEXT NOT NULL, -- Category: Financial, Legal, Technology, etc. + item_number TEXT, -- e.g., "1.1", "2.3" + description TEXT NOT NULL, + priority TEXT DEFAULT 'medium' + CHECK (priority IN ('high', 'medium', 'low')), + created_by TEXT, + buyer_group TEXT, -- Which buyer group + buyer_comment TEXT, -- Inline comment from buyer + seller_comment TEXT, -- Inline comment from seller + linked_file_ids TEXT, -- JSON array of file IDs + atlas_status TEXT + CHECK (atlas_status IN ('fulfilled', 'partial', 'missing', 'not_applicable')), + atlas_confidence REAL, -- 0-100 + atlas_note TEXT, + atlas_assessed_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) +); + +CREATE INDEX idx_requests_deal ON diligence_requests(deal_id); +CREATE INDEX idx_requests_section ON diligence_requests(section); +CREATE INDEX idx_requests_status ON diligence_requests(atlas_status); +``` + +#### buyer_engagement 🆕 +```sql +CREATE TABLE buyer_engagement ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + user_id TEXT, + file_id TEXT, + folder_id TEXT, + event_type TEXT NOT NULL, + dwell_time_seconds REAL, + page_views INTEGER, + metadata TEXT, -- JSON + created_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE, + FOREIGN KEY (file_id) REFERENCES files(id), + FOREIGN KEY (folder_id) REFERENCES folders(id) +); + +CREATE INDEX idx_engagement_deal ON buyer_engagement(deal_id); +CREATE INDEX idx_engagement_user ON buyer_engagement(user_id); +``` + +#### deal_scores 🆕 +```sql +CREATE TABLE deal_scores ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + close_probability REAL NOT NULL, -- 0-100 + risk_tier TEXT NOT NULL, -- low, medium, high + diligence_completion REAL, + buyer_engagement_score REAL, + response_velocity REAL, + red_flag_count INTEGER DEFAULT 0, + recommendations TEXT, -- JSON array of strings + computed_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE +); + +CREATE INDEX idx_scores_deal ON deal_scores(deal_id); +``` + +#### ai_insights 🆕 +```sql +CREATE TABLE ai_insights ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + insight_type TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT, + severity TEXT, + is_dismissed BOOLEAN DEFAULT 0, + source_file_ids TEXT, -- JSON array + created_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE +); + +CREATE INDEX idx_insights_deal ON ai_insights(deal_id); +``` + +#### contacts 🆕 +```sql +CREATE TABLE contacts ( + id TEXT PRIMARY KEY, + organization_id TEXT NOT NULL, + full_name TEXT NOT NULL, + email TEXT, + phone TEXT, + company TEXT, + title TEXT, + contact_type TEXT, -- buyer, advisor, internal, seller + tags TEXT, -- JSON array + notes TEXT, + last_activity_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE +); + +CREATE INDEX idx_contacts_org ON contacts(organization_id); +``` + +#### ic_memos 🆕 +```sql +CREATE TABLE ic_memos ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + organization_id TEXT NOT NULL, + created_by TEXT, + title TEXT NOT NULL, + content TEXT NOT NULL, -- JSON: summary, business_model, financials, key_risks[], diligence_gaps[], valuation_considerations, recommendation, source_documents[] + status TEXT DEFAULT 'draft' + CHECK (status IN ('draft', 'review', 'final', 'archived')), + version INTEGER DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations(id), + FOREIGN KEY (created_by) REFERENCES users(id) +); + +CREATE INDEX idx_memos_deal ON ic_memos(deal_id); +``` + +#### nda_records 🆕 +```sql +CREATE TABLE nda_records ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + signer_name TEXT NOT NULL, + signer_email TEXT NOT NULL, + signer_company TEXT, + user_id TEXT, + status TEXT DEFAULT 'pending', + signed_at INTEGER, + expires_at INTEGER, + nda_document_url TEXT, + ip_address TEXT, + created_at INTEGER NOT NULL, + + FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE +); +``` + +#### tasks 🆕 +```sql +CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + organization_id TEXT NOT NULL, + deal_id TEXT, + title TEXT NOT NULL, + description TEXT, + assigned_to TEXT, + created_by TEXT, + status TEXT DEFAULT 'pending', + priority TEXT DEFAULT 'medium' + CHECK (priority IN ('high', 'medium', 'low')), + due_date TEXT, + completed_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (organization_id) REFERENCES organizations(id), + FOREIGN KEY (deal_id) REFERENCES deals(id), + FOREIGN KEY (assigned_to) REFERENCES users(id) +); +``` + +#### workflow_rules 🆕 +```sql +CREATE TABLE workflow_rules ( + id TEXT PRIMARY KEY, + organization_id TEXT NOT NULL, + name TEXT NOT NULL, + trigger_type TEXT NOT NULL, + trigger_config TEXT NOT NULL, -- JSON + action_type TEXT NOT NULL, + action_config TEXT NOT NULL, -- JSON + is_active BOOLEAN DEFAULT 1, + created_by TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + FOREIGN KEY (organization_id) REFERENCES organizations(id) +); ``` #### sessions @@ -111,7 +392,7 @@ CREATE TABLE sessions ( last_used INTEGER NOT NULL, user_agent TEXT, ip_address TEXT, - + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); @@ -119,160 +400,169 @@ CREATE INDEX idx_sessions_user ON sessions(user_id); CREATE INDEX idx_sessions_expires ON sessions(expires_at); ``` -#### audit_log +#### audit_log (📝 renamed from deal_activity for clarity) ```sql CREATE TABLE audit_log ( id TEXT PRIMARY KEY, user_id TEXT, - entry_id TEXT, - action TEXT NOT NULL, -- view, create, update, delete, download, share - details TEXT, -- JSON with action-specific data + deal_id TEXT, + organization_id TEXT, + action TEXT NOT NULL + CHECK (action IN ('view', 'download', 'upload', 'edit', 'delete', 'share', 'comment', 'permission_change', 'nda_signed', 'login')), + resource_type TEXT, + resource_id TEXT, + resource_name TEXT, + details TEXT, -- JSON ip_address TEXT, user_agent TEXT, created_at INTEGER NOT NULL, - + FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (entry_id) REFERENCES entries(id) + FOREIGN KEY (deal_id) REFERENCES deals(id) ); CREATE INDEX idx_audit_user ON audit_log(user_id); -CREATE INDEX idx_audit_entry ON audit_log(entry_id); +CREATE INDEX idx_audit_deal ON audit_log(deal_id); CREATE INDEX idx_audit_action ON audit_log(action); CREATE INDEX idx_audit_created ON audit_log(created_at); ``` -### Entry Content Schemas - -#### Deal Room Entry -```json -{ - "type": "deal_room", - "description": "Acquisition of TechCorp by PrivateEquity Partners", - "stage": "due_diligence", // sourcing, loi, due_diligence, closing, completed - "target_company": "TechCorp Inc.", - "deal_value": "$50M", - "participants": [ - {"name": "John Smith", "role": "Deal Lead", "organization": "PE Partners"}, - {"name": "Jane Doe", "role": "Analyst", "organization": "PE Partners"} - ], - "key_dates": { - "loi_signed": "2024-01-15", - "dd_start": "2024-02-01", - "close_target": "2024-04-30" - }, - "confidentiality_level": "highly_confidential" -} -``` - -#### Document Entry -```json -{ - "type": "document", - "category": "financial_model", // nda, cim, financial_model, teaser, legal, dd_report - "mime_type": "application/pdf", - "original_name": "TechCorp_Financial_Model_v3.xlsx", - "version": "3.0", - "analysis": { - "summary": "Financial projections showing 15% EBITDA growth", - "key_metrics": ["Revenue: $100M", "EBITDA: $25M", "Growth: 15%"], - "risk_factors": ["Market volatility", "Regulatory changes"], - "ai_confidence": 0.92 - }, - "requires_nda": true -} -``` - -#### Note/Message Entry -```json -{ - "type": "note", - "body": "Updated financial model reflects Q4 performance...", - "mentions": ["user_id_123"], // @mentions for notifications - "attachments": ["entry_id_456"], // Reference to document entries - "thread_context": "document_discussion" // Helps organize conversations -} -``` - ## Permission Model (RBAC) -### Bitmask Values -- **READ (1):** View entry and metadata -- **WRITE (2):** Edit entry, add comments -- **DELETE (4):** Remove entry -- **MANAGE (8):** Grant/revoke access, manage permissions +### 📝 Role-Based (from Lovable prototype) -### Permission Inheritance -- Deal room permissions cascade to all contained documents -- Explicit document permissions override inherited permissions -- Admins have MANAGE (8) on all entries by default +Four roles per organization: +- **Owner:** Full access. Can delete deals, manage org settings, transfer ownership. +- **Admin:** Manage deals, users, integrations. Cannot delete organization. +- **Member:** Create/manage deals, upload documents, use all VDR features. +- **Viewer:** Read-only access to shared deal rooms. -### Common Permission Patterns -- **Viewer:** READ (1) - Can view documents and notes -- **Contributor:** READ + WRITE (3) - Can add notes and upload documents -- **Manager:** READ + WRITE + DELETE (7) - Can manage content -- **Admin:** All permissions (15) - Full control +### Seller vs Buyer View 🆕 +- **Seller** = Owner or Admin role → full platform access +- **Buyer** = Member or Viewer role → restricted view filtered by buyer_group + +| Feature | Seller | Buyer | +|---------|--------|-------| +| Create deal rooms | ✅ | ❌ | +| Upload files | ✅ | ❌ | +| Create/upload request lists | ✅ | ❌ | +| View analytics & engagement | ✅ | ❌ | +| Generate IC memos | ✅ | ❌ | +| View all deals | ✅ | Only assigned deals | +| View all requests | ✅ | Only their buyer_group | +| Add comments on requests | ✅ | ✅ | +| Use Atlas AI | ✅ | ✅ (scoped) | ## API Endpoints ### Authentication ``` -POST /auth/login # Magic link login +POST /auth/login # Email/password login +POST /auth/signup # Create account + org 🆕 GET /auth/verify/{token} # Verify magic link POST /auth/logout # End session GET /auth/me # Current user info +POST /auth/demo/{role} # Demo login (seller/buyer) 🆕 ``` -### Deal Rooms +### Organizations 🆕 ``` -GET /api/deal-rooms # List accessible deal rooms -POST /api/deal-rooms # Create new deal room -GET /api/deal-rooms/{id} # Get deal room details -PUT /api/deal-rooms/{id} # Update deal room -DELETE /api/deal-rooms/{id} # Archive deal room +POST /api/organizations # Create organization +GET /api/organizations/{id} # Get organization +PUT /api/organizations/{id} # Update organization ``` -### Entries (Documents, Notes, etc.) +### Deals 📝 (was: Deal Rooms) ``` -GET /api/entries # List entries (filtered by permissions) -POST /api/entries # Create entry -GET /api/entries/{id} # Get entry details -PUT /api/entries/{id} # Update entry -DELETE /api/entries/{id} # Delete entry -GET /api/entries/{id}/file # Download file (for documents) +GET /api/deals # List deals (filtered by role/buyer_group) +POST /api/deals # Create deal (seller only) +GET /api/deals/{id} # Get deal details +PUT /api/deals/{id} # Update deal +DELETE /api/deals/{id} # Archive deal ``` -### Access Management +### Folders 🆕 ``` -GET /api/entries/{id}/access # List permissions for entry -POST /api/entries/{id}/access # Grant access -PUT /api/entries/{id}/access/{uid} # Update user permissions -DELETE /api/entries/{id}/access/{uid} # Revoke access +GET /api/deals/{id}/folders # List folders (tree) +POST /api/deals/{id}/folders # Create folder +PUT /api/folders/{id} # Update folder +DELETE /api/folders/{id} # Delete folder +POST /api/folders/{id}/summarize # Generate AI summary ``` -### Search & AI +### Files 📝 ``` -GET /api/search?q={query} # Semantic search across accessible content -POST /api/analyze/{entry_id} # Trigger AI analysis -GET /api/similar/{entry_id} # Find similar documents +GET /api/deals/{id}/files # List files (optional folder_id filter) +POST /api/deals/{id}/files # Upload file(s) +GET /api/files/{id} # Get file metadata +GET /api/files/{id}/download # Download file +PUT /api/files/{id} # Update file metadata +DELETE /api/files/{id} # Delete file +``` + +### Diligence Requests 🆕 +``` +GET /api/deals/{id}/requests # List requests for a deal +POST /api/deals/{id}/requests # Create request +POST /api/deals/{id}/requests/bulk # Bulk import (CSV) 🆕 +PUT /api/requests/{id} # Update request (status, comments) +GET /api/requests # Cross-deal request list +``` + +### Contacts 🆕 +``` +GET /api/contacts # List contacts for org +POST /api/contacts # Create contact +PUT /api/contacts/{id} # Update contact +DELETE /api/contacts/{id} # Delete contact +``` + +### IC Memos 🆕 +``` +GET /api/ic-memos # List memos for org +POST /api/ic-memos/generate # Generate memo from deal +GET /api/ic-memos/{id} # Get memo +PUT /api/ic-memos/{id} # Update memo +``` + +### Analytics & Engagement 🆕 +``` +GET /api/deals/{id}/engagement # Buyer engagement data +GET /api/deals/{id}/score # Get deal score +POST /api/deals/{id}/score # Compute deal score +``` + +### AI 📝 +``` +POST /api/ai/folder-summary # Generate folder summary +POST /api/ai/deal-score # Compute deal close probability +POST /api/ai/ic-memo # Generate IC memo +POST /api/ai/chat # Atlas AI chat (context-aware) +GET /api/deals/{id}/insights # AI insights for deal +POST /api/analyze/{file_id} # Trigger file AI analysis +GET /api/search?q={query} # Semantic search ``` ### Activity & Audit ``` -GET /api/activity/{deal_room_id} # Activity feed for deal room -GET /api/audit/{entry_id} # Audit log for specific entry +GET /api/activity # Activity feed (org-wide) +GET /api/audit # Full audit log (filterable) ``` ## Page Routes (Server-Rendered) ``` -GET / # Dashboard - accessible deal rooms -GET /login # Login page -GET /deal-rooms/{id} # Deal room detail view -GET /deal-rooms/{id}/upload # Document upload page -GET /documents/{id} # Document viewer -GET /admin # Admin panel (user/permissions management) -GET /profile # User profile -GET /activity # Global activity feed +GET / # Dashboard +GET /auth # Login/Signup page 📝 +GET /deals # Deal rooms list 🆕 +GET /deals/{id} # Deal room detail (documents + requests) 📝 +GET /requests # Cross-deal request lists 🆕 +GET /analytics # Engagement analytics 🆕 +GET /ic-memos # IC Memo generator + viewer 🆕 +GET /contacts # Contact management 🆕 +GET /audit # Audit log 📝 +GET /settings # Settings (profile, org, security, integrations, workflows) 📝 +GET /guide # Platform documentation/guide 🆕 ``` ## File Storage Design @@ -283,11 +573,12 @@ data/ ├── db/ │ └── dealroom.db # SQLite database ├── files/ -│ ├── 2024/01/ # Date-based partitioning -│ │ ├── abc123.enc # Encrypted + compressed files -│ │ └── def456.enc +│ ├── {deal_id}/ +│ │ ├── {folder_id|root}/ +│ │ │ └── {timestamp}_{filename}.enc +│ │ └── ... │ └── temp/ # Temporary upload staging -└── backups/ # Automated backups +└── backups/ ├── db/ └── files/ ``` @@ -296,14 +587,10 @@ data/ 1. **Upload:** File uploaded to `/temp/{uuid}` 2. **Compress:** Apply zstd compression (level 3) 3. **Encrypt:** AES-256-GCM with random nonce -4. **Store:** Move to date-partitioned directory -5. **Index:** Store metadata + embedding in database -6. **Cleanup:** Remove temp file - -### File Naming -- **Pattern:** `{year}/{month}/{entry_id}.enc` -- **Metadata:** Stored in database, not filesystem -- **Deduplication:** SHA-256 hash prevents duplicate storage +4. **Store:** Move to deal/folder directory +5. **AI Process:** Generate summary, tags, embeddings 🆕 +6. **Index:** Store metadata + embedding in database +7. **Cleanup:** Remove temp file ## AI/Embeddings Pipeline @@ -311,28 +598,26 @@ data/ 1. **Upload:** User uploads document 2. **Extract:** Convert to text (PDF, DOCX, XLSX support) 3. **Analyze:** Send to K2.5 for: - - Content summarization + - Content summarization → `files.ai_summary` + - Tag extraction → `files.ai_tags` - Key metrics extraction - Risk factor identification - - Classification (NDA, CIM, Financial Model, etc.) + - Classification 4. **Embed:** Generate vector embeddings for semantic search -5. **Store:** Save analysis results in entry content JSON +5. **Store:** Save analysis results -### K2.5 Integration -```go -type DocumentAnalysis struct { - Summary string `json:"summary"` - KeyMetrics []string `json:"key_metrics"` - RiskFactors []string `json:"risk_factors"` - Category string `json:"category"` - Confidence float64 `json:"ai_confidence"` -} - -type EmbeddingRequest struct { - Text string `json:"text"` - Model string `json:"model"` -} -``` +### Atlas AI Features 🆕 +1. **Folder Summaries** — analyze folder contents, generate natural language summary + completeness score +2. **Diligence Request Assessment** — auto-match uploaded docs to request items, set fulfilled/partial/missing with confidence scores +3. **Deal Score Computation** — weighted model: + - 35% Diligence completion + - 25% Buyer engagement score + - 20% Response velocity + - 20% Red flag mitigation +4. **IC Memo Generation** — structured memo from deal documents: + - Executive Summary, Business Model, Financial Analysis, Key Risks, Diligence Gaps, Valuation Considerations, Recommendation +5. **Context-Aware Chat** — sidebar chat scoped to current deal, queries files/folders/requests/engagement +6. **AI Insights** — proactive risk flags and recommendations stored per deal ### Semantic Search - **Vector Storage:** SQLite with vector extension @@ -343,28 +628,32 @@ type EmbeddingRequest struct { ## Security Model ### Authentication -- **Magic Link:** Email-based passwordless login +- **Email/Password:** Primary auth method 📝 +- **Magic Link:** Optional passwordless login +- **Demo Login:** Seller/buyer demo accounts 🆕 - **Session Management:** Secure HTTP-only cookies - **Token Expiry:** 24-hour sessions with automatic refresh - **Rate Limiting:** Prevent brute force attacks ### Authorization -- **RBAC:** Entry-level permissions with inheritance -- **Least Privilege:** Users see only what they have access to +- **RBAC:** Organization-level roles (owner/admin/member/viewer) 📝 +- **Seller/Buyer views:** Role-based UI filtering 🆕 +- **Buyer group scoping:** Buyers see only their assigned content 🆕 - **Audit Trail:** All actions logged with user attribution -- **Admin Controls:** User management and permission oversight ### Data Protection - **Encryption at Rest:** AES-256-GCM for files, encrypted SQLite - **Encryption in Transit:** HTTPS only, HSTS headers -- **File Access:** Direct file serving prevented, all through API -- **Backup Encryption:** Automated encrypted backups +- **File Security:** Watermarking, download restrictions, sensitivity flags 🆕 +- **NDA Gating:** Require signed NDA before deal room access 🆕 +- **Tenant Isolation:** Organization-level data isolation 🆕 -### Compliance Features -- **Audit Logging:** Comprehensive activity tracking -- **Data Retention:** Configurable retention policies -- **Access Reviews:** Periodic permission audits -- **Export Controls:** Document download tracking +### Settings-Configurable Security 🆕 +- MFA requirement toggle +- IP allowlisting +- Session timeout configuration +- Download watermarking toggle +- View-only mode for buyers ## Deployment Architecture @@ -402,19 +691,6 @@ SMTP_USER=dealroom@company.com SMTP_PASS= ``` -### Docker Deployment -```dockerfile -FROM golang:1.22-alpine AS builder -# ... build process - -FROM alpine:3.19 -RUN apk --no-cache add ca-certificates tzdata -COPY --from=builder /app/dealroom /usr/local/bin/ -VOLUME ["/data"] -EXPOSE 8080 -CMD ["dealroom"] -``` - ## Development Workflow ### Project Structure @@ -445,26 +721,56 @@ make migrate # Run database migrations make docker # Build Docker image ``` -### Testing Strategy -- **Unit Tests:** Core business logic and RBAC -- **Integration Tests:** Database and file operations -- **E2E Tests:** Critical user journeys with real browser -- **Security Tests:** Permission boundaries and file access -- **Performance Tests:** Large file uploads and search +## Implementation Phases -## Scalability Considerations +### Phase 1: Core Platform (4 weeks) +- Authentication (email/password + sessions) +- Organization + user role management 🆕 +- Deal CRUD with stage tracking 📝 +- Folder tree management 🆕 +- File upload with encryption +- Basic RBAC (seller/buyer views) 📝 -### Current Limits (SQLite-based) -- **Concurrent Users:** ~100-200 active users -- **File Storage:** Limited by disk space -- **Search Performance:** Good up to ~10K documents -- **Database Size:** Efficient up to ~100GB +### Phase 2: Diligence & Collaboration (3 weeks) +- Diligence request lists with sections 🆕 +- CSV bulk import for requests 🆕 +- Buyer/seller inline comments 🆕 +- Buyer group filtering 🆕 +- Activity feeds and audit log +- Contact management 🆕 -### Migration Path (Future) -- **Database:** PostgreSQL for higher concurrency -- **File Storage:** S3-compatible object storage -- **Search:** Dedicated vector database (Pinecone, Weaviate) -- **Caching:** Redis for session and query caching +### Phase 3: AI Integration (3 weeks) 📝 +- K2.5 document analysis (summary, tags) +- Atlas folder summaries + completeness scores 🆕 +- Diligence request auto-assessment 🆕 +- Deal score computation engine 🆕 +- IC Memo generation 🆕 +- Embeddings and semantic search +- AI insights system 🆕 + +### Phase 4: Analytics & Engagement (2 weeks) 🆕 +- Buyer engagement tracking +- Intent signals and alerts +- Deal close probability dashboard +- Engagement timeline + +### Phase 5: Production Readiness (2 weeks) +- NDA gating flow 🆕 +- Security settings (watermarking, download controls) 🆕 +- Settings UI (profile, org, security) 📝 +- Platform guide/documentation page 🆕 +- Performance optimization +- Security hardening + +### Phase 6: Advanced Features (3 weeks) +- Workflow automation engine 🆕 +- CRM integrations (Salesforce, HubSpot) 🆕 +- Task management system 🆕 +- Demo login system 🆕 +- Advanced reporting +- White-label/theming 🆕 + +Total estimated development time: **17 weeks** with dedicated development team. ## Success Metrics @@ -479,69 +785,10 @@ make docker # Build Docker image - **Document Velocity:** Files uploaded/downloaded per day - **Security Events:** Zero unauthorized access incidents - **User Satisfaction:** NPS > 8 for ease of use - -## Risk Assessment - -### Technical Risks -- **Single Point of Failure:** SQLite limits high availability -- **File Corruption:** Encryption key loss = data loss -- **AI Dependency:** K2.5 service availability required -- **Scaling Challenges:** May need architecture changes at scale - -### Mitigation Strategies -- **Automated Backups:** Hourly encrypted backups to S3 -- **Key Management:** Secure key storage and rotation -- **Circuit Breakers:** Graceful degradation when AI unavailable -- **Monitoring:** Comprehensive health checks and alerting - -### Security Risks -- **Data Breach:** Highly sensitive financial information -- **Insider Threat:** Authorized users with malicious intent -- **Compliance:** Regulatory requirements for financial data -- **Access Control:** Complex permission inheritance bugs - -### Security Controls -- **Defense in Depth:** Multiple security layers -- **Principle of Least Privilege:** Minimal required permissions -- **Comprehensive Auditing:** All actions logged and monitored -- **Regular Reviews:** Periodic security assessments - -## Implementation Phases - -### Phase 1: Core Platform (4 weeks) -- Basic authentication and session management -- Deal room creation and management -- Document upload with encryption -- Basic RBAC implementation - -### Phase 2: Collaboration Features (3 weeks) -- Notes and messaging system -- Activity feeds and notifications -- Advanced permission management -- Search functionality - -### Phase 3: AI Integration (2 weeks) -- K2.5 document analysis -- Embeddings and semantic search -- Document summarization -- Similar document recommendations - -### Phase 4: Production Readiness (2 weeks) -- Comprehensive audit logging -- Admin dashboard -- Performance optimization -- Security hardening - -### Phase 5: Advanced Features (3 weeks) -- Deal stage tracking -- Bulk operations -- API for integrations -- Advanced reporting - -Total estimated development time: **14 weeks** with dedicated development team. +- **AI Accuracy:** >80% confidence on diligence auto-assessment 🆕 ## Conclusion Deal Room represents a modern, security-first approach to Investment Banking document management. By leveraging the proven inou-portal pattern with Go's performance characteristics and AI-enhanced document analysis, we deliver a solution that meets the demanding requirements of financial services while maintaining operational simplicity through single-binary deployment. -The architecture prioritizes security, auditability, and user experience while providing a clear path for future scalability as the platform grows. \ No newline at end of file +The Lovable prototype validated the core feature set and revealed several features not in the original spec — particularly the diligence request system, IC memo generation, buyer engagement analytics, and the Atlas AI integration points. These have been incorporated into this spec while maintaining our Go+HTMX architecture decisions. diff --git a/go.mod b/go.mod index 57ade86..ec73fe0 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,11 @@ module dealroom -go 1.22 +go 1.23 + +toolchain go1.23.6 require ( github.com/a-h/templ v0.2.778 - github.com/gorilla/sessions v1.2.2 + github.com/klauspost/compress v1.18.4 github.com/mattn/go-sqlite3 v1.14.18 - github.com/klauspost/compress v1.17.4 - golang.org/x/crypto v0.17.0 ) - -require ( - github.com/gorilla/securecookie v1.1.2 // indirect -) \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6e10caa --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM= +github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= diff --git a/internal/ai/k25.go b/internal/ai/k25.go deleted file mode 100644 index 03095e5..0000000 --- a/internal/ai/k25.go +++ /dev/null @@ -1,211 +0,0 @@ -package ai - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "dealroom/internal/model" -) - -// K25Client handles communication with K2.5 AI service -type K25Client struct { - baseURL string - apiKey string - client *http.Client -} - -// NewK25Client creates a new K2.5 client -func NewK25Client(baseURL, apiKey string) *K25Client { - return &K25Client{ - baseURL: baseURL, - apiKey: apiKey, - client: &http.Client{ - Timeout: 30 * time.Second, - }, - } -} - -// AnalysisRequest represents a request for document analysis -type AnalysisRequest struct { - Text string `json:"text"` - Category string `json:"category,omitempty"` // nda, cim, financial_model, etc. - Language string `json:"language,omitempty"` -} - -// AnalysisResponse represents the response from K2.5 analysis -type AnalysisResponse struct { - Summary string `json:"summary"` - KeyMetrics []string `json:"key_metrics"` - RiskFactors []string `json:"risk_factors"` - Category string `json:"category"` - Confidence float64 `json:"confidence"` - ProcessingMs int64 `json:"processing_ms"` -} - -// EmbeddingRequest represents a request for text embeddings -type EmbeddingRequest struct { - Text string `json:"text"` - Model string `json:"model,omitempty"` -} - -// EmbeddingResponse represents the response from K2.5 embedding -type EmbeddingResponse struct { - Embedding []float64 `json:"embedding"` - Dimension int `json:"dimension"` - Model string `json:"model"` -} - -// AnalyzeDocument sends text to K2.5 for analysis -func (c *K25Client) AnalyzeDocument(text, category string) (*model.DocumentAnalysis, error) { - if c.baseURL == "" || c.apiKey == "" { - return nil, fmt.Errorf("K2.5 client not configured") - } - - req := AnalysisRequest{ - Text: text, - Category: category, - Language: "en", - } - - jsonData, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - - httpReq, err := http.NewRequest("POST", c.baseURL+"/analyze", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) - - resp, err := c.client.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("K2.5 API error %d: %s", resp.StatusCode, string(body)) - } - - var analysisResp AnalysisResponse - if err := json.NewDecoder(resp.Body).Decode(&analysisResp); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return &model.DocumentAnalysis{ - Summary: analysisResp.Summary, - KeyMetrics: analysisResp.KeyMetrics, - RiskFactors: analysisResp.RiskFactors, - AIConfidence: analysisResp.Confidence, - }, nil -} - -// GenerateEmbedding creates vector embeddings for text -func (c *K25Client) GenerateEmbedding(text string) ([]float64, error) { - if c.baseURL == "" || c.apiKey == "" { - return nil, fmt.Errorf("K2.5 client not configured") - } - - req := EmbeddingRequest{ - Text: text, - Model: "default", - } - - jsonData, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - - httpReq, err := http.NewRequest("POST", c.baseURL+"/embed", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) - - resp, err := c.client.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("K2.5 API error %d: %s", resp.StatusCode, string(body)) - } - - var embeddingResp EmbeddingResponse - if err := json.NewDecoder(resp.Body).Decode(&embeddingResp); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return embeddingResp.Embedding, nil -} - -// SearchSimilar finds documents similar to the given text -func (c *K25Client) SearchSimilar(text string, limit int) ([]SimilarDocument, error) { - // This would typically involve: - // 1. Generate embedding for the search text - // 2. Query the database for similar embeddings using cosine similarity - // 3. Return ranked results - - // For now, return a placeholder implementation - return []SimilarDocument{}, fmt.Errorf("not implemented") -} - -// SimilarDocument represents a document similar to the search query -type SimilarDocument struct { - EntryID string `json:"entry_id"` - Title string `json:"title"` - Similarity float64 `json:"similarity"` - Snippet string `json:"snippet"` -} - -// Health checks if the K2.5 service is available -func (c *K25Client) Health() error { - if c.baseURL == "" { - return fmt.Errorf("K2.5 client not configured") - } - - req, err := http.NewRequest("GET", c.baseURL+"/health", nil) - if err != nil { - return fmt.Errorf("failed to create health check request: %w", err) - } - - if c.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+c.apiKey) - } - - resp, err := c.client.Do(req) - if err != nil { - return fmt.Errorf("health check failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("K2.5 service unhealthy: status %d", resp.StatusCode) - } - - return nil -} - -// ExtractTextFromFile extracts text content from various file types -func ExtractTextFromFile(filePath, mimeType string) (string, error) { - // This would implement text extraction from: - // - PDF files - // - Microsoft Office documents (DOCX, XLSX, PPTX) - // - Plain text files - // - Images with OCR - - // For now, return a placeholder - return "", fmt.Errorf("text extraction not implemented for %s", mimeType) -} \ No newline at end of file diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 7f790f2..883fc84 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -3,123 +3,271 @@ package db import ( "database/sql" "fmt" + "log" ) -// Migrate runs all database migrations func Migrate(db *sql.DB) error { migrations := []string{ - createUsersTable, - createEntriesTable, - createAccessTable, - createSessionsTable, - createAuditLogTable, + createOrganizations, + createProfiles, + createDeals, + createFolders, + createFiles, + createDiligenceRequests, + createContacts, + createDealActivity, + createSessions, createIndexes, } - for i, migration := range migrations { - if _, err := db.Exec(migration); err != nil { + for i, m := range migrations { + if _, err := db.Exec(m); err != nil { return fmt.Errorf("migration %d failed: %w", i+1, err) } } + // Seed demo data if empty + var count int + db.QueryRow("SELECT COUNT(*) FROM organizations").Scan(&count) + if count == 0 { + log.Println("Seeding demo data...") + if err := seed(db); err != nil { + return fmt.Errorf("seed failed: %w", err) + } + } + return nil } -const createUsersTable = ` -CREATE TABLE IF NOT EXISTS users ( +const createOrganizations = ` +CREATE TABLE IF NOT EXISTS organizations ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +);` + +const createProfiles = ` +CREATE TABLE IF NOT EXISTS profiles ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, + full_name TEXT NOT NULL, + avatar_url TEXT DEFAULT '', + organization_id TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('owner','admin','member','viewer')), + password_hash TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME, + FOREIGN KEY (organization_id) REFERENCES organizations(id) +);` + +const createDeals = ` +CREATE TABLE IF NOT EXISTS deals ( + id TEXT PRIMARY KEY, + organization_id TEXT NOT NULL, name TEXT NOT NULL, - role TEXT NOT NULL CHECK (role IN ('admin', 'user')), - avatar_url TEXT, - created_at INTEGER NOT NULL, - last_login INTEGER, - is_active BOOLEAN NOT NULL DEFAULT 1 + description TEXT DEFAULT '', + target_company TEXT DEFAULT '', + stage TEXT NOT NULL DEFAULT 'pipeline' CHECK (stage IN ('pipeline','loi','initial_review','due_diligence','final_negotiation','closed','dead')), + deal_size REAL DEFAULT 0, + currency TEXT DEFAULT 'USD', + ioi_date TEXT DEFAULT '', + loi_date TEXT DEFAULT '', + exclusivity_end_date TEXT DEFAULT '', + expected_close_date TEXT DEFAULT '', + close_probability INTEGER DEFAULT 0, + is_archived BOOLEAN DEFAULT 0, + created_by TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (organization_id) REFERENCES organizations(id) );` -const createEntriesTable = ` -CREATE TABLE IF NOT EXISTS entries ( +const createFolders = ` +CREATE TABLE IF NOT EXISTS folders ( id TEXT PRIMARY KEY, - parent_id TEXT, - deal_room_id TEXT NOT NULL, - entry_type TEXT NOT NULL CHECK (entry_type IN ('deal_room', 'document', 'note', 'message', 'analysis')), - title TEXT NOT NULL, - content TEXT NOT NULL, - file_path TEXT, - file_size INTEGER, - file_hash TEXT, - embedding BLOB, - created_by TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - - FOREIGN KEY (created_by) REFERENCES users(id), - FOREIGN KEY (parent_id) REFERENCES entries(id), - FOREIGN KEY (deal_room_id) REFERENCES entries(id) + deal_id TEXT NOT NULL, + parent_id TEXT DEFAULT '', + name TEXT NOT NULL, + description TEXT DEFAULT '', + created_by TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (deal_id) REFERENCES deals(id) );` -const createAccessTable = ` -CREATE TABLE IF NOT EXISTS access ( +const createFiles = ` +CREATE TABLE IF NOT EXISTS files ( id TEXT PRIMARY KEY, - entry_id TEXT NOT NULL, - user_id TEXT NOT NULL, - permissions INTEGER NOT NULL DEFAULT 1, - granted_by TEXT NOT NULL, - granted_at INTEGER NOT NULL, - - FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (granted_by) REFERENCES users(id), - - UNIQUE(entry_id, user_id) + deal_id TEXT NOT NULL, + folder_id TEXT DEFAULT '', + name TEXT NOT NULL, + file_size INTEGER DEFAULT 0, + mime_type TEXT DEFAULT '', + status TEXT DEFAULT 'uploaded' CHECK (status IN ('uploaded','processing','reviewed','flagged','archived')), + uploaded_by TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (deal_id) REFERENCES deals(id) );` -const createSessionsTable = ` +const createDiligenceRequests = ` +CREATE TABLE IF NOT EXISTS diligence_requests ( + id TEXT PRIMARY KEY, + deal_id TEXT NOT NULL, + item_number TEXT DEFAULT '', + section TEXT NOT NULL, + description TEXT NOT NULL, + priority TEXT DEFAULT 'medium' CHECK (priority IN ('high','medium','low')), + atlas_status TEXT DEFAULT 'missing' CHECK (atlas_status IN ('fulfilled','partial','missing','not_applicable')), + atlas_note TEXT DEFAULT '', + confidence INTEGER DEFAULT 0, + buyer_comment TEXT DEFAULT '', + seller_comment TEXT DEFAULT '', + buyer_group TEXT DEFAULT '', + linked_file_ids TEXT DEFAULT '', + created_by TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (deal_id) REFERENCES deals(id) +);` + +const createContacts = ` +CREATE TABLE IF NOT EXISTS contacts ( + id TEXT PRIMARY KEY, + organization_id TEXT NOT NULL, + full_name TEXT NOT NULL, + email TEXT DEFAULT '', + phone TEXT DEFAULT '', + company TEXT DEFAULT '', + title TEXT DEFAULT '', + contact_type TEXT DEFAULT 'buyer', + tags TEXT DEFAULT '', + notes TEXT DEFAULT '', + last_activity_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (organization_id) REFERENCES organizations(id) +);` + +const createDealActivity = ` +CREATE TABLE IF NOT EXISTS deal_activity ( + id TEXT PRIMARY KEY, + organization_id TEXT DEFAULT '', + deal_id TEXT DEFAULT '', + user_id TEXT DEFAULT '', + activity_type TEXT NOT NULL, + resource_type TEXT DEFAULT '', + resource_name TEXT DEFAULT '', + resource_id TEXT DEFAULT '', + details TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +);` + +const createSessions = ` CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id TEXT NOT NULL, - expires_at INTEGER NOT NULL, - created_at INTEGER NOT NULL, - last_used INTEGER NOT NULL, - user_agent TEXT, - ip_address TEXT, - - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -);` - -const createAuditLogTable = ` -CREATE TABLE IF NOT EXISTS audit_log ( - id TEXT PRIMARY KEY, - user_id TEXT, - entry_id TEXT, - action TEXT NOT NULL, - details TEXT, - ip_address TEXT, - user_agent TEXT, - created_at INTEGER NOT NULL, - - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (entry_id) REFERENCES entries(id) + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES profiles(id) );` const createIndexes = ` -CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active); - -CREATE INDEX IF NOT EXISTS idx_entries_deal_room ON entries(deal_room_id); -CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type); -CREATE INDEX IF NOT EXISTS idx_entries_parent ON entries(parent_id); -CREATE INDEX IF NOT EXISTS idx_entries_created ON entries(created_at); -CREATE INDEX IF NOT EXISTS idx_entries_creator ON entries(created_by); - -CREATE INDEX IF NOT EXISTS idx_access_entry ON access(entry_id); -CREATE INDEX IF NOT EXISTS idx_access_user ON access(user_id); -CREATE INDEX IF NOT EXISTS idx_access_permissions ON access(permissions); - +CREATE INDEX IF NOT EXISTS idx_profiles_email ON profiles(email); +CREATE INDEX IF NOT EXISTS idx_profiles_org ON profiles(organization_id); +CREATE INDEX IF NOT EXISTS idx_deals_org ON deals(organization_id); +CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage); +CREATE INDEX IF NOT EXISTS idx_folders_deal ON folders(deal_id); +CREATE INDEX IF NOT EXISTS idx_files_deal ON files(deal_id); +CREATE INDEX IF NOT EXISTS idx_files_folder ON files(folder_id); +CREATE INDEX IF NOT EXISTS idx_requests_deal ON diligence_requests(deal_id); +CREATE INDEX IF NOT EXISTS idx_contacts_org ON contacts(organization_id); +CREATE INDEX IF NOT EXISTS idx_activity_deal ON deal_activity(deal_id); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); -CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); +` -CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id); -CREATE INDEX IF NOT EXISTS idx_audit_entry ON audit_log(entry_id); -CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action); -CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);` \ No newline at end of file +func seed(db *sql.DB) error { + stmts := []string{ + // Organization + `INSERT INTO organizations (id, name, slug) VALUES ('org-1', 'Apex Capital Partners', 'apex-capital')`, + + // Profiles - seller (owner) and buyer (viewer) + `INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES + ('user-seller', 'sarah@apexcapital.com', 'Sarah Chen', 'org-1', 'owner', 'demo'), + ('user-buyer', 'marcus@meridiancap.com', 'Marcus Webb', 'org-1', 'viewer', 'demo')`, + + // Deals + `INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by) VALUES + ('deal-1', 'org-1', 'Project Aurora', 'AI-powered enterprise SaaS platform acquisition', 'TechNova Solutions', 'due_diligence', 45000000, 'USD', '2025-12-15', '2026-01-10', '2026-03-15', '2026-04-30', 72, 'user-seller'), + ('deal-2', 'org-1', 'Project Beacon', 'Healthcare technology platform investment', 'MedFlow Health', 'initial_review', 28000000, 'USD', '2026-01-05', '', '', '2026-06-15', 45, 'user-seller'), + ('deal-3', 'org-1', 'Project Cascade', 'Fintech payment processing acquisition', 'PayStream Inc', 'loi', 62000000, 'USD', '2025-11-20', '2026-02-01', '2026-04-01', '2026-05-15', 58, 'user-seller'), + ('deal-4', 'org-1', 'Project Delta', 'Industrial IoT platform strategic investment', 'SensorGrid Corp', 'pipeline', 15000000, 'USD', '', '', '', '2026-08-01', 30, 'user-seller')`, + + // Folders for deal-1 (Project Aurora) + `INSERT INTO folders (id, deal_id, parent_id, name, description) VALUES + ('folder-1', 'deal-1', '', 'Financial Documents', 'All financial statements and models'), + ('folder-2', 'deal-1', '', 'Legal Documents', 'Contracts, NDAs, and legal agreements'), + ('folder-3', 'deal-1', '', 'Technical Due Diligence', 'Technical documentation and audits'), + ('folder-4', 'deal-1', 'folder-1', 'Q4 2025 Reports', 'Quarterly financial reports'), + ('folder-5', 'deal-2', '', 'Clinical Data', 'Healthcare compliance and clinical data'), + ('folder-6', 'deal-2', '', 'Financial Projections', 'Revenue models and projections'), + ('folder-7', 'deal-3', '', 'Regulatory Filings', 'Payment processing regulatory documents'), + ('folder-8', 'deal-3', '', 'Technology Stack', 'Architecture and infrastructure docs')`, + + // Files + `INSERT INTO files (id, deal_id, folder_id, name, file_size, mime_type, status, uploaded_by) VALUES + ('file-1', 'deal-1', 'folder-1', 'Annual_Report_2025.pdf', 2450000, 'application/pdf', 'reviewed', 'user-seller'), + ('file-2', 'deal-1', 'folder-1', 'Revenue_Model_v3.xlsx', 890000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'user-seller'), + ('file-3', 'deal-1', 'folder-4', 'Q4_Income_Statement.pdf', 1200000, 'application/pdf', 'reviewed', 'user-seller'), + ('file-4', 'deal-1', 'folder-2', 'NDA_Executed.pdf', 450000, 'application/pdf', 'reviewed', 'user-seller'), + ('file-5', 'deal-1', 'folder-2', 'IP_Assignment_Agreement.pdf', 780000, 'application/pdf', 'flagged', 'user-seller'), + ('file-6', 'deal-1', 'folder-3', 'Architecture_Overview.pdf', 3200000, 'application/pdf', 'reviewed', 'user-seller'), + ('file-7', 'deal-1', 'folder-3', 'Security_Audit_2025.pdf', 1800000, 'application/pdf', 'processing', 'user-seller'), + ('file-8', 'deal-2', 'folder-5', 'Clinical_Trial_Results.pdf', 5600000, 'application/pdf', 'uploaded', 'user-seller'), + ('file-9', 'deal-2', 'folder-6', 'Five_Year_Projection.xlsx', 670000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'user-seller'), + ('file-10', 'deal-3', 'folder-7', 'PCI_Compliance_Cert.pdf', 340000, 'application/pdf', 'reviewed', 'user-seller'), + ('file-11', 'deal-3', 'folder-8', 'System_Architecture.pdf', 2100000, 'application/pdf', 'reviewed', 'user-seller'), + ('file-12', 'deal-3', 'folder-8', 'API_Documentation.pdf', 1500000, 'application/pdf', 'uploaded', 'user-seller')`, + + // Diligence requests for deal-1 + `INSERT INTO diligence_requests (id, deal_id, item_number, section, description, priority, atlas_status, atlas_note, confidence, buyer_comment, seller_comment, buyer_group) VALUES + ('req-1', 'deal-1', '1.1', 'Financial', 'Audited financial statements for last 3 fiscal years', 'high', 'fulfilled', 'Found in Annual_Report_2025.pdf', 95, '', 'Uploaded to Financial Documents folder', 'Meridian Capital'), + ('req-2', 'deal-1', '1.2', 'Financial', 'Monthly revenue breakdown by product line', 'high', 'partial', 'Partial data in Revenue_Model_v3.xlsx', 60, 'Need more granular breakdown', 'Working on detailed version', 'Meridian Capital'), + ('req-3', 'deal-1', '1.3', 'Financial', 'Customer concentration analysis (top 20)', 'medium', 'missing', '', 0, 'Critical for our valuation model', '', 'Meridian Capital'), + ('req-4', 'deal-1', '2.1', 'Legal', 'All material contracts and amendments', 'high', 'fulfilled', 'Located in Legal Documents', 88, '', 'All contracts uploaded', 'Meridian Capital'), + ('req-5', 'deal-1', '2.2', 'Legal', 'IP portfolio and patent filings', 'high', 'partial', 'IP Assignment found but patents pending', 45, 'Need complete patent list', 'Patent list being compiled', 'Meridian Capital'), + ('req-6', 'deal-1', '3.1', 'Technical', 'System architecture and infrastructure documentation', 'medium', 'fulfilled', 'Architecture_Overview.pdf covers this', 92, '', '', 'Summit Health Equity'), + ('req-7', 'deal-1', '3.2', 'Technical', 'Security audit and penetration test results', 'high', 'partial', 'Security audit uploaded, pen test pending', 50, 'When was the last pen test?', 'Scheduled for next month', 'Summit Health Equity'), + ('req-8', 'deal-1', '3.3', 'Technical', 'Data privacy and GDPR compliance documentation', 'medium', 'missing', '', 0, '', 'In preparation', 'Summit Health Equity'), + ('req-9', 'deal-1', '4.1', 'HR', 'Organization chart and key personnel bios', 'low', 'fulfilled', 'Found in company overview docs', 85, '', '', 'Meridian Capital'), + ('req-10', 'deal-1', '4.2', 'HR', 'Employee benefit plans and compensation structure', 'medium', 'not_applicable', 'Deferred to Phase 2', 0, '', 'Will provide in Phase 2', 'Summit Health Equity')`, + + // Contacts + `INSERT INTO contacts (id, organization_id, full_name, email, phone, company, title, contact_type, tags) VALUES + ('contact-1', 'org-1', 'Marcus Webb', 'm.webb@alpinecap.com', '+1 415-555-0142', 'Alpine Capital', 'Managing Director', 'buyer', 'active,lead-buyer'), + ('contact-2', 'org-1', 'James Liu', 'j.liu@sequoia.com', '+1 650-555-0198', 'Sequoia Partners', 'Vice President', 'buyer', 'nda-signed'), + ('contact-3', 'org-1', 'Sarah Park', 's.park@kkr.com', '+1 212-555-0267', 'KKR Growth', 'Principal', 'buyer', 'active'), + ('contact-4', 'org-1', 'David Chen', 'd.chen@warburg.com', '+1 212-555-0334', 'Warburg Pincus', 'Director', 'buyer', ''), + ('contact-5', 'org-1', 'Rachel Adams', 'r.adams@bain.com', '+1 617-555-0411', 'Bain Capital', 'Associate', 'buyer', 'new'), + ('contact-6', 'org-1', 'Michael Torres', 'm.torres@acmecap.com', '+1 415-555-0523', 'Acme Capital', 'CFO', 'internal', 'admin'), + ('contact-7', 'org-1', 'Sarah Chen', 's.chen@acmecap.com', '+1 415-555-0678', 'Acme Capital', 'VP Finance', 'internal', 'admin'), + ('contact-8', 'org-1', 'Emily Watson', 'e.watson@skadden.com', '+1 212-555-0789', 'Skadden Arps', 'Partner', 'advisor', 'legal-counsel')`, + + // Activity + `INSERT INTO deal_activity (id, organization_id, deal_id, user_id, activity_type, resource_type, resource_name, created_at) VALUES + ('act-1', 'org-1', 'deal-1', 'user-seller', 'upload', 'file', 'Annual_Report_2025.pdf', '2026-02-14 16:35:00'), + ('act-2', 'org-1', 'deal-1', 'user-buyer', 'view', 'file', 'Revenue_Model_v3.xlsx', '2026-02-14 16:30:00'), + ('act-3', 'org-1', 'deal-1', 'user-seller', 'edit', 'deal', 'Project Aurora', '2026-02-14 15:20:00'), + ('act-4', 'org-1', 'deal-2', 'user-seller', 'upload', 'file', 'Clinical_Trial_Results.pdf', '2026-02-14 14:10:00'), + ('act-5', 'org-1', 'deal-1', 'user-buyer', 'download', 'file', 'NDA_Executed.pdf', '2026-02-14 13:00:00'), + ('act-6', 'org-1', 'deal-3', 'user-seller', 'upload', 'file', 'PCI_Compliance_Cert.pdf', '2026-02-13 10:00:00'), + ('act-7', 'org-1', 'deal-1', 'user-seller', 'comment', 'request', 'Customer concentration analysis', '2026-02-13 09:00:00'), + ('act-8', 'org-1', 'deal-1', 'user-buyer', 'view', 'folder', 'Financial Documents', '2026-02-12 16:00:00')`, + } + + for i, stmt := range stmts { + if _, err := db.Exec(stmt); err != nil { + return fmt.Errorf("seed statement %d failed: %w", i+1, err) + } + } + + return nil +} diff --git a/internal/handler/analytics.go b/internal/handler/analytics.go new file mode 100644 index 0000000..0432cee --- /dev/null +++ b/internal/handler/analytics.go @@ -0,0 +1,31 @@ +package handler + +import ( + "net/http" + + "dealroom/templates" +) + +func (h *Handler) handleAnalytics(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + + var dealCount, fileCount, requestCount, fulfilledCount int + h.db.QueryRow("SELECT COUNT(*) FROM deals WHERE organization_id = ? AND is_archived = 0", profile.OrganizationID).Scan(&dealCount) + h.db.QueryRow("SELECT COUNT(*) FROM files f JOIN deals d ON f.deal_id = d.id WHERE d.organization_id = ?", profile.OrganizationID).Scan(&fileCount) + h.db.QueryRow("SELECT COUNT(*) FROM diligence_requests r JOIN deals d ON r.deal_id = d.id WHERE d.organization_id = ?", profile.OrganizationID).Scan(&requestCount) + h.db.QueryRow("SELECT COUNT(*) FROM diligence_requests r JOIN deals d ON r.deal_id = d.id WHERE d.organization_id = ? AND r.atlas_status = 'fulfilled'", profile.OrganizationID).Scan(&fulfilledCount) + + completionPct := 0 + if requestCount > 0 { + completionPct = (fulfilledCount * 100) / requestCount + } + + stats := &templates.AnalyticsStats{ + DealCount: dealCount, + FileCount: fileCount, + RequestCount: requestCount, + CompletionPct: completionPct, + } + + templates.AnalyticsPage(profile, stats).Render(r.Context(), w) +} diff --git a/internal/handler/audit.go b/internal/handler/audit.go new file mode 100644 index 0000000..b7c5686 --- /dev/null +++ b/internal/handler/audit.go @@ -0,0 +1,13 @@ +package handler + +import ( + "net/http" + + "dealroom/templates" +) + +func (h *Handler) handleAuditLog(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + activities := h.getActivities(profile.OrganizationID, 50) + templates.AuditLogPage(profile, activities).Render(r.Context(), w) +} diff --git a/internal/handler/auth.go b/internal/handler/auth.go new file mode 100644 index 0000000..2325f7f --- /dev/null +++ b/internal/handler/auth.go @@ -0,0 +1,92 @@ +package handler + +import ( + "crypto/rand" + "encoding/hex" + "net/http" + "time" + + "dealroom/templates" +) + +func (h *Handler) handleLoginPage(w http.ResponseWriter, r *http.Request) { + // If already logged in, redirect to dashboard + cookie, err := r.Cookie("session") + if err == nil && cookie.Value != "" { + var count int + h.db.QueryRow("SELECT COUNT(*) FROM sessions WHERE token = ? AND expires_at > datetime('now')", cookie.Value).Scan(&count) + if count > 0 { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + } + templates.Login().Render(r.Context(), w) +} + +func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + email := r.FormValue("email") + password := r.FormValue("password") + + var userID string + var passHash string + err := h.db.QueryRow("SELECT id, password_hash FROM profiles WHERE email = ?", email).Scan(&userID, &passHash) + if err != nil || (passHash != "demo" && passHash != password) { + http.Redirect(w, r, "/login?error=invalid", http.StatusSeeOther) + return + } + + h.createSession(w, userID) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func (h *Handler) handleDemoLogin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + role := r.FormValue("role") // "seller" or "buyer" + var userID string + if role == "buyer" { + userID = "user-buyer" + } else { + userID = "user-seller" + } + + h.createSession(w, userID) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session") + if err == nil { + h.db.Exec("DELETE FROM sessions WHERE token = ?", cookie.Value) + } + http.SetCookie(w, &http.Cookie{Name: "session", Value: "", MaxAge: -1, Path: "/"}) + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +func (h *Handler) createSession(w http.ResponseWriter, userID string) { + token := generateToken() + expires := time.Now().Add(24 * time.Hour) + h.db.Exec("INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)", token, userID, expires) + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: token, + Path: "/", + Expires: expires, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) +} + +func generateToken() string { + b := make([]byte, 32) + rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/internal/handler/contacts.go b/internal/handler/contacts.go new file mode 100644 index 0000000..99c1fe7 --- /dev/null +++ b/internal/handler/contacts.go @@ -0,0 +1,28 @@ +package handler + +import ( + "net/http" + + "dealroom/internal/model" + "dealroom/templates" +) + +func (h *Handler) handleContacts(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + + rows, err := h.db.Query("SELECT id, full_name, email, phone, company, title, contact_type, tags FROM contacts WHERE organization_id = ? ORDER BY full_name", profile.OrganizationID) + if err != nil { + http.Error(w, "Error loading contacts", 500) + return + } + defer rows.Close() + + var contacts []*model.Contact + for rows.Next() { + c := &model.Contact{} + rows.Scan(&c.ID, &c.FullName, &c.Email, &c.Phone, &c.Company, &c.Title, &c.ContactType, &c.Tags) + contacts = append(contacts, c) + } + + templates.ContactsPage(profile, contacts).Render(r.Context(), w) +} diff --git a/internal/handler/context.go b/internal/handler/context.go new file mode 100644 index 0000000..99c02b2 --- /dev/null +++ b/internal/handler/context.go @@ -0,0 +1,19 @@ +package handler + +import ( + "context" + "dealroom/internal/model" +) + +type contextKey string + +const profileKey contextKey = "profile" + +func setProfile(ctx context.Context, p *model.Profile) context.Context { + return context.WithValue(ctx, profileKey, p) +} + +func getProfile(ctx context.Context) *model.Profile { + p, _ := ctx.Value(profileKey).(*model.Profile) + return p +} diff --git a/internal/handler/deals.go b/internal/handler/deals.go new file mode 100644 index 0000000..5e9c339 --- /dev/null +++ b/internal/handler/deals.go @@ -0,0 +1,166 @@ +package handler + +import ( + "net/http" + "strings" + + "dealroom/internal/model" + "dealroom/internal/rbac" + "dealroom/templates" +) + +func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + profile := getProfile(r.Context()) + deals := h.getDeals(profile) + activities := h.getActivities(profile.OrganizationID, 8) + + // Count files per deal + fileCounts := make(map[string]int) + for _, d := range deals { + var count int + h.db.QueryRow("SELECT COUNT(*) FROM files WHERE deal_id = ?", d.ID).Scan(&count) + fileCounts[d.ID] = count + d.FileCount = count + } + + templates.Dashboard(profile, deals, activities, fileCounts).Render(r.Context(), w) +} + +func (h *Handler) handleDealRooms(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + deals := h.getDeals(profile) + for _, d := range deals { + var count int + h.db.QueryRow("SELECT COUNT(*) FROM files WHERE deal_id = ?", d.ID).Scan(&count) + d.FileCount = count + } + templates.DealRooms(profile, deals).Render(r.Context(), w) +} + +func (h *Handler) handleDealRoom(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + dealID := strings.TrimPrefix(r.URL.Path, "/deals/") + if dealID == "" { + http.Redirect(w, r, "/deals", http.StatusSeeOther) + return + } + + var deal model.Deal + err := h.db.QueryRow(`SELECT id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by FROM deals WHERE id = ?`, dealID).Scan( + &deal.ID, &deal.OrganizationID, &deal.Name, &deal.Description, &deal.TargetCompany, &deal.Stage, &deal.DealSize, &deal.Currency, &deal.IOIDate, &deal.LOIDate, &deal.ExclusivityEnd, &deal.ExpectedCloseDate, &deal.CloseProbability, &deal.CreatedBy) + if err != nil { + http.NotFound(w, r) + return + } + + folders := h.getFolders(dealID) + files := h.getFiles(dealID) + requests := h.getRequests(dealID, profile) + + templates.DealRoomDetail(profile, &deal, folders, files, requests).Render(r.Context(), w) +} + +func (h *Handler) getDeals(profile *model.Profile) []*model.Deal { + rows, err := h.db.Query("SELECT id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by, created_at FROM deals WHERE organization_id = ? AND is_archived = 0 ORDER BY created_at DESC", profile.OrganizationID) + if err != nil { + return nil + } + defer rows.Close() + + var deals []*model.Deal + for rows.Next() { + d := &model.Deal{} + rows.Scan(&d.ID, &d.OrganizationID, &d.Name, &d.Description, &d.TargetCompany, &d.Stage, &d.DealSize, &d.Currency, &d.IOIDate, &d.LOIDate, &d.ExclusivityEnd, &d.ExpectedCloseDate, &d.CloseProbability, &d.CreatedBy, &d.CreatedAt) + deals = append(deals, d) + } + return deals +} + +func (h *Handler) getFolders(dealID string) []*model.Folder { + rows, err := h.db.Query("SELECT id, deal_id, parent_id, name, description FROM folders WHERE deal_id = ? ORDER BY name", dealID) + if err != nil { + return nil + } + defer rows.Close() + + var folders []*model.Folder + for rows.Next() { + f := &model.Folder{} + rows.Scan(&f.ID, &f.DealID, &f.ParentID, &f.Name, &f.Description) + folders = append(folders, f) + } + return folders +} + +func (h *Handler) getFiles(dealID string) []*model.File { + rows, err := h.db.Query("SELECT id, deal_id, folder_id, name, file_size, mime_type, status, uploaded_by FROM files WHERE deal_id = ? ORDER BY name", dealID) + if err != nil { + return nil + } + defer rows.Close() + + var files []*model.File + for rows.Next() { + f := &model.File{} + rows.Scan(&f.ID, &f.DealID, &f.FolderID, &f.Name, &f.FileSize, &f.MimeType, &f.Status, &f.UploadedBy) + files = append(files, f) + } + return files +} + +func (h *Handler) getRequests(dealID string, profile *model.Profile) []*model.DiligenceRequest { + query := "SELECT id, deal_id, item_number, section, description, priority, atlas_status, atlas_note, confidence, buyer_comment, seller_comment, buyer_group FROM diligence_requests WHERE deal_id = ?" + args := []interface{}{dealID} + + if rbac.IsBuyer(profile.Role) { + groups := rbac.BuyerGroups(profile) + if len(groups) > 0 { + placeholders := make([]string, len(groups)) + for i, g := range groups { + placeholders[i] = "?" + args = append(args, g) + } + query += " AND buyer_group IN (" + strings.Join(placeholders, ",") + ")" + } + } + query += " ORDER BY item_number" + + rows, err := h.db.Query(query, args...) + if err != nil { + return nil + } + defer rows.Close() + + var reqs []*model.DiligenceRequest + for rows.Next() { + r := &model.DiligenceRequest{} + rows.Scan(&r.ID, &r.DealID, &r.ItemNumber, &r.Section, &r.Description, &r.Priority, &r.AtlasStatus, &r.AtlasNote, &r.Confidence, &r.BuyerComment, &r.SellerComment, &r.BuyerGroup) + reqs = append(reqs, r) + } + return reqs +} + +func (h *Handler) getActivities(orgID string, limit int) []*model.DealActivity { + rows, err := h.db.Query(` + SELECT a.id, a.deal_id, a.user_id, a.activity_type, a.resource_type, a.resource_name, a.created_at, COALESCE(p.full_name, 'Unknown') + FROM deal_activity a LEFT JOIN profiles p ON a.user_id = p.id + WHERE a.organization_id = ? + ORDER BY a.created_at DESC LIMIT ? + `, orgID, limit) + if err != nil { + return nil + } + defer rows.Close() + + var acts []*model.DealActivity + for rows.Next() { + a := &model.DealActivity{} + rows.Scan(&a.ID, &a.DealID, &a.UserID, &a.ActivityType, &a.ResourceType, &a.ResourceName, &a.CreatedAt, &a.UserName) + acts = append(acts, a) + } + return acts +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 7617a1b..ba52950 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,26 +1,17 @@ package handler import ( - "context" "database/sql" "net/http" - "dealroom/internal/rbac" - "dealroom/internal/store" - - "github.com/gorilla/sessions" + "dealroom/internal/model" ) -// Handler contains dependencies for HTTP handlers type Handler struct { - db *sql.DB - store *store.Store - rbac *rbac.Engine - sessions *sessions.CookieStore - config *Config + db *sql.DB + config *Config } -// Config holds configuration for handlers type Config struct { BaseURL string SessionKey string @@ -31,176 +22,58 @@ type Config struct { SMTPPass string } -// New creates a new handler instance -func New(db *sql.DB, fileStore *store.Store, config *Config) *Handler { +func New(db *sql.DB, _ interface{}, config *Config) *Handler { return &Handler{ - db: db, - store: fileStore, - rbac: rbac.New(db), - sessions: sessions.NewCookieStore([]byte(config.SessionKey)), - config: config, + db: db, + config: config, } } -// RegisterRoutes sets up all HTTP routes func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // Static files mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - // Authentication routes - mux.HandleFunc("/auth/login", h.handleLogin) - mux.HandleFunc("/auth/verify/", h.handleVerifyLogin) - mux.HandleFunc("/auth/logout", h.handleLogout) - mux.HandleFunc("/auth/me", h.handleMe) - - // Page routes - mux.HandleFunc("/", h.handleDashboard) + // Auth mux.HandleFunc("/login", h.handleLoginPage) - mux.HandleFunc("/deal-rooms/", h.handleDealRoom) - mux.HandleFunc("/documents/", h.handleDocument) - mux.HandleFunc("/admin", h.requireAuth(h.requireAdmin(h.handleAdmin))) - mux.HandleFunc("/profile", h.requireAuth(h.handleProfile)) - mux.HandleFunc("/activity", h.requireAuth(h.handleActivity)) + mux.HandleFunc("/auth/login", h.handleLogin) + mux.HandleFunc("/auth/demo", h.handleDemoLogin) + mux.HandleFunc("/auth/logout", h.handleLogout) - // API routes - mux.HandleFunc("/api/deal-rooms", h.requireAuth(h.handleAPIEndpoint("deal-rooms"))) - mux.HandleFunc("/api/deal-rooms/", h.requireAuth(h.handleAPIEndpoint("deal-rooms"))) - mux.HandleFunc("/api/entries", h.requireAuth(h.handleAPIEndpoint("entries"))) - mux.HandleFunc("/api/entries/", h.requireAuth(h.handleAPIEndpoint("entries"))) - mux.HandleFunc("/api/search", h.requireAuth(h.handleSearch)) - mux.HandleFunc("/api/activity/", h.requireAuth(h.handleAPIActivity)) + // Pages (auth required) + mux.HandleFunc("/", h.requireAuth(h.handleDashboard)) + mux.HandleFunc("/deals", h.requireAuth(h.handleDealRooms)) + mux.HandleFunc("/deals/", h.requireAuth(h.handleDealRoom)) + mux.HandleFunc("/requests", h.requireAuth(h.handleRequestList)) + mux.HandleFunc("/contacts", h.requireAuth(h.handleContacts)) + mux.HandleFunc("/audit", h.requireAuth(h.handleAuditLog)) + mux.HandleFunc("/analytics", h.requireAuth(h.handleAnalytics)) + + // HTMX partials + mux.HandleFunc("/htmx/request-comment", h.requireAuth(h.handleUpdateComment)) } // Middleware - func (h *Handler) requireAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - session, _ := h.sessions.Get(r, "dealroom") - userID, ok := session.Values["user_id"].(string) - - if !ok || userID == "" { - if isAPIRequest(r) { - http.Error(w, "Authentication required", http.StatusUnauthorized) - return - } + cookie, err := r.Cookie("session") + if err != nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } - // Add user ID to request context - ctx := r.Context() - ctx = setUserID(ctx, userID) - next.ServeHTTP(w, r.WithContext(ctx)) - } -} - -func (h *Handler) requireAdmin(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - userID := getUserID(r.Context()) - - // Check if user is admin - var role string - err := h.db.QueryRow("SELECT role FROM users WHERE id = ?", userID).Scan(&role) - if err != nil || role != "admin" { - http.Error(w, "Admin access required", http.StatusForbidden) + var profile model.Profile + err = h.db.QueryRow(` + SELECT p.id, p.email, p.full_name, p.avatar_url, p.organization_id, p.role + FROM sessions s JOIN profiles p ON s.user_id = p.id + WHERE s.token = ? AND s.expires_at > datetime('now') + `, cookie.Value).Scan(&profile.ID, &profile.Email, &profile.FullName, &profile.AvatarURL, &profile.OrganizationID, &profile.Role) + if err != nil { + http.SetCookie(w, &http.Cookie{Name: "session", Value: "", MaxAge: -1, Path: "/"}) + http.Redirect(w, r, "/login", http.StatusSeeOther) return } - next.ServeHTTP(w, r) + ctx := setProfile(r.Context(), &profile) + next.ServeHTTP(w, r.WithContext(ctx)) } } - -// Placeholder handlers - to be implemented - -func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { - // TODO: Implement magic link login - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleVerifyLogin(w http.ResponseWriter, r *http.Request) { - // TODO: Implement login verification - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) { - session, _ := h.sessions.Get(r, "dealroom") - session.Values["user_id"] = "" - session.Save(r, w) - http.Redirect(w, r, "/login", http.StatusSeeOther) -} - -func (h *Handler) handleMe(w http.ResponseWriter, r *http.Request) { - // TODO: Return current user info - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) { - // TODO: Render dashboard template - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleLoginPage(w http.ResponseWriter, r *http.Request) { - // TODO: Render login template - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleDealRoom(w http.ResponseWriter, r *http.Request) { - // TODO: Handle deal room pages - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleDocument(w http.ResponseWriter, r *http.Request) { - // TODO: Handle document viewing - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleAdmin(w http.ResponseWriter, r *http.Request) { - // TODO: Render admin panel - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) { - // TODO: Render user profile - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleActivity(w http.ResponseWriter, r *http.Request) { - // TODO: Render activity feed - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleAPIEndpoint(endpoint string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // TODO: Implement REST API endpoints - http.Error(w, "Not implemented", http.StatusNotImplemented) - } -} - -func (h *Handler) handleSearch(w http.ResponseWriter, r *http.Request) { - // TODO: Implement search API - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -func (h *Handler) handleAPIActivity(w http.ResponseWriter, r *http.Request) { - // TODO: Implement activity API - http.Error(w, "Not implemented", http.StatusNotImplemented) -} - -// Helper functions - -func isAPIRequest(r *http.Request) bool { - return r.Header.Get("Accept") == "application/json" || - r.Header.Get("Content-Type") == "application/json" || - r.URL.Path[:5] == "/api/" -} - -// Context helpers would go in a separate context.go file -func setUserID(ctx context.Context, userID string) context.Context { - // TODO: Implement context helpers - return ctx -} - -func getUserID(ctx context.Context) string { - // TODO: Implement context helpers - return "" -} \ No newline at end of file diff --git a/internal/handler/requests.go b/internal/handler/requests.go new file mode 100644 index 0000000..94f7304 --- /dev/null +++ b/internal/handler/requests.go @@ -0,0 +1,67 @@ +package handler + +import ( + "net/http" + + "dealroom/internal/rbac" + "dealroom/templates" +) + +func (h *Handler) handleRequestList(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + deals := h.getDeals(profile) + + // Get all requests grouped by deal + dealRequests := make(map[string][]*templates.RequestsByGroup) + for _, deal := range deals { + reqs := h.getRequests(deal.ID, profile) + // Group by buyer_group + groups := make(map[string][]*templates.RequestItem) + for _, req := range reqs { + group := req.BuyerGroup + if group == "" { + group = "Unassigned" + } + groups[group] = append(groups[group], &templates.RequestItem{ + ID: req.ID, + ItemNumber: req.ItemNumber, + Section: req.Section, + Description: req.Description, + Priority: req.Priority, + AtlasStatus: req.AtlasStatus, + AtlasNote: req.AtlasNote, + Confidence: req.Confidence, + BuyerComment: req.BuyerComment, + SellerComment: req.SellerComment, + BuyerGroup: req.BuyerGroup, + }) + } + var groupList []*templates.RequestsByGroup + for name, items := range groups { + groupList = append(groupList, &templates.RequestsByGroup{Name: name, Requests: items}) + } + dealRequests[deal.ID] = groupList + } + + templates.RequestListPage(profile, deals, dealRequests).Render(r.Context(), w) +} + +func (h *Handler) handleUpdateComment(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + profile := getProfile(r.Context()) + reqID := r.FormValue("request_id") + value := r.FormValue("value") + + field := "seller_comment" + if rbac.IsBuyer(profile.Role) { + field = "buyer_comment" + } + + h.db.Exec("UPDATE diligence_requests SET "+field+" = ?, updated_at = datetime('now') WHERE id = ?", value, reqID) + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(`✓ Saved`)) +} diff --git a/internal/model/models.go b/internal/model/models.go index 7709556..f8bf03b 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -1,146 +1,156 @@ package model import ( - "encoding/json" "time" ) -// User represents a system user -type User struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Role string `json:"role"` // admin, user - AvatarURL *string `json:"avatar_url,omitempty"` - CreatedAt time.Time `json:"created_at"` - LastLogin *time.Time `json:"last_login,omitempty"` - IsActive bool `json:"is_active"` +// Now returns the current Unix timestamp +func Now() int64 { + return time.Now().Unix() } -// Entry represents any content in the system (deal room, document, note, etc.) -type Entry struct { - ID string `json:"id"` - ParentID *string `json:"parent_id,omitempty"` - DealRoomID string `json:"deal_room_id"` - EntryType string `json:"entry_type"` - Title string `json:"title"` - Content json.RawMessage `json:"content"` - FilePath *string `json:"file_path,omitempty"` - FileSize *int64 `json:"file_size,omitempty"` - FileHash *string `json:"file_hash,omitempty"` - Embedding []byte `json:"-"` // Not included in JSON output - CreatedBy string `json:"created_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +// TimeFromUnix converts Unix timestamp to time.Time +func TimeFromUnix(unix int64) time.Time { + return time.Unix(unix, 0) } -// Access represents RBAC permissions for entries -type Access struct { - ID string `json:"id"` - EntryID string `json:"entry_id"` - UserID string `json:"user_id"` - Permissions int `json:"permissions"` // Bitmask: read=1, write=2, delete=4, manage=8 - GrantedBy string `json:"granted_by"` - GrantedAt time.Time `json:"granted_at"` +type Organization struct { + ID string + Name string + Slug string + CreatedAt time.Time } -// Permission constants -const ( - PermissionRead = 1 - PermissionWrite = 2 - PermissionDelete = 4 - PermissionManage = 8 -) - -// HasPermission checks if access includes the given permission -func (a *Access) HasPermission(permission int) bool { - return a.Permissions&permission != 0 +type Profile struct { + ID string + Email string + FullName string + AvatarURL string + OrganizationID string + Role string // owner, admin, member, viewer + PasswordHash string + CreatedAt time.Time + LastLogin *time.Time +} + +type Deal struct { + ID string + OrganizationID string + Name string + Description string + TargetCompany string + Stage string // pipeline, loi, initial_review, due_diligence, final_negotiation, closed, dead + DealSize float64 + Currency string + IOIDate string + LOIDate string + ExclusivityEnd string + ExpectedCloseDate string + CloseProbability int + IsArchived bool + CreatedBy string + CreatedAt time.Time + UpdatedAt time.Time + // Computed + FileCount int +} + +type Folder struct { + ID string + DealID string + ParentID string + Name string + Description string + CreatedBy string + CreatedAt time.Time +} + +type File struct { + ID string + DealID string + FolderID string + Name string + FileSize int64 + MimeType string + Status string // uploaded, processing, reviewed, flagged, archived + UploadedBy string + CreatedAt time.Time +} + +type DiligenceRequest struct { + ID string + DealID string + ItemNumber string + Section string + Description string + Priority string // high, medium, low + AtlasStatus string // fulfilled, partial, missing, not_applicable + AtlasNote string + Confidence int + BuyerComment string + SellerComment string + BuyerGroup string + LinkedFileIDs string + CreatedBy string + CreatedAt time.Time + UpdatedAt time.Time +} + +type Contact struct { + ID string + OrganizationID string + FullName string + Email string + Phone string + Company string + Title string + ContactType string // buyer, internal, advisor + Tags string + Notes string + LastActivityAt *time.Time + CreatedAt time.Time +} + +type DealActivity struct { + ID string + OrganizationID string + DealID string + UserID string + ActivityType string + ResourceType string + ResourceName string + ResourceID string + Details string + CreatedAt time.Time + // Computed + UserName string } -// Session represents a user session type Session struct { - Token string `json:"token"` - UserID string `json:"user_id"` - ExpiresAt time.Time `json:"expires_at"` - CreatedAt time.Time `json:"created_at"` - LastUsed time.Time `json:"last_used"` - UserAgent *string `json:"user_agent,omitempty"` - IPAddress *string `json:"ip_address,omitempty"` + Token string + UserID string + ExpiresAt time.Time + CreatedAt time.Time } -// IsExpired checks if the session has expired -func (s *Session) IsExpired() bool { - return time.Now().After(s.ExpiresAt) +// StageName returns human-readable stage name +func StageName(stage string) string { + switch stage { + case "pipeline": + return "Pipeline" + case "loi": + return "LOI Stage" + case "initial_review": + return "Initial Review" + case "due_diligence": + return "Due Diligence" + case "final_negotiation": + return "Final Negotiation" + case "closed": + return "Closed" + case "dead": + return "Dead" + default: + return stage + } } - -// AuditLog represents an audit trail entry -type AuditLog struct { - ID string `json:"id"` - UserID *string `json:"user_id,omitempty"` - EntryID *string `json:"entry_id,omitempty"` - Action string `json:"action"` - Details json.RawMessage `json:"details,omitempty"` - IPAddress *string `json:"ip_address,omitempty"` - UserAgent *string `json:"user_agent,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -// Deal Room specific content types - -// DealRoomContent represents the content structure for deal room entries -type DealRoomContent struct { - Type string `json:"type"` - Description string `json:"description"` - Stage string `json:"stage"` // sourcing, loi, due_diligence, closing, completed - TargetCompany string `json:"target_company"` - DealValue string `json:"deal_value"` - Participants []DealParticipant `json:"participants"` - KeyDates map[string]string `json:"key_dates"` - ConfidentialityLevel string `json:"confidentiality_level"` -} - -type DealParticipant struct { - Name string `json:"name"` - Role string `json:"role"` - Organization string `json:"organization"` -} - -// DocumentContent represents the content structure for document entries -type DocumentContent struct { - Type string `json:"type"` - Category string `json:"category"` // nda, cim, financial_model, teaser, legal, dd_report - MimeType string `json:"mime_type"` - OriginalName string `json:"original_name"` - Version string `json:"version"` - Analysis *DocumentAnalysis `json:"analysis,omitempty"` - RequiresNDA bool `json:"requires_nda"` -} - -type DocumentAnalysis struct { - Summary string `json:"summary"` - KeyMetrics []string `json:"key_metrics"` - RiskFactors []string `json:"risk_factors"` - AIConfidence float64 `json:"ai_confidence"` -} - -// NoteContent represents the content structure for note/message entries -type NoteContent struct { - Type string `json:"type"` - Body string `json:"body"` - Mentions []string `json:"mentions"` // User IDs mentioned with @ - Attachments []string `json:"attachments"` // Entry IDs referenced - ThreadContext string `json:"thread_context"` // Context for organizing conversations -} - -// ActivityItem represents an item in the activity feed -type ActivityItem struct { - ID string `json:"id"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - Action string `json:"action"` - EntryID string `json:"entry_id"` - EntryType string `json:"entry_type"` - Title string `json:"title"` - Details json.RawMessage `json:"details,omitempty"` - CreatedAt time.Time `json:"created_at"` -} \ No newline at end of file diff --git a/internal/model/utils.go b/internal/model/utils.go deleted file mode 100644 index 912306e..0000000 --- a/internal/model/utils.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -import "time" - -// Now returns the current Unix timestamp -func Now() int64 { - return time.Now().Unix() -} - -// TimeFromUnix converts Unix timestamp to time.Time -func TimeFromUnix(unix int64) time.Time { - return time.Unix(unix, 0) -} \ No newline at end of file diff --git a/internal/rbac/rbac.go b/internal/rbac/rbac.go index 22c09e3..bc33beb 100644 --- a/internal/rbac/rbac.go +++ b/internal/rbac/rbac.go @@ -1,246 +1,21 @@ package rbac -import ( - "database/sql" - "fmt" +import "dealroom/internal/model" - "dealroom/internal/model" -) - -// Engine handles role-based access control -type Engine struct { - db *sql.DB +// IsSeller returns true if role is owner or admin +func IsSeller(role string) bool { + return role == "owner" || role == "admin" } -// New creates a new RBAC engine -func New(db *sql.DB) *Engine { - return &Engine{db: db} +// IsBuyer returns true if role is viewer or member +func IsBuyer(role string) bool { + return role == "viewer" || role == "member" } -// GrantAccess grants permissions to a user for an entry -func (e *Engine) GrantAccess(entryID, userID string, permissions int, grantedBy string) error { - query := ` - INSERT OR REPLACE INTO access (id, entry_id, user_id, permissions, granted_by, granted_at) - VALUES (?, ?, ?, ?, ?, ?) - ` - - id := fmt.Sprintf("%s:%s", entryID, userID) - _, err := e.db.Exec(query, id, entryID, userID, permissions, grantedBy, model.Now()) - return err +// BuyerGroups returns the buyer groups for the demo buyer +func BuyerGroups(profile *model.Profile) []string { + if IsBuyer(profile.Role) { + return []string{"Meridian Capital", "Summit Health Equity"} + } + return nil } - -// RevokeAccess removes all permissions for a user on an entry -func (e *Engine) RevokeAccess(entryID, userID string) error { - query := `DELETE FROM access WHERE entry_id = ? AND user_id = ?` - _, err := e.db.Exec(query, entryID, userID) - return err -} - -// CheckAccess verifies if a user has specific permissions for an entry -func (e *Engine) CheckAccess(userID, entryID string, permission int) (bool, error) { - // Admins have all permissions - if isAdmin, err := e.isUserAdmin(userID); err != nil { - return false, err - } else if isAdmin { - return true, nil - } - - // Check direct permissions - access, err := e.getUserAccess(userID, entryID) - if err != nil && err != sql.ErrNoRows { - return false, err - } - - if access != nil && access.HasPermission(permission) { - return true, nil - } - - // Check inherited permissions from deal room - if inherited, err := e.checkInheritedAccess(userID, entryID, permission); err != nil { - return false, err - } else if inherited { - return true, nil - } - - return false, nil -} - -// GetUserAccess returns the access record for a user on an entry -func (e *Engine) GetUserAccess(userID, entryID string) (*model.Access, error) { - return e.getUserAccess(userID, entryID) -} - -// GetEntryAccess returns all access records for an entry -func (e *Engine) GetEntryAccess(entryID string) ([]*model.Access, error) { - query := ` - SELECT id, entry_id, user_id, permissions, granted_by, granted_at - FROM access - WHERE entry_id = ? - ORDER BY granted_at DESC - ` - - rows, err := e.db.Query(query, entryID) - if err != nil { - return nil, err - } - defer rows.Close() - - var accessList []*model.Access - for rows.Next() { - access := &model.Access{} - var grantedAt int64 - - err := rows.Scan(&access.ID, &access.EntryID, &access.UserID, - &access.Permissions, &access.GrantedBy, &grantedAt) - if err != nil { - return nil, err - } - - access.GrantedAt = model.TimeFromUnix(grantedAt) - accessList = append(accessList, access) - } - - return accessList, rows.Err() -} - -// GetUserEntries returns entries accessible to a user with specified permissions -func (e *Engine) GetUserEntries(userID string, entryType string, permission int) ([]*model.Entry, error) { - // Admins can see everything - if isAdmin, err := e.isUserAdmin(userID); err != nil { - return nil, err - } else if isAdmin { - return e.getAllEntriesByType(entryType) - } - - // Get entries with direct access - query := ` - SELECT DISTINCT e.id, e.parent_id, e.deal_room_id, e.entry_type, e.title, - e.content, e.file_path, e.file_size, e.file_hash, - e.created_by, e.created_at, e.updated_at - FROM entries e - JOIN access a ON e.id = a.entry_id - WHERE a.user_id = ? AND (a.permissions & ?) > 0 - AND ($1 = '' OR e.entry_type = $1) - ORDER BY e.created_at DESC - ` - - rows, err := e.db.Query(query, userID, permission, entryType) - if err != nil { - return nil, err - } - defer rows.Close() - - return e.scanEntries(rows) -} - -// Helper methods - -func (e *Engine) isUserAdmin(userID string) (bool, error) { - query := `SELECT role FROM users WHERE id = ? AND is_active = 1` - var role string - err := e.db.QueryRow(query, userID).Scan(&role) - if err != nil { - if err == sql.ErrNoRows { - return false, nil - } - return false, err - } - return role == "admin", nil -} - -func (e *Engine) getUserAccess(userID, entryID string) (*model.Access, error) { - query := ` - SELECT id, entry_id, user_id, permissions, granted_by, granted_at - FROM access - WHERE user_id = ? AND entry_id = ? - ` - - access := &model.Access{} - var grantedAt int64 - - err := e.db.QueryRow(query, userID, entryID).Scan( - &access.ID, &access.EntryID, &access.UserID, - &access.Permissions, &access.GrantedBy, &grantedAt) - - if err != nil { - return nil, err - } - - access.GrantedAt = model.TimeFromUnix(grantedAt) - return access, nil -} - -func (e *Engine) checkInheritedAccess(userID, entryID string, permission int) (bool, error) { - // Get the deal room ID for this entry - query := `SELECT deal_room_id FROM entries WHERE id = ?` - var dealRoomID string - err := e.db.QueryRow(query, entryID).Scan(&dealRoomID) - if err != nil { - return false, err - } - - // If this is already a deal room, no inheritance - if dealRoomID == entryID { - return false, nil - } - - // Check access to the deal room - return e.CheckAccess(userID, dealRoomID, permission) -} - -func (e *Engine) getAllEntriesByType(entryType string) ([]*model.Entry, error) { - query := ` - SELECT id, parent_id, deal_room_id, entry_type, title, - content, file_path, file_size, file_hash, - created_by, created_at, updated_at - FROM entries - WHERE ($1 = '' OR entry_type = $1) - ORDER BY created_at DESC - ` - - rows, err := e.db.Query(query, entryType) - if err != nil { - return nil, err - } - defer rows.Close() - - return e.scanEntries(rows) -} - -func (e *Engine) scanEntries(rows *sql.Rows) ([]*model.Entry, error) { - var entries []*model.Entry - - for rows.Next() { - entry := &model.Entry{} - var createdAt, updatedAt int64 - var parentID, filePath, fileSize, fileHash sql.NullString - var fileSizeInt sql.NullInt64 - - err := rows.Scan(&entry.ID, &parentID, &entry.DealRoomID, &entry.EntryType, - &entry.Title, &entry.Content, &filePath, &fileSizeInt, &fileHash, - &entry.CreatedBy, &createdAt, &updatedAt) - if err != nil { - return nil, err - } - - if parentID.Valid { - entry.ParentID = &parentID.String - } - if filePath.Valid { - entry.FilePath = &filePath.String - } - if fileSizeInt.Valid { - entry.FileSize = &fileSizeInt.Int64 - } - if fileHash.Valid { - entry.FileHash = &fileHash.String - } - - entry.CreatedAt = model.TimeFromUnix(createdAt) - entry.UpdatedAt = model.TimeFromUnix(updatedAt) - - entries = append(entries, entry) - } - - return entries, rows.Err() -} \ No newline at end of file diff --git a/templates/analytics.templ b/templates/analytics.templ new file mode 100644 index 0000000..e9c95dd --- /dev/null +++ b/templates/analytics.templ @@ -0,0 +1,71 @@ +package templates + +import "dealroom/internal/model" +import "fmt" + +type AnalyticsStats struct { + DealCount int + FileCount int + RequestCount int + CompletionPct int +} + +templ AnalyticsPage(profile *model.Profile, stats *AnalyticsStats) { + @Layout(profile, "analytics") { +
+
+

Analytics

+

Key metrics and insights across your deal portfolio.

+
+ +
+
+
Active Deals
+
{ fmt.Sprintf("%d", stats.DealCount) }
+
+
+
Total Documents
+
{ fmt.Sprintf("%d", stats.FileCount) }
+
+
+
Diligence Requests
+
{ fmt.Sprintf("%d", stats.RequestCount) }
+
+
+
Request Completion
+
{ fmt.Sprintf("%d%%", stats.CompletionPct) }
+
+
 
+
+
+
+ + +
+

Request Status Breakdown

+
+
+
+
Fulfilled
+
Items fully addressed
+
+
+
⚠️
+
Partial
+
Partially completed
+
+
+
🔴
+
Missing
+
Not yet provided
+
+
+
+
N/A
+
Not applicable
+
+
+
+
+ } +} diff --git a/templates/audit.templ b/templates/audit.templ new file mode 100644 index 0000000..da65c53 --- /dev/null +++ b/templates/audit.templ @@ -0,0 +1,59 @@ +package templates + +import "dealroom/internal/model" + +templ AuditLogPage(profile *model.Profile, activities []*model.DealActivity) { + @Layout(profile, "audit") { +
+
+

Audit Log

+

Complete activity timeline across all deal rooms.

+
+ +
+
+ for _, act := range activities { +
+ +
+ @activityDot(act.ActivityType) +
+
+
+
+ { act.UserName } + + { act.ActivityType } + +
+

{ act.ResourceType }: { act.ResourceName }

+

{ act.CreatedAt.Format("Jan 2, 2006 3:04 PM") }

+
+
+ } +
+
+
+ } +} + +templ activityDot(actType string) { +
+
+
+} diff --git a/templates/components.templ b/templates/components.templ new file mode 100644 index 0000000..41342cf --- /dev/null +++ b/templates/components.templ @@ -0,0 +1,60 @@ +package templates + +import "dealroom/internal/model" + +templ StageBadge(stage string) { + + { model.StageName(stage) } + +} + +templ StatusIcon(status string) { + if status == "fulfilled" { + + } else if status == "partial" { + ⚠️ + } else if status == "not_applicable" { + + } else { + 🔴 + } +} + +templ PriorityBadge(priority string) { + if priority == "high" { + High + } else if priority == "medium" { + Med + } else { + Low + } +} + +templ FileStatusBadge(status string) { + if status == "reviewed" { + Reviewed + } else if status == "flagged" { + Flagged + } else if status == "processing" { + Processing + } else { + Uploaded + } +} + +templ ContactTypeBadge(contactType string) { + if contactType == "buyer" { + Buyer + } else if contactType == "advisor" { + Advisor + } else { + Internal + } +} diff --git a/templates/contacts.templ b/templates/contacts.templ new file mode 100644 index 0000000..1ddaa7c --- /dev/null +++ b/templates/contacts.templ @@ -0,0 +1,80 @@ +package templates + +import "dealroom/internal/model" +import "fmt" +import "strings" + +templ ContactsPage(profile *model.Profile, contacts []*model.Contact) { + @Layout(profile, "contacts") { +
+
+
+

Contacts

+

{ fmt.Sprintf("%d contacts", len(contacts)) } across all deal rooms.

+
+ +
+ +
+ + + + + + + + + + + + for _, contact := range contacts { + + + + + + + + } + +
NameCompanyEmailTypeTags
+
+
+ { contactInitials(contact.FullName) } +
+
+ { contact.FullName } +

{ contact.Title }

+
+
+
{ contact.Company }{ contact.Email }@ContactTypeBadge(contact.ContactType) +
+ for _, tag := range splitTags(contact.Tags) { + if tag != "" { + { tag } + } + } +
+
+
+
+ } +} + +func contactInitials(name string) string { + parts := strings.Fields(name) + if len(parts) >= 2 { + return string(parts[0][0]) + string(parts[len(parts)-1][0]) + } + if len(name) > 0 { + return string(name[0]) + } + return "?" +} + +func splitTags(tags string) []string { + return strings.Split(tags, ",") +} diff --git a/templates/dashboard.templ b/templates/dashboard.templ index f096177..77b0282 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -1,57 +1,89 @@ package templates -import ( - "fmt" - "dealroom/internal/model" -) +import "dealroom/internal/model" +import "fmt" -templ Dashboard(user *model.User, dealRooms []*model.Entry) { - @Layout("Dashboard", user) { -
- -
-
-
-

Welcome back, { user.Name }

-

You have access to { fmt.Sprintf("%d", len(dealRooms)) } deal rooms

+templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model.DealActivity, fileCounts map[string]int) { + @Layout(profile, "dashboard") { +
+ +
+
+

Dashboard

+

Overview of all active deal rooms and recent activity.

+
+ + + New Room + +
+ + +
+ @statCard("ACTIVE ROOMS", fmt.Sprintf("%d", countActive(deals)), fmt.Sprintf("%d total", len(deals)), "folder") + @statCard("DOCUMENTS", fmt.Sprintf("%d", totalFiles(fileCounts)), "across all rooms", "file") + @statCard("ACTIVE DEALS", fmt.Sprintf("%d", countActive(deals)), fmt.Sprintf("%d in diligence", countByStage(deals, "due_diligence")), "users") + @statCard("AVG. CLOSE PROB.", fmt.Sprintf("%d%%", avgProbability(deals)), "across portfolio", "trend") +
+ + +
+ +
+
+

Active Deal Rooms

+ + View all + +
- if user.Role == "admin" { - + if len(deals) == 0 { +
+ No deal rooms yet. Create one to get started. +
+ } else { + }
-
- -
- for _, room := range dealRooms { - @DealRoomCard(room) - } - - if len(dealRooms) == 0 { -
-
- - - -

No deal rooms

-

Get started by creating a new deal room or wait for an invitation.

-
+ +
+
+

Recent Activity

+ + Full log + +
- } -
- - -
-
-

Recent Activity

-
-
- -
-
Loading recent activity...
+
+ for _, act := range activities { +
+ @activityIcon(act.ActivityType) +
+

+ { act.ActivityType } + { act.ResourceName } +

+

{ act.UserName } · { act.CreatedAt.Format("Jan 2, 3:04 PM") }

+
+
+ }
@@ -59,52 +91,76 @@ templ Dashboard(user *model.User, dealRooms []*model.Entry) { } } -templ DealRoomCard(room *model.Entry) { -
-
-
-

{ room.Title }

- - Active - -
- - -
-
- - - - Target Company -
-
- - - - Deal Value -
-
- - - - { room.CreatedAt.Format("Jan 2, 2006") } -
-
- -
- -
- -
-
-
- +3 -
-
+func countActive(deals []*model.Deal) int { + count := 0 + for _, d := range deals { + if d.Stage != "closed" && d.Stage != "dead" { + count++ + } + } + return count +} + +func countByStage(deals []*model.Deal, stage string) int { + count := 0 + for _, d := range deals { + if d.Stage == stage { + count++ + } + } + return count +} + +func totalFiles(fc map[string]int) int { + total := 0 + for _, c := range fc { + total += c + } + return total +} + +func avgProbability(deals []*model.Deal) int { + if len(deals) == 0 { + return 0 + } + sum := 0 + for _, d := range deals { + sum += d.CloseProbability + } + return sum / len(deals) +} + +templ statCard(label, value, subtitle, iconType string) { +
+
+ { label } +
+ if iconType == "folder" { + + } + if iconType == "file" { + + } + if iconType == "users" { + + } + if iconType == "trend" { + + }
+
{ value }
+

{ subtitle }

-} \ No newline at end of file +} + +templ activityIcon(actType string) { +
+
+
+} diff --git a/templates/dealroom.templ b/templates/dealroom.templ new file mode 100644 index 0000000..ae0e818 --- /dev/null +++ b/templates/dealroom.templ @@ -0,0 +1,208 @@ +package templates + +import "dealroom/internal/model" +import "fmt" + +templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.Folder, files []*model.File, requests []*model.DiligenceRequest) { + @Layout(profile, "deals") { +
+ +
+ + + Back to Deal Rooms + +
+

{ deal.Name }

+ @StageBadge(deal.Stage) +
+

{ deal.TargetCompany } · { deal.Description }

+
+ + +
+
+
Deal Size
+
{ formatDealSize(deal.DealSize) }
+
+
+
Close Probability
+
{ fmt.Sprintf("%d%%", deal.CloseProbability) }
+
+
+
IOI Date
+
+ if deal.IOIDate != "" { + { deal.IOIDate } + } else { + + } +
+
+
+
Exclusivity Ends
+
+ if deal.ExclusivityEnd != "" { + { deal.ExclusivityEnd } + } else { + + } +
+
+
+ + +
+
+ + +
+ + +
+
+ +
+

Folders

+ for _, folder := range folders { + if folder.ParentID == "" { +
+
+ + { folder.Name } +
+ + for _, child := range folders { + if child.ParentID == folder.ID { +
+
+ + { child.Name } +
+
+ } + } +
+ } + } +
+ + +
+ + + + + + + + + + for _, file := range files { + + + + + + } + +
File NameSizeStatus
+
+ @fileIcon(file.Name) + { file.Name } +
+
{ formatFileSize(file.FileSize) }@FileStatusBadge(file.Status)
+
+
+
+ + + +
+ + +
+ } +} + +func formatFileSize(bytes int64) string { + if bytes < 1024 { + return fmt.Sprintf("%d B", bytes) + } + if bytes < 1024*1024 { + return fmt.Sprintf("%d KB", bytes/1024) + } + return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024)) +} + +templ fileIcon(name string) { +
+ if hasSuffix(name, ".pdf") { + PDF + } else if hasSuffix(name, ".xlsx") || hasSuffix(name, ".csv") { + XLS + } else if hasSuffix(name, ".doc") || hasSuffix(name, ".docx") { + DOC + } else { + FILE + } +
+} + +func hasSuffix(s, suffix string) bool { + return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix +} diff --git a/templates/dealrooms.templ b/templates/dealrooms.templ new file mode 100644 index 0000000..c7ebe79 --- /dev/null +++ b/templates/dealrooms.templ @@ -0,0 +1,65 @@ +package templates + +import "dealroom/internal/model" +import "fmt" + +templ DealRooms(profile *model.Profile, deals []*model.Deal) { + @Layout(profile, "deals") { +
+
+
+

Deal Rooms

+

{ fmt.Sprintf("%d deal rooms", len(deals)) } across your organization.

+
+
+ +
+ + + + + + + + + + + + + + + for _, deal := range deals { + + + + + + + + + + + } + +
Deal RoomTargetStageDeal SizeDocumentsClose Prob.IOI DateLOI Date
+ +
+ { string(deal.Name[len(deal.Name)-1:]) } +
+ { deal.Name } +
+
{ deal.TargetCompany }@StageBadge(deal.Stage){ formatDealSize(deal.DealSize) }{ fmt.Sprintf("%d files", deal.FileCount) }{ fmt.Sprintf("%d%%", deal.CloseProbability) }{ deal.IOIDate }{ deal.LOIDate }
+
+
+ } +} + +func formatDealSize(size float64) string { + if size >= 1000000 { + return fmt.Sprintf("$%.0fM", size/1000000) + } + if size >= 1000 { + return fmt.Sprintf("$%.0fK", size/1000) + } + return fmt.Sprintf("$%.0f", size) +} diff --git a/templates/layout.templ b/templates/layout.templ index 0ba923c..3fb5fb2 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -1,79 +1,159 @@ package templates import "dealroom/internal/model" +import "dealroom/internal/rbac" -templ Layout(title string, user *model.User) { +templ Layout(profile *model.Profile, activePage string) { - + - { title } - Deal Room - + Dealspace AI - + - - if user != nil { - @Navigation(user) - } -
- { children... } + + + + + +
+
+ { children... } +
- @Footer() } -templ Navigation(user *model.User) { - +func initials(name string) string { + if len(name) == 0 { + return "?" + } + result := "" + prev := ' ' + for i, c := range name { + if i == 0 || prev == ' ' { + result += string(c) + } + prev = c + } + if len(result) > 2 { + result = result[:2] + } + return result } -templ Footer() { -
-
-
-

© 2024 Deal Room. All rights reserved.

-

Secure • Encrypted • Auditable

-
-
-
-} \ No newline at end of file +templ sidebarLink(href string, label string, active bool, icon templ.Component) { + + @icon + { label } + +} + +templ svgDashboard() { + +} + +templ svgFolder() { + +} + +templ svgClipboard() { + +} + +templ svgChart() { + +} + +templ svgUsers() { + +} + +templ svgShield() { + +} diff --git a/templates/login.templ b/templates/login.templ index 1260d87..199dce2 100644 --- a/templates/login.templ +++ b/templates/login.templ @@ -1,52 +1,113 @@ package templates -templ LoginPage(message string) { +templ Login() { - Login - Deal Room - + Dealspace AI - Sign In - - -
-
-

Deal Room

-

Secure Investment Banking Platform

-
- -
-
-
- - -
- -
- -
-
- -
- if message != "" { -
-

{ message }

-
+ + + + +
+
+ +
+
+ +
+ Dealspace AI +
+ +

Welcome back

+

Sign in to access your deal rooms.

+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ + +
+
+ or +
+
+ + +
+
+ + +
+
+ + +
- -
-

Secure, passwordless authentication

-

Check your email for the login link

+
+ + + -} \ No newline at end of file +} diff --git a/templates/requests.templ b/templates/requests.templ new file mode 100644 index 0000000..7e2eae0 --- /dev/null +++ b/templates/requests.templ @@ -0,0 +1,107 @@ +package templates + +import "dealroom/internal/model" +import "fmt" + +type RequestItem struct { + ID string + ItemNumber string + Section string + Description string + Priority string + AtlasStatus string + AtlasNote string + Confidence int + BuyerComment string + SellerComment string + BuyerGroup string +} + +type RequestsByGroup struct { + Name string + Requests []*RequestItem +} + +templ RequestListPage(profile *model.Profile, deals []*model.Deal, dealRequests map[string][]*RequestsByGroup) { + @Layout(profile, "requests") { +
+
+

Request Lists

+

Diligence request tracking across all deals.

+
+ + for _, deal := range deals { + if groups, ok := dealRequests[deal.ID]; ok && len(groups) > 0 { +
+
+
+

{ deal.Name }

+ @StageBadge(deal.Stage) + · { deal.TargetCompany } +
+
+ + for _, group := range groups { +
+
+ { group.Name } + { fmt.Sprintf("%d items", len(group.Requests)) } +
+ + + + + + + + + + + + + + + + for _, req := range group.Requests { + + + + + + + + + + + + } + +
#SectionRequest ItemPriority + 🤖 Atlas + Conf.💬 Buyer💬 Seller🤖 Atlas Note
{ req.ItemNumber }{ req.Section }{ req.Description }@PriorityBadge(req.Priority)@StatusIcon(req.AtlasStatus) + if req.Confidence > 0 { + { fmt.Sprintf("%d%%", req.Confidence) } + } + + if req.BuyerComment != "" { + { req.BuyerComment } + } else { + Add comment... + } + + if req.SellerComment != "" { + { req.SellerComment } + } else { + Add comment... + } + + { req.AtlasNote } +
+
+ } +
+ } + } +
+ } +}