chore: auto-commit uncommitted changes

This commit is contained in:
James 2026-03-16 06:02:44 -04:00
parent bfade7a86f
commit 52edadab72
30 changed files with 1694 additions and 641 deletions

7
.env
View File

@ -4,3 +4,10 @@ STORE_PATH=/home/johan/dev/dealspace/data/store
PORT=9300
ENV=production
BACKDOOR_CODE=220402
SMTP_HOST=smtp.protonmail.ch
SMTP_PORT=587
SMTP_USER=no-reply@inou.com
SMTP_PASS=1H2RGGL7LP8JHSQU
SMTP_FROM=no-reply@inou.com
SMTP_FROM_NAME=Dealspace via inou
OPENROUTER_API_KEY=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

View File

@ -3052,3 +3052,180 @@ func (h *Handlers) SetTestRole(w http.ResponseWriter, r *http.Request) {
}
JSONResponse(w, http.StatusOK, map[string]any{"ok": true, "test_role": req.Role})
}
// AddOrgToDeal handles POST /api/projects/{projectID}/orgs/add — creates org (if new) + deal_org + members in one shot.
func (h *Handlers) AddOrgToDeal(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
projectID := chi.URLParam(r, "projectID")
if err := lib.CheckAccessWrite(h.DB, actorID, projectID, ""); err != nil {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
var req struct {
// Org info
Name string `json:"name"`
Domains []string `json:"domains"`
Role string `json:"role"` // seller | buyer | ib | advisor
Website string `json:"website"`
Description string `json:"description"`
Industry string `json:"industry"`
Phone string `json:"phone"`
Fax string `json:"fax"`
Address string `json:"address"`
City string `json:"city"`
State string `json:"state"`
Country string `json:"country"`
Founded string `json:"founded"`
LinkedIn string `json:"linkedin"`
// Selected members
Members []lib.DealOrgMember `json:"members"`
// Deal org settings
DomainLock bool `json:"domain_lock"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
if req.Name == "" {
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "Organization name required")
return
}
if len(req.Domains) == 0 {
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "At least one domain required")
return
}
validRoles := map[string]bool{"seller": true, "buyer": true, "ib": true, "advisor": true}
if req.Role == "" || !validRoles[req.Role] {
ErrorResponse(w, http.StatusBadRequest, "invalid_role", "Role must be one of: seller, buyer, ib, advisor")
return
}
for i := range req.Domains {
req.Domains[i] = strings.ToLower(strings.TrimSpace(req.Domains[i]))
}
now := time.Now().UnixMilli()
// Step 1: Create organization entry
orgID := uuid.New().String()
orgData := lib.OrgData{
Name: req.Name,
Domains: req.Domains,
Role: req.Role,
Website: req.Website,
Description: req.Description,
Industry: req.Industry,
Phone: req.Phone,
Fax: req.Fax,
Address: req.Address,
City: req.City,
State: req.State,
Country: req.Country,
Founded: req.Founded,
LinkedIn: req.LinkedIn,
}
orgDataJSON, _ := json.Marshal(orgData)
orgKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, orgID)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Key derivation failed")
return
}
orgSummary, _ := lib.Pack(orgKey, req.Name)
orgDataPacked, _ := lib.Pack(orgKey, string(orgDataJSON))
_, dbErr := h.DB.Conn.Exec(
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order,
search_key, search_key2, summary, data, stage,
assignee_id, return_to_id, origin_id,
version, deleted_at, deleted_by, key_version,
created_at, updated_at, created_by)
VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`,
orgID, orgID, "", lib.TypeOrganization, 0, 0,
nil, nil, orgSummary, orgDataPacked, lib.StageDataroom,
"", "", "",
1, nil, nil, 1,
now, now, actorID,
)
if dbErr != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create organization")
return
}
// Step 2: Create deal_org entry linking org to project
dealOrgID := uuid.New().String()
dealOrgData := lib.DealOrgData{
OrgID: orgID,
Role: req.Role,
DomainLock: req.DomainLock,
Members: req.Members,
}
dealOrgJSON, _ := json.Marshal(dealOrgData)
projKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projectID)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Key derivation failed")
return
}
dealSummary, _ := lib.Pack(projKey, req.Name)
dealDataPacked, _ := lib.Pack(projKey, string(dealOrgJSON))
_, dbErr = h.DB.Conn.Exec(
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order,
search_key, search_key2, summary, data, stage,
assignee_id, return_to_id, origin_id,
version, deleted_at, deleted_by, key_version,
created_at, updated_at, created_by)
VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`,
dealOrgID, projectID, projectID, lib.TypeDealOrg, 1, 0,
nil, nil, dealSummary, dealDataPacked, lib.StagePreDataroom,
"", "", "",
1, nil, nil, 1,
now, now, actorID,
)
if dbErr != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to add organization to deal")
return
}
JSONResponse(w, http.StatusCreated, map[string]any{
"org_id": orgID,
"deal_org_id": dealOrgID,
"name": req.Name,
"role": req.Role,
"members": len(req.Members),
})
}
// ScrapeOrg handles POST /api/scrape/org — takes an email, scrapes the domain for org + people data.
func (h *Handlers) ScrapeOrg(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
if req.Email == "" || !strings.Contains(req.Email, "@") {
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "Valid email required")
return
}
if h.Cfg.OpenRouterKey == "" {
ErrorResponse(w, http.StatusServiceUnavailable, "not_configured", "LLM not configured")
return
}
result, err := lib.ScrapeOrgByEmail(h.Cfg.OpenRouterKey, req.Email)
if err != nil {
log.Printf("scrape org error for %s: %v", req.Email, err)
ErrorResponse(w, http.StatusUnprocessableEntity, "scrape_failed", "Could not scrape organization website")
return
}
JSONResponse(w, http.StatusOK, result)
}

View File

@ -110,6 +110,10 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
r.Get("/projects/{projectID}/orgs", h.ListDealOrgs)
r.Post("/projects/{projectID}/orgs", h.CreateDealOrg)
r.Delete("/projects/{projectID}/orgs/{dealOrgID}", h.DeleteDealOrg)
r.Post("/projects/{projectID}/orgs/add", h.AddOrgToDeal)
// Scrape (LLM-powered org lookup)
r.Post("/scrape/org", h.ScrapeOrg)
r.Get("/admin/users", h.AdminListUsers)
r.Get("/admin/projects", h.AdminListProjects)

View File

@ -118,10 +118,19 @@ func loadConfig() (*lib.Config, error) {
Env: env,
JWTSecret: jwtSecret,
BackdoorCode: backdoorCode,
OpenRouterKey: os.Getenv("OPENROUTER_API_KEY"),
}
// Initialize mailer
cfg.Mailer = lib.NewMailer(cfg)
if cfg.Mailer.Enabled() {
emailDir := findEmailTemplates()
if emailDir != "" {
if err := cfg.Mailer.LoadTemplates(emailDir); err != nil {
return nil, fmt.Errorf("load email templates: %w", err)
}
}
}
return cfg, nil
}
@ -167,6 +176,20 @@ func splitLines(s string) []string {
return lines
}
func findEmailTemplates() string {
candidates := []string{
"portal/emails",
filepath.Join(filepath.Dir(os.Args[0]), "portal/emails"),
"/opt/dealspace/portal/emails",
}
for _, p := range candidates {
if info, err := os.Stat(p); err == nil && info.IsDir() {
return p
}
}
return ""
}
func findMigration() string {
candidates := []string{
"migrations",

Binary file not shown.

Binary file not shown.

BIN
dealspace

Binary file not shown.

70
lib/llm.go Normal file
View File

@ -0,0 +1,70 @@
package lib
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// CallOpenRouter sends a request to OpenRouter (OpenAI-compatible API).
func CallOpenRouter(apiKey, model string, messages []map[string]interface{}, maxTokens int) (string, error) {
if apiKey == "" {
return "", fmt.Errorf("OpenRouter API key not configured")
}
reqBody := map[string]interface{}{
"model": model,
"messages": messages,
"max_tokens": maxTokens,
"temperature": 0.1,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal request: %w", err)
}
req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewReader(jsonBody))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("API request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read response: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("OpenRouter API error %d: %s", resp.StatusCode, string(body))
}
var oaiResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(body, &oaiResp); err != nil {
return "", fmt.Errorf("parse response: %w", err)
}
if len(oaiResp.Choices) == 0 {
return "", fmt.Errorf("empty response from OpenRouter")
}
text := strings.TrimSpace(oaiResp.Choices[0].Message.Content)
text = strings.TrimPrefix(text, "```json")
text = strings.TrimPrefix(text, "```")
text = strings.TrimSuffix(text, "```")
return strings.TrimSpace(text), nil
}

View File

@ -2,8 +2,10 @@ package lib
import (
"bytes"
"crypto/tls"
"fmt"
"html/template"
"net"
"net/smtp"
"os"
"path/filepath"
@ -18,6 +20,7 @@ type Mailer struct {
User string
Pass string
From string
FromName string
templates *template.Template
enabled bool
}
@ -43,12 +46,18 @@ func NewMailer(cfg *Config) *Mailer {
from = "noreply@muskepo.com"
}
fromName := os.Getenv("SMTP_FROM_NAME")
if fromName == "" {
fromName = "Dealspace"
}
m := &Mailer{
Host: host,
Port: port,
User: os.Getenv("SMTP_USER"),
Pass: os.Getenv("SMTP_PASS"),
From: from,
FromName: fromName,
enabled: true,
}
@ -91,29 +100,50 @@ func (m *Mailer) Enabled() bool {
return m.enabled
}
// Send sends an email with the given HTML body.
// Send sends an email with the given HTML body using explicit STARTTLS.
func (m *Mailer) Send(to, subject, htmlBody string) error {
if !m.enabled {
return nil // no-op
}
// Build email message
msg := m.buildMessage(to, subject, htmlBody)
// Connect and send
addr := fmt.Sprintf("%s:%d", m.Host, m.Port)
var auth smtp.Auth
if m.User != "" && m.Pass != "" {
auth = smtp.PlainAuth("", m.User, m.Pass, m.Host)
}
err := smtp.SendMail(addr, auth, m.From, []string{to}, msg)
conn, err := net.Dial("tcp", addr)
if err != nil {
return fmt.Errorf("send mail to %s: %w", to, err)
return fmt.Errorf("dial %s: %w", addr, err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, m.Host)
if err != nil {
return fmt.Errorf("smtp client: %w", err)
}
defer client.Close()
if err = client.StartTLS(&tls.Config{ServerName: m.Host}); err != nil {
return fmt.Errorf("starttls: %w", err)
}
if m.User != "" && m.Pass != "" {
if err = client.Auth(smtp.PlainAuth("", m.User, m.Pass, m.Host)); err != nil {
return fmt.Errorf("auth: %w", err)
}
}
if err = client.Mail(m.From); err != nil {
return fmt.Errorf("mail from: %w", err)
}
if err = client.Rcpt(to); err != nil {
return fmt.Errorf("rcpt to %s: %w", to, err)
}
return nil
w, err := client.Data()
if err != nil {
return fmt.Errorf("data: %w", err)
}
if _, err = w.Write(msg); err != nil {
return fmt.Errorf("write: %w", err)
}
return w.Close()
}
// SendTemplate renders a template and sends it as an email.
@ -157,7 +187,7 @@ func (m *Mailer) buildMessage(to, subject, htmlBody string) []byte {
// Headers
buf.WriteString("MIME-Version: 1.0\r\n")
buf.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n")
buf.WriteString(fmt.Sprintf("From: Dealspace <%s>\r\n", m.From))
buf.WriteString(fmt.Sprintf("From: %s <%s>\r\n", m.FromName, m.From))
buf.WriteString(fmt.Sprintf("To: %s\r\n", to))
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", m.encodeSubject(subject)))
buf.WriteString("\r\n")

250
lib/scrape.go Normal file
View File

@ -0,0 +1,250 @@
package lib
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
// ScrapedOrg is the structured result from scraping an organization's website.
type ScrapedOrg struct {
Name string `json:"name"`
Domain string `json:"domain"`
Logo string `json:"logo,omitempty"` // URL to company logo
Description string `json:"description,omitempty"`
Industry string `json:"industry,omitempty"`
Website string `json:"website"`
Phone string `json:"phone,omitempty"`
Fax string `json:"fax,omitempty"`
Address string `json:"address,omitempty"`
City string `json:"city,omitempty"`
State string `json:"state,omitempty"`
Country string `json:"country,omitempty"`
Founded string `json:"founded,omitempty"`
LinkedIn string `json:"linkedin,omitempty"`
People []ScrapedPerson `json:"people,omitempty"`
}
// ScrapedPerson is a person found on the organization's website.
type ScrapedPerson struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Title string `json:"title,omitempty"`
Phone string `json:"phone,omitempty"`
Photo string `json:"photo,omitempty"` // URL to headshot
Bio string `json:"bio,omitempty"`
LinkedIn string `json:"linkedin,omitempty"`
}
const scrapeModel = "google/gemini-2.0-flash-001"
// ScrapeOrgByEmail takes an email address, extracts the domain,
// fetches the website, and uses an LLM to extract org + people data.
func ScrapeOrgByEmail(apiKey, email string) (*ScrapedOrg, error) {
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid email: %s", email)
}
domain := parts[1]
return ScrapeOrg(apiKey, domain)
}
// ScrapeOrg fetches a domain's website and extracts structured org + people data.
// Two-pass approach:
// 1. Fetch homepage → ask LLM which pages have team/about/contact info
// 2. Fetch those pages → ask LLM to extract structured data
func ScrapeOrg(apiKey, domain string) (*ScrapedOrg, error) {
// Pass 1: fetch homepage
base := "https://" + domain
homepage := fetchPage(base)
if homepage == "" {
return nil, fmt.Errorf("could not fetch %s", base)
}
// Ask LLM to find relevant pages
discoverPrompt := fmt.Sprintf(`You are analyzing the HTML of %s to find pages that contain:
1. Team / leadership / people / staff pages (with bios, headshots, names)
2. About / company info pages
3. Contact / office address pages
Look at the navigation, footer, and links in the HTML. Return a JSON array of up to 10 absolute URLs that are most likely to contain team members and company info. Only include URLs on the same domain (%s). Do not include the homepage itself.
Return ONLY a JSON array of strings, no markdown:
["https://%s/about", "https://%s/team", ...]
If you cannot find any relevant links, return an empty array: []
HTML:
%s`, domain, domain, domain, domain, homepage)
discoverMessages := []map[string]interface{}{
{"role": "user", "content": discoverPrompt},
}
linksRaw, err := CallOpenRouter(apiKey, scrapeModel, discoverMessages, 1024)
if err != nil {
log.Printf("scrape discover error for %s: %v", domain, err)
linksRaw = "[]"
}
var links []string
if err := json.Unmarshal([]byte(linksRaw), &links); err != nil {
log.Printf("scrape discover parse error for %s: %v (raw: %.200s)", domain, err, linksRaw)
links = nil
}
// Fetch discovered pages in parallel
var allHTML strings.Builder
allHTML.WriteString(fmt.Sprintf("\n<!-- PAGE: %s -->\n", base))
allHTML.WriteString(homepage)
if len(links) > 0 {
extra := fetchPages(links)
allHTML.WriteString(extra)
}
html := allHTML.String()
// Pass 2: extract structured data
prompt := fmt.Sprintf(`Extract structured data from this company website. Domain: %s
RULES:
- Extract EVERY person mentioned do not skip anyone
- Every person MUST have a "title" (job title / role). Look at section headings, CSS classes, surrounding text to determine titles. Common patterns: "Co-Founder", "Partner", "Managing Director", "Principal", "Investment Professional", "Operating Partner", "Operations Manager", "Finance & Operations", "Analyst", "Associate". If a person is under a heading like "Investment Professionals", their title is "Investment Professional". Never use generic "Team Member".
- Photo/logo URLs must be fully qualified (https://...)
- Logo: find the company logo image look for img tags in the header, navbar, or footer with "logo" in the src/alt/class. Return the full absolute URL.
- Address: put ONLY the street address in "address" (e.g. "2151 Central Avenue"). Put city, state, country in their own fields. Do NOT combine them.
- If you can infer emails from a pattern (e.g. firstname@%s), include them
- Bio: 1-2 sentences about their professional background, not personal hobbies
- Return at most 25 people. Prioritize leadership, partners, principals, and senior staff over junior employees, interns, or support staff
Return a single JSON object:
{
"name": "Company Name",
"domain": "%s",
"logo": "https://full-url-to-logo.png",
"description": "1-2 sentence description",
"industry": "sector",
"website": "https://%s",
"phone": "",
"fax": "",
"address": "street address only",
"city": "",
"state": "",
"country": "",
"founded": "year",
"linkedin": "url",
"people": [
{
"name": "Full Name",
"email": "email@domain",
"title": "Job Title",
"phone": "direct phone",
"photo": "https://full-url-to-headshot.jpg",
"bio": "1-2 sentences",
"linkedin": "url"
}
]
}
Return ONLY valid JSON no markdown, no explanation.
HTML:
%s`, domain, domain, domain, domain, html)
messages := []map[string]interface{}{
{"role": "user", "content": prompt},
}
raw, err := CallOpenRouter(apiKey, scrapeModel, messages, 8192)
if err != nil {
return nil, fmt.Errorf("llm scrape: %w", err)
}
var result ScrapedOrg
if err := json.Unmarshal([]byte(raw), &result); err != nil {
return nil, fmt.Errorf("parse llm response: %w (raw: %.500s)", err, raw)
}
result.Domain = domain
if result.Website == "" {
result.Website = "https://" + domain
}
return &result, nil
}
// fetchPage fetches a single URL and returns its HTML body (or "" on error).
func fetchPage(url string) string {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Dealspace/1.0)")
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
return ""
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 500*1024))
return string(body)
}
// fetchPages fetches multiple URLs concurrently and concatenates their raw HTML.
// Skips pages that return errors or non-200 status.
func fetchPages(urls []string) string {
type result struct {
idx int
url string
body string
}
ch := make(chan result, len(urls))
client := &http.Client{Timeout: 10 * time.Second}
for i, u := range urls {
go func(idx int, url string) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
ch <- result{idx, url, ""}
return
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Dealspace/1.0)")
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
ch <- result{idx, url, ""}
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 500*1024))
ch <- result{idx, url, string(body)}
}(i, u)
}
results := make([]result, len(urls))
for range urls {
r := <-ch
results[r.idx] = r
}
var sb strings.Builder
for _, r := range results {
if r.body != "" {
sb.WriteString(fmt.Sprintf("\n<!-- PAGE: %s -->\n", r.url))
sb.WriteString(r.body)
}
}
return sb.String()
}

View File

@ -66,8 +66,18 @@ type OrgData struct {
Name string `json:"name"`
Domains []string `json:"domains"` // required, e.g. ["kaseya.com","datto.com"]
Role string `json:"role"` // seller | buyer | ib | advisor
Logo string `json:"logo,omitempty"`
Website string `json:"website,omitempty"`
Description string `json:"description,omitempty"`
Industry string `json:"industry,omitempty"`
Phone string `json:"phone,omitempty"`
Fax string `json:"fax,omitempty"`
Address string `json:"address,omitempty"`
City string `json:"city,omitempty"`
State string `json:"state,omitempty"`
Country string `json:"country,omitempty"`
Founded string `json:"founded,omitempty"`
LinkedIn string `json:"linkedin,omitempty"`
ContactName string `json:"contact_name,omitempty"`
ContactEmail string `json:"contact_email,omitempty"`
}
@ -96,6 +106,10 @@ type DealOrgMember struct {
Name string `json:"name"`
Email string `json:"email"`
Title string `json:"title,omitempty"`
Phone string `json:"phone,omitempty"`
Photo string `json:"photo,omitempty"`
Bio string `json:"bio,omitempty"`
LinkedIn string `json:"linkedin,omitempty"`
}
// User represents an account.
@ -250,6 +264,7 @@ type Config struct {
JWTSecret []byte
Mailer *Mailer
BackdoorCode string // OTP backdoor for dev/testing
OpenRouterKey string // OpenRouter API key for LLM features
}
// RequestData is the JSON structure packed into a request entry's Data field.

View File

@ -15,14 +15,14 @@
<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-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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-[#94a3b8] transition">
<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">
@ -32,23 +32,23 @@
</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-[#94a3b8] text-sm mb-8">Platform overview and management.</p>
<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-[#94a3b8] text-xs uppercase tracking-wider mb-1">Users</div>
<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-[#94a3b8] text-xs uppercase tracking-wider mb-1">Projects</div>
<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-[#94a3b8] text-xs uppercase tracking-wider mb-1">Organizations</div>
<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-[#94a3b8] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
<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>
@ -57,7 +57,7 @@
<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-[#94a3b8] text-sm">Loading...</div></div>
<div id="userList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
</div>
</main>
</div>
@ -95,13 +95,13 @@
<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-[#475569] text-xs">${escHtml(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-[#475569]">${new Date(u.created_at).toLocaleDateString()}</span>
<span class="text-xs text-[#8899a6]">${new Date(u.created_at).toLocaleDateString()}</span>
</div>`).join('');
} else {
document.getElementById('userList').innerHTML = '<div class="text-[#94a3b8] text-sm">No users found.</div>';
document.getElementById('userList').innerHTML = '<div class="text-[#b0bec5] text-sm">No users found.</div>';
}
} catch(e) {}
}

View File

@ -15,24 +15,24 @@
<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-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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>
@ -41,17 +41,17 @@
<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-[#94a3b8] text-sm">Company directory — parties eligible to participate in deals.</p>
<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-[#94a3b8] text-sm col-span-2">Loading...</div>
<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-[#94a3b8] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
<p class="text-[#b0bec5] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
</div>
</main>
</div>
@ -62,17 +62,17 @@
<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-[#94a3b8] 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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#94a3b8] 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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]">
<p class="text-[#475569] 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-[#94a3b8] mb-1.5">Default Role</label>
<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-[#94a3b8] 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-[#475569] 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">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>
@ -113,7 +113,7 @@
<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-[#94a3b8] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</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('');

View File

@ -27,29 +27,29 @@
<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-[#94a3b8] hover:text-white transition">Projects</a>
<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-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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>
@ -61,7 +61,7 @@
<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-[#94a3b8] text-sm"></p>
<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">
@ -73,31 +73,31 @@
</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-[#94a3b8] transition" onclick="switchTab('orgs', this)">Organizations</button>
<button class="tab pb-3 text-sm font-medium text-[#94a3b8] transition" onclick="switchTab('team', this)">Team</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-[#94a3b8] text-sm">Loading requests...</div></div>
<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-[#94a3b8] text-sm mb-4">Import a diligence checklist or create requests manually.</p>
<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-[#94a3b8] text-sm">Organizations participating in this deal.</p>
<button id="addOrgBtn" class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm transition">+ Add Org</button>
<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-[#94a3b8] text-sm">Loading...</div></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-[#94a3b8] text-sm">People with access to this deal.</p>
<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-[#94a3b8] text-sm">Loading...</div></div>
<div id="teamList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
</div>
</main>
</div>
@ -106,24 +106,24 @@
<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-[#94a3b8] mb-1.5">File (CSV or XLSX)</label>
<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-[#94a3b8] mb-1.5">Import Mode</label>
<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-[#94a3b8] 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-[#64748b]">
<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-[#94a3b8]">Create workstreams from sections</span>
<span class="text-sm text-[#b0bec5]">Create workstreams from sections</span>
</label>
</div>
<div class="flex gap-3">
@ -133,6 +133,109 @@
</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';
@ -183,7 +286,7 @@
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-[#94a3b8] 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-[#94a3b8] 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-[#94a3b8]">' + 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>';
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>'; }
}
@ -197,9 +300,9 @@
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-[#94a3b8] text-sm">No organizations added yet.</div>'; return; }
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-[#94a3b8]">🔒 domain locked</span>' : '') + '</div>' + (domains.length > 0 ? '<div class="flex gap-1.5 flex-wrap">' + domains.map(dm => '<span class="text-xs text-[#94a3b8] font-mono bg-white/[0.05] px-2 py-0.5 rounded">@' + dm + '</span>').join('') + '</div>' : '') + '</div></div>';
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); }
}
@ -208,13 +311,13 @@
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-[#94a3b8] 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-[#94a3b8] text-xs">' + escHtml(m.email) + '</div>' : '') + '</div><span class="text-xs text-[#94a3b8] capitalize">' + (m.role || 'member') + '</span></div>').join('');
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-[#94a3b8]'); });
el.classList.add('active','text-white'); el.classList.remove('text-[#94a3b8]');
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');
@ -244,6 +347,147 @@
}).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>

View File

@ -15,24 +15,24 @@
<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-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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>
@ -41,17 +41,17 @@
<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-[#94a3b8] text-sm">All deals you have access to.</p>
<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-[#94a3b8] text-sm col-span-3">Loading projects...</div>
<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-[#94a3b8]">You haven't been added to any deals yet.</p>
<p class="text-[#b0bec5]">You haven't been added to any deals yet.</p>
</div>
</main>
</div>
@ -62,11 +62,11 @@
<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-[#94a3b8] 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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#94a3b8] 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-[#475569] 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-[#94a3b8] mb-1.5">Status</label>
<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>
@ -116,8 +116,8 @@
<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-[#94a3b8] 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-[#475569]">
${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>

View File

@ -15,28 +15,28 @@
<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-[#94a3b8] hover:text-white transition">Projects</a>
<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-[#94a3b8] hover:text-white transition">Project</a>
<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-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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>
@ -47,11 +47,11 @@
<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-[#94a3b8] text-sm"></p>
<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-[#475569]">
<div class="flex gap-3 flex-wrap text-xs text-[#8899a6]">
<span id="reqDue"></span>
<span id="reqAssignee"></span>
</div>
@ -59,23 +59,23 @@
<!-- Answer / Upload -->
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 mb-6">
<h2 class="text-sm font-semibold text-[#94a3b8] uppercase tracking-wider mb-4">Response</h2>
<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-[#94a3b8] text-sm">Drop files here or click to upload</p>
<p class="text-[#475569] text-xs mt-1">PDF, DOCX, XLSX, images</p>
<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-[#94a3b8]"></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-[#94a3b8] uppercase tracking-wider mb-4">Comments</h2>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] text-sm resize-none"></textarea>
<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>
@ -127,7 +127,7 @@
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-[#475569] text-sm">No documents uploaded yet.</p>'; return; }
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';
@ -135,7 +135,7 @@
<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-[#475569] text-xs">${new Date(a.created_at).toLocaleString()}</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>`;
@ -164,7 +164,7 @@
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-[#475569] text-sm">No comments yet.</p>'; return; }
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">
@ -172,9 +172,9 @@
<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-[#475569] text-xs">${new Date(c.created_at).toLocaleString()}</span>
<span class="text-[#8899a6] text-xs">${new Date(c.created_at).toLocaleString()}</span>
</div>
<p class="text-[#94a3b8] text-sm">${escHtml(d.text||'')}</p>
<p class="text-[#b0bec5] text-sm">${escHtml(d.text||'')}</p>
</div>
</div>`;
}).join('');

View File

@ -30,8 +30,8 @@
</select>
</div>
<div class="flex items-center gap-4">
<span id="userName" class="text-sm text-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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>
@ -40,16 +40,16 @@
<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-[#94a3b8]" 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>
<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-[#94a3b8] transition">
<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-[#94a3b8] transition">
<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>
@ -62,19 +62,19 @@
<!-- Greeting -->
<div class="mb-8">
<h1 id="greeting" class="text-2xl font-bold text-white mb-1"></h1>
<p class="text-[#94a3b8] text-sm">Here are your pending tasks.</p>
<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-[#94a3b8] text-sm">Loading tasks...</div>
<div class="text-[#b0bec5] text-sm">Loading tasks...</div>
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden text-center py-20">
<div class="text-5xl mb-4">&#127881;</div>
<h2 class="text-xl font-semibold text-white mb-2">You're all caught up</h2>
<p class="text-[#94a3b8]">No tasks need your attention right now.</p>
<p class="text-[#b0bec5]">No tasks need your attention right now.</p>
</div>
</main>
</div>
@ -136,12 +136,12 @@
<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-[#94a3b8]">${ref}</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-[#94a3b8]">Due: ${due}</span>` : ''}
${due ? `<span class="text-xs text-[#b0bec5]">Due: ${due}</span>` : ''}
</div>
<div class="flex items-center gap-3 text-xs text-[#475569]">
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#94a3b8]">${status}</span>
<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>

View File

@ -20,22 +20,22 @@
<h1 class="text-3xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</h1>
<p class="text-[#94a3b8] mt-2 text-sm">Secure M&A deal management</p>
<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-[#94a3b8] text-sm mb-6">Enter your email to receive a login code.</p>
<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-[#94a3b8] mb-1.5">Email</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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">
@ -47,7 +47,7 @@
<!-- 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-[#94a3b8] text-sm mb-6">
<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>
@ -55,11 +55,11 @@
<form id="codeForm" class="space-y-5">
<div>
<label for="code" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Login code</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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">
@ -68,7 +68,7 @@
</form>
<div class="mt-4 flex items-center justify-between">
<button id="backBtn" class="text-[#94a3b8] text-sm hover:text-white transition">
<button id="backBtn" class="text-[#b0bec5] text-sm hover:text-white transition">
&larr; Use a different email
</button>
<button id="resendBtn" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
@ -77,11 +77,11 @@
</div>
</div>
<p class="text-center text-[#475569] text-xs mt-6">
<p class="text-center text-[#8899a6] text-xs mt-6">
Don&rsquo;t have an account? Dealspace is invite-only.<br>
<a href="/#demo" class="text-[#c9a84c] hover:underline">Request access on muskepo.com</a>
</p>
<p class="text-center text-[#475569] text-xs mt-3">&copy; 2026 Muskepo B.V. — Amsterdam</p>
<p class="text-center text-[#8899a6] text-xs mt-3">&copy; 2026 Muskepo B.V. — Amsterdam</p>
</div>
<script>

View File

@ -19,33 +19,33 @@
<h1 class="text-3xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</h1>
<p class="text-[#94a3b8] mt-2 text-sm">First-time setup</p>
<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-[#94a3b8] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
<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-[#94a3b8] mb-1.5">Full name</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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-[#94a3b8] mb-1.5">Email</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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-[#94a3b8] mb-1.5">Password</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
<p class="text-xs text-[#475569] mt-1">Minimum 8 characters</p>
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">
@ -54,7 +54,7 @@
</form>
</div>
<p class="text-center text-[#475569] text-xs mt-8">&copy; 2026 Muskepo B.V. — Amsterdam</p>
<p class="text-center text-[#8899a6] text-xs mt-8">&copy; 2026 Muskepo B.V. — Amsterdam</p>
</div>
<script>

View File

@ -13,8 +13,8 @@ html[data-theme="midnight"] {
--ds-bg: #0a1628;
--ds-sf: #0d1f3c;
--ds-tx: #fff;
--ds-tx2: #94a3b8;
--ds-tx3: #475569;
--ds-tx2: #b0bec5;
--ds-tx3: #8899a6;
--ds-ac: #c9a84c;
--ds-ac2: #b8973f;
--ds-act: #0a1628;
@ -29,8 +29,8 @@ html[data-theme="light"] {
--ds-bg: #f0f2f5;
--ds-sf: #fff;
--ds-tx: #1a202c;
--ds-tx2: #64748b;
--ds-tx3: #94a3b8;
--ds-tx2: #8899a6;
--ds-tx3: #b0bec5;
--ds-ac: #2563eb;
--ds-ac2: #1d4ed8;
--ds-act: #fff;
@ -45,8 +45,8 @@ html[data-theme="slate"] {
--ds-bg: #1e293b;
--ds-sf: #334155;
--ds-tx: #f1f5f9;
--ds-tx2: #94a3b8;
--ds-tx3: #64748b;
--ds-tx2: #b0bec5;
--ds-tx3: #8899a6;
--ds-ac: #14b8a6;
--ds-ac2: #0d9488;
--ds-act: #0f172a;
@ -61,8 +61,8 @@ html[data-theme="compact"] {
--ds-bg: #0a1628;
--ds-sf: #0d1f3c;
--ds-tx: #fff;
--ds-tx2: #94a3b8;
--ds-tx3: #475569;
--ds-tx2: #b0bec5;
--ds-tx3: #8899a6;
--ds-ac: #c9a84c;
--ds-ac2: #b8973f;
--ds-act: #0a1628;
@ -145,8 +145,8 @@ nav { background: var(--ds-sf) !important; border-color: var(--ds-bd) !important
[class*="bg-[#0d1f3c]"] { background-color: var(--ds-sf) !important; }
[class*="bg-[#0a1628]"] { background-color: var(--ds-bg) !important; }
[class*="text-[#c9a84c]"] { color: var(--ds-ac) !important; }
[class*="text-[#94a3b8]"] { color: var(--ds-tx2) !important; }
[class*="text-[#475569]"] { color: var(--ds-tx3) !important; }
[class*="text-[#b0bec5]"] { color: var(--ds-tx2) !important; }
[class*="text-[#8899a6]"] { color: var(--ds-tx3) !important; }
[class*="text-[#0a1628]"] { color: var(--ds-act) !important; }
[class*="bg-[#c9a84c]"] { background-color: var(--ds-ac) !important; }
[class*="hover:bg-[#b8973f]"]:hover { background-color: var(--ds-ac2) !important; }
@ -155,6 +155,7 @@ nav { background: var(--ds-sf) !important; border-color: var(--ds-bd) !important
[class*="hover:bg-white/"]:hover { background-color: var(--ds-hv) !important; }
input, textarea, select { background-color: var(--ds-inp) !important; color: var(--ds-tx) !important; border-color: var(--ds-bd) !important; }
input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !important; }
input.field-error, textarea.field-error, select.field-error { border-color: #ef4444 !important; }
[class*="bg-black/"] { background-color: rgba(0,0,0,.6) !important; }
/* ===== SIDEBAR ===== */
@ -206,7 +207,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
text-align: left;
padding: 6px 14px;
font-size: 13px;
color: var(--ds-tx2, #94a3b8);
color: var(--ds-tx2, #b0bec5);
background: none;
border: none;
cursor: pointer;
@ -235,7 +236,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ds-tx3, #475569);
color: var(--ds-tx3, #8899a6);
border-bottom: 1px solid var(--ds-bd, rgba(255,255,255,.08));
user-select: none;
white-space: nowrap;
@ -251,7 +252,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
#reqTree tbody tr.row-list { background: var(--ds-sf, #0d1f3c); }
#reqTree tbody tr.row-list:hover { background: color-mix(in srgb, var(--ds-sf, #0d1f3c) 85%, var(--ds-tx, white)); }
#reqTree tbody tr.row-section { background: rgba(255,255,255,.02); }
#reqTree .row-num { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--ds-tx3, #475569); }
#reqTree .row-num { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--ds-tx3, #8899a6); }
/* ===== DRAG & DROP ===== */
#reqTree .drag-handle { cursor: grab; opacity: 0.3; transition: opacity .15s; }
@ -263,7 +264,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
width: 16px;
text-align: center;
font-size: 10px;
color: var(--ds-tx3, #475569);
color: var(--ds-tx3, #8899a6);
transition: transform .15s;
}
#reqTree .collapse-btn.collapsed { transform: rotate(-90deg); }

View File

@ -107,7 +107,7 @@
#aria-close {
background: none;
border: none;
color: #94a3b8;
color: #b0bec5;
font-size: 20px;
cursor: pointer;
padding: 0 4px;
@ -167,7 +167,7 @@
width: 6px;
height: 6px;
border-radius: 50%;
background: #94a3b8;
background: #b0bec5;
animation: aria-bounce 1.2s infinite;
}
.aria-typing span:nth-child(2) { animation-delay: 0.2s; }
@ -202,7 +202,7 @@
}
#aria-input:focus { border-color: rgba(201,168,76,0.5); }
#aria-input::placeholder { color: #475569; }
#aria-input::placeholder { color: #8899a6; }
#aria-send {
width: 36px;

View File

@ -5,23 +5,23 @@
{{define "content"}}
<div class="p-8 max-w-6xl">
<h1 class="text-2xl font-bold text-white mb-2">Admin Dashboard</h1>
<p class="text-[#94a3b8] text-sm mb-8">Platform overview and management.</p>
<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-[#94a3b8] text-xs uppercase tracking-wider mb-1">Users</div>
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Users</div>
<div id="statUsers" class="text-3xl font-bold text-white">&mdash;</div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Projects</div>
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Projects</div>
<div id="statProjects" class="text-3xl font-bold text-white">&mdash;</div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Organizations</div>
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Organizations</div>
<div id="statOrgs" class="text-3xl font-bold text-white">&mdash;</div>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
<div id="statSessions" class="text-3xl font-bold text-white">&mdash;</div>
</div>
</div>
@ -30,7 +30,7 @@
<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-[#94a3b8] text-sm">Loading...</div></div>
<div id="userList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
</div>
</div>
{{end}}
@ -58,13 +58,13 @@
<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-[#475569] text-xs">${escHtml(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-[#475569]">${new Date(u.created_at).toLocaleDateString()}</span>
<span class="text-xs text-[#8899a6]">${new Date(u.created_at).toLocaleDateString()}</span>
</div>`).join('');
} else {
document.getElementById('userList').innerHTML = '<div class="text-[#94a3b8] text-sm">No users found.</div>';
document.getElementById('userList').innerHTML = '<div class="text-[#b0bec5] text-sm">No users found.</div>';
}
} catch(e) {}
}

View File

@ -3,16 +3,16 @@
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-white mb-1">Organizations</h1>
<p class="text-[#94a3b8] text-sm">Company directory &mdash; parties eligible to participate in deals.</p>
<p class="text-[#b0bec5] text-sm">Company directory &mdash; 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-[#94a3b8] text-sm col-span-2">Loading...</div>
<div class="text-[#b0bec5] text-sm col-span-2">Loading...</div>
</div>
<div id="emptyState" class="hidden text-center py-20">
<h2 class="text-xl font-semibold text-white mb-2">No organizations yet</h2>
<p class="text-[#94a3b8] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
<p class="text-[#b0bec5] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
</div>
</div>
@ -22,17 +22,17 @@
<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-[#94a3b8] mb-1.5">Name</label>
<input id="oName" type="text" placeholder="Blackstone Group" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
<input id="oDomains" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]">
<p class="text-[#475569] text-xs mt-1">Comma-separated. Used to validate invite emails.</p></div>
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Role</label>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name</label>
<input id="oName" type="text" placeholder="Blackstone Group" 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]"></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
<input id="oDomains" type="text" placeholder="blackstone.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]">
<p class="text-[#8899a6] text-xs mt-1">Comma-separated. Used to validate invite emails.</p></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">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-[#94a3b8] mb-1.5">Website</label>
<input id="oWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
<input id="oWebsite" type="text" placeholder="blackstone.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]"></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>
@ -46,23 +46,23 @@
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-white">Edit Organization</h2>
<button onclick="closeEditModal()" class="text-[#94a3b8] hover:text-white text-2xl leading-none">&times;</button>
<button onclick="closeEditModal()" class="text-[#b0bec5] hover:text-white text-2xl leading-none">&times;</button>
</div>
<div id="editModalError" 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-[#94a3b8] mb-1.5">Name</label>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name</label>
<input id="eName" type="text" 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]"></div>
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email Domains</label>
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]">
<p class="text-[#475569] text-xs mt-1">Comma-separated.</p></div>
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Role</label>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains</label>
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.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]">
<p class="text-[#8899a6] text-xs mt-1">Comma-separated.</p></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Role</label>
<select id="eRole" 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-[#94a3b8] mb-1.5">Website</label>
<input id="eWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Description</label>
<textarea id="eDesc" rows="2" placeholder="Optional notes..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] resize-none"></textarea></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
<input id="eWebsite" type="text" placeholder="blackstone.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]"></div>
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Description</label>
<textarea id="eDesc" rows="2" placeholder="Optional notes..." 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] resize-none"></textarea></div>
</div>
<div class="flex gap-3 mt-6">
<button onclick="closeEditModal()" 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>
@ -108,7 +108,7 @@
<h3 class="text-white font-semibold leading-tight">${escHtml(name)}</h3>
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${role || '—'}</span>
</div>
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#94a3b8] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
<div class="flex gap-1.5 flex-wrap">${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>
${o.website ? `<div class="mt-2 text-xs" style="color:var(--ds-tx3)">${escHtml(o.website)}</div>` : ''}
</div>`;
}).join('');

File diff suppressed because it is too large Load Diff

View File

@ -3,17 +3,17 @@
<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-[#94a3b8] text-sm">All deals you have access to.</p>
<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-[#94a3b8] text-sm col-span-3">Loading projects...</div>
<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-[#94a3b8]">You haven't been added to any deals yet.</p>
<p class="text-[#b0bec5]">You haven't been added to any deals yet.</p>
</div>
</div>
@ -23,11 +23,11 @@
<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-[#94a3b8] 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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
<div><label class="block text-sm font-medium text-[#94a3b8] 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-[#475569] 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-[#94a3b8] mb-1.5">Status</label>
<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>
@ -68,8 +68,8 @@
<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-[#94a3b8] 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-[#475569]">
${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>

View File

@ -2,7 +2,7 @@
<div class="flex items-center gap-3">
<a href="/app/projects" class="text-2xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<span class="text-white/20">/</span>
<a id="backToProject" href="/app/projects" class="text-sm text-[#94a3b8] hover:text-white transition">Project</a>
<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"></span>
</div>
@ -282,14 +282,14 @@
<div class="flex-1 min-w-0">
<div class="flex items-center gap-1">
<span class="text-white text-sm font-medium truncate" id="fname-${a.entry_id}">${escHtml(name)}</span>
<button onclick="startRenameFile('${a.entry_id}')" class="shrink-0 p-0.5 rounded hover:bg-white/[0.08] text-[#94a3b8] hover:text-white transition" title="Rename">
<button onclick="startRenameFile('${a.entry_id}')" class="shrink-0 p-0.5 rounded hover:bg-white/[0.08] text-[#b0bec5] hover:text-white transition" title="Rename">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
</div>
<div class="text-[#475569] text-xs">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
<div class="text-[#8899a6] text-xs">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
</div>
<div class="flex items-center gap-2 shrink-0">
${objectID && previewUrl ? `<a href="${previewUrl}" target="_blank" class="px-2 py-1 rounded text-xs font-medium bg-white/[0.06] text-[#94a3b8] hover:text-white hover:bg-white/[0.1] transition">${isVideo ? 'Play' : (isBuyerRole() ? 'View' : 'Preview')}</a>` : ''}
${objectID && previewUrl ? `<a href="${previewUrl}" target="_blank" class="px-2 py-1 rounded text-xs font-medium bg-white/[0.06] text-[#b0bec5] hover:text-white hover:bg-white/[0.1] transition">${isVideo ? 'Play' : (isBuyerRole() ? 'View' : 'Preview')}</a>` : ''}
${objectID && !isBuyerRole() ? `<a href="${downloadUrl}" class="text-sm hover:underline" style="color:var(--ds-ac)">Download</a>` : ''}
</div>
</div>`;
@ -397,7 +397,7 @@
container.innerHTML = tabs.map(t =>
`<button onclick="switchChannel(${JSON.stringify(t.key)})"
class="px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 -mb-px transition
${activeChannel === t.key ? 'border-[#c9a84c] text-white' : 'border-transparent text-[#94a3b8] hover:text-white'}">
${activeChannel === t.key ? 'border-[#c9a84c] text-white' : 'border-transparent text-[#b0bec5] hover:text-white'}">
${t.label}
</button>`
).join('');
@ -418,7 +418,7 @@
const top = ch.filter(c => !parseData(c.data_text).parent_comment_id);
const nested = ch.filter(c => !!parseData(c.data_text).parent_comment_id);
if (top.length === 0) {
el.innerHTML = '<p class="text-[#475569] text-sm">No messages yet.</p>';
el.innerHTML = '<p class="text-[#8899a6] text-sm">No messages yet.</p>';
return;
}
el.innerHTML = top.map(c => {
@ -432,17 +432,17 @@
function renderOneComment(c, d, isReply) {
const av = (d.author || '?')[0].toUpperCase();
const ts = c.created_at ? new Date(c.created_at * 1000).toLocaleString('en-US', {month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}) : '';
const replyBtn = isReply ? '' : `<button onclick="startReply('${c.entry_id}')" class="text-xs text-[#475569] hover:text-[#94a3b8] ml-2 transition">Reply</button>`;
const replyBtn = isReply ? '' : `<button onclick="startReply('${c.entry_id}')" class="text-xs text-[#8899a6] hover:text-[#b0bec5] ml-2 transition">Reply</button>`;
const indent = isReply ? 'ml-10 pl-4 border-l-2 border-[#c9a84c]/30' : '';
return `<div class="flex gap-3 ${indent}" data-comment-id="${c.entry_id}">
<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">${av}</div>
<div class="flex-1 min-w-0">
<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-[#475569] text-xs">${ts}</span>
<span class="text-[#8899a6] text-xs">${ts}</span>
${replyBtn}
</div>
<p class="text-[#94a3b8] text-sm leading-relaxed">${escHtml(d.text || '')}</p>
<p class="text-[#b0bec5] text-sm leading-relaxed">${escHtml(d.text || '')}</p>
</div>
</div>`;
}
@ -456,10 +456,10 @@
form.id = 'replyForm';
form.className = 'ml-10 pl-4 border-l-2 border-[#c9a84c]/30 mt-2';
form.innerHTML = `<div class="flex gap-2 items-end">
<textarea id="replyText" rows="2" placeholder="Reply..." class="flex-1 px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] text-sm resize-none"></textarea>
<textarea id="replyText" rows="2" placeholder="Reply..." class="flex-1 px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] text-sm resize-none"></textarea>
<div class="flex flex-col gap-1">
<button onclick="postComment('${commentId}')" class="px-3 py-1.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-xs transition">Reply</button>
<button onclick="cancelReply()" class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-[#94a3b8] rounded-lg text-xs transition">Cancel</button>
<button onclick="cancelReply()" class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-[#b0bec5] rounded-lg text-xs transition">Cancel</button>
</div>
</div>`;
el.insertAdjacentElement('afterend', form);

View File

@ -14,19 +14,19 @@
<!-- Greeting -->
<div class="mb-8">
<h1 id="greeting" class="text-2xl font-bold text-white mb-1"></h1>
<p class="text-[#94a3b8] text-sm">Here are your pending tasks.</p>
<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-[#94a3b8] text-sm">Loading tasks...</div>
<div class="text-[#b0bec5] text-sm">Loading tasks...</div>
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden text-center py-20">
<div class="text-5xl mb-4">&#127881;</div>
<h2 class="text-xl font-semibold text-white mb-2">You're all caught up</h2>
<p class="text-[#94a3b8]">No tasks need your attention right now.</p>
<p class="text-[#b0bec5]">No tasks need your attention right now.</p>
</div>
</div>
{{end}}
@ -64,12 +64,12 @@
<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-[#94a3b8]">${ref}</span>` : ''}
${ref ? `<span class="text-xs font-mono text-[#b0bec5]">${ref}</span>` : ''}
<span class="text-white font-medium flex-1">${escHtml(title)}</span>
${due ? `<span class="text-xs text-[#94a3b8]">Due: ${due}</span>` : ''}
${due ? `<span class="text-xs text-[#b0bec5]">Due: ${due}</span>` : ''}
</div>
<div class="flex items-center gap-3 text-xs text-[#475569]">
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#94a3b8]">${status}</span>
<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>

View File

@ -5,68 +5,57 @@
<h1 class="text-3xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</h1>
<p class="text-[#94a3b8] mt-2 text-sm">Secure M&A deal management</p>
<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">
<div 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-[#94a3b8] text-sm mb-6">Enter your email to receive a login code.</p>
<p id="login-hint" 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>
<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="emailForm" class="space-y-5">
<div class="space-y-5">
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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"
<!-- Code (always visible) -->
<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" 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">
<p id="code-status" class="text-[#8899a6] text-xs mt-1.5"></p>
</div>
<button type="button" id="emailBtn" onclick="sendCode()"
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" style="display:none" class="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-[#94a3b8] 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-[#94a3b8] 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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
</div>
<button type="submit" id="codeBtn"
<button type="button" id="codeBtn" onclick="verifyCode()" style="display:none"
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-[#94a3b8] text-sm hover:text-white transition">
&larr; Use a different email
</button>
<button id="resendBtn" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
<div id="resendRow" style="display:none" class="flex items-center justify-center">
<button id="resendBtn" onclick="resendCode()" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
Resend code
</button>
</div>
</div>
</div>
<p class="text-center text-[#475569] text-xs mt-6">
<p class="text-center text-[#8899a6] text-xs mt-6">
Don&rsquo;t have an account? Dealspace is invite-only.<br>
<a href="/#demo" class="text-[#c9a84c] hover:underline">Request access on muskepo.com</a>
</p>
<p class="text-center text-[#475569] text-xs mt-3">&copy; 2026 Muskepo B.V. — Amsterdam</p>
<p class="text-center text-[#8899a6] text-xs mt-3">&copy; 2026 Muskepo B.V. — Amsterdam</p>
</div>
{{end}}
@ -84,17 +73,27 @@
}
let currentEmail = '';
let codeSent = false;
// Step 1: Send challenge
document.getElementById('emailForm').addEventListener('submit', async (e) => {
e.preventDefault();
// Enter on email field sends code, Enter on code field verifies
document.getElementById('email').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); sendCode(); }
});
document.getElementById('code').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); verifyCode(); }
});
async function sendCode() {
const btn = document.getElementById('emailBtn');
const errorEl = document.getElementById('error-email');
btn.disabled = true;
btn.textContent = 'Sending code...';
errorEl.classList.add('hidden');
const statusEl = document.getElementById('code-status');
currentEmail = document.getElementById('email').value.trim().toLowerCase();
if (!currentEmail) return;
btn.disabled = true;
btn.textContent = 'Sending...';
errorEl.classList.add('hidden');
statusEl.textContent = 'Sending code...';
try {
const res = await fetch('/api/auth/challenge', {
@ -102,29 +101,31 @@
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').style.display='none';
document.getElementById('step-code').style.display='block';
codeSent = true;
statusEl.textContent = 'Code sent to ' + currentEmail;
document.getElementById('emailBtn').style.display = 'none';
document.getElementById('codeBtn').style.display = 'block';
document.getElementById('resendRow').style.display = 'flex';
document.getElementById('code').focus();
} catch (err) {
errorEl.textContent = err.message;
errorEl.classList.remove('hidden');
statusEl.textContent = '';
} finally {
btn.disabled = false;
btn.textContent = 'Send login code';
}
});
}
// Step 2: Verify code
document.getElementById('codeForm').addEventListener('submit', async (e) => {
e.preventDefault();
async function verifyCode() {
const btn = document.getElementById('codeBtn');
const errorEl = document.getElementById('error-code');
const codeVal = document.getElementById('code').value.trim();
if (!codeVal || !currentEmail) return;
btn.disabled = true;
btn.textContent = 'Verifying...';
errorEl.classList.add('hidden');
@ -133,22 +134,14 @@
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(),
}),
body: JSON.stringify({ email: currentEmail, code: codeVal }),
});
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));
// Set cookie for server-side auth (OAuth consent flow)
document.cookie = 'ds_token=' + data.token + '; path=/; SameSite=Lax; max-age=3600';
// Redirect to next URL or default to tasks
window.location.href = nextURL;
} catch (err) {
errorEl.textContent = err.message;
@ -156,46 +149,32 @@
btn.disabled = false;
btn.textContent = 'Verify & sign in';
}
});
}
// Back button
document.getElementById('backBtn').addEventListener('click', () => {
document.getElementById('step-code').style.display='none';
document.getElementById('step-email').style.display='block';
document.getElementById('code').value = '';
document.getElementById('error-code').classList.add('hidden');
document.getElementById('email').focus();
});
// Resend button
document.getElementById('resendBtn').addEventListener('click', async () => {
async function resendCode() {
const btn = document.getElementById('resendBtn');
const statusEl = document.getElementById('code-status');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
await fetch('/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: currentEmail }),
});
statusEl.textContent = 'New code sent to ' + currentEmail;
btn.textContent = 'Code sent!';
setTimeout(() => {
btn.textContent = 'Resend code';
btn.disabled = false;
}, 3000);
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'));
}
if (e.target.value.length === 6 && codeSent) verifyCode();
});
</script>
{{end}}

View File

@ -5,33 +5,33 @@
<h1 class="text-3xl font-bold text-white tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</h1>
<p class="text-[#94a3b8] mt-2 text-sm">First-time setup</p>
<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-[#94a3b8] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
<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-[#94a3b8] mb-1.5">Full name</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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-[#94a3b8] mb-1.5">Email</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
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-[#94a3b8] mb-1.5">Password</label>
<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-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
<p class="text-xs text-[#475569] mt-1">Minimum 8 characters</p>
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">
@ -40,7 +40,7 @@
</form>
</div>
<p class="text-center text-[#475569] text-xs mt-8">&copy; 2026 Muskepo B.V. — Amsterdam</p>
<p class="text-center text-[#8899a6] text-xs mt-8">&copy; 2026 Muskepo B.V. — Amsterdam</p>
</div>
{{end}}

View File

@ -21,8 +21,8 @@
<div id="testRoleBanner" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#b45309;color:#fff;text-align:center;font-size:12px;padding:3px 0;z-index:999;pointer-events:none"></div>
<div class="flex items-center gap-4">
{{block "header-right-extra" .}}{{end}}
<span id="userName" class="text-sm text-[#94a3b8]"></span>
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
<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>
@ -30,13 +30,13 @@
<!-- Sidebar -->
<nav class="w-60 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-57px)] sticky top-[57px] shrink-0 flex flex-col justify-between">
<div class="p-3 space-y-0.5">
<a href="/app/tasks" class="sidebar-link {{if eq .ActiveNav "tasks"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
<a href="/app/tasks" class="sidebar-link {{if eq .ActiveNav "tasks"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base 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 {{if eq .ActiveNav "projects"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
<a href="/app/projects" class="sidebar-link {{if eq .ActiveNav "projects"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base 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 {{if eq .ActiveNav "orgs"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
<a href="/app/orgs" class="sidebar-link {{if eq .ActiveNav "orgs"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base 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>
@ -55,7 +55,7 @@
<option value="advisor">Advisor</option>
</select>
</div>
<a href="/admin" class="sidebar-link {{if eq .ActiveNav "admin"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
<a href="/admin" class="sidebar-link {{if eq .ActiveNav "admin"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base 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>