chore: auto-commit uncommitted changes

This commit is contained in:
James 2026-03-21 06:02:09 -04:00
parent 703b8d9ec0
commit 70baa67dfd
23 changed files with 33 additions and 3729 deletions

0
.opencode/init Normal file
View File

BIN
.opencode/opencode.db Normal file

Binary file not shown.

BIN
.opencode/opencode.db-shm Normal file

Binary file not shown.

BIN
.opencode/opencode.db-wal Normal file

Binary file not shown.

33
OpenCode.md Normal file
View File

@ -0,0 +1,33 @@
# Dealspace Coding Guide
## Build & Test
```bash
make build # Local build
make build-linux # Linux/amd64 with CGO + fts5
make test # Run all tests
CGO_ENABLED=1 go test -tags fts5 ./lib -run TestPackUnpack -v # Single test
```
## Code Style
- **Format**: `gofmt` (standard Go formatting)
- **Imports**: stdlib → third-party → internal (github.com/mish/dealspace)
- **Types**: Use JSON tags with `snake_case`, e.g., `EntryID string `json:"entry_id"``
- **Naming**: PascalCase for exported, camelCase for unexported; constants use prefix like `TypeProject`, `StageDataroom`
- **Errors**: Wrap with `fmt.Errorf("context: %w", err)`; check sentinel errors with `==`
## Architecture Rules
**Database access ONLY through three choke points in `lib/dbcore.go`:**
- `EntryRead(db, cfg, actorID, projectID, filter)` — all reads
- `EntryWrite(db, cfg, actorID, entries...)` — all writes
- `EntryDelete(db, actorID, projectID, entryIDs...)` — all deletes
**FORBIDDEN:** `db.Exec()`, `db.Query()`, `db.QueryRow()` outside dbcore.go; wrapper functions bypassing choke points; modifications to dbcore.go without explicit approval.
`actorID=""` = system/internal access (always granted). RBAC enforced at DB level, not UI.
## Tech Stack
Go 1.24, SQLite with mattn/go-sqlite3 (CGO required), chi router, uuid, pdfcpu, excelize, golang.org/x/crypto

View File

@ -1,85 +0,0 @@
{{define "answer_approved.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your answer was approved ✓</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<!-- Success Icon -->
<div style="text-align: center; margin-bottom: 25px;">
<div style="display: inline-block; width: 60px; height: 60px; background-color: #dcfce7; border-radius: 50%; line-height: 60px; font-size: 28px;">
</div>
</div>
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 20px; font-weight: 600; text-align: center;">
Your answer was approved
</h2>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Hi{{if .RecipientName}} {{.RecipientName}}{{end}},
</p>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Great news! Your answer for <strong>{{.RequestTitle}}</strong> has been approved.
</p>
{{if .Published}}
<div style="background-color: #dcfce7; border-radius: 6px; padding: 15px 20px; margin: 25px 0;">
<p style="margin: 0; color: #166534; font-size: 14px;">
📁 <strong>Published to Data Room</strong> — Your response is now visible to authorized buyers.
</p>
</div>
{{end}}
{{if .DataRoomURL}}
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.DataRoomURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">View in Data Room</a>
</td>
</tr>
</table>
{{end}}
<p style="margin: 20px 0 0 0; color: #4a5568; font-size: 15px; line-height: 1.6;">
Thank you for your prompt response. Keep up the excellent work!
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
Questions? Contact <a href="mailto:support@muskepo.com" style="color: #c9a227; text-decoration: none;">support@muskepo.com</a>
</p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">
© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Privacy Policy</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Terms of Service</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,76 +0,0 @@
{{define "answer_rejected.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your answer needs revision</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 20px; font-weight: 600;">
Your answer needs revision
</h2>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Hi{{if .RecipientName}} {{.RecipientName}}{{end}},
</p>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Your answer for <strong>{{.RequestTitle}}</strong> requires some changes before it can be approved.
</p>
<!-- Feedback Box -->
{{if .Reason}}
<div style="background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 20px; margin: 25px 0;">
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">Feedback</p>
<p style="margin: 0; color: #7f1d1d; font-size: 15px; line-height: 1.5;">{{.Reason}}</p>
</div>
{{end}}
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.RequestURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">View Feedback</a>
</td>
</tr>
</table>
<p style="margin: 20px 0 0 0; color: #4a5568; font-size: 15px; line-height: 1.6;">
Please update your response based on the feedback above. If you have any questions, you can reply directly in the request thread.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
Questions? Contact <a href="mailto:support@muskepo.com" style="color: #c9a227; text-decoration: none;">support@muskepo.com</a>
</p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">
© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Privacy Policy</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Terms of Service</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,92 +0,0 @@
{{define "answer_submitted.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.AnswererName}} submitted an answer for: {{.RequestTitle}}</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<div style="display: inline-block; background-color: #fef3c7; color: #92400e; padding: 6px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 20px;">
ACTION REQUIRED
</div>
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 20px; font-weight: 600;">
New answer submitted for review
</h2>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Hi{{if .RecipientName}} {{.RecipientName}}{{end}},
</p>
<p style="margin: 0 0 25px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
<strong>{{.AnswererName}}</strong> has submitted an answer that needs your review.
</p>
<!-- Request Details -->
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f8f9fa; border-radius: 6px; margin: 0 0 25px 0;">
<tr>
<td style="padding: 20px;">
<p style="margin: 0 0 8px 0; color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Request</p>
<p style="margin: 0 0 15px 0; color: #1a2744; font-size: 16px; font-weight: 600;">{{.RequestTitle}}</p>
{{if .WorkstreamName}}
<p style="margin: 0 0 8px 0; color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Workstream</p>
<p style="margin: 0 0 15px 0; color: #4a5568; font-size: 14px;">{{.WorkstreamName}}</p>
{{end}}
{{if .AnswerPreview}}
<p style="margin: 0 0 8px 0; color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Preview</p>
<p style="margin: 0; color: #4a5568; font-size: 14px; line-height: 1.5; font-style: italic;">"{{truncate .AnswerPreview 200}}"</p>
{{end}}
</td>
</tr>
</table>
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.ReviewURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">Review Answer</a>
</td>
</tr>
</table>
<p style="margin: 20px 0 0 0; color: #9ca3af; font-size: 14px;">
Once approved, this answer will be published to the data room and visible to authorized buyers.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
Questions? Contact <a href="mailto:support@muskepo.com" style="color: #c9a227; text-decoration: none;">support@muskepo.com</a>
</p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">
© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Privacy Policy</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Terms of Service</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,76 +0,0 @@
{{define "invite.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You're invited to {{.ProjectName}}</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 20px; font-weight: 600;">
You've been invited to join {{.ProjectName}}
</h2>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Hi{{if .RecipientName}} {{.RecipientName}}{{end}},
</p>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
<strong>{{.InviterName}}</strong> from <strong>{{.InviterOrg}}</strong> has invited you to join the due diligence process for <strong>{{.ProjectName}}</strong>.
</p>
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.InviteURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">Accept Invitation</a>
</td>
</tr>
</table>
<!-- What is Dealspace -->
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 30px 0;">
<h3 style="margin: 0 0 10px 0; color: #1a2744; font-size: 14px; font-weight: 600;">What is Dealspace?</h3>
<p style="margin: 0; color: #6b7280; font-size: 14px; line-height: 1.5;">
Dealspace is a secure platform for managing M&A due diligence. All documents are encrypted and watermarked. You control what gets shared and when.
</p>
</div>
<p style="margin: 20px 0 0 0; color: #9ca3af; font-size: 14px;">
⏱ This invitation expires in {{if .ExpiresIn}}{{.ExpiresIn}}{{else}}7 days{{end}}.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
Questions? Contact <a href="mailto:support@muskepo.com" style="color: #c9a227; text-decoration: none;">support@muskepo.com</a>
</p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">
© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Privacy Policy</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Terms of Service</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,85 +0,0 @@
{{define "request_forwarded.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.SenderName}} forwarded a request to you</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 20px; font-weight: 600;">
Request forwarded to you
</h2>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Hi{{if .RecipientName}} {{.RecipientName}}{{end}},
</p>
<p style="margin: 0 0 25px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
<strong>{{.SenderName}}</strong> has forwarded a request to you for your input.
</p>
<!-- Request Details -->
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f8f9fa; border-radius: 6px; margin: 0 0 25px 0;">
<tr>
<td style="padding: 20px;">
<p style="margin: 0 0 8px 0; color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Request</p>
<p style="margin: 0; color: #1a2744; font-size: 16px; font-weight: 600;">{{.RequestTitle}}</p>
{{if .HasDueDate}}
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e5e7eb;">
<p style="margin: 0 0 4px 0; color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Due Date</p>
<p style="margin: 0; color: #dc2626; font-size: 14px; font-weight: 500;">{{.DueDate}}</p>
</div>
{{end}}
</td>
</tr>
</table>
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.RequestURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">View Request</a>
</td>
</tr>
</table>
<p style="margin: 20px 0 0 0; color: #9ca3af; font-size: 14px;">
You can respond to this request directly in Dealspace. Your response will be routed back to {{.SenderName}} for review.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
Questions? Contact <a href="mailto:support@muskepo.com" style="color: #c9a227; text-decoration: none;">support@muskepo.com</a>
</p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">
© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Privacy Policy</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Terms of Service</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,87 +0,0 @@
{{define "tasks_assigned.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You have {{.Count}} new task{{if gt .Count 1}}s{{end}} on {{.ProjectName}}</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px;">
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 20px; font-weight: 600;">
You have {{.Count}} new task{{if gt .Count 1}}s{{end}} on {{.ProjectName}}
</h2>
<p style="margin: 0 0 20px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
Hi{{if .RecipientName}} {{.RecipientName}}{{end}},
</p>
<p style="margin: 0 0 25px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
The following request{{if gt .Count 1}}s have{{else}} has{{end}} been assigned to you:
</p>
<!-- Task List -->
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin: 0 0 25px 0;">
{{range $i, $task := .Tasks}}
{{if lt $i 5}}
<tr>
<td style="padding: 12px 15px; border-left: 3px solid {{if eq $task.Priority "high"}}#dc2626{{else}}#c9a227{{end}}; background-color: #f8f9fa; margin-bottom: 8px;">
<p style="margin: 0; color: #1a2744; font-size: 15px; font-weight: 500;">{{$task.Title}}</p>
{{if $task.DueDate}}
<p style="margin: 4px 0 0 0; color: #6b7280; font-size: 13px;">Due: {{$task.DueDate}}{{if eq $task.Priority "high"}} · <span style="color: #dc2626;">High Priority</span>{{end}}</p>
{{end}}
</td>
</tr>
<tr><td style="height: 8px;"></td></tr>
{{end}}
{{end}}
</table>
{{if gt .Count 5}}
<p style="margin: 0 0 25px 0; color: #6b7280; font-size: 14px;">
...and {{sub .Count 5}} more
</p>
{{end}}
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.TasksURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">View My Tasks</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">
You're receiving this because you're assigned to requests in {{.ProjectName}}.
</p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">
© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Manage Notifications</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Unsubscribe</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,61 +0,0 @@
{{define "welcome.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Dealspace</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600; letter-spacing: 1px;">DEALSPACE</h1>
</td>
</tr>
<tr>
<td style="padding: 40px;">
<h2 style="margin: 0 0 20px 0; color: #1a2744; font-size: 24px; font-weight: 600;">
Welcome to Dealspace{{if .RecipientName}}, {{.RecipientName}}{{end}}! 🎉
</h2>
<p style="margin: 0 0 25px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">
You're all set up and ready to go. Here are three quick tips to get you started:
</p>
<div style="margin: 0 0 20px 0; padding: 20px; background-color: #f8f9fa; border-radius: 6px;">
<h3 style="margin: 0 0 8px 0; color: #1a2744; font-size: 16px; font-weight: 600;">📥 Your inbox is your home</h3>
<p style="margin: 0; color: #6b7280; font-size: 14px; line-height: 1.5;">Everything assigned to you appears in your task inbox. Start there each day to see what needs your attention.</p>
</div>
<div style="margin: 0 0 20px 0; padding: 20px; background-color: #f8f9fa; border-radius: 6px;">
<h3 style="margin: 0 0 8px 0; color: #1a2744; font-size: 16px; font-weight: 600;">📋 Requests, not folders</h3>
<p style="margin: 0; color: #6b7280; font-size: 14px; line-height: 1.5;">We organize by requests, not file folders. Each request tracks its own status, comments, and documents — all in one place.</p>
</div>
<div style="margin: 0 0 25px 0; padding: 20px; background-color: #f8f9fa; border-radius: 6px;">
<h3 style="margin: 0 0 8px 0; color: #1a2744; font-size: 16px; font-weight: 600;">🤖 Ask Aria</h3>
<p style="margin: 0; color: #6b7280; font-size: 14px; line-height: 1.5;">Not sure where something is? Ask Aria, our AI assistant. She can find documents, answer questions, and guide you through the process.</p>
</div>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #c9a227; border-radius: 6px;">
<a href="{{.TasksURL}}" style="display: inline-block; padding: 16px 32px; color: #1a2744; font-size: 16px; font-weight: 600; text-decoration: none;">Go to My Tasks</a>
</td>
</tr>
</table>
<p style="margin: 20px 0 0 0; color: #4a5568; font-size: 15px; line-height: 1.6;">We're excited to have you on board. Let's make this deal happen! 🚀</p>
</td>
</tr>
<tr>
<td style="background-color: #1a2744; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 10px 0; color: #9ca3af; font-size: 12px;">Need help? Contact <a href="mailto:support@muskepo.com" style="color: #c9a227; text-decoration: none;">support@muskepo.com</a></p>
<p style="margin: 0; color: #6b7280; font-size: 11px;">© 2026 Dealspace · <a href="#" style="color: #6b7280; text-decoration: none;">Privacy Policy</a> · <a href="#" style="color: #6b7280; text-decoration: none;">Terms of Service</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
{{end}}

View File

@ -1,744 +0,0 @@
/**
* Dealspace Theme System
*
* All colors are CSS custom properties. Zero hardcoded values in templates.
* Switch themes by changing the class on <html>: theme-light, theme-dark, theme-contrast
* Per-project overrides use data-project attribute.
*/
/* ==========================================================================
BASE TOKENS (shared across themes)
========================================================================== */
:root {
/* Spacing */
--space-xs: 0.25rem; /* 4px */
--space-sm: 0.5rem; /* 8px */
--space-md: 1rem; /* 16px */
--space-lg: 1.5rem; /* 24px */
--space-xl: 2rem; /* 32px */
--space-2xl: 3rem; /* 48px */
/* Border radius */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 0.75rem; /* 12px */
--radius-full: 9999px;
/* Typography */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
/* Z-index scale */
--z-dropdown: 100;
--z-modal: 200;
--z-toast: 300;
}
/* ==========================================================================
LIGHT THEME (default)
========================================================================== */
.theme-light,
:root {
/* Surfaces */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f9fafb;
--color-bg-tertiary: #f3f4f6;
--color-bg-inverse: #1f2937;
--color-bg-hover: #f3f4f6;
--color-bg-selected: #eff6ff;
/* Text */
--color-text-primary: #111827;
--color-text-secondary: #6b7280;
--color-text-tertiary: #9ca3af;
--color-text-inverse: #ffffff;
--color-text-link: #2563eb;
--color-text-link-hover: #1d4ed8;
/* Borders */
--color-border-primary: #e5e7eb;
--color-border-secondary: #f3f4f6;
--color-border-focus: #2563eb;
/* Semantic colors */
--color-accent: #2563eb;
--color-accent-hover: #1d4ed8;
--color-accent-light: #dbeafe;
--color-success: #059669;
--color-success-light: #d1fae5;
--color-warning: #d97706;
--color-warning-light: #fef3c7;
--color-error: #dc2626;
--color-error-light: #fee2e2;
--color-info: #0284c7;
--color-info-light: #e0f2fe;
/* Priority indicators */
--color-priority-high: #dc2626;
--color-priority-normal: #d97706;
--color-priority-low: #9ca3af;
/* Status badges */
--color-status-open: #6b7280;
--color-status-open-bg: #f3f4f6;
--color-status-assigned: #2563eb;
--color-status-assigned-bg: #dbeafe;
--color-status-answered: #d97706;
--color-status-answered-bg: #fef3c7;
--color-status-vetted: #7c3aed;
--color-status-vetted-bg: #ede9fe;
--color-status-published: #059669;
--color-status-published-bg: #d1fae5;
--color-status-closed: #9ca3af;
--color-status-closed-bg: #f3f4f6;
/* Components */
--color-header-bg: #ffffff;
--color-header-text: #111827;
--color-header-border: #e5e7eb;
--color-card-bg: #ffffff;
--color-card-border: #e5e7eb;
--color-card-shadow: rgba(0, 0, 0, 0.05);
--color-input-bg: #ffffff;
--color-input-border: #d1d5db;
--color-input-focus: #2563eb;
--color-input-placeholder: #9ca3af;
--color-badge-bg: #ef4444;
--color-badge-text: #ffffff;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-focus: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
/* ==========================================================================
DARK THEME
========================================================================== */
.theme-dark {
/* Surfaces */
--color-bg-primary: #111827;
--color-bg-secondary: #1f2937;
--color-bg-tertiary: #374151;
--color-bg-inverse: #f9fafb;
--color-bg-hover: #374151;
--color-bg-selected: #1e3a5f;
/* Text */
--color-text-primary: #f9fafb;
--color-text-secondary: #9ca3af;
--color-text-tertiary: #6b7280;
--color-text-inverse: #111827;
--color-text-link: #60a5fa;
--color-text-link-hover: #93c5fd;
/* Borders */
--color-border-primary: #374151;
--color-border-secondary: #4b5563;
--color-border-focus: #60a5fa;
/* Semantic colors */
--color-accent: #3b82f6;
--color-accent-hover: #60a5fa;
--color-accent-light: #1e3a5f;
--color-success: #10b981;
--color-success-light: #064e3b;
--color-warning: #f59e0b;
--color-warning-light: #78350f;
--color-error: #ef4444;
--color-error-light: #7f1d1d;
--color-info: #0ea5e9;
--color-info-light: #0c4a6e;
/* Priority indicators */
--color-priority-high: #ef4444;
--color-priority-normal: #f59e0b;
--color-priority-low: #6b7280;
/* Status badges */
--color-status-open: #9ca3af;
--color-status-open-bg: #374151;
--color-status-assigned: #60a5fa;
--color-status-assigned-bg: #1e3a5f;
--color-status-answered: #f59e0b;
--color-status-answered-bg: #78350f;
--color-status-vetted: #a78bfa;
--color-status-vetted-bg: #4c1d95;
--color-status-published: #10b981;
--color-status-published-bg: #064e3b;
--color-status-closed: #6b7280;
--color-status-closed-bg: #374151;
/* Components */
--color-header-bg: #1f2937;
--color-header-text: #f9fafb;
--color-header-border: #374151;
--color-card-bg: #1f2937;
--color-card-border: #374151;
--color-card-shadow: rgba(0, 0, 0, 0.3);
--color-input-bg: #374151;
--color-input-border: #4b5563;
--color-input-focus: #60a5fa;
--color-input-placeholder: #6b7280;
--color-badge-bg: #ef4444;
--color-badge-text: #ffffff;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
--shadow-focus: 0 0 0 3px rgba(96, 165, 250, 0.3);
}
/* ==========================================================================
HIGH CONTRAST THEME
========================================================================== */
.theme-contrast {
/* Surfaces */
--color-bg-primary: #000000;
--color-bg-secondary: #1a1a1a;
--color-bg-tertiary: #333333;
--color-bg-inverse: #ffffff;
--color-bg-hover: #333333;
--color-bg-selected: #003366;
/* Text */
--color-text-primary: #ffffff;
--color-text-secondary: #e0e0e0;
--color-text-tertiary: #b0b0b0;
--color-text-inverse: #000000;
--color-text-link: #66b3ff;
--color-text-link-hover: #99ccff;
/* Borders */
--color-border-primary: #ffffff;
--color-border-secondary: #808080;
--color-border-focus: #ffff00;
/* Semantic colors */
--color-accent: #66b3ff;
--color-accent-hover: #99ccff;
--color-accent-light: #003366;
--color-success: #00ff00;
--color-success-light: #003300;
--color-warning: #ffff00;
--color-warning-light: #333300;
--color-error: #ff3333;
--color-error-light: #330000;
--color-info: #00ffff;
--color-info-light: #003333;
/* Priority indicators */
--color-priority-high: #ff3333;
--color-priority-normal: #ffff00;
--color-priority-low: #808080;
/* Status badges */
--color-status-open: #ffffff;
--color-status-open-bg: #333333;
--color-status-assigned: #66b3ff;
--color-status-assigned-bg: #003366;
--color-status-answered: #ffff00;
--color-status-answered-bg: #333300;
--color-status-vetted: #cc99ff;
--color-status-vetted-bg: #330066;
--color-status-published: #00ff00;
--color-status-published-bg: #003300;
--color-status-closed: #808080;
--color-status-closed-bg: #1a1a1a;
/* Components */
--color-header-bg: #000000;
--color-header-text: #ffffff;
--color-header-border: #ffffff;
--color-card-bg: #1a1a1a;
--color-card-border: #ffffff;
--color-card-shadow: none;
--color-input-bg: #1a1a1a;
--color-input-border: #ffffff;
--color-input-focus: #ffff00;
--color-input-placeholder: #808080;
--color-badge-bg: #ff3333;
--color-badge-text: #ffffff;
/* Shadows - disabled for clarity */
--shadow-sm: none;
--shadow-md: none;
--shadow-lg: none;
--shadow-focus: 0 0 0 3px #ffff00;
}
/* High contrast specific: underline all links */
.theme-contrast a {
text-decoration: underline;
}
.theme-contrast a:focus {
outline: 3px solid var(--color-border-focus);
outline-offset: 2px;
}
/* ==========================================================================
PER-PROJECT BRAND OVERRIDES
========================================================================== */
/* Project overrides cascade on top of the selected theme */
/* Example: data-project="alpha" */
[data-project="alpha"] {
--color-accent: #7c3aed; /* Purple accent */
--color-accent-hover: #6d28d9;
--color-header-bg: #7c3aed;
--color-header-text: #ffffff;
}
[data-project="beta"] {
--color-accent: #059669; /* Emerald accent */
--color-accent-hover: #047857;
--color-header-bg: #059669;
--color-header-text: #ffffff;
}
/* ==========================================================================
COMPONENT STYLES (using theme variables)
========================================================================== */
/* Base reset */
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading-normal);
color: var(--color-text-primary);
background-color: var(--color-bg-primary);
margin: 0;
padding: 0;
}
/* Header */
.global-header {
display: flex;
align-items: center;
gap: var(--space-lg);
padding: var(--space-md) var(--space-lg);
background: var(--color-header-bg);
border-bottom: 1px solid var(--color-header-border);
}
.global-header .logo {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-header-text);
}
/* Project selector */
.project-select {
flex: 1;
max-width: 400px;
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
color: var(--color-text-primary);
background: var(--color-input-bg);
border: 1px solid var(--color-input-border);
border-radius: var(--radius-md);
cursor: pointer;
}
.project-select:focus {
outline: none;
border-color: var(--color-input-focus);
box-shadow: var(--shadow-focus);
}
/* Workstream tabs */
.workstream-tabs {
display: flex;
gap: var(--space-xs);
padding: 0 var(--space-lg);
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-primary);
}
.workstream-tabs a {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-md) var(--space-lg);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-secondary);
text-decoration: none;
border-bottom: 2px solid transparent;
transition: color var(--transition-fast), border-color var(--transition-fast);
}
.workstream-tabs a:hover {
color: var(--color-text-primary);
}
.workstream-tabs a.active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
/* Badge */
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 var(--space-xs);
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-badge-text);
background: var(--color-badge-bg);
border-radius: var(--radius-full);
}
.badge-muted {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
/* Task card */
.task-card {
display: block;
padding: var(--space-md) var(--space-lg);
background: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
text-decoration: none;
color: inherit;
transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
}
.task-card:hover {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-md);
}
.task-card-header {
display: flex;
align-items: flex-start;
gap: var(--space-md);
margin-bottom: var(--space-sm);
}
.task-card-priority {
width: 0.625rem;
height: 0.625rem;
border-radius: var(--radius-full);
flex-shrink: 0;
margin-top: 0.375rem;
}
.task-card-priority.high { background: var(--color-priority-high); }
.task-card-priority.normal { background: var(--color-priority-normal); }
.task-card-priority.low { background: var(--color-priority-low); }
.task-card-ref {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-secondary);
font-family: var(--font-mono);
}
.task-card-title {
flex: 1;
font-size: var(--text-base);
font-weight: 600;
color: var(--color-text-primary);
}
.task-card-due {
font-size: var(--text-sm);
color: var(--color-text-secondary);
white-space: nowrap;
}
.task-card-due.overdue {
color: var(--color-error);
font-weight: 600;
}
.task-card-due.today {
color: var(--color-warning);
font-weight: 600;
}
.task-card-meta {
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin-bottom: var(--space-sm);
}
.task-card-preview {
font-size: var(--text-sm);
color: var(--color-text-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Status badge */
.status-badge {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
border-radius: var(--radius-sm);
}
.status-badge.open { color: var(--color-status-open); background: var(--color-status-open-bg); }
.status-badge.assigned { color: var(--color-status-assigned); background: var(--color-status-assigned-bg); }
.status-badge.answered { color: var(--color-status-answered); background: var(--color-status-answered-bg); }
.status-badge.vetted { color: var(--color-status-vetted); background: var(--color-status-vetted-bg); }
.status-badge.published { color: var(--color-status-published); background: var(--color-status-published-bg); }
.status-badge.closed { color: var(--color-status-closed); background: var(--color-status-closed-bg); }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
font-weight: 500;
text-decoration: none;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-primary {
color: var(--color-text-inverse);
background: var(--color-accent);
border-color: var(--color-accent);
}
.btn-primary:hover {
background: var(--color-accent-hover);
border-color: var(--color-accent-hover);
}
.btn-secondary {
color: var(--color-text-primary);
background: var(--color-bg-secondary);
border-color: var(--color-border-primary);
}
.btn-secondary:hover {
background: var(--color-bg-tertiary);
}
.btn:focus {
outline: none;
box-shadow: var(--shadow-focus);
}
/* Form inputs */
.input, .textarea {
width: 100%;
padding: var(--space-sm) var(--space-md);
font-size: var(--text-base);
color: var(--color-text-primary);
background: var(--color-input-bg);
border: 1px solid var(--color-input-border);
border-radius: var(--radius-md);
}
.input::placeholder, .textarea::placeholder {
color: var(--color-input-placeholder);
}
.input:focus, .textarea:focus {
outline: none;
border-color: var(--color-input-focus);
box-shadow: var(--shadow-focus);
}
.textarea {
min-height: 100px;
resize: vertical;
}
/* Thread messages */
.thread-message {
padding: var(--space-md);
background: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-md);
}
.thread-message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-sm);
}
.thread-message-author {
font-weight: 600;
color: var(--color-text-primary);
}
.thread-message-time {
font-size: var(--text-sm);
color: var(--color-text-tertiary);
}
.thread-message-body {
color: var(--color-text-primary);
line-height: var(--leading-relaxed);
}
.thread-message-attachment {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
margin-top: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: var(--color-bg-tertiary);
border-radius: var(--radius-md);
font-size: var(--text-sm);
color: var(--color-text-link);
text-decoration: none;
}
.thread-message-attachment:hover {
color: var(--color-text-link-hover);
}
/* Routing chain */
.routing-chain {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md);
background: var(--color-bg-secondary);
border-radius: var(--radius-lg);
overflow-x: auto;
}
.routing-step {
display: flex;
align-items: center;
gap: var(--space-sm);
white-space: nowrap;
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.routing-step.current {
font-weight: 600;
color: var(--color-accent);
}
.routing-arrow {
color: var(--color-text-tertiary);
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: var(--space-md);
}
.empty-state-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--space-sm);
}
.empty-state-text {
color: var(--color-text-secondary);
}
/* ==========================================================================
UTILITY CLASSES
========================================================================== */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--space-lg);
}
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-sm { gap: var(--space-sm); }
.gap-md { gap: var(--space-md); }
.gap-lg { gap: var(--space-lg); }
.mt-sm { margin-top: var(--space-sm); }
.mt-md { margin-top: var(--space-md); }
.mt-lg { margin-top: var(--space-lg); }
.mb-sm { margin-bottom: var(--space-sm); }
.mb-md { margin-bottom: var(--space-md); }
.mb-lg { margin-bottom: var(--space-lg); }
.text-sm { font-size: var(--text-sm); }
.text-lg { font-size: var(--text-lg); }
.text-muted { color: var(--color-text-secondary); }
.font-semibold { font-weight: 600; }

View File

@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — Dealspace</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>* { font-family: 'Inter', sans-serif; } body { background: #0a1628; }
.sidebar-link.active { background: rgba(201,168,76,0.1); color: #c9a84c; border-left: 3px solid #c9a84c; }
.sidebar-link:hover:not(.active) { background: rgba(255,255,255,0.04); }
</style>
</head>
<body>
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<div class="flex items-center gap-4">
<span class="text-xs px-2 py-0.5 bg-[#c9a84c]/20 text-[#c9a84c] rounded-full font-medium">Super Admin</span>
<span id="userName" class="text-sm text-[#b0bec5]"></span>
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
</div>
</header>
<div class="flex">
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
App Home</a>
<a href="/admin" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Dashboard</a>
</div>
</nav>
<main class="flex-1 p-8 max-w-6xl">
<h1 class="text-2xl font-bold text-white mb-2">Admin Dashboard</h1>
<p class="text-[#b0bec5] text-sm mb-8">Platform overview and management.</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Users</div>
<div id="statUsers" class="text-3xl font-bold text-white"></div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Projects</div>
<div id="statProjects" class="text-3xl font-bold text-white"></div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Organizations</div>
<div id="statOrgs" class="text-3xl font-bold text-white"></div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
<div id="statSessions" class="text-3xl font-bold text-white"></div>
</div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-white">All Users</h2>
</div>
<div id="userList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
</div>
</main>
</div>
<script>
const token = localStorage.getItem('ds_token');
if (!token) window.location.href = '/app/login';
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
if (!user.is_super_admin) window.location.href = '/app/tasks';
document.getElementById('userName').textContent = user.name || user.email || '';
function fetchAPI(path, opts = {}) {
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
return fetch(path, opts).then(r => { if (r.status === 401) { localStorage.removeItem('ds_token'); window.location.href = '/app/login'; } return r; });
}
function logout() { fetchAPI('/api/auth/logout', { method: 'POST' }).finally(() => { localStorage.clear(); window.location.href = '/app/login'; }); }
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
async function loadStats() {
try {
const [usersRes, projectsRes, orgsRes] = await Promise.all([
fetchAPI('/api/admin/users'), fetchAPI('/api/projects'), fetchAPI('/api/orgs')
]);
const users = await usersRes.json();
const projects = await projectsRes.json();
const orgs = await orgsRes.json();
document.getElementById('statUsers').textContent = Array.isArray(users) ? users.length : '?';
document.getElementById('statProjects').textContent = Array.isArray(projects) ? projects.length : '?';
document.getElementById('statOrgs').textContent = Array.isArray(orgs) ? orgs.length : '?';
document.getElementById('statSessions').textContent = '—';
if (Array.isArray(users) && users.length > 0) {
document.getElementById('userList').innerHTML = users.map(u => `
<div class="flex items-center gap-4 px-4 py-3 rounded-lg bg-[#0a1628] border border-white/[0.05]">
<div class="w-8 h-8 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] text-sm font-semibold">${(u.name||u.email||'?')[0].toUpperCase()}</div>
<div class="flex-1">
<div class="text-white text-sm font-medium">${escHtml(u.name || u.email)}</div>
${u.name ? `<div class="text-[#8899a6] text-xs">${escHtml(u.email)}</div>` : ''}
</div>
${u.is_super_admin ? '<span class="text-xs px-2 py-0.5 bg-[#c9a84c]/20 text-[#c9a84c] rounded-full">super admin</span>' : ''}
<span class="text-xs text-[#8899a6]">${new Date(u.created_at).toLocaleDateString()}</span>
</div>`).join('');
} else {
document.getElementById('userList').innerHTML = '<div class="text-[#b0bec5] text-sm">No users found.</div>';
}
} catch(e) {}
}
loadStats();
</script>
</body>
</html>

View File

@ -1,154 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Organizations — Dealspace</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>* { font-family: 'Inter', sans-serif; } body { background: #0a1628; }
.sidebar-link.active { background: rgba(201,168,76,0.1); color: #c9a84c; border-left: 3px solid #c9a84c; }
.sidebar-link:hover:not(.active) { background: rgba(255,255,255,0.04); }
.card:hover { border-color: rgba(201,168,76,0.3); }
</style>
</head>
<body>
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<div class="flex items-center gap-4">
<span id="userName" class="text-sm text-[#b0bec5]"></span>
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
</div>
</header>
<div class="flex">
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
My Tasks</a>
<a href="/app/projects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
Projects</a>
<a href="/app/orgs" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organizations</a>
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Admin</a></div>
</div>
</nav>
<main class="flex-1 p-8 max-w-5xl">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold text-white mb-1">Organizations</h1>
<p class="text-[#b0bec5] text-sm">Company directory — parties eligible to participate in deals.</p>
</div>
<button id="newOrgBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Organization</button>
</div>
<div id="orgGrid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="text-[#b0bec5] text-sm col-span-2">Loading...</div>
</div>
<div id="emptyState" class="hidden text-center py-20">
<div class="text-5xl mb-4">🏢</div>
<h2 class="text-xl font-semibold text-white mb-2">No organizations yet</h2>
<p class="text-[#b0bec5] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
</div>
</main>
</div>
<!-- New Org Modal -->
<div id="newOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
<h2 class="text-xl font-semibold text-white mb-6">New Organization</h2>
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<div class="space-y-4">
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Organization Name</label>
<input id="oName" type="text" placeholder="James LLC" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Allowed Email Domains <span class="text-red-400">*</span></label>
<input id="oDomains" type="text" placeholder="jamesllc.com, kaseya.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]">
<p class="text-[#8899a6] text-xs mt-1">Comma-separated. Only emails from these domains can be invited for this org.</p></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Default Role</label>
<select id="oRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
</select></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
<input id="oWebsite" type="url" placeholder="https://jamesllc.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
</div>
<div class="flex gap-3 mt-6">
<button onclick="closeModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
<button id="createBtn" onclick="createOrg()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Create</button>
</div>
</div>
</div>
<script>
const token = localStorage.getItem('ds_token');
if (!token) window.location.href = '/app/login';
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
document.getElementById('userName').textContent = user.name || user.email || '';
if (user.is_super_admin) { document.getElementById('adminLinks').classList.remove('hidden'); document.getElementById('newOrgBtn').classList.remove('hidden'); }
function fetchAPI(path, opts = {}) {
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
return fetch(path, opts).then(r => { if (r.status === 401) { localStorage.removeItem('ds_token'); window.location.href = '/app/login'; } return r; });
}
function logout() { fetchAPI('/api/auth/logout', { method: 'POST' }).finally(() => { localStorage.clear(); window.location.href = '/app/login'; }); }
function parseData(t) { try { return JSON.parse(t); } catch { return {}; } }
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
const roleColors = { seller: 'bg-blue-500/20 text-blue-300', buyer: 'bg-green-500/20 text-green-300', ib: 'bg-[#c9a84c]/20 text-[#c9a84c]', advisor: 'bg-purple-500/20 text-purple-300' };
async function loadOrgs() {
try {
const res = await fetchAPI('/api/orgs');
const orgs = await res.json();
const grid = document.getElementById('orgGrid');
if (!orgs || orgs.length === 0) { grid.classList.add('hidden'); document.getElementById('emptyState').classList.remove('hidden'); return; }
grid.innerHTML = orgs.map(o => {
const d = parseData(o.data_text);
const rc = roleColors[d.role] || 'bg-gray-500/20 text-gray-300';
const domains = Array.isArray(d.domains) ? d.domains : (d.domains ? [d.domains] : []);
return `<div class="card bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 transition">
<div class="flex items-start justify-between mb-3">
<h3 class="text-white font-semibold text-lg leading-tight">${escHtml(d.name || o.summary || 'Untitled')}</h3>
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${d.role || '?'}</span>
</div>
<div class="flex gap-1.5 flex-wrap mb-3">${domains.map(dm => `<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
${d.website ? `<a href="${escHtml(d.website)}" target="_blank" class="text-xs text-[#c9a84c] hover:underline">${escHtml(d.website)}</a>` : ''}
</div>`;
}).join('');
} catch(e) { document.getElementById('orgGrid').innerHTML = '<div class="text-red-400 text-sm col-span-2">Failed to load.</div>'; }
}
function openModal() { document.getElementById('newOrgModal').classList.remove('hidden'); document.getElementById('oName').focus(); }
function closeModal() { document.getElementById('newOrgModal').classList.add('hidden'); document.getElementById('modalError').classList.add('hidden'); }
document.getElementById('newOrgBtn').onclick = openModal;
async function createOrg() {
const name = document.getElementById('oName').value.trim();
const domainsRaw = document.getElementById('oDomains').value.trim();
const role = document.getElementById('oRole').value;
const website = document.getElementById('oWebsite').value.trim();
const errEl = document.getElementById('modalError');
const btn = document.getElementById('createBtn');
if (!name) { errEl.textContent = 'Organization name is required'; errEl.classList.remove('hidden'); return; }
if (!domainsRaw) { errEl.textContent = 'At least one email domain is required'; errEl.classList.remove('hidden'); return; }
const domains = domainsRaw.split(',').map(d => d.trim().replace(/^@/, '')).filter(Boolean);
if (!domains.length) { errEl.textContent = 'Invalid domain format'; errEl.classList.remove('hidden'); return; }
btn.disabled = true; btn.textContent = 'Creating...'; errEl.classList.add('hidden');
try {
const res = await fetchAPI('/api/orgs', { method: 'POST', body: JSON.stringify({ name, domains, role, website }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create organization');
closeModal();
document.getElementById('oName').value = '';
document.getElementById('oDomains').value = '';
document.getElementById('oWebsite').value = '';
loadOrgs();
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Create'; }
}
loadOrgs();
</script>
</body>
</html>

View File

@ -1,494 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project — Dealspace</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>* { font-family: 'Inter', sans-serif; } body { background: #0a1628; }
.sidebar-link.active { background: rgba(201,168,76,0.1); color: #c9a84c; border-left: 3px solid #c9a84c; }
.sidebar-link:hover:not(.active) { background: rgba(255,255,255,0.04); }
.tab.active { color: #c9a84c; border-bottom: 2px solid #c9a84c; }
.tab { border-bottom: 2px solid transparent; }
.req-row:hover { background: rgba(255,255,255,0.03); }
.section-header { cursor: pointer; user-select: none; }
.section-header:hover { background: rgba(255,255,255,0.02); }
.priority-high { background: #ef444420; color: #f87171; }
.priority-medium { background: #f59e0b20; color: #fbbf24; }
.priority-low { background: #22c55e20; color: #4ade80; }
.status-open { background: #3b82f620; color: #60a5fa; }
.status-in_progress { background: #f59e0b20; color: #fbbf24; }
.status-answered { background: #22c55e20; color: #4ade80; }
.status-not_applicable { background: #6b728020; color: #9ca3af; }
</style>
</head>
<body>
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
<div class="flex items-center gap-3">
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<span class="text-white/20">/</span>
<a href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Projects</a>
<span class="text-white/20">/</span>
<span id="projectName" class="text-sm text-white font-medium">Loading...</span>
</div>
<div class="flex items-center gap-4">
<span id="userName" class="text-sm text-[#b0bec5]"></span>
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
</div>
</header>
<div class="flex">
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
My Tasks</a>
<a href="/app/projects" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
Projects</a>
<a href="/app/orgs" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organizations</a>
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Admin</a></div>
</div>
</nav>
<main class="flex-1 p-8 max-w-6xl">
<div class="flex items-start justify-between mb-6">
<div>
<div class="flex items-center gap-3 mb-1">
<h1 id="projectTitle" class="text-2xl font-bold text-white">Loading...</h1>
<span id="projectStatus" class="px-2.5 py-0.5 rounded-full text-xs font-medium"></span>
</div>
<p id="projectDesc" class="text-[#b0bec5] text-sm"></p>
</div>
<div class="flex gap-2">
<button id="importBtn" onclick="openImportModal()" class="px-4 py-2 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
Import
</button>
<button id="newRequestBtn" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Request</button>
</div>
</div>
<div class="flex gap-6 border-b border-white/[0.08] mb-6">
<button class="tab active pb-3 text-sm font-medium transition" onclick="switchTab('requests', this)">Requests</button>
<button class="tab pb-3 text-sm font-medium text-[#b0bec5] transition" onclick="switchTab('orgs', this)">Parties</button>
<button class="tab pb-3 text-sm font-medium text-[#b0bec5] transition" onclick="switchTab('team', this)">Team</button>
</div>
<div id="tab-requests">
<div id="requestList" class="space-y-4"><div class="text-[#b0bec5] text-sm">Loading requests...</div></div>
<div id="requestEmpty" class="hidden text-center py-16">
<div class="text-4xl mb-3">📋</div>
<h3 class="text-lg font-semibold text-white mb-1">No requests yet</h3>
<p class="text-[#b0bec5] text-sm mb-4">Import a diligence checklist or create requests manually.</p>
<button onclick="openImportModal()" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Import Checklist</button>
</div>
</div>
<div id="tab-orgs" class="hidden">
<div class="flex justify-between items-center mb-4">
<p class="text-[#b0bec5] text-sm">Parties involved in this deal.</p>
<button id="addOrgBtn" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ Add Party</button>
</div>
<div id="orgList" class="space-y-3"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
</div>
<div id="tab-team" class="hidden">
<div class="flex justify-between items-center mb-4">
<p class="text-[#b0bec5] text-sm">People with access to this deal.</p>
<button id="inviteBtn" class="px-3 py-1.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ Invite</button>
</div>
<div id="teamList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
</div>
</main>
</div>
<div id="importModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-[#0d1f3c] rounded-2xl p-6 w-full max-w-md border border-white/[0.08]">
<h2 class="text-lg font-semibold text-white mb-4">Import Diligence Checklist</h2>
<form id="importForm" enctype="multipart/form-data">
<div class="mb-4">
<label class="block text-sm text-[#b0bec5] mb-1.5">File (CSV or XLSX)</label>
<input type="file" id="importFile" name="file" accept=".csv,.xlsx,.xls" required class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div class="mb-4">
<label class="block text-sm text-[#b0bec5] mb-1.5">Import Mode</label>
<select id="importMode" name="mode" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
<option value="add">Add to existing requests</option>
<option value="replace">Replace all requests</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm text-[#b0bec5] mb-1.5">Section Filter (optional)</label>
<input type="text" id="sectionFilter" name="section_filter" placeholder="e.g. Financial" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder:text-[#8899a6]">
</div>
<div class="mb-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="createWorkstreams" name="create_workstreams" class="rounded border-white/[0.2]">
<span class="text-sm text-[#b0bec5]">Create workstreams from sections</span>
</label>
</div>
<div class="flex gap-3">
<button type="button" onclick="closeImportModal()" class="flex-1 px-4 py-2 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm transition">Cancel</button>
<button type="submit" id="importSubmitBtn" class="flex-1 px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Import</button>
</div>
</form>
</div>
</div>
<!-- Add Org Modal -->
<div id="addOrgModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50" onclick="if(event.target===this)closeAddOrg()">
<div class="bg-[#0d1f3c] rounded-2xl w-full max-w-2xl border border-white/[0.08] max-h-[90vh] overflow-y-auto">
<!-- Step 1: Email -->
<div id="addOrgStep1" class="p-6">
<h2 class="text-lg font-semibold text-white mb-1">Add Party</h2>
<p class="text-[#b0bec5] text-sm mb-5">Enter the email address of someone at the organization.</p>
<div class="flex gap-3">
<input type="email" id="addOrgEmail" placeholder="e.g. michael@andersongroup.com" class="flex-1 px-4 py-2.5 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder:text-[#8899a6]" onkeydown="if(event.key==='Enter')scrapeOrg()">
<button onclick="scrapeOrg()" id="scrapeBtn" class="px-5 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition whitespace-nowrap">Look up</button>
</div>
<div id="scrapeError" class="hidden mt-3 text-red-400 text-sm"></div>
<div id="scrapeLoading" class="hidden mt-4 flex items-center gap-3 text-[#b0bec5] text-sm">
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
Scanning website...
</div>
<div class="flex justify-end mt-4">
<button onclick="closeAddOrg()" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Cancel</button>
</div>
</div>
<!-- Step 2: Org details (prefilled) -->
<div id="addOrgStep2" class="hidden p-6">
<h2 class="text-lg font-semibold text-white mb-4">Organization Details</h2>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="col-span-2">
<label class="block text-xs text-[#b0bec5] mb-1">Name *</label>
<input type="text" id="orgName" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div class="col-span-2">
<label class="block text-xs text-[#b0bec5] mb-1">Description</label>
<textarea id="orgDesc" rows="2" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] resize-none"></textarea>
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">Role in deal *</label>
<select id="orgRole" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
<option value="">Select...</option>
<option value="seller">Seller</option>
<option value="buyer">Buyer</option>
<option value="ib">Investment Bank</option>
<option value="advisor">Advisor</option>
</select>
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">Industry</label>
<input type="text" id="orgIndustry" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">Website</label>
<input type="text" id="orgWebsite" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">Phone</label>
<input type="text" id="orgPhone" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div class="col-span-2">
<label class="block text-xs text-[#b0bec5] mb-1">Address</label>
<input type="text" id="orgAddress" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">City</label>
<input type="text" id="orgCity" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">State</label>
<input type="text" id="orgState" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">Founded</label>
<input type="text" id="orgFounded" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">LinkedIn</label>
<input type="text" id="orgLinkedIn" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
</div>
</div>
<div class="flex justify-between">
<button onclick="showAddOrgStep(1)" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Back</button>
<button onclick="showAddOrgStep(3)" class="px-5 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Next: Select People</button>
</div>
</div>
<!-- Step 3: Colleagues -->
<div id="addOrgStep3" class="hidden p-6">
<h2 class="text-lg font-semibold text-white mb-1">Select Colleagues</h2>
<p class="text-[#b0bec5] text-sm mb-4">Choose who to add to this deal. They'll receive an invite email.</p>
<div class="flex items-center gap-3 mb-4">
<label class="flex items-center gap-2 cursor-pointer text-sm text-[#b0bec5]">
<input type="checkbox" id="selectAllPeople" onchange="toggleAllPeople(this.checked)" class="rounded border-white/[0.2] accent-[#c9a84c]">
Select all
</label>
<span id="selectedCount" class="text-xs text-[#8899a6]">0 selected</span>
</div>
<div id="peopleList" class="space-y-2 mb-4 max-h-[50vh] overflow-y-auto"></div>
<div id="noPeople" class="hidden text-[#b0bec5] text-sm py-8 text-center">No team members found on the website.</div>
<div class="flex justify-between">
<button onclick="showAddOrgStep(2)" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Back</button>
<button onclick="submitAddOrg()" id="submitOrgBtn" class="px-5 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Add Organization</button>
</div>
</div>
</div>
</div>
<script>
const token = localStorage.getItem('ds_token');
if (!token) window.location.href = '/app/login';
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
const projectID = location.pathname.split('/').pop();
document.getElementById('userName').textContent = user.name || user.email || '';
if (user.is_super_admin) document.getElementById('adminLinks').classList.remove('hidden');
function fetchAPI(path, opts = {}) {
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + token };
if (!(opts.body instanceof FormData)) opts.headers['Content-Type'] = 'application/json';
return fetch(path, opts).then(r => { if (r.status === 401) { localStorage.removeItem('ds_token'); window.location.href = '/app/login'; } return r; });
}
function logout() { fetchAPI('/api/auth/logout', { method: 'POST' }).finally(() => { localStorage.clear(); window.location.href = '/app/login'; }); }
function parseData(t) { try { return JSON.parse(t); } catch { return {}; } }
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
const statusColors = { active: 'bg-green-500/20 text-green-300', draft: 'bg-gray-500/20 text-gray-300', closed: 'bg-red-500/20 text-red-300' };
const roleColors = { seller: 'bg-blue-500/20 text-blue-300', buyer: 'bg-green-500/20 text-green-300', ib: 'bg-[#c9a84c]/20 text-[#c9a84c]', advisor: 'bg-purple-500/20 text-purple-300' };
const expandedSections = new Set();
async function loadProject() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/detail');
if (!res.ok) { document.getElementById('projectTitle').textContent = 'Not found'; return; }
const p = await res.json();
const proj = p.project;
const d = parseData(proj.data_text);
const name = d.name || proj.summary || 'Untitled';
document.title = name + ' — Dealspace';
document.getElementById('projectName').textContent = name;
document.getElementById('projectTitle').textContent = name;
document.getElementById('projectDesc').textContent = d.description || '';
const status = d.status || 'active';
const sc = statusColors[status] || 'bg-gray-500/20 text-gray-300';
document.getElementById('projectStatus').className = 'px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ' + sc;
document.getElementById('projectStatus').textContent = status;
} catch(e) { console.error(e); }
}
async function loadRequests() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/requests');
const items = await res.json();
const list = document.getElementById('requestList');
if (!items || items.length === 0) { list.classList.add('hidden'); document.getElementById('requestEmpty').classList.remove('hidden'); return; }
list.classList.remove('hidden'); document.getElementById('requestEmpty').classList.add('hidden');
const sections = {};
for (const r of items) { const sec = r.section || 'Unsorted'; if (!sections[sec]) sections[sec] = []; sections[sec].push(r); }
const sortedSections = Object.keys(sections).sort();
list.innerHTML = sortedSections.map(sec => {
const reqs = sections[sec]; const isExpanded = expandedSections.has(sec);
const statusCounts = { open: 0, in_progress: 0, answered: 0, not_applicable: 0 };
reqs.forEach(r => { statusCounts[r.status || 'open']++; });
return '<div class="border border-white/[0.08] rounded-xl overflow-hidden"><div class="section-header flex items-center justify-between px-5 py-3 bg-[#0d1f3c]" onclick="toggleSection(\'' + escHtml(sec) + '\')"><div class="flex items-center gap-3"><svg class="w-4 h-4 text-[#b0bec5] transition ' + (isExpanded ? 'rotate-90' : '') + '" id="chevron-' + escHtml(sec) + '" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg><span class="text-white font-medium">' + escHtml(sec) + '</span><span class="text-[#b0bec5] text-xs">(' + reqs.length + ' items)</span></div><div class="flex gap-2">' + (statusCounts.open > 0 ? '<span class="px-2 py-0.5 rounded text-xs status-open">' + statusCounts.open + ' open</span>' : '') + (statusCounts.answered > 0 ? '<span class="px-2 py-0.5 rounded text-xs status-answered">' + statusCounts.answered + ' answered</span>' : '') + '</div></div><div id="section-' + escHtml(sec) + '" class="' + (isExpanded ? '' : 'hidden') + '">' + reqs.map(r => '<a href="/app/requests/' + r.entry_id + '" class="req-row flex items-center gap-4 px-5 py-3 border-t border-white/[0.05] transition cursor-pointer"><div class="w-16 shrink-0"><span class="text-xs font-mono text-[#b0bec5]">' + escHtml(r.item_number || '—') + '</span></div><div class="flex-1 min-w-0"><span class="text-white text-sm truncate block">' + escHtml(r.title || 'Untitled') + '</span></div><span class="shrink-0 w-2 h-2 rounded-full ' + (r.priority === 'high' ? 'bg-red-400' : r.priority === 'low' ? 'bg-green-400' : 'bg-yellow-400') + '" title="' + r.priority + ' priority"></span><span class="shrink-0 px-2.5 py-0.5 rounded text-xs font-medium status-' + (r.status || 'open') + '">' + (r.status || 'open').replace('_', ' ') + '</span></a>').join('') + '</div></div>';
}).join('');
} catch(e) { console.error(e); document.getElementById('requestList').innerHTML = '<div class="text-red-400 text-sm">Failed to load requests.</div>'; }
}
function toggleSection(sec) {
const el = document.getElementById('section-' + sec); const chevron = document.getElementById('chevron-' + sec);
if (el.classList.contains('hidden')) { el.classList.remove('hidden'); chevron.classList.add('rotate-90'); expandedSections.add(sec); }
else { el.classList.add('hidden'); chevron.classList.remove('rotate-90'); expandedSections.delete(sec); }
}
async function loadOrgs() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs');
const orgs = await res.json();
const list = document.getElementById('orgList');
if (!orgs || orgs.length === 0) { list.innerHTML = '<div class="text-[#b0bec5] text-sm">No organizations added yet.</div>'; return; }
list.innerHTML = orgs.map(o => { const rc = roleColors[o.role] || 'bg-gray-500/20 text-gray-300'; const domains = o.org_domains || [];
return '<div class="flex items-center gap-4 px-5 py-4 rounded-xl bg-[#0d1f3c] border border-white/[0.08]"><div class="flex-1"><div class="flex items-center gap-2 mb-1"><span class="text-white font-medium">' + escHtml(o.org_name || 'Unknown') + '</span><span class="px-2 py-0.5 rounded-full text-xs font-medium capitalize ' + rc + '">' + (o.role || '?') + '</span>' + (o.domain_lock ? '<span class="px-2 py-0.5 rounded-full text-xs bg-white/[0.05] text-[#b0bec5]">🔒 domain locked</span>' : '') + '</div>' + (domains.length > 0 ? '<div class="flex gap-1.5 flex-wrap">' + domains.map(dm => '<span class="text-xs text-[#b0bec5] font-mono bg-white/[0.05] px-2 py-0.5 rounded">@' + dm + '</span>').join('') + '</div>' : '') + '</div></div>';
}).join('');
} catch(e) { console.error(e); }
}
async function loadTeam() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/members');
const members = await res.json();
const list = document.getElementById('teamList');
if (!members || members.length === 0) { list.innerHTML = '<div class="text-[#b0bec5] text-sm">No team members yet.</div>'; return; }
list.innerHTML = members.map(m => '<div class="flex items-center gap-4 px-5 py-3 rounded-xl bg-[#0d1f3c] border border-white/[0.08]"><div class="w-8 h-8 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] font-semibold text-sm">' + (m.name||m.email||'?')[0].toUpperCase() + '</div><div class="flex-1"><div class="text-white text-sm font-medium">' + escHtml(m.name || m.email) + '</div>' + (m.name ? '<div class="text-[#b0bec5] text-xs">' + escHtml(m.email) + '</div>' : '') + '</div><span class="text-xs text-[#b0bec5] capitalize">' + (m.role || 'member') + '</span></div>').join('');
} catch(e) { console.error(e); }
}
function switchTab(name, el) {
document.querySelectorAll('.tab').forEach(t => { t.classList.remove('active','text-white'); t.classList.add('text-[#b0bec5]'); });
el.classList.add('active','text-white'); el.classList.remove('text-[#b0bec5]');
document.getElementById('tab-requests').classList.toggle('hidden', name !== 'requests');
document.getElementById('tab-orgs').classList.toggle('hidden', name !== 'orgs');
document.getElementById('tab-team').classList.toggle('hidden', name !== 'team');
if (name === 'orgs') loadOrgs(); if (name === 'team') loadTeam();
}
function openImportModal() { document.getElementById('importModal').classList.remove('hidden'); }
function closeImportModal() { document.getElementById('importModal').classList.add('hidden'); document.getElementById('importForm').reset(); }
document.getElementById('importForm').onsubmit = async (e) => {
e.preventDefault();
const btn = document.getElementById('importSubmitBtn'); btn.disabled = true; btn.textContent = 'Importing...';
const formData = new FormData();
formData.append('file', document.getElementById('importFile').files[0]);
formData.append('mode', document.getElementById('importMode').value);
formData.append('section_filter', document.getElementById('sectionFilter').value);
formData.append('create_workstreams', document.getElementById('createWorkstreams').checked ? 'true' : 'false');
try {
const res = await fetch('/api/projects/' + projectID + '/requests/import', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData });
const data = await res.json();
if (res.ok) { closeImportModal(); loadRequests(); alert('Imported ' + data.imported + ' requests from ' + (data.sections?.length || 0) + ' sections. ' + (data.skipped || 0) + ' skipped.'); }
else { alert('Import failed: ' + (data.error || 'Unknown error')); }
} catch (err) { alert('Import failed: ' + err.message); }
finally { btn.disabled = false; btn.textContent = 'Import'; }
};
document.getElementById('newRequestBtn').onclick = () => {
const title = prompt('Request title:'); if (!title) return;
fetchAPI('/api/projects/' + projectID + '/entries', { method: 'POST', body: JSON.stringify({ project_id: projectID, parent_id: projectID, type: 'request', depth: 1, summary: title, data: JSON.stringify({ title: title, status: 'open', priority: 'medium' }), stage: 'pre_dataroom' })
}).then(r => r.json()).then(d => { if (d.entry_id) window.location.href = '/app/requests/' + d.entry_id; else loadRequests(); });
};
document.getElementById('importModal').onclick = (e) => { if (e.target.id === 'importModal') closeImportModal(); };
// ---- Add Org flow ----
let scrapedData = null;
document.getElementById('addOrgBtn').onclick = () => {
scrapedData = null;
document.getElementById('addOrgEmail').value = '';
document.getElementById('scrapeError').classList.add('hidden');
document.getElementById('scrapeLoading').classList.add('hidden');
showAddOrgStep(1);
document.getElementById('addOrgModal').classList.remove('hidden');
setTimeout(() => document.getElementById('addOrgEmail').focus(), 100);
};
function closeAddOrg() { document.getElementById('addOrgModal').classList.add('hidden'); }
function showAddOrgStep(n) {
document.getElementById('addOrgStep1').classList.toggle('hidden', n !== 1);
document.getElementById('addOrgStep2').classList.toggle('hidden', n !== 2);
document.getElementById('addOrgStep3').classList.toggle('hidden', n !== 3);
if (n === 3) renderPeople();
}
async function scrapeOrg() {
const email = document.getElementById('addOrgEmail').value.trim();
if (!email || !email.includes('@')) { document.getElementById('scrapeError').textContent = 'Enter a valid email address.'; document.getElementById('scrapeError').classList.remove('hidden'); return; }
document.getElementById('scrapeError').classList.add('hidden');
document.getElementById('scrapeLoading').classList.remove('hidden');
document.getElementById('scrapeBtn').disabled = true;
try {
const res = await fetchAPI('/api/scrape/org', { method: 'POST', body: JSON.stringify({ email }) });
const data = await res.json();
if (!res.ok) { throw new Error(data.error || 'Scrape failed'); }
scrapedData = data;
// Prefill step 2
document.getElementById('orgName').value = data.name || '';
document.getElementById('orgDesc').value = data.description || '';
document.getElementById('orgIndustry').value = data.industry || '';
document.getElementById('orgWebsite').value = data.website || '';
document.getElementById('orgPhone').value = data.phone || '';
document.getElementById('orgAddress').value = data.address || '';
document.getElementById('orgCity').value = data.city || '';
document.getElementById('orgState').value = data.state || '';
document.getElementById('orgFounded').value = data.founded || '';
document.getElementById('orgLinkedIn').value = data.linkedin || '';
showAddOrgStep(2);
} catch (err) {
document.getElementById('scrapeError').textContent = err.message;
document.getElementById('scrapeError').classList.remove('hidden');
} finally {
document.getElementById('scrapeLoading').classList.add('hidden');
document.getElementById('scrapeBtn').disabled = false;
}
}
function renderPeople() {
const people = scrapedData?.people || [];
const list = document.getElementById('peopleList');
const none = document.getElementById('noPeople');
if (people.length === 0) { list.innerHTML = ''; none.classList.remove('hidden'); return; }
none.classList.add('hidden');
list.innerHTML = people.map((p, i) => `
<label class="flex items-center gap-3 p-3 rounded-xl bg-white/[0.02] border border-white/[0.06] hover:bg-white/[0.04] cursor-pointer transition">
<input type="checkbox" class="person-cb accent-[#c9a84c] rounded" data-idx="${i}" checked onchange="updateSelectedCount()">
<div class="w-10 h-10 rounded-full bg-[#0d1f3c] shrink-0 overflow-hidden border border-white/[0.08]">
${p.photo ? `<img src="${escHtml(p.photo)}" class="w-full h-full object-cover" onerror="this.style.display='none';this.parentElement.innerHTML='<div class=\\'w-full h-full flex items-center justify-center text-[#c9a84c] font-semibold\\'>${escHtml(p.name[0])}</div>'">` : `<div class="w-full h-full flex items-center justify-center text-[#c9a84c] font-semibold">${escHtml(p.name[0])}</div>`}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-white text-sm font-medium">${escHtml(p.name)}</span>
${p.title ? `<span class="text-xs text-[#b0bec5]">${escHtml(p.title)}</span>` : ''}
</div>
<div class="flex items-center gap-3 text-xs text-[#8899a6]">
${p.email ? `<span>${escHtml(p.email)}</span>` : ''}
${p.phone ? `<span>${escHtml(p.phone)}</span>` : ''}
</div>
${p.bio ? `<div class="text-xs text-[#8899a6] mt-0.5 truncate">${escHtml(p.bio)}</div>` : ''}
</div>
</label>
`).join('');
document.getElementById('selectAllPeople').checked = true;
updateSelectedCount();
}
function toggleAllPeople(checked) {
document.querySelectorAll('.person-cb').forEach(cb => cb.checked = checked);
updateSelectedCount();
}
function updateSelectedCount() {
const checked = document.querySelectorAll('.person-cb:checked').length;
const total = document.querySelectorAll('.person-cb').length;
document.getElementById('selectedCount').textContent = checked + ' of ' + total + ' selected';
document.getElementById('selectAllPeople').checked = (checked === total);
}
async function submitAddOrg() {
const name = document.getElementById('orgName').value.trim();
const role = document.getElementById('orgRole').value;
if (!name) { alert('Organization name is required.'); return; }
if (!role) { alert('Please select a role for this organization.'); return; }
const domain = scrapedData?.domain || '';
const people = scrapedData?.people || [];
const selectedMembers = [];
document.querySelectorAll('.person-cb:checked').forEach(cb => {
const p = people[parseInt(cb.dataset.idx)];
if (p) selectedMembers.push({ name: p.name, email: p.email || '', title: p.title || '', phone: p.phone || '', photo: p.photo || '', bio: p.bio || '', linkedin: p.linkedin || '' });
});
const btn = document.getElementById('submitOrgBtn');
btn.disabled = true; btn.textContent = 'Adding...';
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs/add', {
method: 'POST',
body: JSON.stringify({
name,
domains: domain ? [domain] : [],
role,
website: document.getElementById('orgWebsite').value,
description: document.getElementById('orgDesc').value,
industry: document.getElementById('orgIndustry').value,
phone: document.getElementById('orgPhone').value,
address: document.getElementById('orgAddress').value,
city: document.getElementById('orgCity').value,
state: document.getElementById('orgState').value,
founded: document.getElementById('orgFounded').value,
linkedin: document.getElementById('orgLinkedIn').value,
members: selectedMembers,
domain_lock: true,
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to add organization');
closeAddOrg();
loadOrgs();
} catch (err) { alert(err.message); }
finally { btn.disabled = false; btn.textContent = 'Add Organization'; }
}
loadProject(); loadRequests();
</script>
</body>
</html>

View File

@ -1,155 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projects — Dealspace</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>* { font-family: 'Inter', sans-serif; } body { background: #0a1628; }
.sidebar-link.active { background: rgba(201,168,76,0.1); color: #c9a84c; border-left: 3px solid #c9a84c; }
.sidebar-link:hover:not(.active) { background: rgba(255,255,255,0.04); }
.card:hover { border-color: rgba(201,168,76,0.3); transform: translateY(-1px); }
</style>
</head>
<body>
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<div class="flex items-center gap-4">
<span id="userName" class="text-sm text-[#b0bec5]"></span>
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
</div>
</header>
<div class="flex">
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
My Tasks</a>
<a href="/app/projects" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
Projects</a>
<a href="/app/orgs" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Organizations</a>
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Admin</a></div>
</div>
</nav>
<main class="flex-1 p-8 max-w-6xl">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold text-white mb-1">Projects</h1>
<p class="text-[#b0bec5] text-sm">All deals you have access to.</p>
</div>
<button id="newProjectBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Project</button>
</div>
<div id="projectGrid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div class="text-[#b0bec5] text-sm col-span-3">Loading projects...</div>
</div>
<div id="emptyState" class="hidden text-center py-20">
<div class="text-5xl mb-4">📁</div>
<h2 class="text-xl font-semibold text-white mb-2">No projects yet</h2>
<p class="text-[#b0bec5]">You haven't been added to any deals yet.</p>
</div>
</main>
</div>
<!-- New Project Modal -->
<div id="newProjectModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
<h2 class="text-xl font-semibold text-white mb-6">New Project</h2>
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<div class="space-y-4">
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Deal Name</label>
<input id="pName" type="text" placeholder="Project James" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Description</label>
<textarea id="pDesc" rows="2" placeholder="Brief description of this deal..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] resize-none"></textarea></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Status</label>
<select id="pStatus" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
<option value="active">Active</option><option value="draft">Draft</option><option value="closed">Closed</option>
</select></div>
</div>
<div class="flex gap-3 mt-6">
<button onclick="closeModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
<button id="createBtn" onclick="createProject()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Create</button>
</div>
</div>
</div>
<script>
const token = localStorage.getItem('ds_token');
if (!token) window.location.href = '/app/login';
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
document.getElementById('userName').textContent = user.name || user.email || '';
if (user.is_super_admin) {
document.getElementById('adminLinks').classList.remove('hidden');
document.getElementById('newProjectBtn').classList.remove('hidden');
}
function fetchAPI(path, opts = {}) {
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
return fetch(path, opts).then(r => { if (r.status === 401) { localStorage.removeItem('ds_token'); window.location.href = '/app/login'; } return r; });
}
function logout() { fetchAPI('/api/auth/logout', { method: 'POST' }).finally(() => { localStorage.clear(); window.location.href = '/app/login'; }); }
const roleColors = { seller: 'bg-blue-500/20 text-blue-300', buyer: 'bg-green-500/20 text-green-300', ib: 'bg-[#c9a84c]/20 text-[#c9a84c]', advisor: 'bg-purple-500/20 text-purple-300' };
const statusColors = { active: 'bg-green-500/20 text-green-300', draft: 'bg-gray-500/20 text-gray-300', closed: 'bg-red-500/20 text-red-300', completed: 'bg-blue-500/20 text-blue-300' };
async function loadProjects() {
try {
const res = await fetchAPI('/api/projects');
const projects = await res.json();
const grid = document.getElementById('projectGrid');
if (!projects || projects.length === 0) {
grid.classList.add('hidden');
document.getElementById('emptyState').classList.remove('hidden');
return;
}
grid.innerHTML = projects.map(p => {
const d = parseData(p.data_text);
const status = d.status || 'active';
const sc = statusColors[status] || statusColors.active;
return `<a href="/app/projects/${p.entry_id}" class="card block bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 transition cursor-pointer">
<div class="flex items-start justify-between mb-3">
<h3 class="text-white font-semibold text-lg leading-tight">${escHtml(d.name || p.summary || 'Untitled')}</h3>
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium ${sc} capitalize">${status}</span>
</div>
${d.description ? `<p class="text-[#b0bec5] text-sm mb-4 line-clamp-2">${escHtml(d.description)}</p>` : '<div class="mb-4"></div>'}
<div class="flex items-center gap-2 text-xs text-[#8899a6]">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
${new Date(p.created_at).toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'})}
</div>
</a>`;
}).join('');
} catch(e) { document.getElementById('projectGrid').innerHTML = '<div class="text-red-400 text-sm col-span-3">Failed to load projects.</div>'; }
}
function openModal() { document.getElementById('newProjectModal').classList.remove('hidden'); document.getElementById('pName').focus(); }
function closeModal() { document.getElementById('newProjectModal').classList.add('hidden'); document.getElementById('modalError').classList.add('hidden'); }
document.getElementById('newProjectBtn').onclick = openModal;
async function createProject() {
const name = document.getElementById('pName').value.trim();
const desc = document.getElementById('pDesc').value.trim();
const status = document.getElementById('pStatus').value;
const errEl = document.getElementById('modalError');
const btn = document.getElementById('createBtn');
if (!name) { errEl.textContent = 'Deal name is required'; errEl.classList.remove('hidden'); return; }
btn.disabled = true; btn.textContent = 'Creating...';
errEl.classList.add('hidden');
try {
const res = await fetchAPI('/api/projects', { method: 'POST', body: JSON.stringify({ name, description: desc, status }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create project');
window.location.href = '/app/projects/' + data.entry_id;
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Create'; }
}
function parseData(t) { try { return JSON.parse(t); } catch { return {}; } }
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
loadProjects();
</script>
</body>
</html>

View File

@ -1,204 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Request — Dealspace</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>* { font-family: 'Inter', sans-serif; } body { background: #0a1628; }
.sidebar-link.active { background: rgba(201,168,76,0.1); color: #c9a84c; border-left: 3px solid #c9a84c; }
.sidebar-link:hover:not(.active) { background: rgba(255,255,255,0.04); }
</style>
</head>
<body>
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
<div class="flex items-center gap-3">
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<span class="text-white/20">/</span>
<a href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Projects</a>
<span class="text-white/20">/</span>
<a id="backToProject" href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Project</a>
<span class="text-white/20">/</span>
<span id="reqRef" class="text-sm text-white font-medium">Request</span>
</div>
<div class="flex items-center gap-4">
<span id="userName" class="text-sm text-[#b0bec5]"></span>
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
</div>
</header>
<div class="flex">
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
My Tasks</a>
<a href="/app/projects" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
Projects</a>
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Admin</a></div>
</div>
</nav>
<main class="flex-1 p-8 max-w-4xl">
<!-- Request Header -->
<div id="reqHeader" class="mb-8">
<div class="flex items-start gap-4 mb-3">
<div class="flex-1">
<h1 id="reqTitle" class="text-2xl font-bold text-white mb-2">Loading...</h1>
<p id="reqDesc" class="text-[#b0bec5] text-sm"></p>
</div>
<span id="reqStatus" class="shrink-0 px-3 py-1 rounded-full text-sm font-medium"></span>
</div>
<div class="flex gap-3 flex-wrap text-xs text-[#8899a6]">
<span id="reqDue"></span>
<span id="reqAssignee"></span>
</div>
</div>
<!-- Answer / Upload -->
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 mb-6">
<h2 class="text-sm font-semibold text-[#b0bec5] uppercase tracking-wider mb-4">Response</h2>
<div id="answers" class="space-y-4 mb-6"></div>
<div id="uploadArea" class="border-2 border-dashed border-white/[0.08] rounded-xl p-8 text-center hover:border-[#c9a84c]/40 transition cursor-pointer" onclick="document.getElementById('fileInput').click()">
<div class="text-3xl mb-2">📎</div>
<p class="text-[#b0bec5] text-sm">Drop files here or click to upload</p>
<p class="text-[#8899a6] text-xs mt-1">PDF, DOCX, XLSX, images</p>
<input id="fileInput" type="file" multiple class="hidden" onchange="uploadFiles(this.files)">
</div>
<div id="uploadStatus" class="mt-3 text-sm text-[#b0bec5]"></div>
</div>
<!-- Activity / Comments -->
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6">
<h2 class="text-sm font-semibold text-[#b0bec5] uppercase tracking-wider mb-4">Comments</h2>
<div id="comments" class="space-y-3 mb-4"></div>
<div class="flex gap-3">
<textarea id="commentText" rows="2" placeholder="Add a comment..." class="flex-1 px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] text-sm resize-none"></textarea>
<button onclick="postComment()" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition self-end">Post</button>
</div>
</div>
</main>
</div>
<script>
const token = localStorage.getItem('ds_token');
if (!token) window.location.href = '/app/login';
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
const reqID = location.pathname.split('/').pop();
document.getElementById('userName').textContent = user.name || user.email || '';
if (user.is_super_admin) document.getElementById('adminLinks').classList.remove('hidden');
function fetchAPI(path, opts = {}) {
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + token };
if (opts.body && typeof opts.body === 'string') opts.headers['Content-Type'] = 'application/json';
return fetch(path, opts).then(r => { if (r.status === 401) { localStorage.removeItem('ds_token'); window.location.href = '/app/login'; } return r; });
}
function logout() { fetchAPI('/api/auth/logout', { method: 'POST' }).finally(() => { localStorage.clear(); window.location.href = '/app/login'; }); }
function parseData(t) { try { return JSON.parse(t); } catch { return {}; } }
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
const statusColors = { open: 'bg-yellow-500/20 text-yellow-300', answered: 'bg-green-500/20 text-green-300', closed: 'bg-gray-500/20 text-gray-300', 'under-review': 'bg-blue-500/20 text-blue-300' };
async function loadRequest() {
try {
const res = await fetchAPI('/api/entries/' + reqID);
if (!res.ok) { document.getElementById('reqTitle').textContent = 'Not found'; return; }
const req = await res.json();
const d = parseData(req.data_text);
const title = d.title || req.summary || 'Untitled';
document.title = title + ' — Dealspace';
document.getElementById('reqRef').textContent = d.ref || title;
document.getElementById('reqTitle').textContent = title;
document.getElementById('reqDesc').textContent = d.description || '';
if (req.project_id) document.getElementById('backToProject').href = '/app/projects/' + req.project_id;
const status = d.status || 'open';
const sc = statusColors[status] || 'bg-gray-500/20 text-gray-300';
document.getElementById('reqStatus').className = 'shrink-0 px-3 py-1 rounded-full text-sm font-medium capitalize ' + sc;
document.getElementById('reqStatus').textContent = status;
if (d.due_date) document.getElementById('reqDue').textContent = '📅 Due: ' + d.due_date;
if (d.assignee) document.getElementById('reqAssignee').textContent = '👤 ' + d.assignee;
} catch(e) {}
}
async function loadAnswers() {
try {
const res = await fetchAPI('/api/entries/' + reqID + '/children?type=answer');
const items = await res.json();
const el = document.getElementById('answers');
if (!items || items.length === 0) { el.innerHTML = '<p class="text-[#8899a6] text-sm">No documents uploaded yet.</p>'; return; }
el.innerHTML = items.map(a => {
const d = parseData(a.data_text);
const name = d.filename || d.name || a.summary || 'Document';
return `<div class="flex items-center gap-3 px-4 py-3 rounded-lg bg-[#0a1628] border border-white/[0.05]">
<span class="text-2xl">${name.endsWith('.pdf') ? '📄' : name.match(/\.(jpg|jpeg|png|gif)$/i) ? '🖼️' : '📎'}</span>
<div class="flex-1 min-w-0">
<div class="text-white text-sm font-medium truncate">${escHtml(name)}</div>
<div class="text-[#8899a6] text-xs">${new Date(a.created_at).toLocaleString()}</div>
</div>
<a href="/api/entries/${a.entry_id}/download" class="text-[#c9a84c] text-sm hover:underline">Download</a>
</div>`;
}).join('');
} catch(e) {}
}
async function uploadFiles(files) {
const status = document.getElementById('uploadStatus');
for (const file of files) {
status.textContent = 'Uploading ' + file.name + '...';
const fd = new FormData();
fd.append('file', file);
fd.append('parent_id', reqID);
try {
const res = await fetchAPI('/api/upload', { method: 'POST', body: fd });
if (res.ok) { status.textContent = file.name + ' uploaded.'; loadAnswers(); }
else { status.textContent = 'Upload failed for ' + file.name; }
} catch(e) { status.textContent = 'Error: ' + e.message; }
}
setTimeout(() => status.textContent = '', 3000);
}
async function loadComments() {
try {
const res = await fetchAPI('/api/entries/' + reqID + '/children?type=comment');
const items = await res.json();
const el = document.getElementById('comments');
if (!items || items.length === 0) { el.innerHTML = '<p class="text-[#8899a6] text-sm">No comments yet.</p>'; return; }
el.innerHTML = items.map(c => {
const d = parseData(c.data_text);
return `<div class="flex gap-3">
<div class="w-7 h-7 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] text-xs font-semibold shrink-0 mt-0.5">${(d.author||'?')[0].toUpperCase()}</div>
<div>
<div class="flex items-baseline gap-2 mb-1">
<span class="text-white text-sm font-medium">${escHtml(d.author||'Unknown')}</span>
<span class="text-[#8899a6] text-xs">${new Date(c.created_at).toLocaleString()}</span>
</div>
<p class="text-[#b0bec5] text-sm">${escHtml(d.text||'')}</p>
</div>
</div>`;
}).join('');
} catch(e) {}
}
async function postComment() {
const text = document.getElementById('commentText').value.trim();
if (!text) return;
try {
const res = await fetchAPI('/api/entries/' + reqID + '/children', { method: 'POST', body: JSON.stringify({ type: 'comment', data: { text, author: user.name || user.email } }) });
if (res.ok) { document.getElementById('commentText').value = ''; loadComments(); }
} catch(e) {}
}
// Drag and drop
const ua = document.getElementById('uploadArea');
ua.addEventListener('dragover', e => { e.preventDefault(); ua.classList.add('border-[#c9a84c]/60'); });
ua.addEventListener('dragleave', () => ua.classList.remove('border-[#c9a84c]/60'));
ua.addEventListener('drop', e => { e.preventDefault(); ua.classList.remove('border-[#c9a84c]/60'); uploadFiles(e.dataTransfer.files); });
loadRequest();
loadAnswers();
loadComments();
</script>
</body>
</html>

View File

@ -1,169 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Tasks — Dealspace</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
* { font-family: 'Inter', sans-serif; }
body { background: #0a1628; margin: 0; }
.sidebar-link.active { background: rgba(201,168,76,0.1); color: #c9a84c; border-left: 3px solid #c9a84c; }
.sidebar-link:hover:not(.active) { background: rgba(255,255,255,0.04); }
.task-card:hover { border-color: rgba(201,168,76,0.3); transform: translateY(-1px); }
.priority-high { background: #ef4444; }
.priority-normal { background: #c9a84c; }
.priority-low { background: #22c55e; }
</style>
</head>
<body>
<!-- Top Nav -->
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
<div class="flex items-center gap-6">
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</a>
<select id="projectSwitcher" class="bg-[#0a1628] border border-white/[0.08] rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:border-[#c9a84c]">
<option value="">All Projects</option>
</select>
</div>
<div class="flex items-center gap-4">
<span id="userName" class="text-sm text-[#b0bec5]"></span>
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
</div>
</header>
<div class="flex">
<!-- Sidebar -->
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
<svg class="w-4 h-4 text-[#b0bec5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
My Tasks
</a>
<a href="/app/projects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
Projects
</a>
<div id="adminLinks" class="hidden">
<div class="border-t border-white/[0.08] my-3"></div>
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Admin
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="flex-1 p-8 max-w-5xl">
<!-- Greeting -->
<div class="mb-8">
<h1 id="greeting" class="text-2xl font-bold text-white mb-1"></h1>
<p class="text-[#b0bec5] text-sm">Here are your pending tasks.</p>
</div>
<!-- Task List -->
<div id="taskList" class="space-y-3">
<div class="text-[#b0bec5] text-sm">Loading tasks...</div>
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden text-center py-20">
<div class="text-5xl mb-4">&#127881;</div>
<h2 class="text-xl font-semibold text-white mb-2">You're all caught up</h2>
<p class="text-[#b0bec5]">No tasks need your attention right now.</p>
</div>
</main>
</div>
<script>
// Auth check
const token = localStorage.getItem('ds_token');
if (!token) { window.location.href = '/app/login'; }
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
function fetchAPI(path, opts = {}) {
opts.headers = { ...opts.headers, 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
return fetch(path, opts).then(r => {
if (r.status === 401) { localStorage.removeItem('ds_token'); window.location.href = '/app/login'; }
return r;
});
}
function logout() {
fetchAPI('/api/auth/logout', { method: 'POST' }).finally(() => {
localStorage.removeItem('ds_token');
localStorage.removeItem('ds_user');
window.location.href = '/app/login';
});
}
// Greeting
const hour = new Date().getHours();
const greetWord = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
document.getElementById('greeting').textContent = greetWord + ', ' + (user.name || 'there');
document.getElementById('userName').textContent = user.name || user.email || '';
if (user.is_super_admin) document.getElementById('adminLinks').classList.remove('hidden');
// Load tasks
async function loadTasks() {
try {
const res = await fetchAPI('/api/tasks');
const tasks = await res.json();
const list = document.getElementById('taskList');
const empty = document.getElementById('emptyState');
if (!tasks || tasks.length === 0) {
list.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
list.innerHTML = tasks.map(t => {
const data = parseData(t.data_text);
const priority = data.priority || 'normal';
const title = data.title || t.summary || 'Untitled';
const ref = data.ref || '';
const due = data.due_date || '';
const status = data.status || 'open';
const projectName = t.project_id ? t.project_id.substring(0, 8) : '';
return `
<a href="/app/requests/${t.entry_id}" class="task-card block bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5 transition cursor-pointer">
<div class="flex items-center gap-3 mb-2">
<span class="w-2.5 h-2.5 rounded-full priority-${priority} shrink-0"></span>
${ref ? `<span class="text-xs font-mono text-[#b0bec5]">${ref}</span>` : ''}
<span class="text-white font-medium flex-1">${escapeHtml(title)}</span>
${due ? `<span class="text-xs text-[#b0bec5]">Due: ${due}</span>` : ''}
</div>
<div class="flex items-center gap-3 text-xs text-[#8899a6]">
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#b0bec5]">${status}</span>
<span>${t.type || 'request'}</span>
</div>
</a>
`;
}).join('');
} catch (err) {
document.getElementById('taskList').innerHTML = '<div class="text-red-400 text-sm">Failed to load tasks.</div>';
}
}
function parseData(text) {
if (!text) return {};
try { return JSON.parse(text); } catch { return {}; }
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
loadTasks();
</script>
</body>
</html>

View File

@ -1,206 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login — Dealspace</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
* { font-family: 'Inter', sans-serif; }
body { background: #0a1628; }
.code-input { letter-spacing: 0.5em; text-align: center; font-size: 1.5rem; font-weight: 600; }
</style>
</head>
<body class="min-h-screen flex items-center justify-center">
<div class="w-full max-w-md px-6">
<!-- Logo -->
<div class="text-center mb-10">
<h1 class="text-3xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</h1>
<p class="text-[#b0bec5] mt-2 text-sm">Secure M&A deal management</p>
</div>
<!-- Step 1: Email -->
<div id="step-email" class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
<h2 class="text-xl font-semibold text-white mb-2">Sign in</h2>
<p class="text-[#b0bec5] text-sm mb-6">Enter your email to receive a login code.</p>
<div id="error-email" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<form id="emailForm" class="space-y-5">
<div>
<label for="email" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email</label>
<input type="email" id="email" name="email" required autocomplete="email" autofocus
placeholder="you@company.com"
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
</div>
<button type="submit" id="emailBtn"
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
Send login code
</button>
</form>
</div>
<!-- Step 2: OTP Code -->
<div id="step-code" class="hidden bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
<h2 class="text-xl font-semibold text-white mb-2">Enter your code</h2>
<p class="text-[#b0bec5] text-sm mb-6">
We sent a 6-digit code to <span id="sent-email" class="text-white font-medium"></span>
</p>
<div id="error-code" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<form id="codeForm" class="space-y-5">
<div>
<label for="code" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Login code</label>
<input type="text" id="code" name="code" required autocomplete="one-time-code"
maxlength="6" inputmode="numeric" pattern="[0-9]*"
placeholder="000000"
class="code-input w-full px-4 py-3 bg-[#0a1628] border border-white/[0.08] rounded-lg text-[#c9a84c] placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
</div>
<button type="submit" id="codeBtn"
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
Verify & sign in
</button>
</form>
<div class="mt-4 flex items-center justify-between">
<button id="backBtn" class="text-[#b0bec5] text-sm hover:text-white transition">
&larr; Use a different email
</button>
<button id="resendBtn" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
Resend code
</button>
</div>
</div>
<p class="text-center text-[#8899a6] text-xs mt-6">
Don&rsquo;t have an account? Dealspace is invite-only.<br>
<a href="/#demo" class="text-[#c9a84c] hover:underline">Request access on muskepo.com</a>
</p>
<p class="text-center text-[#8899a6] text-xs mt-3">&copy; 2026 Muskepo B.V. — Amsterdam</p>
</div>
<script>
// If already logged in, redirect
if (localStorage.getItem('ds_token')) {
window.location.href = '/app/tasks';
}
let currentEmail = '';
// Step 1: Send challenge
document.getElementById('emailForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('emailBtn');
const errorEl = document.getElementById('error-email');
btn.disabled = true;
btn.textContent = 'Sending code...';
errorEl.classList.add('hidden');
currentEmail = document.getElementById('email').value.trim().toLowerCase();
try {
const res = await fetch('/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: currentEmail }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to send code');
// Show code step
document.getElementById('sent-email').textContent = currentEmail;
document.getElementById('step-email').classList.add('hidden');
document.getElementById('step-code').classList.remove('hidden');
document.getElementById('code').focus();
} catch (err) {
errorEl.textContent = err.message;
errorEl.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = 'Send login code';
}
});
// Step 2: Verify code
document.getElementById('codeForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('codeBtn');
const errorEl = document.getElementById('error-code');
btn.disabled = true;
btn.textContent = 'Verifying...';
errorEl.classList.add('hidden');
try {
const res = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: currentEmail,
code: document.getElementById('code').value.trim(),
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Invalid or expired code');
localStorage.setItem('ds_token', data.token);
localStorage.setItem('ds_user', JSON.stringify(data.user));
// Everyone lands on /app/tasks — admin panel accessible from nav
window.location.href = '/app/tasks';
} catch (err) {
errorEl.textContent = err.message;
errorEl.classList.remove('hidden');
btn.disabled = false;
btn.textContent = 'Verify & sign in';
}
});
// Back button
document.getElementById('backBtn').addEventListener('click', () => {
document.getElementById('step-code').classList.add('hidden');
document.getElementById('step-email').classList.remove('hidden');
document.getElementById('code').value = '';
document.getElementById('error-code').classList.add('hidden');
document.getElementById('email').focus();
});
// Resend button
document.getElementById('resendBtn').addEventListener('click', async () => {
const btn = document.getElementById('resendBtn');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
await fetch('/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: currentEmail }),
});
btn.textContent = 'Code sent!';
setTimeout(() => {
btn.textContent = 'Resend code';
btn.disabled = false;
}, 3000);
} catch {
btn.textContent = 'Resend code';
btn.disabled = false;
}
});
// Auto-submit when 6 digits entered
document.getElementById('code').addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6);
if (e.target.value.length === 6) {
document.getElementById('codeForm').dispatchEvent(new Event('submit'));
}
});
</script>
</body>
</html>

View File

@ -1,100 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setup — Dealspace</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
* { font-family: 'Inter', sans-serif; }
body { background: #0a1628; }
</style>
</head>
<body class="min-h-screen flex items-center justify-center">
<div class="w-full max-w-md px-6">
<!-- Logo -->
<div class="text-center mb-10">
<h1 class="text-3xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</h1>
<p class="text-[#b0bec5] mt-2 text-sm">First-time setup</p>
</div>
<!-- Setup Card -->
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
<h2 class="text-xl font-semibold text-white mb-2">Create admin account</h2>
<p class="text-[#b0bec5] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
<div id="error" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<div id="success" class="hidden mb-4 p-3 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400 text-sm"></div>
<form id="setupForm" class="space-y-5">
<div>
<label for="name" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Full name</label>
<input type="text" id="name" name="name" required autofocus
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
</div>
<div>
<label for="email" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email</label>
<input type="email" id="email" name="email" required autocomplete="email"
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
</div>
<div>
<label for="password" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Password</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password"
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
<p class="text-xs text-[#8899a6] mt-1">Minimum 8 characters</p>
</div>
<button type="submit" id="submitBtn"
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
Create account
</button>
</form>
</div>
<p class="text-center text-[#8899a6] text-xs mt-8">&copy; 2026 Muskepo B.V. — Amsterdam</p>
</div>
<script>
document.getElementById('setupForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('submitBtn');
const errorEl = document.getElementById('error');
const successEl = document.getElementById('success');
btn.disabled = true;
btn.textContent = 'Creating...';
errorEl.classList.add('hidden');
successEl.classList.add('hidden');
try {
const res = await fetch('/api/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: document.getElementById('name').value,
email: document.getElementById('email').value,
password: document.getElementById('password').value,
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Setup failed');
}
successEl.textContent = 'Admin account created! Redirecting to login...';
successEl.classList.remove('hidden');
setTimeout(() => { window.location.href = '/app/login'; }, 1500);
} catch (err) {
errorEl.textContent = err.message;
errorEl.classList.remove('hidden');
btn.disabled = false;
btn.textContent = 'Create account';
}
});
</script>
</body>
</html>

View File

@ -1,572 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="theme-light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FIN-042: Audited financials FY2024 — Dealspace</title>
<link rel="stylesheet" href="../static/themes.css">
<style>
/* Page-specific layout */
.page-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding: var(--space-lg);
}
/* Back link */
.back-link {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
font-size: var(--text-sm);
color: var(--color-text-link);
text-decoration: none;
margin-bottom: var(--space-lg);
}
.back-link:hover {
color: var(--color-text-link-hover);
}
/* Request header */
.request-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-lg);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--color-border-primary);
margin-bottom: var(--space-lg);
}
.request-header-main {
flex: 1;
}
.request-ref-row {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-sm);
}
.request-ref {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-secondary);
font-family: var(--font-mono);
}
.request-title {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text-primary);
margin: 0 0 var(--space-sm) 0;
}
.request-meta {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.request-priority {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.priority-dot {
width: 0.75rem;
height: 0.75rem;
border-radius: var(--radius-full);
}
.priority-dot.high { background: var(--color-priority-high); }
.priority-dot.normal { background: var(--color-priority-normal); }
.priority-dot.low { background: var(--color-priority-low); }
.priority-label {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-primary);
}
/* Routing section */
.section-title {
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-secondary);
margin-bottom: var(--space-md);
}
.routing-section {
margin-bottom: var(--space-xl);
}
.routing-box {
padding: var(--space-lg);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
}
.routing-chain {
display: flex;
align-items: center;
gap: var(--space-md);
flex-wrap: wrap;
margin-bottom: var(--space-lg);
}
.routing-step {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.routing-step.current {
padding: var(--space-xs) var(--space-sm);
background: var(--color-accent-light);
color: var(--color-accent);
font-weight: 600;
border-radius: var(--radius-md);
}
.routing-arrow {
color: var(--color-text-tertiary);
}
.routing-return {
font-size: var(--text-sm);
color: var(--color-text-secondary);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border-secondary);
}
.routing-return strong {
color: var(--color-text-primary);
}
/* Actions bar */
.actions-bar {
display: flex;
gap: var(--space-md);
margin-top: var(--space-lg);
}
/* Thread section */
.thread-section {
margin-bottom: var(--space-xl);
}
.thread-list {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
/* Response section */
.response-section {
margin-bottom: var(--space-xl);
}
.response-box {
padding: var(--space-lg);
background: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-lg);
}
.response-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border-secondary);
}
.response-tools {
display: flex;
gap: var(--space-sm);
}
.response-submit {
display: flex;
gap: var(--space-sm);
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: pointer;
font-size: var(--text-lg);
}
.icon-btn:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
/* Global nav area */
.global-nav {
display: flex;
align-items: center;
gap: var(--space-lg);
margin-left: auto;
}
.inbox-link {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-inverse);
background: var(--color-accent);
border-radius: var(--radius-md);
text-decoration: none;
}
.user-menu {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.avatar {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-bg-tertiary);
}
.user-name {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-header-text);
}
/* Theme switcher (demo only) */
.theme-switcher {
position: fixed;
bottom: var(--space-lg);
right: var(--space-lg);
display: flex;
gap: var(--space-sm);
padding: var(--space-sm);
background: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.theme-switcher button {
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
color: var(--color-text-primary);
cursor: pointer;
}
.theme-switcher button:hover {
background: var(--color-bg-tertiary);
}
/* Two-column layout on larger screens */
@media (min-width: 1024px) {
.request-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-xl);
}
.request-main {
order: 1;
}
.request-sidebar {
order: 2;
}
}
/* Sidebar info card */
.info-card {
padding: var(--space-lg);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
margin-bottom: var(--space-lg);
}
.info-card-title {
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-secondary);
margin-bottom: var(--space-md);
}
.info-row {
display: flex;
justify-content: space-between;
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--color-border-secondary);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.info-value {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-primary);
}
.info-value.overdue {
color: var(--color-error);
}
</style>
</head>
<body>
<div class="page-layout">
<!-- Global Header -->
<header class="global-header">
<div class="logo">Dealspace</div>
<select class="project-select" aria-label="Select project">
<option selected>Project Alpha — TargetCo Acquisition</option>
<option>Project Beta — MergeCo Deal</option>
</select>
<nav class="global-nav">
<a href="/inbox" class="inbox-link">
Inbox
<span class="badge">3</span>
</a>
<div class="user-menu">
<div class="avatar" role="img" aria-label="User avatar"></div>
<span class="user-name">S. Johnson</span>
</div>
</nav>
</header>
<!-- Main Content -->
<main class="main-content container">
<!-- Back navigation -->
<a href="/inbox" class="back-link">
← Back to Inbox
</a>
<!-- Request header -->
<header class="request-header">
<div class="request-header-main">
<div class="request-ref-row">
<span class="request-ref">FIN-042</span>
<span class="status-badge assigned">Assigned</span>
</div>
<h1 class="request-title">Audited financials FY2024</h1>
<p class="request-meta">Project Alpha • Finance • Due: March 15, 2026</p>
</div>
<div class="request-priority">
<span class="priority-dot high" aria-hidden="true"></span>
<span class="priority-label">High Priority</span>
</div>
</header>
<div class="request-content">
<div class="request-main">
<!-- Routing chain -->
<section class="routing-section">
<h2 class="section-title">Routing</h2>
<div class="routing-box">
<div class="routing-chain" role="navigation" aria-label="Request routing chain">
<span class="routing-step">
Acme Capital (Buyer)
</span>
<span class="routing-arrow" aria-hidden="true"></span>
<span class="routing-step">
J. Smith (IB)
</span>
<span class="routing-arrow" aria-hidden="true"></span>
<span class="routing-step">
M. Chen (CFO)
</span>
<span class="routing-arrow" aria-hidden="true"></span>
<span class="routing-step current">
★ You
</span>
</div>
<div class="routing-return">
When done, returns to: <strong>M. Chen (CFO)</strong>
</div>
<div class="actions-bar">
<button class="btn btn-primary">Mark Done</button>
<button class="btn btn-secondary">Forward</button>
</div>
</div>
</section>
<!-- Thread -->
<section class="thread-section">
<h2 class="section-title">Thread</h2>
<div class="thread-list">
<!-- Message 1: Original request -->
<article class="thread-message">
<header class="thread-message-header">
<span class="thread-message-author">J. Smith (IB)</span>
<time class="thread-message-time">Feb 25, 2026, 10:30 AM</time>
</header>
<div class="thread-message-body">
<p>Please provide audited financial statements for FY2024.</p>
<p>We need the following documents:</p>
<ul>
<li>Income statement (P&L)</li>
<li>Balance sheet</li>
<li>Cash flow statement</li>
<li>Auditor's opinion letter</li>
</ul>
<p>Please ensure these are the final audited versions, not draft.</p>
</div>
</article>
<!-- Message 2: CFO forwarding -->
<article class="thread-message">
<header class="thread-message-header">
<span class="thread-message-author">M. Chen (CFO)</span>
<time class="thread-message-time">Feb 26, 2026, 2:15 PM</time>
</header>
<div class="thread-message-body">
<p>@accountant Can you pull these from the ERP? The audit was finalized last month.</p>
<p>Attaching last year's format for reference so we maintain consistency.</p>
</div>
<a href="#" class="thread-message-attachment">
📎 fy2023-financials-format.xlsx
</a>
</article>
<!-- Message 3: Prior update -->
<article class="thread-message">
<header class="thread-message-header">
<span class="thread-message-author">S. Johnson (You)</span>
<time class="thread-message-time">Feb 27, 2026, 9:45 AM</time>
</header>
<div class="thread-message-body">
<p>On it. Just waiting for the auditor to send the final signed opinion letter. Should have everything by EOD Thursday.</p>
</div>
</article>
</div>
</section>
<!-- Response box -->
<section class="response-section">
<h2 class="section-title">Your Response</h2>
<div class="response-box">
<textarea
class="textarea"
placeholder="Write a message or upload files..."
aria-label="Response message"
></textarea>
<div class="response-actions">
<div class="response-tools">
<button class="icon-btn" title="Attach file" aria-label="Attach file">
📎
</button>
<button class="icon-btn" title="Format text" aria-label="Format text">
𝐁
</button>
</div>
<div class="response-submit">
<button class="btn btn-secondary">Save Draft</button>
<button class="btn btn-primary">Send & Mark Done</button>
</div>
</div>
</div>
</section>
</div>
<aside class="request-sidebar">
<!-- Request details card -->
<div class="info-card">
<h3 class="info-card-title">Details</h3>
<div class="info-row">
<span class="info-label">Status</span>
<span class="status-badge assigned">Assigned</span>
</div>
<div class="info-row">
<span class="info-label">Priority</span>
<span class="info-value">High</span>
</div>
<div class="info-row">
<span class="info-label">Due Date</span>
<span class="info-value overdue">Mar 15, 2026 (Overdue)</span>
</div>
<div class="info-row">
<span class="info-label">Workstream</span>
<span class="info-value">Finance</span>
</div>
<div class="info-row">
<span class="info-label">Created</span>
<span class="info-value">Feb 25, 2026</span>
</div>
<div class="info-row">
<span class="info-label">Assigned to</span>
<span class="info-value">S. Johnson</span>
</div>
</div>
<!-- Related requests card -->
<div class="info-card">
<h3 class="info-card-title">Related Requests</h3>
<div style="padding: var(--space-sm) 0;">
<a href="#" style="font-size: var(--text-sm); color: var(--color-text-link); text-decoration: none;">
FIN-041 — Audited financials FY2023
</a>
<span class="status-badge published" style="margin-left: var(--space-sm);">Published</span>
</div>
<div style="padding: var(--space-sm) 0;">
<a href="#" style="font-size: var(--text-sm); color: var(--color-text-link); text-decoration: none;">
FIN-045 — Revenue recognition policy
</a>
<span class="status-badge assigned" style="margin-left: var(--space-sm);">Assigned</span>
</div>
</div>
</aside>
</div>
</main>
</div>
<!-- Theme Switcher (demo/testing only) -->
<div class="theme-switcher" aria-label="Theme switcher">
<button onclick="document.documentElement.className='theme-light'">Light</button>
<button onclick="document.documentElement.className='theme-dark'">Dark</button>
<button onclick="document.documentElement.className='theme-contrast'">Contrast</button>
</div>
</body>
</html>

View File

@ -1,258 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="theme-light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Tasks — Dealspace</title>
<link rel="stylesheet" href="../static/themes.css">
<style>
/* Page-specific layout */
.page-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding: var(--space-lg);
}
.inbox-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
}
.inbox-title {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text-primary);
margin: 0;
}
.view-select {
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
color: var(--color-text-primary);
background: var(--color-input-bg);
border: 1px solid var(--color-input-border);
border-radius: var(--radius-md);
}
.task-list {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
/* Global nav area */
.global-nav {
display: flex;
align-items: center;
gap: var(--space-lg);
margin-left: auto;
}
.inbox-link {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-header-text);
background: var(--color-accent);
border-radius: var(--radius-md);
text-decoration: none;
}
.user-menu {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.avatar {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-bg-tertiary);
}
.user-name {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-header-text);
}
/* Theme switcher (demo only) */
.theme-switcher {
position: fixed;
bottom: var(--space-lg);
right: var(--space-lg);
display: flex;
gap: var(--space-sm);
padding: var(--space-sm);
background: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.theme-switcher button {
padding: var(--space-sm) var(--space-md);
font-size: var(--text-sm);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
color: var(--color-text-primary);
cursor: pointer;
}
.theme-switcher button:hover {
background: var(--color-bg-tertiary);
}
</style>
</head>
<body>
<div class="page-layout">
<!-- Global Header -->
<header class="global-header">
<div class="logo">Dealspace</div>
<!-- Project selector (hidden for single-project workers) -->
<select class="project-select" aria-label="Select project">
<option selected>Project Alpha — TargetCo Acquisition</option>
<option>Project Beta — MergeCo Deal</option>
<option>Project Gamma — TechStartup Buyout</option>
</select>
<nav class="global-nav">
<a href="/inbox" class="inbox-link" aria-current="page">
Inbox
<span class="badge">3</span>
</a>
<div class="user-menu">
<div class="avatar" role="img" aria-label="User avatar"></div>
<span class="user-name">S. Johnson</span>
</div>
</nav>
</header>
<!-- Main Content -->
<main class="main-content container">
<div class="inbox-header">
<h1 class="inbox-title">My Tasks</h1>
<select class="view-select" aria-label="Filter tasks">
<option selected>All Tasks</option>
<option>Overdue</option>
<option>Due Today</option>
<option>Waiting</option>
</select>
</div>
<div class="task-list" role="list">
<!-- Task 1: High priority, overdue -->
<a href="/p/alpha/r/fin-042" class="task-card" role="listitem">
<div class="task-card-header">
<span class="task-card-priority high" aria-label="High priority"></span>
<span class="task-card-ref">FIN-042</span>
<span class="task-card-title">Audited financials FY2024</span>
<span class="task-card-due overdue">
<span aria-hidden="true">⚠️</span> Overdue: Mar 15
</span>
</div>
<div class="task-card-meta">
Project Alpha • Finance • From: J. Smith (IB)
</div>
<div class="task-card-preview">
"Please provide audited financial statements including P&L, balance sheet, and cash flow..."
</div>
</a>
<!-- Task 2: Normal priority, due today -->
<a href="/p/alpha/r/leg-018" class="task-card" role="listitem">
<div class="task-card-header">
<span class="task-card-priority normal" aria-label="Normal priority"></span>
<span class="task-card-ref">LEG-018</span>
<span class="task-card-title">Board meeting minutes 2025</span>
<span class="task-card-due today">Due: Today</span>
</div>
<div class="task-card-meta">
Project Alpha • Legal • From: M. Chen (CFO)
</div>
<div class="task-card-preview">
"Board approval documentation needed for the acquisition committee review..."
</div>
</a>
<!-- Task 3: Low priority, future -->
<a href="/p/beta/r/it-007" class="task-card" role="listitem">
<div class="task-card-header">
<span class="task-card-priority low" aria-label="Low priority"></span>
<span class="task-card-ref">IT-007</span>
<span class="task-card-title">Network architecture diagram</span>
<span class="task-card-due">Due: Mar 22</span>
</div>
<div class="task-card-meta">
Project Beta • IT • From: L. Park (IB)
</div>
<div class="task-card-preview">
"Provide a current network topology diagram showing all production systems..."
</div>
</a>
<!-- Task 4: Normal priority, future -->
<a href="/p/alpha/r/hr-003" class="task-card" role="listitem">
<div class="task-card-header">
<span class="task-card-priority normal" aria-label="Normal priority"></span>
<span class="task-card-ref">HR-003</span>
<span class="task-card-title">Employee headcount by department</span>
<span class="task-card-due">Due: Mar 25</span>
</div>
<div class="task-card-meta">
Project Alpha • HR • From: A. Williams (IB)
</div>
<div class="task-card-preview">
"Breakdown of full-time and contract employees by department, including salary bands..."
</div>
</a>
<!-- Task 5: High priority, future -->
<a href="/p/alpha/r/fin-045" class="task-card" role="listitem">
<div class="task-card-header">
<span class="task-card-priority high" aria-label="High priority"></span>
<span class="task-card-ref">FIN-045</span>
<span class="task-card-title">Revenue recognition policy</span>
<span class="task-card-due">Due: Mar 18</span>
</div>
<div class="task-card-meta">
Project Alpha • Finance • From: J. Smith (IB)
</div>
<div class="task-card-preview">
"Document your revenue recognition policy and any changes in the past 3 years..."
</div>
</a>
</div>
</main>
</div>
<!-- Theme Switcher (demo/testing only) -->
<div class="theme-switcher" aria-label="Theme switcher">
<button onclick="document.documentElement.className='theme-light'">Light</button>
<button onclick="document.documentElement.className='theme-dark'">Dark</button>
<button onclick="document.documentElement.className='theme-contrast'">Contrast</button>
</div>
<!-- Empty State (hidden - shown when no tasks) -->
<!--
<div class="empty-state">
<div class="empty-state-icon"></div>
<h2 class="empty-state-title">You're all caught up</h2>
<p class="empty-state-text">No tasks need your attention right now.</p>
</div>
-->
</body>
</html>