fix(slides): remove duplicate cacheElements/bindEvents functions causing null error
The duplicate functions at lines 455-486 were redefining cacheElements and bindEvents with wrong element IDs (kebab-case vs camelCase in HTML). This caused 'Cannot read properties of null' error on slides app init.
This commit is contained in:
parent
08469ecbf6
commit
e3b5929b99
39 changed files with 3376 additions and 1697 deletions
33
PROMPT.md
33
PROMPT.md
|
|
@ -569,25 +569,30 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Final Steps Before Commit
|
## COMPILATION POLICY - CRITICAL
|
||||||
|
|
||||||
```bash
|
**NEVER compile during development. NEVER run `cargo build` or `cargo check`. Use static analysis only.**
|
||||||
# Check for warnings
|
|
||||||
cargo check 2>&1 | grep warning
|
|
||||||
|
|
||||||
# Audit dependencies (must be 0 warnings)
|
### Workflow
|
||||||
cargo audit
|
|
||||||
|
|
||||||
# Build both modes
|
1. Make all code changes
|
||||||
cargo build
|
2. Use `diagnostics` tool for static analysis (NOT compilation)
|
||||||
cargo build --features desktop
|
3. Fix any errors found by diagnostics
|
||||||
|
4. **At the end**, inform user what needs restart
|
||||||
|
|
||||||
# Verify no dead code with _ prefixes
|
### After All Changes Complete
|
||||||
grep -r "let _" src/ --include="*.rs"
|
|
||||||
|
|
||||||
# Verify no CDN references
|
| Change Type | User Action Required |
|
||||||
grep -r "unpkg.com\|cdnjs\|jsdelivr" ui/
|
|-------------|----------------------|
|
||||||
```
|
| Rust code (`.rs` files) | "Recompile and restart **botui**" |
|
||||||
|
| HTML templates (`.html` in ui/) | "Browser refresh only" |
|
||||||
|
| CSS/JS files | "Browser refresh only" |
|
||||||
|
| Askama templates (`.html` in src/) | "Recompile and restart **botui**" |
|
||||||
|
| Cargo.toml changes | "Recompile and restart **botui**" |
|
||||||
|
|
||||||
|
**Format:** At the end of your response, always state:
|
||||||
|
- ✅ **No restart needed** - browser refresh only
|
||||||
|
- 🔄 **Restart botui** - recompile required
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ const SUITE_DIRS: &[&str] = &[
|
||||||
"goals",
|
"goals",
|
||||||
"player",
|
"player",
|
||||||
"canvas",
|
"canvas",
|
||||||
|
"people",
|
||||||
|
"crm",
|
||||||
|
"billing",
|
||||||
|
"products",
|
||||||
|
"tickets",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub async fn index() -> impl IntoResponse {
|
pub async fn index() -> impl IntoResponse {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="admin/admin.css" />
|
||||||
|
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<!-- Sidebar Navigation -->
|
<!-- Sidebar Navigation -->
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
|
|
@ -716,5 +718,5 @@
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<link rel="stylesheet" href="admin.css" />
|
<link rel="stylesheet" href="admin/admin.css" />
|
||||||
<script src="admin.js"></script>
|
<script src="admin/admin.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||||
|
|
||||||
<div class="analytics-container" id="analytics-app">
|
<div class="analytics-container" id="analytics-app">
|
||||||
<header class="analytics-header">
|
<header class="analytics-header">
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
|
|
@ -534,6 +536,6 @@
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="analytics.css" />
|
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||||
<script src="analytics.js"></script>
|
<script src="analytics/analytics.js"></script>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Attendant Console - General Bots -->
|
<!-- Attendant Console - General Bots -->
|
||||||
<link rel="stylesheet" href="attendant.css" />
|
<link rel="stylesheet" href="attendant/attendant.css" />
|
||||||
|
|
||||||
<!-- CRM Disabled State -->
|
<!-- CRM Disabled State -->
|
||||||
<!-- CRM Disabled State - Hidden by default, CRM is now enabled by default -->
|
<!-- CRM Disabled State - Hidden by default, CRM is now enabled by default -->
|
||||||
|
|
@ -572,4 +572,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<script src="attendant.js"></script>
|
<script src="attendant/attendant.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- Billing - Invoices, Payments & Quotes -->
|
<!-- Billing - Invoices, Payments & Quotes -->
|
||||||
<!-- Dynamics nomenclature: Quote → Invoice → Payment -->
|
<!-- Dynamics nomenclature: Quote → Invoice → Payment -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/billing/billing.css">
|
<link rel="stylesheet" href="billing/billing.css" />
|
||||||
|
|
||||||
<div class="billing-container">
|
<div class="billing-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
@ -9,26 +9,62 @@
|
||||||
<div class="billing-header-left">
|
<div class="billing-header-left">
|
||||||
<h1 data-i18n="billing-title">Billing</h1>
|
<h1 data-i18n="billing-title">Billing</h1>
|
||||||
<nav class="billing-tabs">
|
<nav class="billing-tabs">
|
||||||
<button class="billing-tab active" data-view="invoices" data-i18n="billing-invoices">Invoices</button>
|
<button
|
||||||
<button class="billing-tab" data-view="payments" data-i18n="billing-payments">Payments</button>
|
class="billing-tab active"
|
||||||
<button class="billing-tab" data-view="quotes" data-i18n="billing-quotes">Quotes</button>
|
data-view="invoices"
|
||||||
|
data-i18n="billing-invoices"
|
||||||
|
>
|
||||||
|
Invoices
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="billing-tab"
|
||||||
|
data-view="payments"
|
||||||
|
data-i18n="billing-payments"
|
||||||
|
>
|
||||||
|
Payments
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="billing-tab"
|
||||||
|
data-view="quotes"
|
||||||
|
data-i18n="billing-quotes"
|
||||||
|
>
|
||||||
|
Quotes
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="billing-header-right">
|
<div class="billing-header-right">
|
||||||
<div class="billing-search">
|
<div class="billing-search">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.35-4.35" />
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
placeholder="Search invoices, quotes..."
|
placeholder="Search invoices, quotes..."
|
||||||
data-i18n-placeholder="billing-search-placeholder"
|
data-i18n-placeholder="billing-search-placeholder"
|
||||||
hx-get="/api/billing/search"
|
hx-get="/api/billing/search"
|
||||||
hx-trigger="keyup changed delay:300ms"
|
hx-trigger="keyup changed delay:300ms"
|
||||||
hx-target="#billing-search-results">
|
hx-target="#billing-search-results"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="billing-new-invoice">
|
<button class="btn-primary" id="billing-new-invoice">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="billing-new-invoice">New Invoice</span>
|
<span data-i18n="billing-new-invoice">New Invoice</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -42,47 +78,111 @@
|
||||||
<div class="billing-summary">
|
<div class="billing-summary">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon pending">
|
<div class="summary-icon pending">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-pending">Pending</span>
|
<span class="summary-label" data-i18n="billing-pending"
|
||||||
<span class="summary-value" hx-get="/api/billing/stats/pending" hx-trigger="load">$0</span>
|
>Pending</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value"
|
||||||
|
hx-get="/api/billing/stats/pending"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon overdue">
|
<div class="summary-icon overdue">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
width="20"
|
||||||
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||||
|
/>
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-overdue">Overdue</span>
|
<span class="summary-label" data-i18n="billing-overdue"
|
||||||
<span class="summary-value overdue" hx-get="/api/billing/stats/overdue" hx-trigger="load">$0</span>
|
>Overdue</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value overdue"
|
||||||
|
hx-get="/api/billing/stats/overdue"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon paid">
|
<div class="summary-icon paid">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-paid-month">Paid This Month</span>
|
<span class="summary-label" data-i18n="billing-paid-month"
|
||||||
<span class="summary-value paid" hx-get="/api/billing/stats/paid-month" hx-trigger="load">$0</span>
|
>Paid This Month</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value paid"
|
||||||
|
hx-get="/api/billing/stats/paid-month"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon total">
|
<div class="summary-icon total">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="1" x2="12" y2="23" />
|
||||||
|
<path
|
||||||
|
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-revenue-month">Revenue This Month</span>
|
<span class="summary-label" data-i18n="billing-revenue-month"
|
||||||
<span class="summary-value" hx-get="/api/billing/stats/revenue-month" hx-trigger="load">$0</span>
|
>Revenue This Month</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value"
|
||||||
|
hx-get="/api/billing/stats/revenue-month"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,23 +191,64 @@
|
||||||
<div id="billing-invoices-view" class="billing-view active">
|
<div id="billing-invoices-view" class="billing-view active">
|
||||||
<div class="billing-list-header">
|
<div class="billing-list-header">
|
||||||
<div class="list-filters">
|
<div class="list-filters">
|
||||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="status">
|
<select
|
||||||
<option value="all" data-i18n="billing-filter-all">All Invoices</option>
|
hx-get="/api/billing/invoices"
|
||||||
<option value="draft" data-i18n="billing-filter-draft">Draft</option>
|
hx-trigger="change"
|
||||||
<option value="sent" data-i18n="billing-filter-sent">Sent</option>
|
hx-target="#invoices-table-body"
|
||||||
<option value="paid" data-i18n="billing-filter-paid">Paid</option>
|
hx-include="this"
|
||||||
<option value="overdue" data-i18n="billing-filter-overdue">Overdue</option>
|
name="status"
|
||||||
<option value="cancelled" data-i18n="billing-filter-cancelled">Cancelled</option>
|
>
|
||||||
|
<option value="all" data-i18n="billing-filter-all">
|
||||||
|
All Invoices
|
||||||
|
</option>
|
||||||
|
<option value="draft" data-i18n="billing-filter-draft">
|
||||||
|
Draft
|
||||||
|
</option>
|
||||||
|
<option value="sent" data-i18n="billing-filter-sent">
|
||||||
|
Sent
|
||||||
|
</option>
|
||||||
|
<option value="paid" data-i18n="billing-filter-paid">
|
||||||
|
Paid
|
||||||
|
</option>
|
||||||
|
<option value="overdue" data-i18n="billing-filter-overdue">
|
||||||
|
Overdue
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="cancelled"
|
||||||
|
data-i18n="billing-filter-cancelled"
|
||||||
|
>
|
||||||
|
Cancelled
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="period">
|
<select
|
||||||
<option value="all" data-i18n="billing-period-all">All Time</option>
|
hx-get="/api/billing/invoices"
|
||||||
<option value="month" data-i18n="billing-period-month">This Month</option>
|
hx-trigger="change"
|
||||||
<option value="quarter" data-i18n="billing-period-quarter">This Quarter</option>
|
hx-target="#invoices-table-body"
|
||||||
<option value="year" data-i18n="billing-period-year">This Year</option>
|
hx-include="this"
|
||||||
|
name="period"
|
||||||
|
>
|
||||||
|
<option value="all" data-i18n="billing-period-all">
|
||||||
|
All Time
|
||||||
|
</option>
|
||||||
|
<option value="month" data-i18n="billing-period-month">
|
||||||
|
This Month
|
||||||
|
</option>
|
||||||
|
<option value="quarter" data-i18n="billing-period-quarter">
|
||||||
|
This Quarter
|
||||||
|
</option>
|
||||||
|
<option value="year" data-i18n="billing-period-year">
|
||||||
|
This Year
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-actions">
|
<div class="list-actions">
|
||||||
<button class="action-btn" hx-get="/api/billing/invoices/export" data-i18n="billing-export">Export</button>
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
hx-get="/api/billing/invoices/export"
|
||||||
|
data-i18n="billing-export"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="billing-table">
|
<table class="billing-table">
|
||||||
|
|
@ -122,7 +263,11 @@
|
||||||
<th data-i18n="billing-col-actions">Actions</th>
|
<th data-i18n="billing-col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="invoices-table-body" hx-get="/api/billing/invoices" hx-trigger="load">
|
<tbody
|
||||||
|
id="invoices-table-body"
|
||||||
|
hx-get="/api/billing/invoices"
|
||||||
|
hx-trigger="load"
|
||||||
|
>
|
||||||
<!-- Invoices loaded via HTMX -->
|
<!-- Invoices loaded via HTMX -->
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -132,18 +277,49 @@
|
||||||
<div id="billing-payments-view" class="billing-view">
|
<div id="billing-payments-view" class="billing-view">
|
||||||
<div class="billing-list-header">
|
<div class="billing-list-header">
|
||||||
<div class="list-filters">
|
<div class="list-filters">
|
||||||
<select hx-get="/api/billing/payments" hx-trigger="change" hx-target="#payments-table-body" hx-include="this" name="method">
|
<select
|
||||||
<option value="all" data-i18n="billing-method-all">All Methods</option>
|
hx-get="/api/billing/payments"
|
||||||
<option value="bank" data-i18n="billing-method-bank">Bank Transfer</option>
|
hx-trigger="change"
|
||||||
<option value="card" data-i18n="billing-method-card">Credit Card</option>
|
hx-target="#payments-table-body"
|
||||||
<option value="pix" data-i18n="billing-method-pix">PIX</option>
|
hx-include="this"
|
||||||
<option value="boleto" data-i18n="billing-method-boleto">Boleto</option>
|
name="method"
|
||||||
<option value="cash" data-i18n="billing-method-cash">Cash</option>
|
>
|
||||||
|
<option value="all" data-i18n="billing-method-all">
|
||||||
|
All Methods
|
||||||
|
</option>
|
||||||
|
<option value="bank" data-i18n="billing-method-bank">
|
||||||
|
Bank Transfer
|
||||||
|
</option>
|
||||||
|
<option value="card" data-i18n="billing-method-card">
|
||||||
|
Credit Card
|
||||||
|
</option>
|
||||||
|
<option value="pix" data-i18n="billing-method-pix">
|
||||||
|
PIX
|
||||||
|
</option>
|
||||||
|
<option value="boleto" data-i18n="billing-method-boleto">
|
||||||
|
Boleto
|
||||||
|
</option>
|
||||||
|
<option value="cash" data-i18n="billing-method-cash">
|
||||||
|
Cash
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" hx-get="/suite/billing/partials/payment-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
hx-get="/suite/billing/partials/payment-form.html"
|
||||||
|
hx-target="#billing-modal-content"
|
||||||
|
hx-on::after-request="openBillingModal()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="billing-record-payment">Record Payment</span>
|
<span data-i18n="billing-record-payment">Record Payment</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -160,8 +336,11 @@
|
||||||
<th data-i18n="billing-col-actions">Actions</th>
|
<th data-i18n="billing-col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="payments-table-body" hx-get="/api/billing/payments" hx-trigger="load">
|
<tbody
|
||||||
</tbody>
|
id="payments-table-body"
|
||||||
|
hx-get="/api/billing/payments"
|
||||||
|
hx-trigger="load"
|
||||||
|
></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -169,18 +348,49 @@
|
||||||
<div id="billing-quotes-view" class="billing-view">
|
<div id="billing-quotes-view" class="billing-view">
|
||||||
<div class="billing-list-header">
|
<div class="billing-list-header">
|
||||||
<div class="list-filters">
|
<div class="list-filters">
|
||||||
<select hx-get="/api/billing/quotes" hx-trigger="change" hx-target="#quotes-table-body" hx-include="this" name="status">
|
<select
|
||||||
<option value="all" data-i18n="billing-quote-all">All Quotes</option>
|
hx-get="/api/billing/quotes"
|
||||||
<option value="draft" data-i18n="billing-quote-draft">Draft</option>
|
hx-trigger="change"
|
||||||
<option value="sent" data-i18n="billing-quote-sent">Sent</option>
|
hx-target="#quotes-table-body"
|
||||||
<option value="accepted" data-i18n="billing-quote-accepted">Accepted</option>
|
hx-include="this"
|
||||||
<option value="rejected" data-i18n="billing-quote-rejected">Rejected</option>
|
name="status"
|
||||||
<option value="expired" data-i18n="billing-quote-expired">Expired</option>
|
>
|
||||||
|
<option value="all" data-i18n="billing-quote-all">
|
||||||
|
All Quotes
|
||||||
|
</option>
|
||||||
|
<option value="draft" data-i18n="billing-quote-draft">
|
||||||
|
Draft
|
||||||
|
</option>
|
||||||
|
<option value="sent" data-i18n="billing-quote-sent">
|
||||||
|
Sent
|
||||||
|
</option>
|
||||||
|
<option value="accepted" data-i18n="billing-quote-accepted">
|
||||||
|
Accepted
|
||||||
|
</option>
|
||||||
|
<option value="rejected" data-i18n="billing-quote-rejected">
|
||||||
|
Rejected
|
||||||
|
</option>
|
||||||
|
<option value="expired" data-i18n="billing-quote-expired">
|
||||||
|
Expired
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" hx-get="/suite/billing/partials/quote-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
hx-get="/suite/billing/partials/quote-form.html"
|
||||||
|
hx-target="#billing-modal-content"
|
||||||
|
hx-on::after-request="openBillingModal()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="billing-new-quote">New Quote</span>
|
<span data-i18n="billing-new-quote">New Quote</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -198,8 +408,11 @@
|
||||||
<th data-i18n="billing-col-actions">Actions</th>
|
<th data-i18n="billing-col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="quotes-table-body" hx-get="/api/billing/quotes" hx-trigger="load">
|
<tbody
|
||||||
</tbody>
|
id="quotes-table-body"
|
||||||
|
hx-get="/api/billing/quotes"
|
||||||
|
hx-trigger="load"
|
||||||
|
></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -215,35 +428,47 @@
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
// Tab switching
|
// Tab switching
|
||||||
document.querySelectorAll('.billing-tab').forEach(tab => {
|
document.querySelectorAll(".billing-tab").forEach((tab) => {
|
||||||
tab.addEventListener('click', function() {
|
tab.addEventListener("click", function () {
|
||||||
document.querySelectorAll('.billing-tab').forEach(t => t.classList.remove('active'));
|
document
|
||||||
document.querySelectorAll('.billing-view').forEach(v => v.classList.remove('active'));
|
.querySelectorAll(".billing-tab")
|
||||||
this.classList.add('active');
|
.forEach((t) => t.classList.remove("active"));
|
||||||
|
document
|
||||||
|
.querySelectorAll(".billing-view")
|
||||||
|
.forEach((v) => v.classList.remove("active"));
|
||||||
|
this.classList.add("active");
|
||||||
const view = this.dataset.view;
|
const view = this.dataset.view;
|
||||||
document.getElementById(`billing-${view}-view`).classList.add('active');
|
document
|
||||||
|
.getElementById(`billing-${view}-view`)
|
||||||
|
.classList.add("active");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// New Invoice button
|
// New Invoice button
|
||||||
document.getElementById('billing-new-invoice').addEventListener('click', function() {
|
document
|
||||||
htmx.ajax('GET', '/suite/billing/partials/invoice-form.html', '#billing-modal-content').then(() => {
|
.getElementById("billing-new-invoice")
|
||||||
|
.addEventListener("click", function () {
|
||||||
|
htmx.ajax(
|
||||||
|
"GET",
|
||||||
|
"/suite/billing/partials/invoice-form.html",
|
||||||
|
"#billing-modal-content",
|
||||||
|
).then(() => {
|
||||||
openBillingModal();
|
openBillingModal();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modal functions
|
// Modal functions
|
||||||
window.openBillingModal = function () {
|
window.openBillingModal = function () {
|
||||||
document.getElementById('billing-modal').classList.add('open');
|
document.getElementById("billing-modal").classList.add("open");
|
||||||
};
|
};
|
||||||
|
|
||||||
window.closeBillingModal = function () {
|
window.closeBillingModal = function () {
|
||||||
document.getElementById('billing-modal').classList.remove('open');
|
document.getElementById("billing-modal").classList.remove("open");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keyboard shortcut: Escape to close modal
|
// Keyboard shortcut: Escape to close modal
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
closeBillingModal();
|
closeBillingModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="calendar/calendar.css" />
|
||||||
|
|
||||||
<!-- Calendar - Event Management -->
|
<!-- Calendar - Event Management -->
|
||||||
<div class="calendar-container" id="calendar-app">
|
<div class="calendar-container" id="calendar-app">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
|
|
|
||||||
|
|
@ -7,58 +7,128 @@
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<aside class="canvas-toolbar">
|
<aside class="canvas-toolbar">
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<button class="tool-btn active" data-tool="select" onclick="selectTool('select')" title="Select (V)">
|
<button
|
||||||
|
class="tool-btn active"
|
||||||
|
data-tool="select"
|
||||||
|
onclick="selectTool('select')"
|
||||||
|
title="Select (V)"
|
||||||
|
>
|
||||||
<span>↖</span>
|
<span>↖</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="pan" onclick="selectTool('pan')" title="Pan (H)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="pan"
|
||||||
|
onclick="selectTool('pan')"
|
||||||
|
title="Pan (H)"
|
||||||
|
>
|
||||||
<span>✋</span>
|
<span>✋</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<button class="tool-btn" data-tool="pencil" onclick="selectTool('pencil')" title="Pencil (P)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="pencil"
|
||||||
|
onclick="selectTool('pencil')"
|
||||||
|
title="Pencil (P)"
|
||||||
|
>
|
||||||
<span>✏️</span>
|
<span>✏️</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="brush" onclick="selectTool('brush')" title="Brush (B)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="brush"
|
||||||
|
onclick="selectTool('brush')"
|
||||||
|
title="Brush (B)"
|
||||||
|
>
|
||||||
<span>🖌️</span>
|
<span>🖌️</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="eraser" onclick="selectTool('eraser')" title="Eraser (E)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="eraser"
|
||||||
|
onclick="selectTool('eraser')"
|
||||||
|
title="Eraser (E)"
|
||||||
|
>
|
||||||
<span>🧹</span>
|
<span>🧹</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<button class="tool-btn" data-tool="rectangle" onclick="selectTool('rectangle')" title="Rectangle (R)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="rectangle"
|
||||||
|
onclick="selectTool('rectangle')"
|
||||||
|
title="Rectangle (R)"
|
||||||
|
>
|
||||||
<span>▢</span>
|
<span>▢</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="ellipse" onclick="selectTool('ellipse')" title="Ellipse (O)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="ellipse"
|
||||||
|
onclick="selectTool('ellipse')"
|
||||||
|
title="Ellipse (O)"
|
||||||
|
>
|
||||||
<span>◯</span>
|
<span>◯</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="line" onclick="selectTool('line')" title="Line (L)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="line"
|
||||||
|
onclick="selectTool('line')"
|
||||||
|
title="Line (L)"
|
||||||
|
>
|
||||||
<span>╱</span>
|
<span>╱</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="arrow" onclick="selectTool('arrow')" title="Arrow (A)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="arrow"
|
||||||
|
onclick="selectTool('arrow')"
|
||||||
|
title="Arrow (A)"
|
||||||
|
>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<button class="tool-btn" data-tool="text" onclick="selectTool('text')" title="Text (T)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="text"
|
||||||
|
onclick="selectTool('text')"
|
||||||
|
title="Text (T)"
|
||||||
|
>
|
||||||
<span>T</span>
|
<span>T</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="sticky" onclick="selectTool('sticky')" title="Sticky Note (S)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="sticky"
|
||||||
|
onclick="selectTool('sticky')"
|
||||||
|
title="Sticky Note (S)"
|
||||||
|
>
|
||||||
<span>📝</span>
|
<span>📝</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="image" onclick="selectTool('image')" title="Image (I)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="image"
|
||||||
|
onclick="selectTool('image')"
|
||||||
|
title="Image (I)"
|
||||||
|
>
|
||||||
<span>🖼️</span>
|
<span>🖼️</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<button class="tool-btn" data-tool="connector" onclick="selectTool('connector')" title="Connector (C)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="connector"
|
||||||
|
onclick="selectTool('connector')"
|
||||||
|
title="Connector (C)"
|
||||||
|
>
|
||||||
<span>⤳</span>
|
<span>⤳</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-tool="frame" onclick="selectTool('frame')" title="Frame (F)">
|
<button
|
||||||
|
class="tool-btn"
|
||||||
|
data-tool="frame"
|
||||||
|
onclick="selectTool('frame')"
|
||||||
|
title="Frame (F)"
|
||||||
|
>
|
||||||
<span>⬚</span>
|
<span>⬚</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,12 +136,28 @@
|
||||||
<div class="toolbar-divider"></div>
|
<div class="toolbar-divider"></div>
|
||||||
|
|
||||||
<div class="toolbar-section colors">
|
<div class="toolbar-section colors">
|
||||||
<input type="color" id="stroke-color" value="#000000" title="Stroke Color" onchange="setStrokeColor(this.value)">
|
<input
|
||||||
<input type="color" id="fill-color" value="#ffffff" title="Fill Color" onchange="setFillColor(this.value)">
|
type="color"
|
||||||
|
id="stroke-color"
|
||||||
|
value="#000000"
|
||||||
|
title="Stroke Color"
|
||||||
|
onchange="setStrokeColor(this.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id="fill-color"
|
||||||
|
value="#ffffff"
|
||||||
|
title="Fill Color"
|
||||||
|
onchange="setFillColor(this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-section">
|
<div class="toolbar-section">
|
||||||
<select id="stroke-width" onchange="setStrokeWidth(this.value)" title="Stroke Width">
|
<select
|
||||||
|
id="stroke-width"
|
||||||
|
onchange="setStrokeWidth(this.value)"
|
||||||
|
title="Stroke Width"
|
||||||
|
>
|
||||||
<option value="1">1px</option>
|
<option value="1">1px</option>
|
||||||
<option value="2" selected>2px</option>
|
<option value="2" selected>2px</option>
|
||||||
<option value="4">4px</option>
|
<option value="4">4px</option>
|
||||||
|
|
@ -86,34 +172,82 @@
|
||||||
<!-- Top Bar -->
|
<!-- Top Bar -->
|
||||||
<header class="canvas-header">
|
<header class="canvas-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<input type="text" id="canvas-name" value="Untitled Canvas" class="canvas-name-input"
|
<input
|
||||||
onblur="renameCanvas(this.value)">
|
type="text"
|
||||||
|
id="canvas-name"
|
||||||
|
value="Untitled Canvas"
|
||||||
|
class="canvas-name-input"
|
||||||
|
onblur="renameCanvas(this.value)"
|
||||||
|
/>
|
||||||
<span class="save-status" id="save-status">Saved</span>
|
<span class="save-status" id="save-status">Saved</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-center">
|
<div class="header-center">
|
||||||
<div class="zoom-controls">
|
<div class="zoom-controls">
|
||||||
<button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
|
<button
|
||||||
|
class="zoom-btn"
|
||||||
|
onclick="zoomOut()"
|
||||||
|
title="Zoom Out"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
<span id="zoom-level">100%</span>
|
<span id="zoom-level">100%</span>
|
||||||
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
|
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">
|
||||||
<button class="zoom-btn" onclick="resetZoom()" title="Reset Zoom">⟲</button>
|
+
|
||||||
<button class="zoom-btn" onclick="fitToScreen()" title="Fit to Screen">⤢</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="zoom-btn"
|
||||||
|
onclick="resetZoom()"
|
||||||
|
title="Reset Zoom"
|
||||||
|
>
|
||||||
|
⟲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="zoom-btn"
|
||||||
|
onclick="fitToScreen()"
|
||||||
|
title="Fit to Screen"
|
||||||
|
>
|
||||||
|
⤢
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="btn-icon" onclick="undo()" title="Undo (Ctrl+Z)" data-i18n-title="canvas-undo">
|
<button
|
||||||
|
class="btn-icon"
|
||||||
|
onclick="undo()"
|
||||||
|
title="Undo (Ctrl+Z)"
|
||||||
|
data-i18n-title="canvas-undo"
|
||||||
|
>
|
||||||
<span>↶</span>
|
<span>↶</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon" onclick="redo()" title="Redo (Ctrl+Y)" data-i18n-title="canvas-redo">
|
<button
|
||||||
|
class="btn-icon"
|
||||||
|
onclick="redo()"
|
||||||
|
title="Redo (Ctrl+Y)"
|
||||||
|
data-i18n-title="canvas-redo"
|
||||||
|
>
|
||||||
<span>↷</span>
|
<span>↷</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
<button class="btn-icon" onclick="clearCanvas()" title="Clear Canvas" data-i18n-title="canvas-clear">
|
<button
|
||||||
|
class="btn-icon"
|
||||||
|
onclick="clearCanvas()"
|
||||||
|
title="Clear Canvas"
|
||||||
|
data-i18n-title="canvas-clear"
|
||||||
|
>
|
||||||
<span>🗑️</span>
|
<span>🗑️</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-secondary" onclick="exportCanvas()" data-i18n="canvas-export">
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
onclick="exportCanvas()"
|
||||||
|
data-i18n="canvas-export"
|
||||||
|
>
|
||||||
<span>📤</span> Export
|
<span>📤</span> Export
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" onclick="shareCanvas()" data-i18n="canvas-collaborate">
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
onclick="shareCanvas()"
|
||||||
|
data-i18n="canvas-collaborate"
|
||||||
|
>
|
||||||
<span>👥</span> Share
|
<span>👥</span> Share
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -145,32 +279,72 @@
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Position</label>
|
<label>Position</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" id="prop-x" placeholder="X" onchange="updateElementProperty('x', this.value)">
|
<input
|
||||||
<input type="number" id="prop-y" placeholder="Y" onchange="updateElementProperty('y', this.value)">
|
type="number"
|
||||||
|
id="prop-x"
|
||||||
|
placeholder="X"
|
||||||
|
onchange="updateElementProperty('x', this.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="prop-y"
|
||||||
|
placeholder="Y"
|
||||||
|
onchange="updateElementProperty('y', this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Size</label>
|
<label>Size</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" id="prop-width" placeholder="W" onchange="updateElementProperty('width', this.value)">
|
<input
|
||||||
<input type="number" id="prop-height" placeholder="H" onchange="updateElementProperty('height', this.value)">
|
type="number"
|
||||||
|
id="prop-width"
|
||||||
|
placeholder="W"
|
||||||
|
onchange="updateElementProperty('width', this.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="prop-height"
|
||||||
|
placeholder="H"
|
||||||
|
onchange="updateElementProperty('height', this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Rotation</label>
|
<label>Rotation</label>
|
||||||
<input type="number" id="prop-rotation" placeholder="0°" onchange="updateElementProperty('rotation', this.value)">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="prop-rotation"
|
||||||
|
placeholder="0°"
|
||||||
|
onchange="updateElementProperty('rotation', this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Opacity</label>
|
<label>Opacity</label>
|
||||||
<input type="range" id="prop-opacity" min="0" max="100" value="100" onchange="updateElementProperty('opacity', this.value)">
|
<input
|
||||||
|
type="range"
|
||||||
|
id="prop-opacity"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value="100"
|
||||||
|
onchange="updateElementProperty('opacity', this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Stroke</label>
|
<label>Stroke</label>
|
||||||
<input type="color" id="prop-stroke" onchange="updateElementProperty('strokeColor', this.value)">
|
<input
|
||||||
|
type="color"
|
||||||
|
id="prop-stroke"
|
||||||
|
onchange="updateElementProperty('strokeColor', this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Fill</label>
|
<label>Fill</label>
|
||||||
<input type="color" id="prop-fill" onchange="updateElementProperty('fillColor', this.value)">
|
<input
|
||||||
|
type="color"
|
||||||
|
id="prop-fill"
|
||||||
|
onchange="updateElementProperty('fillColor', this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -178,7 +352,12 @@
|
||||||
<h4>Canvas</h4>
|
<h4>Canvas</h4>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Background</label>
|
<label>Background</label>
|
||||||
<input type="color" id="canvas-bg" value="#ffffff" onchange="setCanvasBackground(this.value)">
|
<input
|
||||||
|
type="color"
|
||||||
|
id="canvas-bg"
|
||||||
|
value="#ffffff"
|
||||||
|
onchange="setCanvasBackground(this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Grid</label>
|
<label>Grid</label>
|
||||||
|
|
@ -191,7 +370,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="property-row">
|
<div class="property-row">
|
||||||
<label>Snap to Grid</label>
|
<label>Snap to Grid</label>
|
||||||
<input type="checkbox" id="snap-grid" onchange="toggleSnapToGrid(this.checked)">
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="snap-grid"
|
||||||
|
onchange="toggleSnapToGrid(this.checked)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -252,13 +435,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="export-bg">
|
<div class="export-bg">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="export-bg" checked>
|
<input type="checkbox" id="export-bg" checked />
|
||||||
Include Background
|
Include Background
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-secondary" onclick="closeExportModal()">Cancel</button>
|
<button class="btn-secondary" onclick="closeExportModal()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
<button class="btn-primary" onclick="doExport()">Export</button>
|
<button class="btn-primary" onclick="doExport()">Export</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -905,7 +1090,7 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentTool = 'select';
|
let currentTool = "select";
|
||||||
let isDrawing = false;
|
let isDrawing = false;
|
||||||
let startX, startY;
|
let startX, startY;
|
||||||
let elements = [];
|
let elements = [];
|
||||||
|
|
@ -913,33 +1098,34 @@ let selectedElement = null;
|
||||||
let history = [];
|
let history = [];
|
||||||
let historyIndex = -1;
|
let historyIndex = -1;
|
||||||
let zoom = 1;
|
let zoom = 1;
|
||||||
let panX = 0, panY = 0;
|
let panX = 0,
|
||||||
let strokeColor = '#000000';
|
panY = 0;
|
||||||
let fillColor = '#ffffff';
|
let strokeColor = "#000000";
|
||||||
|
let fillColor = "#ffffff";
|
||||||
let strokeWidth = 2;
|
let strokeWidth = 2;
|
||||||
let canvas, ctx;
|
let canvas, ctx;
|
||||||
let gridType = 'dots';
|
let gridType = "dots";
|
||||||
let snapToGrid = false;
|
let snapToGrid = false;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
canvas = document.getElementById('main-canvas');
|
canvas = document.getElementById("main-canvas");
|
||||||
ctx = canvas.getContext('2d');
|
ctx = canvas.getContext("2d");
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
drawGrid();
|
drawGrid();
|
||||||
|
|
||||||
window.addEventListener('resize', resizeCanvas);
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
|
||||||
canvas.addEventListener('mousedown', handleMouseDown);
|
canvas.addEventListener("mousedown", handleMouseDown);
|
||||||
canvas.addEventListener('mousemove', handleMouseMove);
|
canvas.addEventListener("mousemove", handleMouseMove);
|
||||||
canvas.addEventListener('mouseup', handleMouseUp);
|
canvas.addEventListener("mouseup", handleMouseUp);
|
||||||
canvas.addEventListener('wheel', handleWheel);
|
canvas.addEventListener("wheel", handleWheel);
|
||||||
canvas.addEventListener('contextmenu', handleContextMenu);
|
canvas.addEventListener("contextmenu", handleContextMenu);
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
const container = document.getElementById('canvas-container');
|
const container = document.getElementById("canvas-container");
|
||||||
canvas.width = container.clientWidth;
|
canvas.width = container.clientWidth;
|
||||||
canvas.height = container.clientHeight;
|
canvas.height = container.clientHeight;
|
||||||
redraw();
|
redraw();
|
||||||
|
|
@ -947,23 +1133,23 @@ function resizeCanvas() {
|
||||||
|
|
||||||
function selectTool(tool) {
|
function selectTool(tool) {
|
||||||
currentTool = tool;
|
currentTool = tool;
|
||||||
document.querySelectorAll('.tool-btn').forEach(btn => {
|
document.querySelectorAll(".tool-btn").forEach((btn) => {
|
||||||
btn.classList.toggle('active', btn.dataset.tool === tool);
|
btn.classList.toggle("active", btn.dataset.tool === tool);
|
||||||
});
|
});
|
||||||
|
|
||||||
const container = document.getElementById('canvas-container');
|
const container = document.getElementById("canvas-container");
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case 'select':
|
case "select":
|
||||||
container.style.cursor = 'default';
|
container.style.cursor = "default";
|
||||||
break;
|
break;
|
||||||
case 'pan':
|
case "pan":
|
||||||
container.style.cursor = 'grab';
|
container.style.cursor = "grab";
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case "text":
|
||||||
container.style.cursor = 'text';
|
container.style.cursor = "text";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
container.style.cursor = 'crosshair';
|
container.style.cursor = "crosshair";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -973,15 +1159,16 @@ function handleMouseDown(e) {
|
||||||
startY = (e.clientY - rect.top - panY) / zoom;
|
startY = (e.clientY - rect.top - panY) / zoom;
|
||||||
isDrawing = true;
|
isDrawing = true;
|
||||||
|
|
||||||
if (currentTool === 'select') {
|
if (currentTool === "select") {
|
||||||
selectedElement = findElementAt(startX, startY);
|
selectedElement = findElementAt(startX, startY);
|
||||||
if (selectedElement) {
|
if (selectedElement) {
|
||||||
showElementProperties(selectedElement);
|
showElementProperties(selectedElement);
|
||||||
} else {
|
} else {
|
||||||
hideElementProperties();
|
hideElementProperties();
|
||||||
}
|
}
|
||||||
} else if (currentTool === 'pan') {
|
} else if (currentTool === "pan") {
|
||||||
document.getElementById('canvas-container').style.cursor = 'grabbing';
|
document.getElementById("canvas-container").style.cursor =
|
||||||
|
"grabbing";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -989,4 +1176,273 @@ function handleMouseMove(e) {
|
||||||
if (!isDrawing) return;
|
if (!isDrawing) return;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const currentX = (e.
|
const currentX = (e.clientX - rect.left - panX) / zoom;
|
||||||
|
const currentY = (e.clientY - rect.top - panY) / zoom;
|
||||||
|
|
||||||
|
if (currentTool === "pan") {
|
||||||
|
panX += e.movementX;
|
||||||
|
panY += e.movementY;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(e) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
isDrawing = false;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const endX = (e.clientX - rect.left - panX) / zoom;
|
||||||
|
const endY = (e.clientY - rect.top - panY) / zoom;
|
||||||
|
|
||||||
|
if (currentTool === "pan") {
|
||||||
|
document.getElementById("canvas-container").style.cursor = "grab";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTool !== "select" && currentTool !== "pan") {
|
||||||
|
const element = {
|
||||||
|
id: Date.now(),
|
||||||
|
type: currentTool,
|
||||||
|
x: Math.min(startX, endX),
|
||||||
|
y: Math.min(startY, endY),
|
||||||
|
width: Math.abs(endX - startX),
|
||||||
|
height: Math.abs(endY - startY),
|
||||||
|
stroke: strokeColor,
|
||||||
|
fill: fillColor,
|
||||||
|
strokeWidth: strokeWidth,
|
||||||
|
};
|
||||||
|
elements.push(element);
|
||||||
|
saveHistory();
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWheel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
|
zoom *= delta;
|
||||||
|
zoom = Math.max(0.1, Math.min(5, zoom));
|
||||||
|
document.getElementById("zoom-level").textContent =
|
||||||
|
Math.round(zoom * 100) + "%";
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e) {
|
||||||
|
if (e.key === "Delete" && selectedElement) {
|
||||||
|
elements = elements.filter((el) => el !== selectedElement);
|
||||||
|
selectedElement = null;
|
||||||
|
hideElementProperties();
|
||||||
|
saveHistory();
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && e.key === "z") {
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && e.key === "y") {
|
||||||
|
redo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGrid() {
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = "var(--border-color, #e0e0e0)";
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
const gridSize = 20;
|
||||||
|
|
||||||
|
for (let x = 0; x < canvas.width; x += gridSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y < canvas.height; y += gridSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(canvas.width, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redraw() {
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
drawGrid();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(panX, panY);
|
||||||
|
ctx.scale(zoom, zoom);
|
||||||
|
|
||||||
|
elements.forEach((el) => drawElement(el));
|
||||||
|
|
||||||
|
if (selectedElement) {
|
||||||
|
drawSelection(selectedElement);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawElement(el) {
|
||||||
|
ctx.strokeStyle = el.stroke;
|
||||||
|
ctx.fillStyle = el.fill;
|
||||||
|
ctx.lineWidth = el.strokeWidth;
|
||||||
|
|
||||||
|
switch (el.type) {
|
||||||
|
case "rectangle":
|
||||||
|
ctx.strokeRect(el.x, el.y, el.width, el.height);
|
||||||
|
if (el.fill !== "transparent") {
|
||||||
|
ctx.fillRect(el.x, el.y, el.width, el.height);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ellipse":
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(
|
||||||
|
el.x + el.width / 2,
|
||||||
|
el.y + el.height / 2,
|
||||||
|
el.width / 2,
|
||||||
|
el.height / 2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Math.PI * 2,
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
if (el.fill !== "transparent") ctx.fill();
|
||||||
|
break;
|
||||||
|
case "line":
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(el.x, el.y);
|
||||||
|
ctx.lineTo(el.x + el.width, el.y + el.height);
|
||||||
|
ctx.stroke();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSelection(el) {
|
||||||
|
ctx.strokeStyle = "#007bff";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([5, 5]);
|
||||||
|
ctx.strokeRect(el.x - 5, el.y - 5, el.width + 10, el.height + 10);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findElementAt(x, y) {
|
||||||
|
for (let i = elements.length - 1; i >= 0; i--) {
|
||||||
|
const el = elements[i];
|
||||||
|
if (
|
||||||
|
x >= el.x &&
|
||||||
|
x <= el.x + el.width &&
|
||||||
|
y >= el.y &&
|
||||||
|
y <= el.y + el.height
|
||||||
|
) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showElementProperties(el) {
|
||||||
|
const panel = document.getElementById("properties-panel");
|
||||||
|
if (panel) panel.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideElementProperties() {
|
||||||
|
const panel = document.getElementById("properties-panel");
|
||||||
|
if (panel) panel.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHistory() {
|
||||||
|
history = history.slice(0, historyIndex + 1);
|
||||||
|
history.push(JSON.stringify(elements));
|
||||||
|
historyIndex = history.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
elements = JSON.parse(history[historyIndex]);
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
if (historyIndex < history.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
elements = JSON.parse(history[historyIndex]);
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStrokeColor(color) {
|
||||||
|
strokeColor = color;
|
||||||
|
if (selectedElement) {
|
||||||
|
selectedElement.stroke = color;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFillColor(color) {
|
||||||
|
fillColor = color;
|
||||||
|
if (selectedElement) {
|
||||||
|
selectedElement.fill = color;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStrokeWidth(width) {
|
||||||
|
strokeWidth = parseInt(width);
|
||||||
|
if (selectedElement) {
|
||||||
|
selectedElement.strokeWidth = strokeWidth;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
zoom = Math.min(5, zoom * 1.2);
|
||||||
|
document.getElementById("zoom-level").textContent =
|
||||||
|
Math.round(zoom * 100) + "%";
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
zoom = Math.max(0.1, zoom / 1.2);
|
||||||
|
document.getElementById("zoom-level").textContent =
|
||||||
|
Math.round(zoom * 100) + "%";
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
zoom = 1;
|
||||||
|
panX = 0;
|
||||||
|
panY = 0;
|
||||||
|
document.getElementById("zoom-level").textContent = "100%";
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCanvas() {
|
||||||
|
if (confirm("Clear all elements?")) {
|
||||||
|
elements = [];
|
||||||
|
selectedElement = null;
|
||||||
|
saveHistory();
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCanvas() {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "canvas.png";
|
||||||
|
link.href = canvas.toDataURL();
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCanvas() {
|
||||||
|
console.log("Canvas saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCollaborate() {
|
||||||
|
console.log("Collaboration toggled");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- CRM - Customer Relationship Management -->
|
<!-- CRM - Customer Relationship Management -->
|
||||||
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/crm/crm.css">
|
<link rel="stylesheet" href="crm/crm.css">
|
||||||
|
|
||||||
<div class="crm-container">
|
<div class="crm-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
||||||
|
|
||||||
<div class="dashboards-container" id="dashboards-app">
|
<div class="dashboards-container" id="dashboards-app">
|
||||||
<header class="dashboards-header">
|
<header class="dashboards-header">
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,11 @@
|
||||||
const toolboxItems = document.querySelectorAll('.toolbox-item');
|
const toolboxItems = document.querySelectorAll('.toolbox-item');
|
||||||
const canvas = document.getElementById('canvas-inner');
|
const canvas = document.getElementById('canvas-inner');
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn('initDragAndDrop: canvas-inner not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toolboxItems.forEach(item => {
|
toolboxItems.forEach(item => {
|
||||||
item.addEventListener('dragstart', (e) => {
|
item.addEventListener('dragstart', (e) => {
|
||||||
e.dataTransfer.setData('nodeType', item.dataset.nodeType);
|
e.dataTransfer.setData('nodeType', item.dataset.nodeType);
|
||||||
|
|
@ -1779,6 +1784,11 @@
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const container = document.getElementById('canvas-container');
|
const container = document.getElementById('canvas-container');
|
||||||
|
|
||||||
|
if (!canvas || !container) {
|
||||||
|
console.warn('initCanvasInteraction: canvas or canvas-container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Pan with middle mouse or space+drag
|
// Pan with middle mouse or space+drag
|
||||||
let isPanning = false;
|
let isPanning = false;
|
||||||
let panStart = { x: 0, y: 0 };
|
let panStart = { x: 0, y: 0 };
|
||||||
|
|
@ -2295,6 +2305,11 @@
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const contextMenu = document.getElementById('context-menu');
|
const contextMenu = document.getElementById('context-menu');
|
||||||
|
|
||||||
|
if (!canvas || !contextMenu) {
|
||||||
|
console.warn('initContextMenu: canvas or context-menu not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
canvas.addEventListener('contextmenu', (e) => {
|
canvas.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const nodeEl = e.target.closest('.node');
|
const nodeEl = e.target.closest('.node');
|
||||||
|
|
@ -2312,7 +2327,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideContextMenu() {
|
function hideContextMenu() {
|
||||||
document.getElementById('context-menu').classList.remove('visible');
|
const menu = document.getElementById('context-menu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.remove('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context Menu Actions
|
// Context Menu Actions
|
||||||
|
|
@ -2353,14 +2371,20 @@
|
||||||
|
|
||||||
// Modal Management
|
// Modal Management
|
||||||
function showModal(id) {
|
function showModal(id) {
|
||||||
document.getElementById(id).classList.add('visible');
|
const modal = document.getElementById(id);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('visible');
|
||||||
if (id === 'open-modal') {
|
if (id === 'open-modal') {
|
||||||
htmx.trigger('#file-list-content', 'load');
|
htmx.trigger('#file-list-content', 'load');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hideModal(id) {
|
function hideModal(id) {
|
||||||
document.getElementById(id).classList.remove('visible');
|
const modal = document.getElementById(id);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save Design
|
// Save Design
|
||||||
|
|
@ -2579,7 +2603,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideMagicPanel() {
|
function hideMagicPanel() {
|
||||||
document.getElementById('magic-panel').classList.remove('visible');
|
const panel = document.getElementById('magic-panel');
|
||||||
|
if (panel) {
|
||||||
|
panel.classList.remove('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function analyzeMagicSuggestions() {
|
async function analyzeMagicSuggestions() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="docs/docs.css" />
|
||||||
|
|
||||||
<div class="docs-app" id="docs-app">
|
<div class="docs-app" id="docs-app">
|
||||||
<div class="docs-toolbar">
|
<div class="docs-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
|
|
|
||||||
|
|
@ -10,35 +10,37 @@
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#d4f505" />
|
<meta name="theme-color" content="#d4f505" />
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Critical CSS - minimal styles for shell/layout (loaded sync) -->
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<link rel="stylesheet" href="css/apps-extended.css" />
|
|
||||||
<link rel="stylesheet" href="css/components.css" />
|
|
||||||
<link rel="stylesheet" href="css/base.css" />
|
<link rel="stylesheet" href="css/base.css" />
|
||||||
<link rel="stylesheet" href="css/partials.css" />
|
|
||||||
<link rel="stylesheet" href="css/theme-sentient.css" />
|
<link rel="stylesheet" href="css/theme-sentient.css" />
|
||||||
|
|
||||||
<!-- App-specific CSS -->
|
<!-- Non-critical CSS - loaded async for faster initial paint -->
|
||||||
<link rel="stylesheet" href="chat/chat.css" />
|
<link
|
||||||
<link rel="stylesheet" href="calendar/calendar.css" />
|
rel="stylesheet"
|
||||||
<link rel="stylesheet" href="drive/drive.css" />
|
href="css/components.css"
|
||||||
<link rel="stylesheet" href="mail/mail.css" />
|
media="print"
|
||||||
<link rel="stylesheet" href="meet/meet.css" />
|
onload="this.media='all'"
|
||||||
<link rel="stylesheet" href="paper/paper.css" />
|
/>
|
||||||
<link rel="stylesheet" href="sheet/sheet.css" />
|
<link
|
||||||
<link rel="stylesheet" href="slides/slides.css" />
|
rel="stylesheet"
|
||||||
<link rel="stylesheet" href="research/research.css" />
|
href="css/partials.css"
|
||||||
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
|
media="print"
|
||||||
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" />
|
onload="this.media='all'"
|
||||||
<link rel="stylesheet" href="analytics/analytics.css" />
|
/>
|
||||||
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
<link
|
||||||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
rel="stylesheet"
|
||||||
<link rel="stylesheet" href="crm/crm.css" />
|
href="css/apps-extended.css"
|
||||||
<link rel="stylesheet" href="billing/billing.css" />
|
media="print"
|
||||||
<link rel="stylesheet" href="products/products.css" />
|
onload="this.media='all'"
|
||||||
<link rel="stylesheet" href="tickets/tickets.css" />
|
/>
|
||||||
<link rel="stylesheet" href="docs/docs.css" />
|
<noscript>
|
||||||
<link rel="stylesheet" href="social/social.css" />
|
<link rel="stylesheet" href="css/components.css" />
|
||||||
|
<link rel="stylesheet" href="css/partials.css" />
|
||||||
|
<link rel="stylesheet" href="css/apps-extended.css" />
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- App-specific CSS loaded lazily by each view via CSSLoader -->
|
||||||
|
|
||||||
<!-- Local Libraries (no external CDN dependencies) -->
|
<!-- Local Libraries (no external CDN dependencies) -->
|
||||||
<script src="js/vendor/htmx.min.js"></script>
|
<script src="js/vendor/htmx.min.js"></script>
|
||||||
|
|
@ -53,6 +55,9 @@
|
||||||
<!-- i18n -->
|
<!-- i18n -->
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
|
|
||||||
|
<!-- CSS Lazy Loader - enables per-screen CSS loading -->
|
||||||
|
<script src="js/css-loader.js"></script>
|
||||||
|
|
||||||
<!-- Enable HTMX to process inline scripts in swapped content -->
|
<!-- Enable HTMX to process inline scripts in swapped content -->
|
||||||
<script>
|
<script>
|
||||||
htmx.config.allowEval = true;
|
htmx.config.allowEval = true;
|
||||||
|
|
@ -3286,7 +3291,6 @@
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
currentLoadedSection = null;
|
currentLoadedSection = null;
|
||||||
} finally {
|
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3316,13 +3320,39 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also listen for response errors
|
||||||
|
document.body.addEventListener(
|
||||||
|
"htmx:responseError",
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
event.detail.target &&
|
||||||
|
event.detail.target.id === "main-content"
|
||||||
|
) {
|
||||||
|
isLoadingSection = false;
|
||||||
|
currentLoadedSection = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Load initial content based on hash or default to chat
|
// Load initial content based on hash or default to chat
|
||||||
window.addEventListener("hashchange", handleHashChange);
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
||||||
// Initial load
|
// Initial load - wait for HTMX to be ready
|
||||||
setTimeout(() => {
|
function initialLoad() {
|
||||||
|
if (typeof htmx !== "undefined" && htmx.ajax) {
|
||||||
handleHashChange();
|
handleHashChange();
|
||||||
}, 100);
|
} else {
|
||||||
|
setTimeout(initialLoad, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
setTimeout(initialLoad, 50);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
setTimeout(initialLoad, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// GBAlerts - Global Notification System
|
// GBAlerts - Global Notification System
|
||||||
|
|
|
||||||
212
ui/suite/js/css-loader.js
Normal file
212
ui/suite/js/css-loader.js
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
/**
|
||||||
|
* CSS Lazy Loader - Efficient on-demand stylesheet loading
|
||||||
|
* Prevents duplicate loads and handles caching automatically
|
||||||
|
*/
|
||||||
|
const CSSLoader = (function () {
|
||||||
|
const loadedStyles = new Set();
|
||||||
|
const loadingPromises = new Map();
|
||||||
|
|
||||||
|
function getAbsoluteUrl(href) {
|
||||||
|
if (href.startsWith("http://") || href.startsWith("https://")) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
const base = window.location.pathname.includes("/suite/")
|
||||||
|
? "/suite/"
|
||||||
|
: "/";
|
||||||
|
if (href.startsWith("/")) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
return base + href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(href, options = {}) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
|
||||||
|
if (loadedStyles.has(absoluteUrl)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingPromises.has(absoluteUrl)) {
|
||||||
|
return loadingPromises.get(absoluteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
const existingLink = document.querySelector(
|
||||||
|
`link[href="${href}"], link[href="${absoluteUrl}"]`
|
||||||
|
);
|
||||||
|
if (existingLink) {
|
||||||
|
loadedStyles.add(absoluteUrl);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = href;
|
||||||
|
|
||||||
|
if (options.media) {
|
||||||
|
link.media = options.media;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.crossOrigin) {
|
||||||
|
link.crossOrigin = options.crossOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
link.onload = function () {
|
||||||
|
loadedStyles.add(absoluteUrl);
|
||||||
|
loadingPromises.delete(absoluteUrl);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
link.onerror = function () {
|
||||||
|
loadingPromises.delete(absoluteUrl);
|
||||||
|
reject(new Error(`Failed to load CSS: ${href}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertPoint =
|
||||||
|
options.insertAfter ||
|
||||||
|
document.querySelector('link[rel="stylesheet"]:last-of-type') ||
|
||||||
|
document.head.lastChild;
|
||||||
|
|
||||||
|
if (insertPoint && insertPoint.parentNode) {
|
||||||
|
insertPoint.parentNode.insertBefore(
|
||||||
|
link,
|
||||||
|
insertPoint.nextSibling
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadingPromises.set(absoluteUrl, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMultiple(hrefs, options = {}) {
|
||||||
|
return Promise.all(hrefs.map((href) => load(href, options)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function preload(href) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
|
||||||
|
if (loadedStyles.has(absoluteUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPreload = document.querySelector(
|
||||||
|
`link[rel="preload"][href="${href}"]`
|
||||||
|
);
|
||||||
|
if (existingPreload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "preload";
|
||||||
|
link.as = "style";
|
||||||
|
link.href = href;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoaded(href) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
return loadedStyles.has(absoluteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload(href) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
const link = document.querySelector(
|
||||||
|
`link[href="${href}"], link[href="${absoluteUrl}"]`
|
||||||
|
);
|
||||||
|
if (link) {
|
||||||
|
link.remove();
|
||||||
|
loadedStyles.delete(absoluteUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadForApp(appName) {
|
||||||
|
const appCssMap = {
|
||||||
|
admin: ["admin/admin.css"],
|
||||||
|
analytics: ["analytics/analytics.css"],
|
||||||
|
attendant: ["attendant/attendant.css"],
|
||||||
|
auth: ["auth/auth.css"],
|
||||||
|
billing: ["billing/billing.css"],
|
||||||
|
calendar: ["calendar/calendar.css"],
|
||||||
|
chat: ["chat/chat.css"],
|
||||||
|
crm: ["crm/crm.css"],
|
||||||
|
dashboards: ["dashboards/dashboards.css"],
|
||||||
|
docs: ["docs/docs.css"],
|
||||||
|
drive: ["drive/drive.css"],
|
||||||
|
learn: ["learn/learn.css"],
|
||||||
|
mail: ["mail/mail.css"],
|
||||||
|
meet: ["meet/meet.css"],
|
||||||
|
monitoring: ["monitoring/monitoring.css"],
|
||||||
|
paper: ["paper/paper.css"],
|
||||||
|
people: ["people/people.css"],
|
||||||
|
products: ["products/products.css"],
|
||||||
|
research: ["research/research.css"],
|
||||||
|
settings: ["settings/settings.css"],
|
||||||
|
sheet: ["sheet/sheet.css"],
|
||||||
|
slides: ["slides/slides.css"],
|
||||||
|
social: ["social/social.css"],
|
||||||
|
sources: ["sources/sources.css"],
|
||||||
|
tasks: ["tasks/tasks.css"],
|
||||||
|
tickets: ["tickets/tickets.css"],
|
||||||
|
tools: ["tools/tools.css"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssFiles = appCssMap[appName];
|
||||||
|
if (cssFiles && cssFiles.length > 0) {
|
||||||
|
return loadMultiple(cssFiles);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||||
|
if (link.href) {
|
||||||
|
loadedStyles.add(link.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:beforeSwap", function (event) {
|
||||||
|
const content = event.detail.serverResponse;
|
||||||
|
if (content && typeof content === "string") {
|
||||||
|
const cssMatches = content.match(
|
||||||
|
/<link[^>]+rel=["']stylesheet["'][^>]*>/gi
|
||||||
|
);
|
||||||
|
if (cssMatches) {
|
||||||
|
cssMatches.forEach((match) => {
|
||||||
|
const hrefMatch = match.match(/href=["']([^"']+)["']/i);
|
||||||
|
if (hrefMatch && hrefMatch[1]) {
|
||||||
|
load(hrefMatch[1]).catch((err) => {
|
||||||
|
console.warn(
|
||||||
|
"CSS preload failed:",
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: load,
|
||||||
|
loadMultiple: loadMultiple,
|
||||||
|
preload: preload,
|
||||||
|
isLoaded: isLoaded,
|
||||||
|
unload: unload,
|
||||||
|
loadForApp: loadForApp,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.CSSLoader = CSSLoader;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
|
<link rel="stylesheet" href="learn/learn.css" />
|
||||||
|
|
||||||
<div class="learn-container">
|
<div class="learn-container">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="learn-sidebar">
|
<aside class="learn-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h2 data-i18n="learn-title">📚 Learn</h2>
|
<h2 data-i18n="learn-title">📚 Learn</h2>
|
||||||
<button class="btn-icon-sm" onclick="toggleLearnSidebar()">
|
<button class="btn-icon-sm" onclick="toggleLearnSidebar()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -19,19 +28,27 @@
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statCoursesCompleted">0</span>
|
<span class="stat-value" id="statCoursesCompleted">0</span>
|
||||||
<span class="stat-label" data-i18n="learn-completed">Concluídos</span>
|
<span class="stat-label" data-i18n="learn-completed"
|
||||||
|
>Concluídos</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statCoursesInProgress">0</span>
|
<span class="stat-value" id="statCoursesInProgress">0</span>
|
||||||
<span class="stat-label" data-i18n="learn-in-progress">Em Andamento</span>
|
<span class="stat-label" data-i18n="learn-in-progress"
|
||||||
|
>Em Andamento</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statCertificates">0</span>
|
<span class="stat-value" id="statCertificates">0</span>
|
||||||
<span class="stat-label" data-i18n="learn-certificates">Certificados</span>
|
<span class="stat-label" data-i18n="learn-certificates"
|
||||||
|
>Certificados</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statTimeSpent">0h</span>
|
<span class="stat-value" id="statTimeSpent">0h</span>
|
||||||
<span class="stat-label" data-i18n="learn-time-spent">Tempo Total</span>
|
<span class="stat-label" data-i18n="learn-time-spent"
|
||||||
|
>Tempo Total</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,15 +95,21 @@
|
||||||
<h3 data-i18n="learn-difficulty">Dificuldade</h3>
|
<h3 data-i18n="learn-difficulty">Dificuldade</h3>
|
||||||
<div class="difficulty-filter">
|
<div class="difficulty-filter">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" checked data-difficulty="beginner">
|
<input type="checkbox" checked data-difficulty="beginner" />
|
||||||
<span class="difficulty-badge beginner">Iniciante</span>
|
<span class="difficulty-badge beginner">Iniciante</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" checked data-difficulty="intermediate">
|
<input
|
||||||
<span class="difficulty-badge intermediate">Intermediário</span>
|
type="checkbox"
|
||||||
|
checked
|
||||||
|
data-difficulty="intermediate"
|
||||||
|
/>
|
||||||
|
<span class="difficulty-badge intermediate"
|
||||||
|
>Intermediário</span
|
||||||
|
>
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" checked data-difficulty="advanced">
|
<input type="checkbox" checked data-difficulty="advanced" />
|
||||||
<span class="difficulty-badge advanced">Avançado</span>
|
<span class="difficulty-badge advanced">Avançado</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,7 +126,9 @@
|
||||||
<div class="certificates-preview" id="certificatesPreview">
|
<div class="certificates-preview" id="certificatesPreview">
|
||||||
<div class="empty-state-small">
|
<div class="empty-state-small">
|
||||||
<span>🏆</span>
|
<span>🏆</span>
|
||||||
<p data-i18n="learn-no-certificates">Nenhum certificado ainda</p>
|
<p data-i18n="learn-no-certificates">
|
||||||
|
Nenhum certificado ainda
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,34 +140,84 @@
|
||||||
<div class="learn-header">
|
<div class="learn-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" data-tab="catalog" onclick="switchTab('catalog')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab active"
|
||||||
|
data-tab="catalog"
|
||||||
|
onclick="switchTab('catalog')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
||||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
<path
|
||||||
|
d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-catalog">Catálogo</span>
|
<span data-i18n="learn-catalog">Catálogo</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="my-courses" onclick="switchTab('my-courses')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab"
|
||||||
|
data-tab="my-courses"
|
||||||
|
onclick="switchTab('my-courses')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
||||||
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-my-courses">Meus Cursos</span>
|
<span data-i18n="learn-my-courses">Meus Cursos</span>
|
||||||
<span class="badge" id="myCoursesCount">0</span>
|
<span class="badge" id="myCoursesCount">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="mandatory" onclick="switchTab('mandatory')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab"
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
data-tab="mandatory"
|
||||||
|
onclick="switchTab('mandatory')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||||
|
></path>
|
||||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-pending">Pendentes</span>
|
<span data-i18n="learn-pending">Pendentes</span>
|
||||||
<span class="badge warning" id="mandatoryCount">0</span>
|
<span class="badge warning" id="mandatoryCount">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="certificates" onclick="switchTab('certificates')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab"
|
||||||
|
data-tab="certificates"
|
||||||
|
onclick="switchTab('certificates')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="8" r="7"></circle>
|
<circle cx="12" cy="8" r="7"></circle>
|
||||||
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>
|
<polyline
|
||||||
|
points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"
|
||||||
|
></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-certificates">Certificados</span>
|
<span data-i18n="learn-certificates">Certificados</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -150,27 +225,57 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text" id="searchCourses" placeholder="Buscar cursos..." data-i18n-placeholder="learn-search-placeholder">
|
<input
|
||||||
|
type="text"
|
||||||
|
id="searchCourses"
|
||||||
|
placeholder="Buscar cursos..."
|
||||||
|
data-i18n-placeholder="learn-search-placeholder"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select id="sortCourses" class="sort-select">
|
<select id="sortCourses" class="sort-select">
|
||||||
<option value="recent" data-i18n="learn-sort-recent">Mais Recentes</option>
|
<option value="recent" data-i18n="learn-sort-recent">
|
||||||
<option value="popular" data-i18n="learn-sort-popular">Mais Populares</option>
|
Mais Recentes
|
||||||
<option value="duration-asc" data-i18n="learn-sort-duration-asc">Menor Duração</option>
|
</option>
|
||||||
<option value="duration-desc" data-i18n="learn-sort-duration-desc">Maior Duração</option>
|
<option value="popular" data-i18n="learn-sort-popular">
|
||||||
|
Mais Populares
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="duration-asc"
|
||||||
|
data-i18n="learn-sort-duration-asc"
|
||||||
|
>
|
||||||
|
Menor Duração
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="duration-desc"
|
||||||
|
data-i18n="learn-sort-duration-desc"
|
||||||
|
>
|
||||||
|
Maior Duração
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mandatory Training Alert -->
|
<!-- Mandatory Training Alert -->
|
||||||
<div class="mandatory-alert" id="mandatoryAlert" style="display: none;">
|
<div class="mandatory-alert" id="mandatoryAlert" style="display: none">
|
||||||
<div class="alert-icon">⚠️</div>
|
<div class="alert-icon">⚠️</div>
|
||||||
<div class="alert-content">
|
<div class="alert-content">
|
||||||
<strong data-i18n="learn-mandatory-alert-title">Treinamentos Obrigatórios Pendentes</strong>
|
<strong data-i18n="learn-mandatory-alert-title"
|
||||||
<p id="mandatoryAlertText">Você possui treinamentos obrigatórios com prazo próximo.</p>
|
>Treinamentos Obrigatórios Pendentes</strong
|
||||||
|
>
|
||||||
|
<p id="mandatoryAlertText">
|
||||||
|
Você possui treinamentos obrigatórios com prazo próximo.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary-sm" onclick="switchTab('mandatory')">
|
<button class="btn-primary-sm" onclick="switchTab('mandatory')">
|
||||||
<span data-i18n="learn-view-pending">Ver Pendentes</span>
|
<span data-i18n="learn-view-pending">Ver Pendentes</span>
|
||||||
|
|
@ -184,7 +289,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">✨</span>
|
<span class="section-icon">✨</span>
|
||||||
<span data-i18n="learn-recommended">Recomendados para Você</span>
|
<span data-i18n="learn-recommended"
|
||||||
|
>Recomendados para Você</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-carousel" id="recommendedCourses">
|
<div class="courses-carousel" id="recommendedCourses">
|
||||||
|
|
@ -197,14 +304,18 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">📚</span>
|
<span class="section-icon">📚</span>
|
||||||
<span data-i18n="learn-all-courses">Todos os Cursos</span>
|
<span data-i18n="learn-all-courses"
|
||||||
|
>Todos os Cursos</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
<span class="courses-count" id="coursesCountLabel">0 cursos</span>
|
<span class="courses-count" id="coursesCountLabel"
|
||||||
|
>0 cursos</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-grid" id="coursesGrid">
|
<div class="courses-grid" id="coursesGrid">
|
||||||
<!-- Courses loaded dynamically -->
|
<!-- Courses loaded dynamically -->
|
||||||
</div>
|
</div>
|
||||||
<div class="load-more" id="loadMore" style="display: none;">
|
<div class="load-more" id="loadMore" style="display: none">
|
||||||
<button class="btn-secondary" onclick="loadMoreCourses()">
|
<button class="btn-secondary" onclick="loadMoreCourses()">
|
||||||
<span data-i18n="learn-load-more">Carregar Mais</span>
|
<span data-i18n="learn-load-more">Carregar Mais</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -219,7 +330,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">▶️</span>
|
<span class="section-icon">▶️</span>
|
||||||
<span data-i18n="learn-continue">Continuar Aprendendo</span>
|
<span data-i18n="learn-continue"
|
||||||
|
>Continuar Aprendendo</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-list" id="continueLearning">
|
<div class="courses-list" id="continueLearning">
|
||||||
|
|
@ -232,7 +345,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">✅</span>
|
<span class="section-icon">✅</span>
|
||||||
<span data-i18n="learn-completed-courses">Cursos Concluídos</span>
|
<span data-i18n="learn-completed-courses"
|
||||||
|
>Cursos Concluídos</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-list" id="completedCourses">
|
<div class="courses-list" id="completedCourses">
|
||||||
|
|
@ -247,7 +362,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">⚠️</span>
|
<span class="section-icon">⚠️</span>
|
||||||
<span data-i18n="learn-mandatory-training">Treinamentos Obrigatórios</span>
|
<span data-i18n="learn-mandatory-training"
|
||||||
|
>Treinamentos Obrigatórios</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="mandatory-list" id="mandatoryList">
|
<div class="mandatory-list" id="mandatoryList">
|
||||||
|
|
@ -262,7 +379,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">🏆</span>
|
<span class="section-icon">🏆</span>
|
||||||
<span data-i18n="learn-my-certificates">Meus Certificados</span>
|
<span data-i18n="learn-my-certificates"
|
||||||
|
>Meus Certificados</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="certificates-grid" id="certificatesGrid">
|
<div class="certificates-grid" id="certificatesGrid">
|
||||||
|
|
@ -278,7 +397,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="modalCourseTitle">Título do Curso</h3>
|
<h3 id="modalCourseTitle">Título do Curso</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeCourseModal()">
|
<button class="btn-icon-sm" onclick="closeCourseModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -288,22 +414,49 @@
|
||||||
<div class="course-detail">
|
<div class="course-detail">
|
||||||
<div class="course-detail-header">
|
<div class="course-detail-header">
|
||||||
<div class="course-thumbnail" id="modalThumbnail">
|
<div class="course-thumbnail" id="modalThumbnail">
|
||||||
<img src="" alt="Course thumbnail">
|
<img src="" alt="Course thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<div class="course-info">
|
<div class="course-info">
|
||||||
<div class="course-meta">
|
<div class="course-meta">
|
||||||
<span class="difficulty-badge" id="modalDifficulty">Iniciante</span>
|
<span
|
||||||
|
class="difficulty-badge"
|
||||||
|
id="modalDifficulty"
|
||||||
|
>Iniciante</span
|
||||||
|
>
|
||||||
<span class="duration" id="modalDuration">
|
<span class="duration" id="modalDuration">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<polyline points="12 6 12 12 16 14"></polyline>
|
<polyline
|
||||||
|
points="12 6 12 12 16 14"
|
||||||
|
></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span>30 min</span>
|
<span>30 min</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="lessons-count" id="modalLessonsCount">
|
<span
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="lessons-count"
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
id="modalLessonsCount"
|
||||||
<polyline points="14 2 14 8 20 8"></polyline>
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||||
|
></path>
|
||||||
|
<polyline
|
||||||
|
points="14 2 14 8 20 8"
|
||||||
|
></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span>5 aulas</span>
|
<span>5 aulas</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -311,11 +464,23 @@
|
||||||
<p class="course-description" id="modalDescription">
|
<p class="course-description" id="modalDescription">
|
||||||
Descrição do curso...
|
Descrição do curso...
|
||||||
</p>
|
</p>
|
||||||
<div class="course-progress" id="modalProgress" style="display: none;">
|
<div
|
||||||
|
class="course-progress"
|
||||||
|
id="modalProgress"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" id="modalProgressFill" style="width: 0%"></div>
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
id="modalProgressFill"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="progress-text" id="modalProgressText">0% completo</span>
|
<span
|
||||||
|
class="progress-text"
|
||||||
|
id="modalProgressText"
|
||||||
|
>0% completo</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,16 +494,28 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quiz Section -->
|
<!-- Quiz Section -->
|
||||||
<div class="quiz-section" id="modalQuizSection" style="display: none;">
|
<div
|
||||||
|
class="quiz-section"
|
||||||
|
id="modalQuizSection"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<h4 data-i18n="learn-quiz">Avaliação</h4>
|
<h4 data-i18n="learn-quiz">Avaliação</h4>
|
||||||
<div class="quiz-info">
|
<div class="quiz-info">
|
||||||
<div class="quiz-meta">
|
<div class="quiz-meta">
|
||||||
<span id="modalQuizQuestions">10 questões</span>
|
<span id="modalQuizQuestions">10 questões</span>
|
||||||
<span id="modalQuizTime">15 min</span>
|
<span id="modalQuizTime">15 min</span>
|
||||||
<span id="modalQuizPassing">70% para aprovação</span>
|
<span id="modalQuizPassing"
|
||||||
|
>70% para aprovação</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="startQuizBtn" onclick="startQuiz()">
|
<button
|
||||||
<span data-i18n="learn-start-quiz">Iniciar Avaliação</span>
|
class="btn-primary"
|
||||||
|
id="startQuizBtn"
|
||||||
|
onclick="startQuiz()"
|
||||||
|
>
|
||||||
|
<span data-i18n="learn-start-quiz"
|
||||||
|
>Iniciar Avaliação</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -348,8 +525,19 @@
|
||||||
<button class="btn-secondary" onclick="closeCourseModal()">
|
<button class="btn-secondary" onclick="closeCourseModal()">
|
||||||
<span data-i18n="learn-close">Fechar</span>
|
<span data-i18n="learn-close">Fechar</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" id="startCourseBtn" onclick="startCourse()">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
|
id="startCourseBtn"
|
||||||
|
onclick="startCourse()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-start-course">Iniciar Curso</span>
|
<span data-i18n="learn-start-course">Iniciar Curso</span>
|
||||||
|
|
@ -365,7 +553,14 @@
|
||||||
<div class="quiz-header-info">
|
<div class="quiz-header-info">
|
||||||
<h3 id="quizTitle">Avaliação</h3>
|
<h3 id="quizTitle">Avaliação</h3>
|
||||||
<div class="quiz-timer" id="quizTimer">
|
<div class="quiz-timer" id="quizTimer">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<polyline points="12 6 12 12 16 14"></polyline>
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -373,7 +568,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-icon-sm" onclick="confirmExitQuiz()">
|
<button class="btn-icon-sm" onclick="confirmExitQuiz()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -382,9 +584,15 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="quiz-progress">
|
<div class="quiz-progress">
|
||||||
<div class="quiz-progress-bar">
|
<div class="quiz-progress-bar">
|
||||||
<div class="quiz-progress-fill" id="quizProgressFill" style="width: 0%"></div>
|
<div
|
||||||
|
class="quiz-progress-fill"
|
||||||
|
id="quizProgressFill"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="quiz-progress-text" id="quizProgressText">Questão 1 de 10</span>
|
<span class="quiz-progress-text" id="quizProgressText"
|
||||||
|
>Questão 1 de 10</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quiz-question" id="quizQuestion">
|
<div class="quiz-question" id="quizQuestion">
|
||||||
|
|
@ -396,19 +604,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-secondary" id="prevQuestionBtn" onclick="prevQuestion()" disabled>
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-secondary"
|
||||||
|
id="prevQuestionBtn"
|
||||||
|
onclick="prevQuestion()"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-previous">Anterior</span>
|
<span data-i18n="learn-previous">Anterior</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" id="nextQuestionBtn" onclick="nextQuestion()">
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
id="nextQuestionBtn"
|
||||||
|
onclick="nextQuestion()"
|
||||||
|
>
|
||||||
<span data-i18n="learn-next">Próxima</span>
|
<span data-i18n="learn-next">Próxima</span>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" id="submitQuizBtn" onclick="submitQuiz()" style="display: none;">
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
id="submitQuizBtn"
|
||||||
|
onclick="submitQuiz()"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<span data-i18n="learn-submit">Enviar Respostas</span>
|
<span data-i18n="learn-submit">Enviar Respostas</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -421,7 +657,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
|
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeQuizResult()">
|
<button class="btn-icon-sm" onclick="closeQuizResult()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -431,42 +674,84 @@
|
||||||
<div class="quiz-result" id="quizResult">
|
<div class="quiz-result" id="quizResult">
|
||||||
<div class="result-icon" id="resultIcon">🎉</div>
|
<div class="result-icon" id="resultIcon">🎉</div>
|
||||||
<h2 class="result-title" id="resultTitle">Parabéns!</h2>
|
<h2 class="result-title" id="resultTitle">Parabéns!</h2>
|
||||||
<p class="result-message" id="resultMessage">Você passou na avaliação!</p>
|
<p class="result-message" id="resultMessage">
|
||||||
|
Você passou na avaliação!
|
||||||
|
</p>
|
||||||
<div class="result-score">
|
<div class="result-score">
|
||||||
<div class="score-circle" id="scoreCircle">
|
<div class="score-circle" id="scoreCircle">
|
||||||
<span class="score-value" id="scoreValue">85%</span>
|
<span class="score-value" id="scoreValue">85%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-details">
|
<div class="score-details">
|
||||||
<div class="score-detail">
|
<div class="score-detail">
|
||||||
<span class="detail-label" data-i18n="learn-correct-answers">Acertos</span>
|
<span
|
||||||
<span class="detail-value" id="correctAnswers">8/10</span>
|
class="detail-label"
|
||||||
|
data-i18n="learn-correct-answers"
|
||||||
|
>Acertos</span
|
||||||
|
>
|
||||||
|
<span class="detail-value" id="correctAnswers"
|
||||||
|
>8/10</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-detail">
|
<div class="score-detail">
|
||||||
<span class="detail-label" data-i18n="learn-time-taken">Tempo</span>
|
<span
|
||||||
<span class="detail-value" id="timeTaken">12:30</span>
|
class="detail-label"
|
||||||
|
data-i18n="learn-time-taken"
|
||||||
|
>Tempo</span
|
||||||
|
>
|
||||||
|
<span class="detail-value" id="timeTaken"
|
||||||
|
>12:30</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-detail">
|
<div class="score-detail">
|
||||||
<span class="detail-label" data-i18n="learn-attempt">Tentativa</span>
|
<span
|
||||||
<span class="detail-value" id="attemptNumber">1</span>
|
class="detail-label"
|
||||||
|
data-i18n="learn-attempt"
|
||||||
|
>Tentativa</span
|
||||||
|
>
|
||||||
|
<span class="detail-value" id="attemptNumber"
|
||||||
|
>1</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-certificate" id="resultCertificate" style="display: none;">
|
<div
|
||||||
<p data-i18n="learn-certificate-earned">🏆 Certificado conquistado!</p>
|
class="result-certificate"
|
||||||
<button class="btn-secondary" onclick="downloadCertificate()">
|
id="resultCertificate"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
style="display: none"
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
>
|
||||||
|
<p data-i18n="learn-certificate-earned">
|
||||||
|
🏆 Certificado conquistado!
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
onclick="downloadCertificate()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
|
||||||
|
></path>
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-download-certificate">Baixar Certificado</span>
|
<span data-i18n="learn-download-certificate"
|
||||||
|
>Baixar Certificado</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-secondary" onclick="reviewAnswers()">
|
<button class="btn-secondary" onclick="reviewAnswers()">
|
||||||
<span data-i18n="learn-review-answers">Revisar Respostas</span>
|
<span data-i18n="learn-review-answers"
|
||||||
|
>Revisar Respostas</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" onclick="closeQuizResult()">
|
<button class="btn-primary" onclick="closeQuizResult()">
|
||||||
<span data-i18n="learn-continue">Continuar</span>
|
<span data-i18n="learn-continue">Continuar</span>
|
||||||
|
|
@ -480,21 +765,50 @@
|
||||||
<div class="modal-content modal-fullscreen">
|
<div class="modal-content modal-fullscreen">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="lesson-nav">
|
<div class="lesson-nav">
|
||||||
<button class="btn-icon-sm" onclick="prevLesson()" id="prevLessonBtn">
|
<button
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-icon-sm"
|
||||||
|
onclick="prevLesson()"
|
||||||
|
id="prevLessonBtn"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span id="lessonNavTitle">Aula 1 de 5</span>
|
<span id="lessonNavTitle">Aula 1 de 5</span>
|
||||||
<button class="btn-icon-sm" onclick="nextLesson()" id="nextLessonBtn">
|
<button
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-icon-sm"
|
||||||
|
onclick="nextLesson()"
|
||||||
|
id="nextLessonBtn"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 id="lessonTitle">Título da Aula</h3>
|
<h3 id="lessonTitle">Título da Aula</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeLessonModal()">
|
<button class="btn-icon-sm" onclick="closeLessonModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -514,14 +828,31 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="lesson-progress">
|
<div class="lesson-progress">
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" id="lessonProgressFill" style="width: 0%"></div>
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
id="lessonProgressFill"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="completeLessonBtn" onclick="completeLesson()">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
|
id="completeLessonBtn"
|
||||||
|
onclick="completeLesson()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-mark-complete">Marcar como Concluída</span>
|
<span data-i18n="learn-mark-complete"
|
||||||
|
>Marcar como Concluída</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -533,7 +864,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 data-i18n="learn-certificate">Certificado</h3>
|
<h3 data-i18n="learn-certificate">Certificado</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeCertificateModal()">
|
<button class="btn-icon-sm" onclick="closeCertificateModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -552,12 +890,27 @@
|
||||||
<h3 id="certUserName">Nome do Usuário</h3>
|
<h3 id="certUserName">Nome do Usuário</h3>
|
||||||
<p>concluiu com sucesso o curso</p>
|
<p>concluiu com sucesso o curso</p>
|
||||||
<h4 id="certCourseName">Nome do Curso</h4>
|
<h4 id="certCourseName">Nome do Curso</h4>
|
||||||
<p class="cert-score">com aproveitamento de <strong id="certScore">85%</strong></p>
|
<p class="cert-score">
|
||||||
|
com aproveitamento de
|
||||||
|
<strong id="certScore">85%</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="certificate-footer">
|
<div class="certificate-footer">
|
||||||
<div class="cert-date">
|
<div class="cert-date">
|
||||||
<span data-i18n="learn-issued-on">Emitido em</span>
|
<span data-i18n="learn-issued-on"
|
||||||
|
>Emitido em</span
|
||||||
|
>
|
||||||
<strong id="certDate">01/01/2025</strong>
|
<strong id="certDate">01/01/2025</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="cert-code">
|
<div class="cert-code">
|
||||||
<span data-i18n="learn-verification-code">Código de Verificação
|
<span data-i18n="learn-verification-code"
|
||||||
|
>Código de Verificação
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="mail/mail.css" />
|
||||||
|
|
||||||
<div class="mail-layout">
|
<div class="mail-layout">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="panel mail-sidebar">
|
<div class="panel mail-sidebar">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="meet/meet.css" />
|
||||||
|
|
||||||
<!-- Meet - Video Conferencing -->
|
<!-- Meet - Video Conferencing -->
|
||||||
<div class="meet-container" id="meet-app">
|
<div class="meet-container" id="meet-app">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Alerts partial -->
|
<!-- Alerts partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/alerts.css">
|
<link rel="stylesheet" href="monitoring/alerts.css">
|
||||||
<div class="alerts-container">
|
<div class="alerts-container">
|
||||||
<!-- Alerts Header -->
|
<!-- Alerts Header -->
|
||||||
<div class="alerts-header">
|
<div class="alerts-header">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Health partial -->
|
<!-- Health partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/health.css">
|
<link rel="stylesheet" href="monitoring/health.css">
|
||||||
<div class="health-container">
|
<div class="health-container">
|
||||||
<!-- Health Overview -->
|
<!-- Health Overview -->
|
||||||
<div class="health-overview"
|
<div class="health-overview"
|
||||||
|
|
|
||||||
|
|
@ -491,5 +491,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/monitoring.css" />
|
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||||
<script src="/static/suite/monitoring/monitoring.js"></script>
|
<script src="/static/suite/monitoring/monitoring.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Logs partial -->
|
<!-- Logs partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/logs.css" />
|
<link rel="stylesheet" href="monitoring/logs.css" />
|
||||||
<div class="logs-container">
|
<div class="logs-container">
|
||||||
<!-- Logs Header -->
|
<!-- Logs Header -->
|
||||||
<div class="logs-header">
|
<div class="logs-header">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Metrics partial -->
|
<!-- Metrics partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/metrics.css">
|
<link rel="stylesheet" href="monitoring/metrics.css">
|
||||||
<div class="metrics-container">
|
<div class="metrics-container">
|
||||||
<!-- Metrics Header -->
|
<!-- Metrics Header -->
|
||||||
<div class="metrics-header">
|
<div class="metrics-header">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||||
|
|
||||||
<div class="monitoring-container" id="monitoring-app">
|
<div class="monitoring-container" id="monitoring-app">
|
||||||
<header class="monitoring-header">
|
<header class="monitoring-header">
|
||||||
<h2>
|
<h2>
|
||||||
|
|
@ -1374,7 +1376,6 @@
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||||
<link rel="stylesheet" href="monitoring.css" />
|
<script src="monitoring/monitoring.js"></script>
|
||||||
<script src="monitoring.js"></script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Resources partial -->
|
<!-- Resources partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/resources.css">
|
<link rel="stylesheet" href="monitoring/resources.css">
|
||||||
<div class="resources-container">
|
<div class="resources-container">
|
||||||
<!-- Resource Overview Cards -->
|
<!-- Resource Overview Cards -->
|
||||||
<div class="resource-cards">
|
<div class="resource-cards">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
|
<link rel="stylesheet" href="monitoring/services.css" />
|
||||||
|
|
||||||
<div class="services-container">
|
<div class="services-container">
|
||||||
<!-- Services Header -->
|
<!-- Services Header -->
|
||||||
<div class="services-header">
|
<div class="services-header">
|
||||||
<div class="header-stats"
|
<div
|
||||||
|
class="header-stats"
|
||||||
hx-get="/api/services/summary"
|
hx-get="/api/services/summary"
|
||||||
hx-trigger="load, every 10s"
|
hx-trigger="load, every 10s"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
<div class="stat-item running">
|
<div class="stat-item running">
|
||||||
<span class="stat-number">--</span>
|
<span class="stat-number">--</span>
|
||||||
<span class="stat-label">Running</span>
|
<span class="stat-label">Running</span>
|
||||||
|
|
@ -24,14 +28,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
id="service-search"
|
id="service-search"
|
||||||
placeholder="Search services..."
|
placeholder="Search services..."
|
||||||
onkeyup="filterServices(this.value)">
|
onkeyup="filterServices(this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select id="status-filter" onchange="filterByStatus(this.value)">
|
<select id="status-filter" onchange="filterByStatus(this.value)">
|
||||||
<option value="all">All Status</option>
|
<option value="all">All Status</option>
|
||||||
|
|
@ -39,8 +52,19 @@
|
||||||
<option value="warning">Warning</option>
|
<option value="warning">Warning</option>
|
||||||
<option value="stopped">Stopped</option>
|
<option value="stopped">Stopped</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="action-btn" onclick="restartAllServices()" title="Restart All">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="action-btn"
|
||||||
|
onclick="restartAllServices()"
|
||||||
|
title="Restart All"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -50,10 +74,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services Grid -->
|
<!-- Services Grid -->
|
||||||
<div class="services-grid" id="services-grid"
|
<div
|
||||||
|
class="services-grid"
|
||||||
|
id="services-grid"
|
||||||
hx-get="/api/services/status"
|
hx-get="/api/services/status"
|
||||||
hx-trigger="load, every 10s"
|
hx-trigger="load, every 10s"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
<!-- Loading placeholder -->
|
<!-- Loading placeholder -->
|
||||||
<div class="service-card skeleton">
|
<div class="service-card skeleton">
|
||||||
<div class="skeleton-line"></div>
|
<div class="skeleton-line"></div>
|
||||||
|
|
@ -78,7 +105,14 @@
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3 id="detail-service-name">Service Details</h3>
|
<h3 id="detail-service-name">Service Details</h3>
|
||||||
<button class="close-btn" onclick="closeServiceDetail()">
|
<button class="close-btn" onclick="closeServiceDetail()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -95,9 +129,30 @@
|
||||||
<div class="service-card" data-status="running" data-service="service-name">
|
<div class="service-card" data-status="running" data-service="service-name">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="service-icon">
|
<div class="service-icon">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
width="24"
|
||||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="2"
|
||||||
|
width="20"
|
||||||
|
height="8"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
></rect>
|
||||||
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="14"
|
||||||
|
width="20"
|
||||||
|
height="8"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
></rect>
|
||||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -112,21 +167,42 @@
|
||||||
<p class="service-description">Service description goes here</p>
|
<p class="service-description">Service description goes here</p>
|
||||||
<div class="service-meta">
|
<div class="service-meta">
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<polyline points="12 6 12 12 16 14"></polyline>
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
Uptime: 24d 5h
|
Uptime: 24d 5h
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||||
<rect x="9" y="9" width="6" height="6"></rect>
|
<rect x="9" y="9" width="6" height="6"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
CPU: 12%
|
CPU: 12%
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
Mem: 256MB
|
Mem: 256MB
|
||||||
|
|
@ -134,28 +210,74 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="card-btn" onclick="viewServiceDetails('service-id')" title="Details">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
|
onclick="viewServiceDetails('service-id')"
|
||||||
|
title="Details"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="card-btn" onclick="restartService('service-id')" title="Restart">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
|
onclick="restartService('service-id')"
|
||||||
|
title="Restart"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="card-btn" onclick="stopService('service-id')" title="Stop">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
|
onclick="stopService('service-id')"
|
||||||
|
title="Stop"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<rect x="6" y="4" width="4" height="16"></rect>
|
<rect x="6" y="4" width="4" height="16"></rect>
|
||||||
<rect x="14" y="4" width="4" height="16"></rect>
|
<rect x="14" y="4" width="4" height="16"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="card-btn" onclick="viewServiceLogs('service-id')" title="Logs">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
onclick="viewServiceLogs('service-id')"
|
||||||
|
title="Logs"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||||
|
></path>
|
||||||
<polyline points="14 2 14 8 20 8"></polyline>
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -167,73 +289,76 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function filterServices(query) {
|
function filterServices(query) {
|
||||||
const cards = document.querySelectorAll('.service-card:not(.skeleton)');
|
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
const name = card.dataset.service?.toLowerCase() || '';
|
const name = card.dataset.service?.toLowerCase() || "";
|
||||||
const text = card.textContent.toLowerCase();
|
const text = card.textContent.toLowerCase();
|
||||||
const matches = name.includes(lowerQuery) || text.includes(lowerQuery);
|
const matches =
|
||||||
card.classList.toggle('hidden', !matches);
|
name.includes(lowerQuery) || text.includes(lowerQuery);
|
||||||
|
card.classList.toggle("hidden", !matches);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByStatus(status) {
|
function filterByStatus(status) {
|
||||||
const cards = document.querySelectorAll('.service-card:not(.skeleton)');
|
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
if (status === 'all') {
|
if (status === "all") {
|
||||||
card.classList.remove('hidden');
|
card.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
const cardStatus = card.dataset.status;
|
const cardStatus = card.dataset.status;
|
||||||
card.classList.toggle('hidden', cardStatus !== status);
|
card.classList.toggle("hidden", cardStatus !== status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewServiceDetails(serviceId) {
|
function viewServiceDetails(serviceId) {
|
||||||
const panel = document.getElementById('service-detail-panel');
|
const panel = document.getElementById("service-detail-panel");
|
||||||
const content = document.getElementById('service-detail-content');
|
const content = document.getElementById("service-detail-content");
|
||||||
|
|
||||||
// Load service details via HTMX
|
// Load service details via HTMX
|
||||||
htmx.ajax('GET', `/api/services/${serviceId}/details`, {
|
htmx.ajax("GET", `/api/services/${serviceId}/details`, {
|
||||||
target: content,
|
target: content,
|
||||||
swap: 'innerHTML'
|
swap: "innerHTML",
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('detail-service-name').textContent = serviceId;
|
document.getElementById("detail-service-name").textContent = serviceId;
|
||||||
panel.classList.add('open');
|
panel.classList.add("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeServiceDetail() {
|
function closeServiceDetail() {
|
||||||
document.getElementById('service-detail-panel').classList.remove('open');
|
document
|
||||||
|
.getElementById("service-detail-panel")
|
||||||
|
.classList.remove("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartService(serviceId) {
|
function restartService(serviceId) {
|
||||||
if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
|
if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
|
||||||
htmx.ajax('POST', `/api/services/${serviceId}/restart`, {
|
htmx.ajax("POST", `/api/services/${serviceId}/restart`, {
|
||||||
swap: 'none'
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopService(serviceId) {
|
function stopService(serviceId) {
|
||||||
if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
|
if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
|
||||||
htmx.ajax('POST', `/api/services/${serviceId}/stop`, {
|
htmx.ajax("POST", `/api/services/${serviceId}/stop`, {
|
||||||
swap: 'none'
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startService(serviceId) {
|
function startService(serviceId) {
|
||||||
htmx.ajax('POST', `/api/services/${serviceId}/start`, {
|
htmx.ajax("POST", `/api/services/${serviceId}/start`, {
|
||||||
swap: 'none'
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,38 +368,44 @@
|
||||||
if (logsLink) {
|
if (logsLink) {
|
||||||
logsLink.click();
|
logsLink.click();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const serviceFilter = document.getElementById('service-filter');
|
const serviceFilter = document.getElementById("service-filter");
|
||||||
if (serviceFilter) {
|
if (serviceFilter) {
|
||||||
serviceFilter.value = serviceId;
|
serviceFilter.value = serviceId;
|
||||||
serviceFilter.dispatchEvent(new Event('change'));
|
serviceFilter.dispatchEvent(new Event("change"));
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartAllServices() {
|
function restartAllServices() {
|
||||||
if (confirm('Are you sure you want to restart all services? This may cause temporary downtime.')) {
|
if (
|
||||||
htmx.ajax('POST', '/api/services/restart-all', {
|
confirm(
|
||||||
swap: 'none'
|
"Are you sure you want to restart all services? This may cause temporary downtime.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
htmx.ajax("POST", "/api/services/restart-all", {
|
||||||
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close panel on escape key
|
// Close panel on escape key
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
closeServiceDetail();
|
closeServiceDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close panel when clicking outside
|
// Close panel when clicking outside
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener("click", function (e) {
|
||||||
const panel = document.getElementById('service-detail-panel');
|
const panel = document.getElementById("service-detail-panel");
|
||||||
if (panel.classList.contains('open') &&
|
if (
|
||||||
|
panel.classList.contains("open") &&
|
||||||
!panel.contains(e.target) &&
|
!panel.contains(e.target) &&
|
||||||
!e.target.closest('.card-btn')) {
|
!e.target.closest(".card-btn")
|
||||||
|
) {
|
||||||
closeServiceDetail();
|
closeServiceDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="paper/paper.css" />
|
||||||
|
|
||||||
<!-- Paper - AI Writing & Notes -->
|
<!-- Paper - AI Writing & Notes -->
|
||||||
<div class="paper-container" id="paper-app">
|
<div class="paper-container" id="paper-app">
|
||||||
<!-- Sidebar - Notes List -->
|
<!-- Sidebar - Notes List -->
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -23,7 +30,14 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="openAddContact()">
|
<button class="btn btn-primary" onclick="openAddContact()">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="8.5" cy="7" r="4" />
|
<circle cx="8.5" cy="7" r="4" />
|
||||||
<line x1="20" y1="8" x2="20" y2="14" />
|
<line x1="20" y1="8" x2="20" y2="14" />
|
||||||
|
|
@ -36,15 +50,39 @@
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<nav class="tab-nav" role="tablist">
|
<nav class="tab-nav" role="tablist">
|
||||||
<button class="tab-btn active" role="tab" aria-selected="true" onclick="showTab('contacts', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn active"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="true"
|
||||||
|
onclick="showTab('contacts', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<circle cx="9" cy="7" r="4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-contacts">Contacts</span>
|
<span data-i18n="people-tab-contacts">Contacts</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('groups', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
onclick="showTab('groups', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<circle cx="9" cy="7" r="4" />
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
|
@ -52,15 +90,41 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-groups">Groups</span>
|
<span data-i18n="people-tab-groups">Groups</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('directory', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
onclick="showTab('directory', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
<path
|
||||||
|
d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-directory">Directory</span>
|
<span data-i18n="people-tab-directory">Directory</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('recent', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
onclick="showTab('recent', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<polyline points="12 6 12 12 16 14" />
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -74,33 +138,90 @@
|
||||||
<div id="contacts-tab" class="tab-content active">
|
<div id="contacts-tab" class="tab-content active">
|
||||||
<!-- Alphabet Filter -->
|
<!-- Alphabet Filter -->
|
||||||
<div class="alphabet-filter">
|
<div class="alphabet-filter">
|
||||||
<button class="alpha-btn active" onclick="filterByLetter('all', this)">All</button>
|
<button
|
||||||
<button class="alpha-btn" onclick="filterByLetter('A', this)">A</button>
|
class="alpha-btn active"
|
||||||
<button class="alpha-btn" onclick="filterByLetter('B', this)">B</button>
|
onclick="filterByLetter('all', this)"
|
||||||
<button class="alpha-btn" onclick="filterByLetter('C', this)">C</button>
|
>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('D', this)">D</button>
|
All
|
||||||
<button class="alpha-btn" onclick="filterByLetter('E', this)">E</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('F', this)">F</button>
|
<button class="alpha-btn" onclick="filterByLetter('A', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('G', this)">G</button>
|
A
|
||||||
<button class="alpha-btn" onclick="filterByLetter('H', this)">H</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('I', this)">I</button>
|
<button class="alpha-btn" onclick="filterByLetter('B', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('J', this)">J</button>
|
B
|
||||||
<button class="alpha-btn" onclick="filterByLetter('K', this)">K</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('L', this)">L</button>
|
<button class="alpha-btn" onclick="filterByLetter('C', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('M', this)">M</button>
|
C
|
||||||
<button class="alpha-btn" onclick="filterByLetter('N', this)">N</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('O', this)">O</button>
|
<button class="alpha-btn" onclick="filterByLetter('D', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('P', this)">P</button>
|
D
|
||||||
<button class="alpha-btn" onclick="filterByLetter('Q', this)">Q</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('R', this)">R</button>
|
<button class="alpha-btn" onclick="filterByLetter('E', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('S', this)">S</button>
|
E
|
||||||
<button class="alpha-btn" onclick="filterByLetter('T', this)">T</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('U', this)">U</button>
|
<button class="alpha-btn" onclick="filterByLetter('F', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('V', this)">V</button>
|
F
|
||||||
<button class="alpha-btn" onclick="filterByLetter('W', this)">W</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('X', this)">X</button>
|
<button class="alpha-btn" onclick="filterByLetter('G', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('Y', this)">Y</button>
|
G
|
||||||
<button class="alpha-btn" onclick="filterByLetter('Z', this)">Z</button>
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('H', this)">
|
||||||
|
H
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('I', this)">
|
||||||
|
I
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('J', this)">
|
||||||
|
J
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('K', this)">
|
||||||
|
K
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('L', this)">
|
||||||
|
L
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('M', this)">
|
||||||
|
M
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('N', this)">
|
||||||
|
N
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('O', this)">
|
||||||
|
O
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('P', this)">
|
||||||
|
P
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('Q', this)">
|
||||||
|
Q
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('R', this)">
|
||||||
|
R
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('S', this)">
|
||||||
|
S
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('T', this)">
|
||||||
|
T
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('U', this)">
|
||||||
|
U
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('V', this)">
|
||||||
|
V
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('W', this)">
|
||||||
|
W
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('X', this)">
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('Y', this)">
|
||||||
|
Y
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('Z', this)">
|
||||||
|
Z
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contacts List -->
|
<!-- Contacts List -->
|
||||||
|
|
@ -139,22 +260,53 @@
|
||||||
<div class="contact-panel" id="contact-panel">
|
<div class="contact-panel" id="contact-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<button class="close-btn" onclick="closeContactPanel()">
|
<button class="close-btn" onclick="closeContactPanel()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button class="icon-btn" title="Edit" onclick="editContact()">
|
<button class="icon-btn" title="Edit" onclick="editContact()">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
width="18"
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" title="Delete" onclick="deleteContact()">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="icon-btn"
|
||||||
|
title="Delete"
|
||||||
|
onclick="deleteContact()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="3 6 5 6 21 6" />
|
<polyline points="3 6 5 6 21 6" />
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path
|
||||||
|
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -171,7 +323,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
|
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
|
||||||
<button class="close-btn" onclick="closeModal()">
|
<button class="close-btn" onclick="closeModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -209,24 +368,34 @@
|
||||||
<textarea name="notes" rows="3"></textarea>
|
<textarea name="notes" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeModal()" data-i18n="cancel">Cancel</button>
|
<button
|
||||||
<button type="submit" class="btn btn-primary" data-i18n="save">Save</button>
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="closeModal()"
|
||||||
|
data-i18n="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" data-i18n="save">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
(function () {
|
||||||
let currentContact = null;
|
let currentContact = null;
|
||||||
let contacts = [];
|
let contacts = [];
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
loadContacts();
|
loadContacts();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadContacts() {
|
async function loadContacts() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/contacts');
|
const response = await fetch("/api/contacts");
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
contacts = await response.json();
|
contacts = await response.json();
|
||||||
renderContacts(contacts);
|
renderContacts(contacts);
|
||||||
|
|
@ -234,20 +403,20 @@ async function loadContacts() {
|
||||||
renderEmptyState();
|
renderEmptyState();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load contacts:', error);
|
console.error("Failed to load contacts:", error);
|
||||||
renderEmptyState();
|
renderEmptyState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContacts(contactsList) {
|
function renderContacts(contactsList) {
|
||||||
const container = document.getElementById('contacts-list');
|
const container = document.getElementById("contacts-list");
|
||||||
if (!contactsList || contactsList.length === 0) {
|
if (!contactsList || contactsList.length === 0) {
|
||||||
renderEmptyState();
|
renderEmptyState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grouped = groupByLetter(contactsList);
|
const grouped = groupByLetter(contactsList);
|
||||||
let html = '';
|
let html = "";
|
||||||
|
|
||||||
for (const [letter, group] of Object.entries(grouped)) {
|
for (const [letter, group] of Object.entries(grouped)) {
|
||||||
html += `<div class="contact-group" data-letter="${letter}">
|
html += `<div class="contact-group" data-letter="${letter}">
|
||||||
|
|
@ -258,7 +427,7 @@ function renderContacts(contactsList) {
|
||||||
html += renderContactCard(contact);
|
html += renderContactCard(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div></div>';
|
html += "</div></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
@ -272,7 +441,7 @@ function renderContactCard(contact) {
|
||||||
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
|
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="contact-name">${name}</div>
|
<div class="contact-name">${name}</div>
|
||||||
<div class="contact-detail">${contact.email || contact.phone || ''}</div>
|
<div class="contact-detail">${contact.email || contact.phone || ""}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-actions">
|
<div class="contact-actions">
|
||||||
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
|
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
|
||||||
|
|
@ -291,7 +460,7 @@ function renderContactCard(contact) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmptyState() {
|
function renderEmptyState() {
|
||||||
document.getElementById('contacts-list').innerHTML = `
|
document.getElementById("contacts-list").innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
|
@ -315,7 +484,9 @@ function renderEmptyState() {
|
||||||
function groupByLetter(contactsList) {
|
function groupByLetter(contactsList) {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
for (const contact of contactsList) {
|
for (const contact of contactsList) {
|
||||||
const letter = (contact.lastName || contact.firstName || '#').charAt(0).toUpperCase();
|
const letter = (contact.lastName || contact.firstName || "#")
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase();
|
||||||
if (!grouped[letter]) grouped[letter] = [];
|
if (!grouped[letter]) grouped[letter] = [];
|
||||||
grouped[letter].push(contact);
|
grouped[letter].push(contact);
|
||||||
}
|
}
|
||||||
|
|
@ -323,11 +494,26 @@ function groupByLetter(contactsList) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitials(firstName, lastName) {
|
function getInitials(firstName, lastName) {
|
||||||
return ((firstName?.charAt(0) || '') + (lastName?.charAt(0) || '')).toUpperCase() || '?';
|
return (
|
||||||
|
(
|
||||||
|
(firstName?.charAt(0) || "") + (lastName?.charAt(0) || "")
|
||||||
|
).toUpperCase() || "?"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvatarColor(name) {
|
function getAvatarColor(name) {
|
||||||
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'];
|
const colors = [
|
||||||
|
"#6366f1",
|
||||||
|
"#8b5cf6",
|
||||||
|
"#ec4899",
|
||||||
|
"#ef4444",
|
||||||
|
"#f97316",
|
||||||
|
"#eab308",
|
||||||
|
"#22c55e",
|
||||||
|
"#14b8a6",
|
||||||
|
"#06b6d4",
|
||||||
|
"#3b82f6",
|
||||||
|
];
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < name.length; i++) {
|
for (let i = 0; i < name.length; i++) {
|
||||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
|
@ -336,65 +522,81 @@ function getAvatarColor(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTab(tabId, btn) {
|
function showTab(tabId, btn) {
|
||||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
document
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
.querySelectorAll(".tab-content")
|
||||||
b.classList.remove('active');
|
.forEach((tab) => tab.classList.remove("active"));
|
||||||
b.setAttribute('aria-selected', 'false');
|
document.querySelectorAll(".tab-btn").forEach((b) => {
|
||||||
|
b.classList.remove("active");
|
||||||
|
b.setAttribute("aria-selected", "false");
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById(tabId + '-tab').classList.add('active');
|
document.getElementById(tabId + "-tab").classList.add("active");
|
||||||
btn.classList.add('active');
|
btn.classList.add("active");
|
||||||
btn.setAttribute('aria-selected', 'true');
|
btn.setAttribute("aria-selected", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByLetter(letter, btn) {
|
function filterByLetter(letter, btn) {
|
||||||
document.querySelectorAll('.alpha-btn').forEach(b => b.classList.remove('active'));
|
document
|
||||||
btn.classList.add('active');
|
.querySelectorAll(".alpha-btn")
|
||||||
|
.forEach((b) => b.classList.remove("active"));
|
||||||
|
btn.classList.add("active");
|
||||||
|
|
||||||
document.querySelectorAll('.contact-group').forEach(group => {
|
document.querySelectorAll(".contact-group").forEach((group) => {
|
||||||
if (letter === 'all' || group.dataset.letter === letter) {
|
if (letter === "all" || group.dataset.letter === letter) {
|
||||||
group.style.display = '';
|
group.style.display = "";
|
||||||
} else {
|
} else {
|
||||||
group.style.display = 'none';
|
group.style.display = "none";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showContact(id) {
|
function showContact(id) {
|
||||||
currentContact = contacts.find(c => c.id === id);
|
currentContact = contacts.find((c) => c.id === id);
|
||||||
if (!currentContact) return;
|
if (!currentContact) return;
|
||||||
|
|
||||||
const panel = document.getElementById('contact-panel');
|
const panel = document.getElementById("contact-panel");
|
||||||
const detail = document.getElementById('contact-detail');
|
const detail = document.getElementById("contact-detail");
|
||||||
|
|
||||||
detail.innerHTML = `
|
detail.innerHTML = `
|
||||||
<div class="contact-header">
|
<div class="contact-header">
|
||||||
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + ' ' + currentContact.lastName)}">
|
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + " " + currentContact.lastName)}">
|
||||||
${getInitials(currentContact.firstName, currentContact.lastName)}
|
${getInitials(currentContact.firstName, currentContact.lastName)}
|
||||||
</div>
|
</div>
|
||||||
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
|
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
|
||||||
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ''}
|
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ""}
|
||||||
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ''}
|
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-fields">
|
<div class="contact-fields">
|
||||||
${currentContact.email ? `
|
${
|
||||||
|
currentContact.email
|
||||||
|
? `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
|
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
${currentContact.phone ? `
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
currentContact.phone
|
||||||
|
? `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Phone</label>
|
<label>Phone</label>
|
||||||
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
|
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
${currentContact.notes ? `
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
currentContact.notes
|
||||||
|
? `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Notes</label>
|
<label>Notes</label>
|
||||||
<p>${currentContact.notes}</p>
|
<p>${currentContact.notes}</p>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-quick-actions">
|
<div class="contact-quick-actions">
|
||||||
<button class="action-btn" onclick="startChat('${currentContact.id}')">
|
<button class="action-btn" onclick="startChat('${currentContact.id}')">
|
||||||
|
|
@ -422,37 +624,37 @@ function showContact(id) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
panel.classList.add('open');
|
panel.classList.add("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeContactPanel() {
|
function closeContactPanel() {
|
||||||
document.getElementById('contact-panel').classList.remove('open');
|
document.getElementById("contact-panel").classList.remove("open");
|
||||||
currentContact = null;
|
currentContact = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAddContact() {
|
function openAddContact() {
|
||||||
currentContact = null;
|
currentContact = null;
|
||||||
document.getElementById('modal-title').textContent = 'Add Contact';
|
document.getElementById("modal-title").textContent = "Add Contact";
|
||||||
document.getElementById('contact-form').reset();
|
document.getElementById("contact-form").reset();
|
||||||
document.getElementById('contact-modal').showModal();
|
document.getElementById("contact-modal").showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function editContact() {
|
function editContact() {
|
||||||
if (!currentContact) return;
|
if (!currentContact) return;
|
||||||
document.getElementById('modal-title').textContent = 'Edit Contact';
|
document.getElementById("modal-title").textContent = "Edit Contact";
|
||||||
const form = document.getElementById('contact-form');
|
const form = document.getElementById("contact-form");
|
||||||
form.firstName.value = currentContact.firstName || '';
|
form.firstName.value = currentContact.firstName || "";
|
||||||
form.lastName.value = currentContact.lastName || '';
|
form.lastName.value = currentContact.lastName || "";
|
||||||
form.email.value = currentContact.email || '';
|
form.email.value = currentContact.email || "";
|
||||||
form.phone.value = currentContact.phone || '';
|
form.phone.value = currentContact.phone || "";
|
||||||
form.company.value = currentContact.company || '';
|
form.company.value = currentContact.company || "";
|
||||||
form.title.value = currentContact.title || '';
|
form.title.value = currentContact.title || "";
|
||||||
form.notes.value = currentContact.notes || '';
|
form.notes.value = currentContact.notes || "";
|
||||||
document.getElementById('contact-modal').showModal();
|
document.getElementById("contact-modal").showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('contact-modal').close();
|
document.getElementById("contact-modal").close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveContact(event) {
|
async function saveContact(event) {
|
||||||
|
|
@ -465,17 +667,19 @@ async function saveContact(event) {
|
||||||
phone: form.phone.value,
|
phone: form.phone.value,
|
||||||
company: form.company.value,
|
company: form.company.value,
|
||||||
title: form.title.value,
|
title: form.title.value,
|
||||||
notes: form.notes.value
|
notes: form.notes.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = currentContact ? `/api/contacts/${currentContact.id}` : '/api/contacts';
|
const url = currentContact
|
||||||
const method = currentContact ? 'PUT' : 'POST';
|
? `/api/contacts/${currentContact.id}`
|
||||||
|
: "/api/contacts";
|
||||||
|
const method = currentContact ? "PUT" : "POST";
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
@ -484,21 +688,26 @@ async function saveContact(event) {
|
||||||
if (currentContact) closeContactPanel();
|
if (currentContact) closeContactPanel();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save contact:', error);
|
console.error("Failed to save contact:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteContact() {
|
async function deleteContact() {
|
||||||
if (!currentContact || !confirm('Delete this contact?')) return;
|
if (!currentContact || !confirm("Delete this contact?")) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/contacts/${currentContact.id}`, { method: 'DELETE' });
|
const response = await fetch(
|
||||||
|
`/api/contacts/${currentContact.id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
closeContactPanel();
|
closeContactPanel();
|
||||||
loadContacts();
|
loadContacts();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete contact:', error);
|
console.error("Failed to delete contact:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,13 +724,32 @@ function scheduleMeeting(contactId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search functionality
|
// Search functionality
|
||||||
document.getElementById('people-search')?.addEventListener('input', (e) => {
|
document
|
||||||
|
.getElementById("people-search")
|
||||||
|
?.addEventListener("input", (e) => {
|
||||||
const query = e.target.value.toLowerCase();
|
const query = e.target.value.toLowerCase();
|
||||||
const filtered = contacts.filter(c =>
|
const filtered = contacts.filter(
|
||||||
(c.firstName + ' ' + c.lastName).toLowerCase().includes(query) ||
|
(c) =>
|
||||||
(c.email || '').toLowerCase().includes(query) ||
|
(c.firstName + " " + c.lastName)
|
||||||
(c.company || '').toLowerCase().includes(query)
|
.toLowerCase()
|
||||||
|
.includes(query) ||
|
||||||
|
(c.email || "").toLowerCase().includes(query) ||
|
||||||
|
(c.company || "").toLowerCase().includes(query),
|
||||||
);
|
);
|
||||||
renderContacts(filtered);
|
renderContacts(filtered);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.showTab = showTab;
|
||||||
|
window.filterByLetter = filterByLetter;
|
||||||
|
window.showContact = showContact;
|
||||||
|
window.closeContactPanel = closeContactPanel;
|
||||||
|
window.openAddContact = openAddContact;
|
||||||
|
window.editContact = editContact;
|
||||||
|
window.closeModal = closeModal;
|
||||||
|
window.saveContact = saveContact;
|
||||||
|
window.deleteContact = deleteContact;
|
||||||
|
window.startChat = startChat;
|
||||||
|
window.sendEmail = sendEmail;
|
||||||
|
window.scheduleMeeting = scheduleMeeting;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- Products - Product & Service Catalog -->
|
<!-- Products - Product & Service Catalog -->
|
||||||
<!-- Dynamics nomenclature: Product, Service, PriceList -->
|
<!-- Dynamics nomenclature: Product, Service, PriceList -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/products/products.css">
|
<link rel="stylesheet" href="products/products.css">
|
||||||
|
|
||||||
<div class="products-container">
|
<div class="products-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="research/research.css" />
|
||||||
|
|
||||||
<!-- Research - AI-Powered Search & Discovery -->
|
<!-- Research - AI-Powered Search & Discovery -->
|
||||||
<div class="research-container" id="research-app">
|
<div class="research-container" id="research-app">
|
||||||
<!-- Sidebar - Sources & Collections -->
|
<!-- Sidebar - Sources & Collections -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="settings/settings.css" />
|
||||||
|
|
||||||
<div class="settings-layout">
|
<div class="settings-layout">
|
||||||
<!-- Sidebar Navigation -->
|
<!-- Sidebar Navigation -->
|
||||||
<aside class="settings-sidebar">
|
<aside class="settings-sidebar">
|
||||||
|
|
|
||||||
|
|
@ -452,40 +452,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheElements() {
|
|
||||||
elements.container = document.querySelector(".slides-container");
|
|
||||||
elements.sidebar = document.getElementById("slides-sidebar");
|
|
||||||
elements.thumbnails = document.getElementById("slide-thumbnails");
|
|
||||||
elements.canvas = document.getElementById("slide-canvas");
|
|
||||||
elements.canvasContainer = document.getElementById("canvas-container");
|
|
||||||
elements.selectionHandles = document.getElementById("selection-handles");
|
|
||||||
elements.propertiesPanel = document.getElementById("properties-panel");
|
|
||||||
elements.presentationName = document.getElementById("presentation-name");
|
|
||||||
elements.collaborators = document.getElementById("collaborators");
|
|
||||||
elements.contextMenu = document.getElementById("context-menu");
|
|
||||||
elements.slideContextMenu = document.getElementById("slide-context-menu");
|
|
||||||
elements.cursorIndicators = document.getElementById("cursor-indicators");
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindEvents() {
|
|
||||||
elements.canvas.addEventListener("mousedown", handleCanvasMouseDown);
|
|
||||||
elements.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
document.addEventListener("click", hideContextMenus);
|
|
||||||
elements.canvas.addEventListener("contextmenu", handleContextMenu);
|
|
||||||
|
|
||||||
const handles = elements.selectionHandles.querySelectorAll(
|
|
||||||
".handle, .rotate-handle",
|
|
||||||
);
|
|
||||||
handles.forEach((handle) => {
|
|
||||||
handle.addEventListener("mousedown", handleResizeStart);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewPresentation() {
|
function createNewPresentation() {
|
||||||
const titleSlide = createSlide("title");
|
const titleSlide = createSlide("title");
|
||||||
state.slides = [titleSlide];
|
state.slides = [titleSlide];
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="social/social.css" />
|
||||||
|
|
||||||
<div class="social-app">
|
<div class="social-app">
|
||||||
<div class="social-header">
|
<div class="social-header">
|
||||||
<div class="social-tabs">
|
<div class="social-tabs">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="sources/sources.css" />
|
||||||
|
|
||||||
<div class="sources-container">
|
<div class="sources-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="sources-header">
|
<header class="sources-header">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tasks/autotask.css" />
|
||||||
|
|
||||||
<div class="autotask-container" data-theme="sentient">
|
<div class="autotask-container" data-theme="sentient">
|
||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
<div class="autotask-topbar">
|
<div class="autotask-topbar">
|
||||||
|
|
@ -476,6 +478,6 @@ Examples:
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="progress-panel.css" />
|
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
||||||
<script src="progress-panel.js"></script>
|
<script src="tasks/progress-panel.js"></script>
|
||||||
<script src="autotask.js"></script>
|
<script src="tasks/autotask.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tasks/tasks.css" />
|
||||||
|
|
||||||
<!-- =============================================================================
|
<!-- =============================================================================
|
||||||
TASKS APP - Autonomous Task Management
|
TASKS APP - Autonomous Task Management
|
||||||
Respects Theme Manager - No hardcoded theme
|
Respects Theme Manager - No hardcoded theme
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- Tickets - AI-Assisted Support Cases -->
|
<!-- Tickets - AI-Assisted Support Cases -->
|
||||||
<!-- Dynamics nomenclature: Case, Resolution, Activity -->
|
<!-- Dynamics nomenclature: Case, Resolution, Activity -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/tickets/tickets.css" />
|
<link rel="stylesheet" href="tickets/tickets.css" />
|
||||||
|
|
||||||
<div class="tickets-container">
|
<div class="tickets-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tools/tools.css" />
|
||||||
|
|
||||||
<div class="compliance-container" id="compliance-app">
|
<div class="compliance-container" id="compliance-app">
|
||||||
<style>
|
<style>
|
||||||
.compliance-container {
|
.compliance-container {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tools/tools.css" />
|
||||||
|
|
||||||
<div class="security-container" id="security-app">
|
<div class="security-container" id="security-app">
|
||||||
<style>
|
<style>
|
||||||
.security-container {
|
.security-container {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,9 @@
|
||||||
>
|
>
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<div class="empty-page">
|
<div class="empty-page">
|
||||||
<p>Press <kbd>/</kbd> for commands or start typing...</p>
|
<p>
|
||||||
|
Press <kbd>/</kbd> for commands or start typing...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -513,21 +515,27 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function toggleMembersPanel() {
|
function toggleMembersPanel() {
|
||||||
const panel = document.getElementById('members-panel');
|
const panel = document.getElementById("members-panel");
|
||||||
panel.classList.toggle('collapsed');
|
if (panel) {
|
||||||
|
panel.classList.toggle("collapsed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const blocksContainer = document.getElementById('blocks-container');
|
const blocksContainer = document.getElementById("blocks-container");
|
||||||
if (blocksContainer) {
|
if (blocksContainer) {
|
||||||
blocksContainer.addEventListener('keydown', function(e) {
|
blocksContainer.addEventListener("keydown", function (e) {
|
||||||
if (e.key === '/') {
|
if (e.key === "/") {
|
||||||
const slashMenu = document.getElementById('slash-menu');
|
const slashMenu = document.getElementById("slash-menu");
|
||||||
slashMenu.classList.remove('hidden');
|
if (slashMenu) {
|
||||||
|
slashMenu.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
const slashMenu = document.getElementById("slash-menu");
|
||||||
|
if (slashMenu) {
|
||||||
|
slashMenu.classList.add("hidden");
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') {
|
|
||||||
const slashMenu = document.getElementById('slash-menu');
|
|
||||||
slashMenu.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue