chore: auto-commit uncommitted changes
This commit is contained in:
parent
703b8d9ec0
commit
70baa67dfd
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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; }
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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">🎉</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>
|
|
||||||
|
|
@ -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">
|
|
||||||
← 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’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">© 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>
|
|
||||||
|
|
@ -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">© 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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
Loading…
Reference in New Issue