491 lines
26 KiB
Plaintext
491 lines
26 KiB
Plaintext
package templates
|
|
|
|
import "dealroom/internal/model"
|
|
import "fmt"
|
|
import "time"
|
|
|
|
templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model.DealActivity, fileCounts map[string]int, lastActivity map[string]*time.Time) {
|
|
@Layout(profile, "dashboard") {
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">Dashboard</h1>
|
|
<p class="text-sm text-gray-500 mt-1">Overview of all active deal rooms and recent activity.</p>
|
|
</div>
|
|
<button onclick="document.getElementById('newRoomModal').classList.remove('hidden')" class="h-9 px-4 rounded-lg bg-teal-500 text-white text-sm font-medium flex items-center gap-1.5 hover:bg-teal-600 transition">
|
|
<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="M12 4v16m8-8H4"></path></svg>
|
|
New Room
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="grid grid-cols-6 gap-3">
|
|
@statCard("ACTIVE ROOMS", fmt.Sprintf("%d", countActive(deals)), fmt.Sprintf("%d total", len(deals)), "folder")
|
|
@statCard("PROSPECT", fmt.Sprintf("%d", countByStages(deals, "prospect", "pipeline")), "prospecting", "file")
|
|
@statCard("INTERNAL", fmt.Sprintf("%d", countByStages(deals, "internal", "")), "internal review", "users")
|
|
@statCard("INITIAL MARKETING", fmt.Sprintf("%d", countByStages(deals, "initial_marketing", "initial_review")), "marketing", "trend")
|
|
@statCard("IOI", fmt.Sprintf("%d", countByStages(deals, "ioi", "due_diligence")), "indication of interest", "trend")
|
|
@statCard("LOI", fmt.Sprintf("%d", countByStages(deals, "loi", "final_negotiation")), "letter of intent", "trend")
|
|
</div>
|
|
|
|
<!-- Content Grid -->
|
|
<div class="grid grid-cols-3 gap-6">
|
|
<!-- Deal Rooms -->
|
|
<div class="col-span-2 bg-gray-900 rounded-lg border border-gray-800">
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-800">
|
|
<h2 class="text-sm font-semibold">Active Deal Rooms</h2>
|
|
<a href="/deals" class="text-xs text-teal-400 hover:underline flex items-center gap-1">
|
|
View all
|
|
<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="M7 17L17 7M17 7H7m10 0v10"></path></svg>
|
|
</a>
|
|
</div>
|
|
if len(deals) == 0 {
|
|
<div class="p-8 text-center text-gray-500 text-sm">
|
|
No deal rooms yet. Create one to get started.
|
|
</div>
|
|
} else {
|
|
<div class="divide-y divide-gray-800">
|
|
for _, deal := range deals {
|
|
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s", deal.ID)) } class="flex items-center gap-4 px-4 py-3 hover:bg-gray-800/50 transition group">
|
|
<div class="w-8 h-8 rounded bg-teal-500/10 flex items-center justify-center text-teal-400 text-xs font-bold shrink-0">
|
|
{ string(deal.Name[len(deal.Name)-1:]) }
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium group-hover:text-teal-400 transition">{ deal.Name }</span>
|
|
@StageBadge(deal.Stage)
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-0.5">{ fmt.Sprintf("%d docs", fileCounts[deal.ID]) } · { deal.TargetCompany }</p>
|
|
</div>
|
|
<div class="text-xs text-gray-500">{ formatLastAccessed(lastActivity[deal.ID]) }</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800">
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-800">
|
|
<h2 class="text-sm font-semibold">Recent Activity</h2>
|
|
<a href="/audit" class="text-xs text-teal-400 hover:underline flex items-center gap-1">
|
|
Full log
|
|
<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="M7 17L17 7M17 7H7m10 0v10"></path></svg>
|
|
</a>
|
|
</div>
|
|
<div class="p-4 space-y-4">
|
|
for _, act := range activities {
|
|
<div class="flex items-start gap-3">
|
|
@activityIcon(act.ActivityType)
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm">
|
|
<span class="font-medium">{ act.ActivityType }</span>
|
|
<span class="text-gray-400"> { act.ResourceName }</span>
|
|
</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">
|
|
{ act.UserName } · { act.CreatedAt.Format("Jan 2, 3:04 PM") }
|
|
if act.DealName != "" {
|
|
<span class="text-gray-600"> · </span>
|
|
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s", act.DealID)) } class="text-teal-400 hover:underline">{ act.DealName }</a>
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New Room Modal -->
|
|
<div id="newRoomModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
|
|
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('newRoomModal').classList.add('hidden')"></div>
|
|
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-xl mx-4 p-6">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<h2 class="text-lg font-bold">Create New Room</h2>
|
|
<button onclick="document.getElementById('newRoomModal').classList.add('hidden')" class="text-gray-500 hover:text-gray-300">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
</button>
|
|
</div>
|
|
@newRoomForm()
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
func countActive(deals []*model.Deal) int {
|
|
count := 0
|
|
for _, d := range deals {
|
|
if d.Stage != "closed" && d.Stage != "dead" {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countByStage(deals []*model.Deal, stage string) int {
|
|
count := 0
|
|
for _, d := range deals {
|
|
if d.Stage == stage {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countByStages(deals []*model.Deal, stage1 string, stage2 string) int {
|
|
count := 0
|
|
for _, d := range deals {
|
|
if d.Stage == stage1 || (stage2 != "" && d.Stage == stage2) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func totalFiles(fc map[string]int) int {
|
|
total := 0
|
|
for _, c := range fc {
|
|
total += c
|
|
}
|
|
return total
|
|
}
|
|
|
|
func formatLastAccessed(t *time.Time) string {
|
|
if t == nil {
|
|
return "Never accessed"
|
|
}
|
|
return "Last accessed " + t.Format("Jan 2")
|
|
}
|
|
|
|
templ statCard(label, value, subtitle, iconType string) {
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800 p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">{ label }</span>
|
|
<div class="text-teal-400">
|
|
if iconType == "folder" {
|
|
<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"></path></svg>
|
|
}
|
|
if iconType == "file" {
|
|
<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 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
|
}
|
|
if iconType == "users" {
|
|
<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
|
}
|
|
if iconType == "trend" {
|
|
<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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="text-2xl font-bold">{ value }</div>
|
|
<p class="text-xs text-gray-500 mt-1">{ subtitle }</p>
|
|
</div>
|
|
}
|
|
|
|
templ newRoomForm() {
|
|
<form id="newRoomFormEl" action="/deals/create" method="POST" class="max-h-[75vh] overflow-y-auto pr-1">
|
|
<!-- Step Indicator -->
|
|
<div class="flex items-center gap-2 mb-5">
|
|
<div id="stepIndicator1" class="flex items-center gap-1.5">
|
|
<div class="w-6 h-6 rounded-full bg-teal-500 text-white text-xs flex items-center justify-center font-bold">1</div>
|
|
<span class="text-xs font-medium text-teal-400">Deal Info</span>
|
|
</div>
|
|
<div class="w-8 h-px bg-gray-700"></div>
|
|
<div id="stepIndicator2" class="flex items-center gap-1.5">
|
|
<div class="w-6 h-6 rounded-full bg-gray-700 text-gray-400 text-xs flex items-center justify-center font-bold">2</div>
|
|
<span class="text-xs text-gray-500">Add Users</span>
|
|
</div>
|
|
<div class="w-8 h-px bg-gray-700"></div>
|
|
<div id="stepIndicator3" class="flex items-center gap-1.5">
|
|
<div class="w-6 h-6 rounded-full bg-gray-700 text-gray-400 text-xs flex items-center justify-center font-bold">3</div>
|
|
<span class="text-xs text-gray-500">Folders</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1: Deal Info -->
|
|
<div id="step1" class="space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Project Name <span class="text-red-400">*</span></label>
|
|
<input type="text" name="name" required class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Company Name</label>
|
|
<input type="text" name="target_company" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Stage</label>
|
|
<select name="stage" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
|
|
<option value="prospect">Prospect</option>
|
|
<option value="internal">Internal</option>
|
|
<option value="initial_marketing">Initial Marketing</option>
|
|
<option value="ioi">IOI</option>
|
|
<option value="loi">LOI</option>
|
|
<option value="closed">Closed</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Industry</label>
|
|
<div class="relative">
|
|
<input type="text" name="industry" id="industryInput" placeholder="Type to search or add..." autocomplete="off"
|
|
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
<div id="industrySuggestions" class="hidden absolute z-20 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-xl max-h-48 overflow-y-auto"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Deal Size</label>
|
|
<input type="number" name="deal_size" step="0.01" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Currency</label>
|
|
<select name="currency" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
|
|
<option value="USD">USD</option>
|
|
<option value="EUR">EUR</option>
|
|
<option value="GBP">GBP</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Description</label>
|
|
<textarea name="description" rows="2" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none resize-none"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Internal Deal Team <span class="text-gray-600">(type email and press Enter)</span></label>
|
|
<div id="teamTags" class="flex flex-wrap gap-1.5 p-2 bg-gray-800 border border-gray-700 rounded-lg min-h-[42px] cursor-text" onclick="document.getElementById('teamInput').focus()">
|
|
<input type="text" id="teamInput" placeholder="team@company.com" autocomplete="off"
|
|
class="flex-1 min-w-[150px] bg-transparent border-none text-sm text-gray-100 focus:outline-none placeholder-gray-600"/>
|
|
</div>
|
|
<input type="hidden" name="invite_emails" id="inviteEmailsHidden"/>
|
|
<div id="teamSuggestions" class="hidden relative z-20 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-xl max-h-36 overflow-y-auto"></div>
|
|
</div>
|
|
<div class="flex justify-end gap-3 pt-2">
|
|
<button type="button" onclick="document.getElementById('newRoomModal').classList.add('hidden')" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Cancel</button>
|
|
<button type="button" onclick="goToStep(2)" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Add Users</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Add Users (Groups & Roles) -->
|
|
<div id="step2" class="space-y-4 hidden">
|
|
<p class="text-sm text-gray-400 mb-2">Create groups and assign permissions. Each group gets a specific role.</p>
|
|
<div id="userGroups" class="space-y-3">
|
|
<div class="p-3 bg-gray-800/50 border border-gray-700 rounded-lg">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<input type="text" name="group_name_0" value="Administrators" placeholder="Group name" class="bg-transparent text-sm font-medium text-gray-100 focus:outline-none border-b border-transparent focus:border-teal-500"/>
|
|
<select name="group_role_0" class="px-2 py-1 bg-gray-700 border border-gray-600 rounded text-xs text-gray-100 focus:outline-none">
|
|
<option value="administrator">Administrator</option>
|
|
<option value="contributor">Contributor</option>
|
|
<option value="viewer_no_download">Viewer (no downloads)</option>
|
|
<option value="viewer_watermark">Viewer (downloads w/ watermark)</option>
|
|
<option value="viewer_full">Viewer (downloads, no watermark)</option>
|
|
</select>
|
|
</div>
|
|
<input type="text" name="group_emails_0" placeholder="Add emails separated by commas..." class="w-full px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="addUserGroup()" class="flex items-center gap-1.5 text-xs text-teal-400 hover:text-teal-300 transition">
|
|
<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="M12 4v16m8-8H4"></path></svg>
|
|
Add Group
|
|
</button>
|
|
<div class="flex justify-between pt-2">
|
|
<button type="button" onclick="goToStep(1)" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Back</button>
|
|
<button type="button" onclick="goToStep(3)" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Folder Structure</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Folder Structure -->
|
|
<div id="step3" class="space-y-4 hidden">
|
|
<p class="text-sm text-gray-400 mb-2">Choose how to set up your folder structure.</p>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<button type="button" onclick="selectFolderPath('manual')" id="btnManual" class="p-4 border-2 border-teal-500 rounded-lg text-left hover:bg-gray-800/50 transition">
|
|
<div class="text-sm font-medium text-teal-400 mb-1">Build Manually</div>
|
|
<p class="text-xs text-gray-500">Create your own folder structure</p>
|
|
</button>
|
|
<button type="button" onclick="selectFolderPath('upload')" id="btnUpload" class="p-4 border-2 border-gray-700 rounded-lg text-left hover:bg-gray-800/50 transition">
|
|
<div class="text-sm font-medium text-gray-300 mb-1">Upload Request List</div>
|
|
<p class="text-xs text-gray-500">Auto-generate from Excel file</p>
|
|
</button>
|
|
</div>
|
|
<!-- Manual path -->
|
|
<div id="folderManual">
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Folder Structure <span class="text-gray-600">(one path per line)</span></label>
|
|
<textarea name="folder_structure" rows="4" placeholder="Financial Documents Legal Documents Technical DD/Architecture Technical DD/Security" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none resize-none font-mono"></textarea>
|
|
<div class="mt-2">
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Save as template <span class="text-gray-600">(optional)</span></label>
|
|
<input type="text" name="template_name" placeholder="e.g. Standard M&A" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
|
</div>
|
|
</div>
|
|
<!-- Upload path (hidden by default) -->
|
|
<div id="folderUpload" class="hidden">
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Upload Excel Request List</label>
|
|
<div class="border-2 border-dashed border-gray-700 rounded-lg p-6 text-center hover:border-teal-500/50 transition cursor-pointer" onclick="document.getElementById('excelUploadInput').click()">
|
|
<svg class="w-8 h-8 text-gray-500 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
|
|
<p class="text-sm text-gray-400">Click to upload or drag and drop</p>
|
|
<p class="text-xs text-gray-600 mt-1">Excel files (.xlsx, .xls, .csv)</p>
|
|
</div>
|
|
<input type="file" id="excelUploadInput" name="request_list_file" accept=".xlsx,.xls,.csv" class="hidden"/>
|
|
<div id="excelFileName" class="mt-2 text-xs text-teal-400 hidden"></div>
|
|
</div>
|
|
<div class="flex justify-between pt-2">
|
|
<button type="button" onclick="goToStep(2)" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Back</button>
|
|
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Create Room</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<script>
|
|
// Multi-step navigation
|
|
var currentStep = 1;
|
|
function goToStep(step) {
|
|
if (step === 2) {
|
|
var nameInput = document.querySelector('#step1 input[name="name"]');
|
|
if (!nameInput.value.trim()) { nameInput.focus(); return; }
|
|
}
|
|
document.getElementById('step1').classList.toggle('hidden', step !== 1);
|
|
document.getElementById('step2').classList.toggle('hidden', step !== 2);
|
|
document.getElementById('step3').classList.toggle('hidden', step !== 3);
|
|
for (var i = 1; i <= 3; i++) {
|
|
var el = document.getElementById('stepIndicator' + i);
|
|
var circle = el.querySelector('div');
|
|
var text = el.querySelector('span');
|
|
if (i <= step) {
|
|
circle.className = 'w-6 h-6 rounded-full bg-teal-500 text-white text-xs flex items-center justify-center font-bold';
|
|
text.className = 'text-xs font-medium text-teal-400';
|
|
} else {
|
|
circle.className = 'w-6 h-6 rounded-full bg-gray-700 text-gray-400 text-xs flex items-center justify-center font-bold';
|
|
text.className = 'text-xs text-gray-500';
|
|
}
|
|
}
|
|
currentStep = step;
|
|
}
|
|
|
|
// User groups
|
|
var groupCount = 1;
|
|
function addUserGroup() {
|
|
var html = '<div class="p-3 bg-gray-800/50 border border-gray-700 rounded-lg">' +
|
|
'<div class="flex items-center justify-between mb-2">' +
|
|
'<input type="text" name="group_name_' + groupCount + '" placeholder="Group name" class="bg-transparent text-sm font-medium text-gray-100 focus:outline-none border-b border-transparent focus:border-teal-500"/>' +
|
|
'<select name="group_role_' + groupCount + '" class="px-2 py-1 bg-gray-700 border border-gray-600 rounded text-xs text-gray-100 focus:outline-none">' +
|
|
'<option value="administrator">Administrator</option>' +
|
|
'<option value="contributor">Contributor</option>' +
|
|
'<option value="viewer_no_download">Viewer (no downloads)</option>' +
|
|
'<option value="viewer_watermark">Viewer (downloads w/ watermark)</option>' +
|
|
'<option value="viewer_full">Viewer (downloads, no watermark)</option>' +
|
|
'</select></div>' +
|
|
'<input type="text" name="group_emails_' + groupCount + '" placeholder="Add emails separated by commas..." class="w-full px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-100 focus:border-teal-500 focus:outline-none"/>' +
|
|
'</div>';
|
|
document.getElementById('userGroups').insertAdjacentHTML('beforeend', html);
|
|
groupCount++;
|
|
}
|
|
|
|
// Folder path selection
|
|
function selectFolderPath(path) {
|
|
document.getElementById('folderManual').classList.toggle('hidden', path !== 'manual');
|
|
document.getElementById('folderUpload').classList.toggle('hidden', path !== 'upload');
|
|
document.getElementById('btnManual').className = path === 'manual'
|
|
? 'p-4 border-2 border-teal-500 rounded-lg text-left hover:bg-gray-800/50 transition'
|
|
: 'p-4 border-2 border-gray-700 rounded-lg text-left hover:bg-gray-800/50 transition';
|
|
document.getElementById('btnUpload').className = path === 'upload'
|
|
? 'p-4 border-2 border-teal-500 rounded-lg text-left hover:bg-gray-800/50 transition'
|
|
: 'p-4 border-2 border-gray-700 rounded-lg text-left hover:bg-gray-800/50 transition';
|
|
}
|
|
document.getElementById('excelUploadInput').addEventListener('change', function(e) {
|
|
if (e.target.files.length > 0) {
|
|
var el = document.getElementById('excelFileName');
|
|
el.textContent = 'Selected: ' + e.target.files[0].name;
|
|
el.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
// Industry autocomplete
|
|
var industries = [];
|
|
fetch('/api/industries').then(function(r) { return r.json(); }).then(function(data) { industries = data || []; });
|
|
var industryInput = document.getElementById('industryInput');
|
|
var industrySuggestions = document.getElementById('industrySuggestions');
|
|
industryInput.addEventListener('input', function() {
|
|
var val = this.value.toLowerCase();
|
|
if (!val) { industrySuggestions.classList.add('hidden'); return; }
|
|
var matches = industries.filter(function(i) { return i.toLowerCase().includes(val); });
|
|
if (matches.length === 0) {
|
|
industrySuggestions.innerHTML = '<div class="px-3 py-2 text-xs text-gray-500 italic">Press Enter to add "' + this.value + '" as custom industry</div>';
|
|
} else {
|
|
industrySuggestions.innerHTML = matches.map(function(m) {
|
|
return '<button type="button" class="block w-full text-left px-3 py-1.5 text-sm text-gray-200 hover:bg-gray-700 transition" onclick="selectIndustry(\'' + m.replace(/'/g, "\\'") + '\')">' + m + '</button>';
|
|
}).join('');
|
|
}
|
|
industrySuggestions.classList.remove('hidden');
|
|
});
|
|
industryInput.addEventListener('blur', function() { setTimeout(function() { industrySuggestions.classList.add('hidden'); }, 200); });
|
|
function selectIndustry(val) { industryInput.value = val; industrySuggestions.classList.add('hidden'); }
|
|
|
|
// Internal Deal Team: tag/chip input with auto-suggest
|
|
var teamEmails = [];
|
|
var teamInput = document.getElementById('teamInput');
|
|
var teamTags = document.getElementById('teamTags');
|
|
var teamSuggestions = document.getElementById('teamSuggestions');
|
|
|
|
function addTeamEmail(email) {
|
|
email = email.trim();
|
|
if (!email || teamEmails.includes(email)) return;
|
|
teamEmails.push(email);
|
|
var tag = document.createElement('span');
|
|
tag.className = 'inline-flex items-center gap-1 px-2 py-0.5 bg-teal-500/10 text-teal-400 text-xs rounded-full';
|
|
tag.innerHTML = email + '<button type="button" class="hover:text-red-400 ml-0.5" onclick="removeTeamEmail(this, \'' + email + '\')">×</button>';
|
|
teamTags.insertBefore(tag, teamInput);
|
|
document.getElementById('inviteEmailsHidden').value = teamEmails.join('\n');
|
|
teamInput.value = '';
|
|
teamSuggestions.classList.add('hidden');
|
|
}
|
|
|
|
function removeTeamEmail(btn, email) {
|
|
teamEmails = teamEmails.filter(function(e) { return e !== email; });
|
|
btn.parentElement.remove();
|
|
document.getElementById('inviteEmailsHidden').value = teamEmails.join('\n');
|
|
}
|
|
|
|
teamInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
|
|
e.preventDefault();
|
|
var val = this.value.replace(/,/g, '').trim();
|
|
if (val) addTeamEmail(val);
|
|
}
|
|
if (e.key === 'Backspace' && !this.value && teamEmails.length > 0) {
|
|
removeTeamEmail(teamTags.querySelector('span:last-of-type button'), teamEmails[teamEmails.length - 1]);
|
|
}
|
|
});
|
|
|
|
var searchTimeout;
|
|
teamInput.addEventListener('input', function() {
|
|
var val = this.value.trim();
|
|
clearTimeout(searchTimeout);
|
|
if (val.length < 2) { teamSuggestions.classList.add('hidden'); return; }
|
|
searchTimeout = setTimeout(function() {
|
|
fetch('/api/contacts/search?q=' + encodeURIComponent(val))
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!data || data.length === 0) { teamSuggestions.classList.add('hidden'); return; }
|
|
teamSuggestions.innerHTML = data.map(function(c) {
|
|
return '<button type="button" class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-700 transition" onclick="addTeamEmail(\'' + c.email + '\')">' +
|
|
'<span class="text-gray-200">' + c.name + '</span> <span class="text-gray-500 text-xs">' + c.email + '</span></button>';
|
|
}).join('');
|
|
teamSuggestions.classList.remove('hidden');
|
|
});
|
|
}, 200);
|
|
});
|
|
teamInput.addEventListener('blur', function() { setTimeout(function() { teamSuggestions.classList.add('hidden'); }, 200); });
|
|
</script>
|
|
}
|
|
|
|
templ activityIcon(actType string) {
|
|
<div class={ "w-6 h-6 rounded-full flex items-center justify-center shrink-0",
|
|
templ.KV("bg-teal-500/20 text-teal-400", actType == "upload"),
|
|
templ.KV("bg-blue-500/20 text-blue-400", actType == "view"),
|
|
templ.KV("bg-amber-500/20 text-amber-400", actType == "edit"),
|
|
templ.KV("bg-purple-500/20 text-purple-400", actType == "download"),
|
|
templ.KV("bg-gray-500/20 text-gray-400", actType != "upload" && actType != "view" && actType != "edit" && actType != "download") }>
|
|
<div class="w-2 h-2 rounded-full bg-current"></div>
|
|
</div>
|
|
}
|